티스토리 뷰
'싱글톤(Singleton)'이란 인스턴스를 오직 하나만 생성할 수 있는 클래스를 말합니다.
싱글톤을 만드는 방식은 일반적으로 두 가지를 사용하는데, 두 방식 모두 생성자를 private로 감춰두고, 생성된 유일한 인스턴스에 접근할 수 있는 수단을 만들어 놓습니다. 하나씩 살펴봅시다.
public class Hello {
public static final Hello INSTANCE = new Hello();
private Hello() {}
public void print() {
System.out.println("Hello World!");
}
}
매우 일반적인 방법으로, private 생성자는 public static final 필드인 INSTANCE를 초기화할 때 딱 한번만 호출됩니다.
그리고 public이나 protected 생성자가 없으므로 Hello 클래스는 초기화될 때 만들어진 인스턴스가 전체 시스템에서 하나뿐임을 보장합니다.
하지만, 한가지 예외가 존재하는데요 권한이 있는 Client는 리플렉션 API를 사용해 private 생성자를 호출할 수 있습니다.
public class SingletonMain {
public static void main(String[] args) throws ClassNotFoundException {
Class hello = Class.forName("example.effectivejava.item3.Hello");
try {
Constructor constructor = hello.getDeclaredConstructor();
constructor.setAccessible(true);
Hello world = (Hello)constructor.newInstance();
world.print();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
생성자를 다시 호출할 수 있기 때문에 싱글톤이 유지되지 않는 문제가 발생하게 됩니다.
이러한 공격을 방어하는 방법은 생각보다 간단하게 구현할 수 있는데, 바로 생성자에서 첫 호출이 아니면 예외를 던지는 방법을 사용하면 됩니다.
public class Hello {
public static final Hello INSTANCE = new Hello();
private Hello() {
if(INSTANCE != null) throw new IllegalStateException("Singleton 유지!!");
}
public void print() {
System.out.println("Hello World!");
}
}
Hello의 생성자에 위와같이 예외를 던지는 부분을 추가하고, 다시한번 Main을 실행해보면 아래와 같은 예외를 받을 수 있습니다.
Caused by: java.lang.IllegalStateException: Singleton 유지!!
at example.effectivejava.item3.Hello.<init>(Hello.java:7)
... 6 more
이번에는 정적 팩토리 메서드를 이용해 싱글톤을 만드는 방법에 대해 알아보겠습니다.
public class World {
private static final World INSTANCE = new World();
private World() {}
public static World getInstance() {
return INSTANCE;
}
public void print() {
System.out.println("Hello World!!!");
}
}
Hello class와 같이 초기화될 때 생성자를 기본적으로 호출하고, 생성자를 private로 만들어 외부에서 생성자를 호출하지 못하게 만들었다는 점은 동일합니다. 차이점은 INSTANCE를 private로 선언했고, 정적 팩토리 메서드인 getInstance를 생성했습니다.
World.getInstance는 항상 같은 객체의 참조를 반환하므로, 제 2의 World 인스턴스는 만들어지지 않습니다. 물론 앞서 봤던 리플렉션 API는 예외입니다.
이 둘은 거의 유사해보이지만, 각각 장단점이 존재합니다.
Hello.class의 public 필드 방식의 가장 큰 장점은 해당 클래스가 싱글톤임이 API에 명백히 드러나며 코드가 간결합니다.
반면 World.class의 private 필드 방식의 장점은 더이상 싱글톤을 유지하지 않도록 확장하기가 쉽습니다.
또한 '정적 팩토리를 제네릭 싱글톤 팩토리로 만들 수 있다'는 장점도 존재하며, 정적 팩토리의 메서드 참조를 '공급자'로 사용할 수 있습니다.
갑자기 어려운 내용이 훅 들어왔지만, 걱정할 필요는 없습니다 미래의 제가 다시 정리할 내용이기 때문이에요.
싱글톤 팩토리에 관한 이야기는 Item 30에 등장하는 내용이고, 정적 팩토리의 메서드 참조를 '공급자'로 사용은 Item 43, 44에 등장할 함수형 프로그래밍 용어이기에 넘어가도록 하겠습니다.
중요한 것은 private 생성자를 이용한 싱글톤 방식은 public 생성자 싱글톤 방식에 비해 얻을 수 있는 여러 장점들이 있다는 점입니다.
또 고려해야 하는 문제가 존재하는데, 위의 방식으로 만든 싱글톤 클래스를 직렬화하는 과정에서 싱글톤이 깨질 수 있다는 문제입니다.
객체를 역직렬화하면 readObject가 새롭게 만든 객체를 얻게 됩니다. 그러면 싱글톤이 깨지게 되고, 이를 방지하기 위해서는 아래와 같은 readResolve 메서드를 추가해 싱글톤 객체를 반환하도록 만들어야 합니다.
private Object readResolve() { return INSTANCE; }
글을 시작할때, 일반적으로 싱글톤을 만드는 방식이 2가지라고 언급했는데, 한가지 방법이 더 존재합니다.
세 번째 방법은 원소를 하나만 가지는 Enum type을 선언해 싱글톤을 유지하는 방법입니다.
public enum HelloWorld {
INSTANCE;
int value;
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
}
public 필드 방식과 비슷하지만, 더 간결하고, 직렬화를 위한 추가적인 노력도 필요 없으며 리플렉션 공격에서도 제2의 인스턴스가 생기는 일을 완벽히 막아주기 때문에 대부분의 상황에서는 원소가 하나뿐인 enum 타입이 싱글톤을 만드는 가장 좋은 방법입니다.
주의해야 할점은 만드려는 싱글톤이 Enum 이외의 클래스를 상속해야 한다면, 사용할 수 없는 방법이라는 것입니다.
이렇게 만든 enum 타입 싱글톤은 아래와 같이 사용할 수 있습니다.
public static void main(String[] args) {
HelloWorld helloWorld = HelloWorld.INSTANCE;
System.out.println("helloWorld.getValue() = " + helloWorld.getValue());
helloWorld.setValue(2);
System.out.println("helloWorld.getValue() = " + helloWorld.getValue());
}
Ref : 이펙티브 자바 Effective Java 3/E
'Language > Java' 카테고리의 다른 글
[Java] Item 6 : 불필요한 객체 생성을 피하라 (1) | 2022.10.14 |
---|---|
[Java] Item 5 : 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라 (1) | 2022.10.13 |
[Java] Item 4 : 인스턴스화를 막으려거든 private 생성자를 사용하라 (0) | 2022.10.11 |
[Java] Item 2 : 생성자에 매개변수가 많다면 빌더를 고려하라 (0) | 2022.10.09 |
[Java] Item 1 : 생성자 대신 정적 팩토리 메서드를 고려하라 (1) | 2022.10.09 |
- Total
- Today
- Yesterday
- fiber
- go
- spring
- algorithm
- paging
- cs
- effective
- Effective Java
- OS
- mmu
- Database
- 공지
- soft delete
- GORM
- Operating System
- network
- java
- ARP
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |