(Effective Java) 싱글턴 패턴을 만드는 방법 + 싱글턴 패턴

2022. 9. 20. 16:49독서

반응형
private 생성자나 열거 타입으로 싱글턴임을 보증하라.

 

이펙티브 자바 아이템 3에서는 싱글턴에 대해서 간략하게 설명하고 있다.

 

 

싱글턴은 인스턴스를 오직 하나만 생성할 수 있는 클래스를 말한다. 

 

 

스프링 사용자들은 싱글턴에 대해서 알고 있을 것이다.

 

 

왜냐하면 스프링의 컨테이너가 객체를 싱글턴으로 관리하기 때문이다.

 

 

각설하고 싱글턴을 만드는 방식에 대해서 책에서는 2가지를 소개하고 있다.

 

 

🧐public static final 필드 방식의 싱글턴


 

public class Elvis {
    public static final Elvis INSTANCE = new Elvis();
    
    private Elvis() { ... }
    
    public void leaveTheBuilding() { ... }
}

 

Elvis.INSTANCE를 초기화할 때 딱 한 번만 생성자가 호출되고,

 

 

생성자가 priavte이기 때문에 외부에서 접근할 수 없어서

 

 

클래스가 싱글턴임이 보장된다.

 

 

책에서는 리플렉션 API인 AccessibleObject.setAccesible을 사용해서 private 생성자를 호출할 수 있다고 하는데,

 

 

private 생성자가 무조건 적으로 INSTANCE를 반환하게 하면 된다.

 

 

아래와 같이 말이다.

 

 

public class Elvis {
    public static final Elvis INSTANCE = new Elvis();
    
    private Elvis() {
       return INSTANCE;
    }
    
    public void leaveTheBuilding() { ... }
}

 

이렇게 하면 private 생성자를 호출하더라도, 이미 INSTANCE가 Elvis로 초기화되어 있는 상태이기 때문에

 

 

싱글턴임을 보증할 수 있다.

 

 

🧐정적 팩터리 방식의 싱글턴


public class Elvis {
    private static final Elvis INSTANCE = new Elvis();
    priate Elvis() { ... }
    public static Elvis getInstance() { return INSTANCE; }
    
    public void leaveTheBuilding() { ... }
}

 

아이템 1에서 사용한 방식인 정적 팩터리 메서드를 사용해서 접근하는 방식이다.

 

 

이 방식은 위의 public static final 방식과는 다르게

 

 

INSTANCE가 private이기 때문에 다른 클래스에서 접근이 불가능하고

 

 

오직 getInstance() 메서드로만 접근이 가능하다.

 

 

물론 이것 역시 리플렉션 예외가 적용되기 때문에 위의 예외 수정 코드처럼 private 생성자를 변경해줘야 한다.

 

 

 

🧐고전적인 싱글턴


비슷하긴 하지만 약간 다른 방법으로 작성된 고전적인 싱글턴 코드를 보자.

 

 

public class Singleton {
    private static Singleton INSTANCE;
    
    private Singleton() {} //생성자
    
    public static Singleton getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new Singleton();
        }
        return INSTANCE;
    }
    
}

 

주로 이렇게 싱글턴을 많이 사용한 것 같다.

 

 

클래스의 처음부터 초기화를 하는 것이 아니라,

 

 

null 일 때만 초기화를 해서 사용하는 방식이다.

 

 

하지만 이 방법에서도 문제가 있는데, 바로 멀티 스레드 환경에서 문제가 생길 수 있다.

 

 

이 문제를 해결한 방법이 위에서 설명한 2가지 케이스이다.

 

 

그리고 2가지 케이스가 더 있다.

 

 

 

🧐간단한 멀티 스레딩 문제 해결


 

public class Singleton {
    private static Singleton INSTANCE;
    
    private Singleton() {} //생성자
    
    public static Singleton getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new Singleton();
        }
        return INSTANCE;
    }
    
}

 

 

이 코드의 문제는 멀티 스레드 환경에서 싱글턴을 보증하지 못하는 데 있다.

 

 

왜 보증하지 못할까?

 

 

스레드 1번에서 INSTANCE가 초기화되지 않은 상황에서, 스레드 2번에서 if 체크를 하고 있다면

 

 

if는 true가 될 것이고, 2개의 인스턴스가 생성된다.

 

 

결국 문제는 getInstacne() 메서드가 다중 스레드 접근을 허용하는 메서드인 데서 기인한다.

 

 

그렇기 때문에 다중 스레드 접근을 해결하면 간단하게 문제가 해결된다.

 

 

public class Singleton {
    private static Singleton INSTANCE;
    
    private Singleton() {} //생성자
    
    public static synchronized Singleton getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new Singleton();
        }
        return INSTANCE;
    }
    
}

 

A synchronized statement acquires a mutual-exclusion lock (§17.1) on behalf of the executing thread, executes a block, then releases the lock. While the executing thread owns the lock, no other thread may acquire the lock.

링크

 

synchronized를 활용하면 한 스레드가 메서드 사용을 끝내기 전까지 

 

 

다른 스레드는 wait 상태가 된다.

 

 

즉, 다중 스레드 접근이 허용되지 않는 상황을 만든 것이다.

 

 

그런데 이는 어쩌면 불필요한 오버헤드를 발생시킬지도 모르는다는 생각이 들 수 있다.

 

 

왜냐하면 초기화만 진행하면 더 이상 동기화 상태로 유지할 필요가 없기 때문이다.

 

 

그래서 앞서 소개한 2가지 방법과 더불어 DCL을 소개하려 한다.

 

 

 

🧐DCL로 멀티 스레딩 문제 해결


DCL은 Double Checked Locking의 약자로

 

그냥 2번 확인한다는 의미이다.

 

 

여기서는 싱글턴이 null인지를 2번 확인하는 것이다. 

 

 

하지만 단순히 2번 확인하는 것이 아닌 synchronized를 이용해서 말이다.

 

public cass Singleton {
    private volatile static Singleton INSTANCE;
    
    private Singleton() {}
    
    public static Singleton getInstance() {
        if (INSTANCE == null) {
            synchronized (Singleton.class) {
                if (INSTANCE == null) {
                    INSTANCE = new Singleton();
                }
            }
        }
        return INSTANCE;
    }
}

 

이렇게 사용하면 멀티 스레드가 getInstace()에 접근해도

 

 

결국 초기화는 동기화된 상태에서만 할 수 있기 때문에

 

 

완벽하게 싱글톤을 보장한다.

 

 

java volatile에 대해서는 다음 글을 참고하는 게 도움이 될 것이다.

 

Java volatile

 

johngrib.github.io

 

 

반응형