티스토리 뷰
싱글턴이란 애플리케이션 전체에서 단 하나만 존재하는 인스턴스를 의미한다
그러면 왜 단 하나만 필요할까?
크게 보면 두 가지 이유가 있다
1. 함수형 프로그래밍에서 자주 사용되는 불변 객체로 사용하기 위해
2. 생성 비용이 너무 비싸고, 스레드 간 공유해도 안전한 컴포넌트인 경우
1번은 Java Web Application을 만들 때 흔히들 사용하는 DTO 형태에서 상태 변경만 하지 못하도록 하면 된다
2번은 JPA에서 사용되는 EntityManagerFactory와 같은 객체를 예로 들 수 있다
EntityManagerFactory는 스레드 간 공유해도 안전하며 생성 비용이 크다
싱글턴을 만드는 방식은 어떻게 될까?
1. 생성자를 private으로 만들고 인스턴스 반환은 static method를 이용한다
2. 1번 방식을 응용하는데 클래스 로딩 시점이 아닌 객체를 실제로 사용하는 시점에 초기화되는 LazyHolder
3. 의도한 방식은 아니지만 Enum 을 사용한다
1번 방식의 문제점은 동기화를 보장하기 위해 synchronized 키워드를 사용해야 하는데
메서드에 걸면 성능 저하가 심해지기 때문에 범위를 좁혀 메서드 내부에서 synchronized 사용해야 한다
사실 현실적인 문제라고 보기는 어려운 것이 위 상황에서 문제 되는 건 정말 찰나의 순간이다
인스턴스 생성이 완료되기 전에 메모리 할당이 되는데 메모리 할당 -> 인스턴스 생성 완료로 가는 그 순간에
다른 스레드에 의해 instance 가 사용되면 아직 인스턴스 생성이 완료되지 않았기 때문에 문제가 터질 수 있는 것이다
이를 막기 위해서는 Application 을 띄우고 캐시를 로드하는 것처럼 객체를 한번 띄워주면 문제가 없긴 하다
애플리케이션 전체에서 이런 방식으로 싱글턴을 운용할 때 싱글턴 객체가 하나라면 큰 문제는 아니겠지만
싱글턴 수가 많아질수록 수동으로 띄워줘야하는 일이 많아진다
아래는 Double Checked Locking, DCL이라 부르는데 null check를 두 번 하기 때문에 그렇게 부른다
현재로는 broken idiom 이라 하고 더 나은 방식이 있으니 사용을 지양하자
public class Singleton1 {
private volatile static Singleton1 instance;
private Singleton1() {
}
public static Singleton1 getInstance() {
if (instance == null) {
synchronized (Singleton1.class) {
if (instance == null) {
instance = new Singleton1();
}
}
}
return instance;
}
}
싱글턴을 만들기 위해서 synchronized 가 필수는 아니다
싱글턴 클래스 안에 static class로 중첩 클래스를 만든 후 내부에서 INSTANCE 필드를 초기화한다
싱글턴을 사용하는 곳에서는 Singleton2.getInstance()로 인스턴스를 반환 받는다
외부에서 직접 LazyHolder 에 접근할 수 없도록 private으로 막아둬야 한다
아래 방식은 LazyHolder 라 부르는 방식이다
이는 스레드 관리를 직접 하지 않고 JVM에 넘겨서 스무스하게 처리한다
JVM의 클래스 로딩 프로세스에서 자체적으로 스레드 안전성을 보장하기 때문에 믿고 사용하면 된다
public class Singleton2 {
private Singleton2() {
}
public static Singleton2 getInstance() {
return LazyHolder.INSTANCE;
}
private static class LazyHolder {
private static final Singleton2 INSTANCE = new Singleton2();
}
}
1번이나 2번 사용을 하기로 했다면 고려해야 할 점이 남아있다
둘 모두 Reflection API를 이용해 private 필드에 접근할 수 있다는 것이 문제이고
단순히 implements Serializable로 직렬화를 끝냈다고 생각해서는 안 된다
모든 필드에 transient 선언해서 직렬화 대상에 포함되지 못하도록 하고 (이때 필드는 유지되며 값이 null로 들어간다)
커스텀 readResolve 메서드를 생성해 자동으로 호출되는 readObject를 대체할 수 있게 해야 한다
readObject는 여전히 자동 호출되나 생성된 객체는 바로 GC 대상이 되고 readResolve를 사용해 반환받은 객체를 이용한다
이렇게 복잡한 방식까지 고려하지 않으면 싱글턴 객체를 역직렬화할 때 다른 객체가 반환되어
동일성 비교를 했을 때 false 가 나게 된다 이는 싱글턴이 깨짐을 의미한다
싱글턴을 구현하는 가장 쉬운 길은 열거 타입을 사용하는 것이다
정상적인 방법은 아니기 때문에 나도 깨름칙 하지만 1번, 2번은 위에서 언급한 많은 고려사항이 있기 때문에 복잡해진다
단순히 아래와 같이 두고 필요하다면 커스텀 메서드를 작성해 원하는 일을 시키면 된다
리플렉션 공격에도 안전하고 직렬화 & 역직렬화도 자바가 다 처리해놨다
조슈아 블로치도 믿고 쓰라는데 이 찜찜함은 무엇일까..?
public enum Singleton03 {
INSTANCE
public void customMethod() {
System.out.println("blah blah");
}
}
'Java > Effective Java' 카테고리의 다른 글
[Item06] 재사용으로 성능 향상 시켜보자 (0) | 2022.02.10 |
---|---|
[Item05] 때려박는 코딩은 그만, 확장성을 고려하자 (0) | 2022.02.08 |
[Item04] 인스턴스화 방지, setter 막아두기 과연 옳은가 (0) | 2022.02.05 |
[Item02] Builder Pattern의 장단점은 무엇인가 (0) | 2022.01.28 |
[Item01] 생성자 대신 정적 팩터리 메서드를 고려하라 (0) | 2022.01.22 |