싱글톤 (Singleton) 패턴 구현 방법

ideal dev·2024년 7월 1일
0

design-pattern

목록 보기
1/1

<백기선 강사님 강의 요약>

Singleton Pattern을 공부해보자

싱글턴 패턴을 가장 단순히 구현하는 방법

아래와 같이 Settings를 만들면 어떻게 될까?

public class App {
    public static void main(String[] args){
        Settings settings1 = new Settings();
        Settings settings2 = new Settings();
        System.out.println(settings == settings1);
    }
}

Settings에 대해 여러 instance가 만들어진다.

만약 하나의 Settings로 내부 값을 관리하고 싶다면?🧐

인스턴스를 오직 하!나!만 제공해주는 클래스가 필요하다.
하나의 Settings instance를 만들어주고, Settings가 필요할 때 만들어둔 instance를 리턴하자.

public class Settings {

    private static Settings instance;

    private Settings(){}

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

⚠️ 여기서 다시 Settings 인스턴스 2개를 생성한다면?

  • 똑같은 Settings 인스턴스가 return 된다!
    • 내부에서 객체를 생성하고, 생성한 객체를 return 해주는 방식!
public class App {
    public static void main(String[] args){
        Settings settings1 = Settings.getInstance();
        Settings settings2 = Settings.getInstance();
        System.out.println(settings1 == settings2);
    }
}

✅ 싱글턴의 필요성
시스템 런타임, 환경 세팅에 대한 정보 등, 인스턴스가 여러개 일 때 문제가 생길 수 있는
경우가 있다. 인스턴스를 오직 한개만 만들어 제공하는 클래스가 필요할 때 사용

저 패턴 좋네. 그럼 언제든지 사용해도 되는거야? ❌❌❌

멀티스레드에서 안전하지 않아!

Q) 생각해봅시다.

  1. 생성자를 private으로 만든 이유?
    A) 외부에서 생성을 못하게 하기 위해 ( = 내부에서만 생성하기 위해 )

  2. getInstance() 메소드를 static으로 선언한 이유?
    A) 외부에서 인스턴스에 접근하기 위해

  3. getInstance()가 멀티쓰레드 환경에서 안전하지 않은 이유?
    A) 두 개의 thread가 getInstance의 if문에 동시에 접근한다면 2개의 instance가 생성될 수 있음!

그럼 어떻게 thread safe하게 구현할 수 있을까?

멀티 쓰레드 환경에서 안전하게 구현하는 방법

synchronized 키워드 사용하기

=> synchronized를 사용해서, 하나의 스레드만 메소드를 실행할 수 있게 하자!(Thread safe!)

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

✅ 장점 : 동시에 여러 스레드가 접근할 수 없음
❎ 단점 : 성능 저하

  • getInstance()를 호출할 때 마다 동기화 처리를 함
    • 동기화 처리 : lock -> key를 사용해서 그 key를 가진 스레드만 실행 -> 끝나면 lock해제

Q) 생각해봅시다.
1. 자바의 동기화 블럭 처리 방법은?
A) 잘 정리된 링크[https://github.com/kim-se-jin/CS-JAVA-Study/blob/main/Java/Synchronization.md]

  1. getInstance() 메소드 동기화시 사용하는 락(lock)은 인스턴스의 락인가 클래스의 락인가? 그 이유는?
    A) 클래스 락, 메소드가 static이기 때문이며, 클래스 객체에 대한 락을 사용하여 클래스 수준에서 동기화가 이루어짐. 이를 통해 싱글톤 인스턴스 생성 과정에서의 스레드 안전성을 보장

(GPT 답변)
인스턴스의 락 : 인스턴스 메소드에서 synchronized 키워드를 사용하면, 그 메소드를 호출하는 특정 인스턴스에 대한 락이 적용됩니다. 즉, 같은 인스턴스의 다른 스레드가 해당 메소드에 동시에 접근할 수 없습니다.

클래스 락 : static synchronized 메소드는 클래스 전체에 적용되므로, 클래스 객체에 대한 락이 걸립니다. 클래스 객체에 대한 락은 해당 클래스의 모든 static synchronized 메소드들이 공유합니다. 즉, 한 스레드가 static synchronized 메소드를 실행하고 있을 때, 다른 스레드는 그 클래스의 다른 static synchronized 메소드에 접근할 수 없습니다.

흠.. 그럼 synchronized를 안쓰는 방법은 없을까?

이른 초기화(eager initialization) 사용

객체를 final로 하나 만들어두고, getInstance()를 호출할 때 생성해둔 객체를 return 해주자!

public class Settings {

    private static final Settings INSTANCE = new Settings();

    private Settings(){}

    public static Settings getInstance(){
        return INSTANCE;
    }
}

✅ 장점 : 동시에 여러 스레드가 접근할 수 없음 ( = 스레드 safe함 )
❎ 단점 : 인스턴스 만드는 과정이 오래 걸리거나, 생성 이후 안쓰게 되는 상황 등이 있을 수 있음.

변수를 선언할 때 생성해, INSTANCE 변수가 클래스 로딩 시점에 초기화됨 -> final을 사용해, 변수가 초기화된 이후 변경될 수 없음(= 변수에 값을 한 번만 할당할 수 있음) -> thread safe 해짐

Q) 생각해 봅시다.
1. 이른 초기화가 단점이 될 수도 있는 이유?
A) 위 ❎ 단점 내용

  1. 만약에 생성자에서 checked 예외를 던진다면 이 코드를 어떻게 변경해야 할까요?
    A) 고민중...

그럼 Synchorinized를 최소한으로 쓰고, 내가 쓰고싶을 때 만들 수 있는 방법은 없나?


double checked locking 방식 사용

  • getInstance()를 호출할 때 변수가 없으면 생성해주자 -> 변수를 생성할 때, synchronized를 사용해 thread safe하게 만들자.
public class Settings {

    private static volatile Settings instance;

    private Settings(){}

    public static Settings getInstance(){
        if(instance == null){
            synchronized (Settings.class){
                if(instance == null){
                    instance = new Settings();
                }
            }
        }
        return instance;
    }
}

✅ 장점 : 동시에 여러 스레드가 접근할 수 없고, 인스턴스를 사용할 때 생성함.
❎ 단점 : 코드가 복잡함, volatile은 자바 1.5 부터 사용할 수 있음.

Q)
1. double check locking이라고 부르는 이유?

  • 첫 번째 체크: 동기화 블록 밖에서 instance가 null인지 확인
  • 두 번째 체크: 동기화 블록 안에서 다시 한 번 instance가 null인지 확인
    -> 이를 통해, 불필요한 동기화 비용 줄이고! 스레드 안전성 확보
  1. instacne 변수는 어떻게 정의해야 하는가? 그 이유는?
  • volatile 선언 private static volatile Settings instance;
  • volatile 키워드는 변수의 값이 모든 스레드에 즉시 반영됨을 보장
  • 자바 메모리 모델에서는 변수의 값이 각 스레드의 캐시 메모리에 저장될 수 있으며, 다른 스레드에서 해당 변수의 최신 값을 보지 못할 가능성이 있지만, volatile 키워드를 사용해 문제 방지

그럼 1.4에서나 코드를 간결하게 쓰고싶으면 어떡하지?

  • static inner 클래스 사용
public class Settings {

    private Settings(){}

    private static final class InstanceHolder {
        private static final Settings instance = new Settings();
    }

    public static Settings getInstance(){
        return InstanceHolder.instance;
    }
}

✅ 장점 : double checked locking과 동일하지만 코드가 더 간결!

이 방법을 깨트릴 다양한 방법이 존재

Q) 생각해 봅시다.
1.이 방법은 static final를 썼는데도 왜 지연 초기화 (lazy initialization)라고 볼 수 있는가?
( 지연 초기화란? 인스턴스를 처음 사용할 때까지 인스턴스를 생성하지 않고, 필요할 때 인스턴스를 생성하는 방법 )
A) SettingsHolder 클래스 내부에 SETTINGS라는 static final 변수가 선언되어 있고, 이 변수는 Settings 클래스가 처음으로 로드될 때가 아니라, getInstance() 메소드가 호출될 때 생성되기 때문!

0개의 댓글