Effective Java 3/E 판을 읽고 정리한 기록입니다.
"private 생성자나 열거타입으로 싱글톤을 보장한다"는 말은
객체지향 프로그래밍에서 사용되는 디자인 패턴 중 하나인 싱글톤(Singleton) 패턴을 의미합니다.
싱글톤 패턴은 애플리케이션에서 인스턴스가 단 하나만 생성되도록 보장하는 패턴입니다.
Private 생성자
클래스의 생성자를 private 로 선언하여 외부에서 직접적으로 인스턴스를 생성할 수 없도록 합니다.
이는 외부에서 새로운 인스턴스를 생성하는 것을 막아, 오직 클래스 내부에서만 인스턴스를 생성할 수 있도록 합니다.
열거타입(Enum)
Java에서는 열거타입을 이용해 싱글톤을 구현할 수 있습니다.
열거타입은 JVM에 의해 클래스가 로딩될 때 하나의 인스턴스로 보장되기 때문에 싱글톤을 보장하는데 적합합니다.
싱글톤을 구현하는 두 가지 방법
Private 생성자를 사용하는 방법
이 방법은 클래스의 생성자를 private로 선언하여 외부에서의 직접적인 인스턴스 생성을 막습니다.
대신에 클래스 내부에서 스스로 인스턴스를 생성하고 유일한 인스턴스를 반환하는 정적 메서드를 제공합니다.
특징
- 외부에서의 직접적인 인스턴스 생성을 막아 싱글톤 패턴을 강제화합니다.
- 하지만 리플렉션을 통해 private 생성자에 접근하여 인스턴스를 생성하는 것을 방지할 수는 없습니다.
- 다중 스레드 환경에서 안전한지 여부는 구현 방식에 따라 다릅니다. 필요시 동기화 처리가 필요합니다.
private 생성자를 사용하는 방법으로 싱글톤 구현 예시코드
public class SingletonUsingPrivateConstructor {
// private 생성자로 외부에서의 인스턴스화를 막습니다.
private SingletonUsingPrivateConstructor() {}
// 유일한 인스턴스를 보관하기 위한 정적 필드
private static SingletonUsingPrivateConstructor instance;
// 외부에서 인스턴스를 얻을 수 있는 정적 메서드를 제공합니다.
public static SingletonUsingPrivateConstructor getInstance() {
if (instance == null) {
instance = new SingletonUsingPrivateConstructor();
}
return instance;
}
}
열거 타입(Enum)을 사용하는 방법
이 방법은 Java에서 제공하는 열거 타입을 이용하여 싱글톤을 구현합니다.
열거 타입은 JVM에 의해 인스턴스가 보장되므로 싱글톤을 보장합니다.
특징
- 열거 타입은 JVM에 의해 인스턴스 생성이 보장되므로 다중 스레드 환경에서 안전합니다.
- 열거 타입은 직렬화와 역직렬화에 대한 내장 자원을 제공하므로 객체의 직렬화 문제를 해결할 필요가 없습니다.
- 코드가 간결하고 가독성이 좋으며, enum 상수로 인스턴스에 접근할 수 있어 편리합니다.
싱글톤을 보장하는 방법으로 열거 타입을 사용하는 것이 가장 좋습니다.
열거 타입을 사용해 싱글톤 구현한 예시코드
public enum SingletonUsingEnum {
// 유일한 인스턴스를 보관하는 enum 상수
INSTANCE;
// 싱글톤의 동작을 정의하는 메서드
public void performAction() {
System.out.println("Singleton instance is performing an action.");
}
}
두 방식 모두 생성자는 private으로 감춰두고, 유일한 인스턴스에 접근할 수 있는 수단으로 public static 멤버를 마련해 둔다.
public static final 필드 방식을 사용하여 싱글턴을 구현하는 방법
public class Singleton {
// public static final 필드로 단일 인스턴스를 정의합니다.
public static final Singleton INSTANCE = new Singleton();
// private 생성자로 외부에서의 인스턴스화를 막습니다.
private Singleton() {}
// 싱글턴 인스턴스를 반환하는 정적 메서드를 제공합니다.
public static Singleton getInstance() {
return INSTANCE;
}
}
이 방식의 장점은 간결하고, 쉽게 이해할 수 있다는 것입니다.
또한 JVM이 클래스를 로드하는 과정에서 초기화되므로 스레드 안정성이 보장됩니다.
그러나 단점으로는 미리 인스턴스를 생성하여 메모리를 사용하게 된다는 점과,
인스턴스가 항상 생성되어 있어야 한다는 것입니다.
이 방식의 가장 큰 장점은,
해당 클래스가 싱글턴임이 API에 명백하게 드러난다는 것입니다.
이것은 클래스 내부의 정적 필드가 public으로 선언되어있고, final 키워드로 인해 변경될 수 없음을 의미합니다.
두 번째 장점은 간결함입니다.
정적 팩토리 메서드를 public static 멤버로 제공해 싱글턴을 구현하는 방법
public class Singleton {
// private static 멤버로 싱글턴 인스턴스를 보관합니다.
private static final Singleton instance = new Singleton();
// private 생성자로 외부에서의 인스턴스화를 막습니다.
private Singleton() {}
// 정적 팩터리 메서드를 통해 싱글턴 인스턴스에 접근할 수 있도록 합니다.
public static Singleton getInstance() {
return instance;
}
}
정적 팩터리 방식은 객체 생성을 위해 정적(static) 메서드를 사용하는 디자인 패턴입니다.
여기서 말하는 "정적 팩터리 방식"이란 정적 메서드를 통해 객체를 생성하고 반환하는 방식을 의미합니다.
정적 팩터리 메서드 방식의 장점
- API 변경 없이 다른 인스턴스 반환
- 정적 팩토리를 사용하면 API를 변경하지 않고도 싱글톤이 아닌 다른 인스턴스를 반환할 수 있다는 것입니다.
- 즉, 코드의 수정 없이도 원하는 경우에는 싱글톤이 아닌 다른 인스턴스를 반환할 수 있습니다.
- 이것은 객체 생성 방식의 유연성을 높입니다.
- 예제코드
public class ObjectFactory {
private ObjectFactory() {} // 외부에서 객체 생성 방지
// 정적 팩터리 메서드
public static Object createObject() {
return new Object();
}
}
// 사용 예시
Object obj1 = ObjectFactory.createObject(); // 기본 객체 생성
Object obj2 = ObjectFactory.createObject(); // 두 번째 객체 생성
// obj1과 obj2는 다른 인스턴스일 수 있음
- 제네릭 싱글턴 팩터리
- 정적 팩터리를 제네릭하게 설계해 싱글턴 객체를 생성할 수 있다는 것입니다.
- 제네릭을 사용하면 객체의 타입에 관계없이 다양한 객체를 생성할 수 있습니다.
- 예제코드
public class SingletonFactory<T> {
private static T instance;
private SingletonFactory() {} // 외부에서 객체 생성 방지
// 제네릭 싱글턴 팩터리 메서드
public static <T> T getInstance(Class<T> clazz) throws IllegalAccessException, InstantiationException {
if (instance == null) {
instance = clazz.newInstance(); // 클래스의 인스턴스 생성
}
return instance;
}
}
// 사용 예시
SomeClass instance1 = SingletonFactory.getInstance(SomeClass.class);
SomeOtherClass instance2 = SingletonFactory.getInstance(SomeOtherClass.class);
// instance1과 instance2는 동일한 인스턴스일 수 있음
- 메서드 참조를 공급자로 사용
- 정적 팩토리의 메서드를 공급자로 사용할 수 있다는 것입니다.
- 즉, 메서드 참조를 사용해 다른 메서드의 인자로 정적 팩터리 메서드를 전달할 수 있습니다.
- 이것은 함수형 프로그래밍에서 유용하게 활용될 수 있습니다.
- 예시코드
import java.util.function.Supplier;
public class SupplierFactory {
private SupplierFactory() {} // 외부에서 객체 생성 방지
// 정적 팩터리 메서드를 공급자로 사용
public static String createString() {
return "Hello, world!";
}
// 사용자 정의 공급자
public static Supplier<String> stringSupplier = SupplierFactory::createString;
}
// 사용 예시
String message = SupplierFactory.stringSupplier.get(); // "Hello, world!"를 반환
- 이러한 장점들은 코드를 유연하고 응집력 있게 만들어주는데, 만약 이러한 유연성과 활용성이 필요하지 않다면 간단히 public 필드 방식을 사용하는 것이 좋을 수 있습니다. 하지만 일반적으로는 위 장점들을 활용해 더 좋은 코드 구조를 유지하는 것을 권장합니다.
바로 위, 둘 중 하나의 방식으로 만든 싱글턴 클래스를 직렬화하려면, 몇 가지 주의할 점이 있다.
직렬화(Serialization)는 객체를 바이트 스트림으로 변환하여 파일이나 네트워크를 통해 전송하거나 저장하는 프로세스를 말합니다.
자바에서는 Serializable 인터페이스를 구현함으로써 객체를 직렬화할 수 있습니다.
그러나 싱글턴 패턴을 적용한 클래스를 직렬화하려면 주의할 점이 있습니다.
객체의 유일성 보장
직렬화된 객체를 역직렬화(Deserialization)할 때, 싱글턴 패턴을 유지하기 위해선 객체의 유일성을 보장해야 합니다.
그렇지 않으면 역직렬화된 객체가 새로운 인스턴스로 생성될 수 있습니다.
readResolve() 메서드의 재정의
Serializable 인터페이스를 구현하는 클래스에서는 readResolve() 메서드를 재정의해 역직렬화된 객체가 싱글턴 객체임을 보장해야합니다. 이 메서드는 역직렬화된 객체를 반환하는 역할을 합니다.
따라서 이 메서드를 사용하여 싱글턴 객체를 반환하도록 구현해야 합니다.
예시코드
import java.io.*;
// 싱글턴 클래스
class Singleton implements Serializable {
private static final long serialVersionUID = 1L;
private static Singleton instance;
private Singleton() {} // 외부에서 객체 생성 방지
// 싱글턴 객체 반환 메서드
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
// readResolve 메서드 재정의
protected Object readResolve() {
return getInstance();
}
}
public class SerializationExample {
public static void main(String[] args) throws IOException, ClassNotFoundException {
// 직렬화된 객체를 파일에 저장
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("singleton.ser"));
out.writeObject(Singleton.getInstance());
out.close();
// 직렬화된 객체를 파일에서 읽어들임
ObjectInputStream in = new ObjectInputStream(new FileInputStream("singleton.ser"));
Singleton singleton = (Singleton) in.readObject();
in.close();
// 두 객체가 동일한지 확인
System.out.println("Original Singleton: " + Singleton.getInstance().hashCode());
System.out.println("Deserialized Singleton: " + singleton.hashCode());
}
}
이 예제코드에서는 Singleton 클래스를 직렬화하고 역직렬화합니다.
Singleton 클래스는 싱글턴 패턴을 따르므로 하나의 인스턴스만을 가지고 있어야 합니다.
그러나 직렬화 및 역직렬화 과정에서 객체의 유일성이 깨질 수 있습니다.
위의 코드에서는 readResolve() 메서드를 재정의하여 역직렬화된 객체가 싱글턴 객체임을 보장합니다.
이 메서드는 역직렬화 과정에서 호출되어 싱글턴 객체를 반환합니다.
실행 결과를 보면, 원래의 싱글턴 객체와 역직렬화된 객체의 해시코드가 동일함을 확인할 수 있는데,
이는 싱글턴 객체의 유일성이 유지되었음을 의미합니다.
싱글턴 만드는 세 번째 방법은 원소가 하나인 열거 타입을 선언하는 것이다.
위에서 설명한 그 열거 타입인데, public 필드 방식과 비슷하지만, 더 간결하고 추가 노력 없이 직렬화할 수 있어 복잡한 직렬화 상황이나 리플렉션 공격에서도 제2의 인스턴스가 생기는 일을 완벽히 막아줄 수 있습니다.
조금 부자연스러워 보일 수 있지만, 대부분 상황에서 원소가 하나뿐인 열거 타입이 싱글턴을 만드는 가장 좋은 방법입니다.
하지만, 만들려는 싱글턴이 Enum 외에 클래스를 상속해야 한다면, 이 방법은 사용할 수 없습니다.
(열거 타입이 다른 인터페이스를 구현하도록 선언은 가능합니다.)
하지만, 클래스를 싱글톤으로 만들면 이를 사용하는 클라이언트를 테스트하기 어렵다.
의존성 문제(Dependency Problem)
싱글톤 클래스는 다른 클래스에서 의존성을 갖는 경우가 많습니다.
이는 해당 클래스를 테스트할 때 다른 클래스에 대한 의존성을 해결해야 하는데,
이때 테스트를 위해 의존하는 객체를 모의(mock) 객체로 대체하기 어려워집니다.
모의 객체를 대체하는 것이 어려워지면 테스트의 복잡도가 증가하고, 유지보수가 어려워집니다.
상태 공유(State Sharing)
싱글톤 클래스는 애플리케이션 전역에서 상태를 공유하는 경우가 많습니다.
이는 여러 테스트 케이스에서 동일한 싱글톤 인스턴스를 공유하게 되어 테스트 간에 영향을 주고받을 수 있습니다.
이로 인해 테스트의 결과가 예측하기 어려워집니다.
인스턴스 제어 어려움
테스트에서 싱글톤 클래스의 인스턴스를 제어하기 어려운 경우가 있습니다.
테스트에서 특정 상태로 인스턴스를 초기화하거나, 인스턴스를 리셋하는 등의 작업이 필요할 때 이를 제어하기 어려울 수 있습니다.
변경에 대한 취약성
싱글톤 클래스는 애플리케이션 전체에서 유일한 인스턴스를 가지므로,
해당 클래스에 대한 변경이 테스트 코드에도 영향을 줄 수 있습니다.
이는 테스트 코드를 변경해야 하는 경우를 발생시킬 수 있습니다.
예시코드
아래는 싱글톤 패턴을 사용해 구현된 클래스입니다
public class Singleton {
private static Singleton instance;
private Singleton() {
// private 생성자
}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
public void doSomething() {
System.out.println("Singleton instance is doing something.");
}
}
아래는 이 싱글톤 클래스를 사용하는 클라이언트를 테스트하는 코드입니다.
public class Client {
public static void main(String[] args) {
Singleton singleton = Singleton.getInstance();
singleton.doSomething();
}
}
아래는 이 코드를 테스트코드로 작성한 것입니다.
public class ClientTest {
@Test
public void testSingleton() {
Singleton singleton = Singleton.getInstance();
singleton.doSomething();
// 테스트 코드 추가
}
}
요약
객체 지향 프로그래밍에서 싱글턴 패턴은 클래스의 인스턴스가 하나만 생성되도록 보장하는 디자인 패턴입니다.
싱글턴 패턴을 사용하면 애플리케이션 전체에서 하나의 인스턴스만 사용되도록 보장할 수 있어,
메모리 및 자원의 낭비를 방지하고, 예상치 못한 오류를 방지합니다.
여러 곳에서 동일한 인스턴스를 사용하므로 코드의 일관성을 유지할 수 있습니다.
그리고 스레드의 안전성이 보장되고 직렬화와 리플렉션을 방어할 수 있습니다.
오늘은 이펙티브 자바 3장을 공부했는데요.
저한테는 아주 심오한 내용이라, 한줄한줄 이해하고 생각하기가 매우 힘들었지만,
그래도 다들 제 글을 통해 조금이라도 도움이 되시길 바라며,
오늘도 공부이자 기록인 제3장 블로깅을 마무리 짓겠습니다..
이 책을 선물해 주신 저희 CTO 준혁님께 감사인사를 드리며,
항상 즐거운 개발공부 되세요!! :)
'Effective Java' 카테고리의 다른 글
Effective Java | Item 6. 불필요한 객체 생성을 피해라 (0) | 2024.05.27 |
---|---|
Effective Java | Item 5. 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라 (0) | 2024.04.23 |
Effective Java | Item 4. 인스턴스화를 막으려거든 private 생성자를 사용하라 (1) | 2024.04.23 |
Effective Java | Item 2. 생성자에 매개변수가 많다면 빌더를 고려하라. (0) | 2024.03.27 |
Effective Java | Item 1. 생성자 대신 정적 팩터리 메서드를 고려하라. (0) | 2024.03.13 |