목차

Singleton pattern

🗓️

Singleton

  • 싱글톤 패턴은 인스턴스를 하나만 만들어 사용하기 위한 패턴이다. Connection pool, thread pool, device configuration 객체 등과 같은 경우 인스턴스를 여러개 만들게 되면 불필요한 자원을 사용하게 되고, connection pool의 예를 들면 계속 커넥션을 맺고 끊는 작업이 반복되거나 요청이 많아지면 DBMS에 부담이 많이 가게 되는 문제가 발생한다. (마치 php같아진다)
  • 싱글톤 패턴으로 객체를 생성하면 두개의 인스턴스가 존재할 수 없게 된다. 이를 구현하려면 생성자에 제약을 걸어야 하고 단일 객체를 반환할 수 있는 메소드가 필요하다.
  • Company.java
package org.platanus.designpattern.singleton;

public class Company {

    private static Company instance = new Company();

    private Company() {
    }

    public static Company getInstance() {
        if (instance == null) {
            instance = new Company();
        }
        return instance;
    }

}
  1. 생성자를 private으로 만들어 외부에서 인스턴스를 생성할 수 없도록 한다.
  2. static으로 클래스 내부에서 인스턴스를 생성한다.
  3. 외부에서는 내부에서 생성된 인스턴스만 반환하도록 한다.
package org.platanus.designpattern.singleton;

public class CompanyRun {

    public static void main(String[] args) {
        Company myCompany1 = Company.getInstance();
        Company myCompany2 = Company.getInstance();
        System.out.println(myCompany1 == myCompany2);
    }

}
  • myCompany1myCompany2의 인스턴스는 같기 때문에 true를 반환한다.

싱글스레드, 멀티스레드

  • 회사에 근로자가 많아져서 출근하는 사람들이 많아졌다.
  • 식사시간에 배식을 받기로 하는 상황을 만들어보자
  • 아래 식당에서 급식을 배급하는 매소드와 toString을 구현한다.
// Company.java
...
public void foodDistribute(String foodDist) {
    System.out.println(foodDist);
}

@Override
public String toString() {
    return instance.getClass().getSimpleName() + "@" + instance.hashCode();
}
...
  • 근로자 클래스를 만들고 점심을 먹는 메소드를 만든다.
public class Worker {
    private String name;

    public Worker(String name) {
        this.name = name;
    }

    public void doLaunch() {
        Company company = Company.getInstance();
        company.foodDistribute(this.name + " is receiving food in " + company.toString());
    }
}
  • 회사에 총 7명의 직원이 있고 하나의 배식구를 통해 줄을 서서 급식을 배급받는다.
public class CompanyWorkerLaunchTest {

    public static void main(String[] args) {
        Worker[] workers = new Worker[7];
        for (int i = 0; i < workers.length; i++) {
            workers[i] = new Worker((i + 1) + " worker");
            workers[i].doLaunch();
        }
    }
}
  • 7명의 근로자들은 한 회사의 식구이므로 같은 구내식당에서 밥을 먹는다.
1 worker is receiving food in Company@1418481495
2 worker is receiving food in Company@1418481495
3 worker is receiving food in Company@1418481495
4 worker is receiving food in Company@1418481495
5 worker is receiving food in Company@1418481495
6 worker is receiving food in Company@1418481495
7 worker is receiving food in Company@1418481495

문제점

  1. Company 인스턴스가 생성되지 않았을 때 Thread 1 이 getInstance()의 if문을 실행해 이미 인스턴스가 생성되었는지 확인한다. 현재 Company는 NULL 상태다.
  2. 만약 Thread 1이 생성자를 호출해 인스턴스를 만들기 전 Thread 2가 if문을 실행해 Company가 NULL인지 확인한다. 현재 NULL이므로 생성자를 호출한다.
  3. Thread 1도 Thread 2와 마찬가지로 생성자를 호출하면 결과적으로 Company는 두개의 인스턴스가 생긴다.

이 시나리오는 Race condition 경합 조건을 발생시킨다. 경합조건은 동일한 자원을 2개 이상의 thread가 이용하려고 경쟁하는 현상을 말한다.

  • 테스트를 통해서 실제로 인스턴스가 중복생성 되는 과정이 발생하는지 보자
public class CompanyThread {

    private static CompanyThread instance = null;

    private CompanyThread() {
    }

    public static CompanyThread getInstance() {
        if (instance == null) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
            }
            instance = new CompanyThread();
        }
        return instance;
    }

    public void foodDistribute(String foodDist) {
        System.out.println(foodDist);
    }

    @Override
    public String toString() {
        return instance.getClass().getSimpleName() + "@" + instance.hashCode();
    }

}
  • Thread.sleep() : 처리가 너무 빨라서 thread race condition을 관찰하기 힘들어 포함시켰다.
public class WorkerMultiLine extends Thread {

    public WorkerMultiLine(String name) {
        super(name);
    }

    public void run() {
        CompanyThread company = CompanyThread.getInstance();
        company.foodDistribute(
            "No." + Thread.currentThread().getName() +
                " ration food in " + company.toString()
        );
    }
}
  • 회사는 번창해서 직원들이 늘어 구내식당의 급식대를 여러개 만들었고, 급식대 번호(thread)를 부여하고 있다.
public class CompanyWorkerLaunchThreadTest {

    private static final int THREAD_NUM = 12;

    public static void main(String[] args) {
        WorkerMultiLine[] worker = new WorkerMultiLine[THREAD_NUM];
        for (int i = 0; i < THREAD_NUM; i++) {
            worker[i] = new WorkerMultiLine(String.valueOf(i + 1));
            worker[i].start();
        }
    }
}
  • 급식창구는 총 12개고 직원들은 구내식당으로 몰린다.
No.7 ration food in CompanyThread@830774632
No.10 ration food in CompanyThread@830774632
No.11 ration food in CompanyThread@830774632
No.1 ration food in CompanyThread@7929378
No.9 ration food in CompanyThread@1450099523
No.8 ration food in CompanyThread@830774632
No.5 ration food in CompanyThread@830774632
No.3 ration food in CompanyThread@830774632
No.2 ration food in CompanyThread@830774632
No.12 ration food in CompanyThread@830774632
No.6 ration food in CompanyThread@830774632
No.4 ration food in CompanyThread@1785412971
  • 점심시간이 되어 식당을 열었는데 직원들이 다른 830774632이 아닌 7929378, 1450099523 회사의 구내식당에 가서 밥을 먹고 있다!

해결책

  • 다중 스레드 어플리케이션에서 발생하는 스레드 경합 문제를 해결하는 대표적인 방법 두가지가 있다.
    • static 변수에 인스턴스를 만들어 바로 초기화 하는 방법
    • 인스턴스를 만드는 메소드에 동기화를 적용하는 방법
  • 두가지 방법은 간단하므로 조치해보겠다.

static 변수에 인스턴스를 만들어 바로 초기화 하는 방법

public class CompanyThread {

    //    private static CompanyThread instance = null;
    private static CompanyThread instance = new CompanyThread();
  • static 변수는 객체가 생성되기 전 클래스가 메모리에 로딩 될 때 만들어져 초기화가 한번만 실행된다.
  • 또한 static 변소는 프로그램이 시작될 때 부터 종료 될 때 까지 없어지지 않고 메모리에 계속 상주하며 클래스에 생성된 모든 객체에서 참조될 수 있다.
  • 이러한 특징으로 static변수에 생성자를 호출하면 같은 프로그램이 종료 될 때 까지 같은 스레드를 본다.
No.9 ration food in CompanyThread@1450099523
No.1 ration food in CompanyThread@1450099523
No.12 ration food in CompanyThread@1450099523
No.7 ration food in CompanyThread@1450099523
No.10 ration food in CompanyThread@1450099523
No.5 ration food in CompanyThread@1450099523
No.3 ration food in CompanyThread@1450099523
No.11 ration food in CompanyThread@1450099523
No.8 ration food in CompanyThread@1450099523
No.6 ration food in CompanyThread@1450099523
No.2 ration food in CompanyThread@1450099523
No.4 ration food in CompanyThread@1450099523

인스턴스를 만드는 메소드에 동기화를 적용하는 방법

  • 생성자에 synchronized 키워드를 넣으면 thread-safe하게 작동한다.
  • 변수가 보호되어야 하는 일반 메소드에도 똑같이 적용할 수 있다.
...
    public synchronized static CompanyThread getInstance() {
        if (instance == null) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
            }
            instance = new CompanyThread();
        }
        return instance;
    }

    public void foodDistribute(String foodDist) {
        synchronized (this) {
            counter++;
            System.out.println(foodDist + ". total counter " + counter);
        }
    }
...
  • 아래는 실행 결과다
No.8 ration food in CompanyThread@1358916791. total counter 1
No.11 ration food in CompanyThread@1358916791. total counter 2
No.6 ration food in CompanyThread@1358916791. total counter 3
No.2 ration food in CompanyThread@1358916791. total counter 4
No.10 ration food in CompanyThread@1358916791. total counter 5
No.12 ration food in CompanyThread@1358916791. total counter 6
No.5 ration food in CompanyThread@1358916791. total counter 7
No.9 ration food in CompanyThread@1358916791. total counter 8
No.3 ration food in CompanyThread@1358916791. total counter 9
No.4 ration food in CompanyThread@1358916791. total counter 10
No.1 ration food in CompanyThread@1358916791. total counter 11
No.7 ration food in CompanyThread@1358916791. total counter 12
  • 이 방법 말고도 static 변수에 생성자가 실행되는 타이밍이나 추가로 발생할 수 있는 경합을 방지할 수 있는 방법이 있다