일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- 무료 오라클 데이터베이스
- Orace 18c
- Oracle 18c HR
- Oracle 사용자명
- 서평단
- Oracle 윈도우 설치
- ORA-00922
- 윈도우 Oracle
- 오라클 캐릭터셋 변경
- oracle 18c
- Oracle 테이블 대소문자
- Oracle 사용자명 입력
- 비전공자를 위한 데이터베이스 입문
- 오라클 캐릭터셋 조회
- Oracle Express Edition
- Oracle 18c 설치
- ora-01722
- Oracle 초기 사용자
- 오라클 캐릭터셋 확인
- Oracle 18c HR schema
- ORA-12899
- 무료 오라클 설치
- oracle
- Oracle 테이블 띄어쓰기
- Today
- Total
Nirsa's Learning Lab
[Effective Java 3/E - 객체 생성과 파괴] Item 3. private 생성자나 열거 타입으로 싱글턴임을 보증하라 본문
[Effective Java 3/E - 객체 생성과 파괴] Item 3. private 생성자나 열거 타입으로 싱글턴임을 보증하라
Nirsa 2025. 6. 5. 15:54싱글턴(Singleton) 이란?
클래스의 인스턴스가 오직 하나만 생성이 가능하도록 보장하는 디자인 패턴
public static final 필드 방식의 싱글턴
public static final을 사용하여 인스턴스를 생성하는 방식입니다. 하지만 리플렉션(AccessibleObject.setAccessible)에 의해 private 생성자를 호출할 수 있는 방법이 존재하여 싱글턴임을 100% 보장하지 않습니다.
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
public class Main {
public static void main(String[] args) throws NoSuchMethodException, SecurityException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
// 1. 일반적인 방법으로 인스턴스 호출
Singleton s1 = Singleton.INSTANCE;
s1.doSomething();
// 2. 리플렉션을 사용해 private 생성자 접근
Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true); // private 무시
Singleton s2 = constructor.newInstance();
s2.doSomething();
System.out.println(s1 == s2); // false
}
}
class Singleton {
public static final Singleton INSTANCE = new Singleton();
private Singleton() {}
public void doSomething() {
System.out.println("싱글턴 메서드 호출");
}
}
정적 팩터리 방식의 싱글턴
정적 팩터리 방식은 getInstance()를 활용하여 항상 같은 객체의 참조를 반환하도록 합니다.
※ 단, public static final과 마찬가지로 리플렉션에 의한 생성자 호출은 똑같이 적용되며, 테스트가 필요할 경우 위의 코드에서 s2 부분을 가져와 확인하시면 됩니다.
public class Main {
public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException {
Singleton s1 = Singleton.getInstance();
s1.doSomething();
}
}
class Singleton implements Serializable {
private static final Singleton INSTANCE = new Singleton();
private Singleton() {}
public static Singleton getInstance() { return INSTANCE; }
public void doSomething() {
System.out.println("싱글턴 메서드 호출");
}
}
두 가지 방식에서의 직렬화(Serializable)
위에서 설명한 두 가지의 방식에서 직렬화를 하려면 Serializable을 구현한다고 선언을 해두는 것만이 아니라, readResolve() 메서드를 추가적으로 제공하여 싱글턴임을 보장해야 합니다.
직렬화 : 객체를 바이트 형태의 데이터로 변환하여 파일, 네트워크 등에 저장하거나 전송할 수 있도록 만드는 과정
역직렬화 : 바이트 형태의 데이터를 다시 객체의 형태로 복원하는 과정
public class Main {
public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException {
// 1. 일반적인 방법으로 인스턴스 호출
Singleton s1 = Singleton.getInstance();
s1.doSomething();
// 2. 객체를 파일로 저장 (직렬화)
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("obj"));
out.writeObject(s1);
out.close();
// 3. 파일에서 객체를 읽음 (역직렬화)
ObjectInputStream in = new ObjectInputStream(new FileInputStream("obj"));
Singleton s2 = (Singleton) in.readObject();
in.close();
System.out.println(s1 == s2);
}
}
class Singleton implements Serializable {
private static final Singleton INSTANCE = new Singleton();
private Singleton() {}
public static Singleton getInstance() { return INSTANCE; }
private Object readResolve() {
return INSTANCE;
}
public void doSomething() {
System.out.println("싱글턴 메서드 호출");
}
}
위의 코드에서 싱글턴 객체(s1)를 직렬화하여 파일에 저장한 후, 저장된 파일을 역직렬화 하게 되면 JVM은 새로운 인스턴스를 생성하게 됩니다. 즉, 기존의 인스턴스(s1)와 새로운 인스턴스(s2)가 생기게 되므로 s1 == s2의 결과가 false가 나오며 싱글턴을 보장할 수 없게 됩니다.
이러한 이유로 public static final 또는 정적 팩터리를 사용하여 싱글턴을 구현할 경우 readResolve() 메서드를 생성하여 싱글턴을 보장해주어야 하는데, readResolve()는 역직렬화 과정에서 JVM이 자동으로 호출하는 메서드로써 기존의 인스턴스를 반환하여 싱글턴임을 보장해줍니다.
즉, public static fianl과 정적 팩터리 방식의 경우 역직렬화 및 리플렉션에 의한 싱글턴임을 100% 보장받을 수 없는 단점이 있습니다.
열거 타입(Enum) 방식의 싱글턴 (권장)
아래와 같이 열거 타입(Enum) 방식의 싱글턴을 사용할 경우 역직렬화와 리플렉션으로 부터 보호받을 수 있기 때문에 싱글턴임을 보장받을 수 있습니다.
아래의 코드에서 INSTANCE는 enum의 유일한 상수이며, 자바에서 enum은 JVM 내에서 클래스 로딩 시점에 단 한 번만 생성됩니다. 즉, Singleton.INSTANCE는 단 하나의 객체로만 존재하게 됩니다.
또한 enum은 리플렉션으로 생성자 호출 시 예외를 발생시켜 차단하며, 역직렬화 시 새로운 객체를 만들지 않고 원래 존재하던 상수 객체를 반환합니다. 이러한 특성들로 인하여 싱글턴임을 보장해야할 땐 열거 타입으로 사용하는걸 권장하기도 합니다.
public class Main {
public static void main(String[] args) {
Singleton s1 = Singleton.INSTANCE;
Singleton s2 = Singleton.INSTANCE;
s1.doSomething();
System.out.println(s1 == s2); // true
}
}
enum Singleton {
INSTANCE;
public void doSomething() {
System.out.println("싱글턴 메서드 호출");
}
}
하지만 단점도 존재 합니다.
enum은 이미 java.lang.Enum 클래스를 자동 상속하므로 다른 클래스를 상속받을 수 없습니다. (자바에서는 단일 상속만을 지원, 인터페이스 형태의 상속은 다중 상속을 지원하므로 사용 가능)
이러한 이유로 상속이 필요한 경우 static final, 정적 팩터리를 사용하면 되며 일반적인 상황에서는 enum을 활용한 싱글턴을 사용하는것이 좋다고 합니다.