본문 바로가기

Language/JAVA

[이펙티브 자바][아이템 3] private 생성자나 열거타입으로 싱글턴임을 보증하라 - 컴도리돌이

728x90

싱글턴 패턴은 일반적으로 자원의 효율적인 사용과 프로그램의 일관성을 유지하기 위해 사용돼요. 예를 들어, 애플리케이션에서 로깅, 캐시, 설정 정보와 같은 클래스는 인스턴스가 여러 개일 필요가 있을까요?? 당연히 없겠죠? 

만약 이런 클래스의 인스턴스가 여러 개라면 상태가 서로 달라질 수 있고, 이는 예기치 않은 버그를 유발할 수 있어요 😔

그렇기 때문에 이런 클래스를 싱글톤으로 구현하여, 프로그램 내에서 단 하나의 인스턴스만 존재하도록 하는 것이 중요합니다. 


🛠️ private 생성자와 싱글톤 보증

자바에서는 싱글톤 패턴을 구현할 때 가장 많이 사용되는 방법 중 하나는 private 생성자를 활용하는 것입니다. 이 방식은 클래스의 인스턴스를 외부에서 생성할 수 없도로 막아줘요.

public class Singleton {
    private static final Singleton INSTANCE = new Singleton();

    private Singleton() {
        // 외부에서 인스턴스 생성을 막기 위해 생성자를 private으로 설정
    }

    public static Singleton getInstance() {
        return INSTANCE;
    }
}

 

생성자를 private으로 설정하면 클래스 외부에서 직접 인스턴스를 만들 수 없어요. 이로써 클래스의 인스턴스가 하나만 생성되도록 보장됩니다. 또한 INSTANCE 필드를 static으로 선언하고, 클래스 로드될 때 단 한 번 초기화되게 합니다. 이 과정에서 JVM이 자동으로 스레드 안전성을 보장하므로, 멀티스레딩 환경에서도 안전하게 사용할 수 있게 됩니다. 

 

 

[Design Pattern] 싱글톤 패턴(Singleton Pattern)에 대해서 - 컴도리돌이

싱글톤 패턴은 하나의 클래스에 대해 단 하나의 인스턴스만 생성하고, 이를 전역적으로 접근할 수 있도록 하는 디자인 패턴이에요. 싱글톤 패턴을 구현하는 방법은 여러 가지가 있지만, 가장 쉬

comdolidol-i.tistory.com

 

 


 

📚Enum을 이용한 싱글톤 구현

Enum을 이용한 싱글톤 구현 방법은 자바에서 가장 권장하는 방식이에요. 이 방법은 코드가 간결하고, JVMenum 인스턴스 생성을 보장하기 때문에 다중 스레드 환경에서도 안전하죠.

public enum Singleton {
    INSTANCE;

    // 필요한 메서드와 필드를 추가
    public void doSomething() {
        System.out.println("Singleton instance method called");
    }
}

 

위 코드에서는 Singleton이라는 enum을 정의하고, INSTANCE라는 유일한 인스턴스를 선언합니다. doSomthing 메서드는 이 인스턴스에서 호출할 수 있는 메서드입니다. 

 

자바 21에서는 enum에 대한 몇 가지 개선 사항이 도입되었어요. 특히 enum 클래스의 기능이 확장되었죠. 이를 통해 더욱 유연하게 싱글톤 패턴을 활용할 수 있게 되었습니다. 자세한 거는 아래 포스팅에 제가 첨부해 놓겠습니다. 

 

 

[JAVA 21] Pattern Matching for switch - 컴도리돌이

스위치문과 null (switches and null) 전통적으로, switch 문과 표현식은 선택기 표현식이 null을 검증할 때, NullPointerException을 throw 합니다. 따라서 null을 테스트하기 위해 switch 바깥에서 테스트해야 했습

comdolidol-i.tistory.com

 

 

그러면 왜 Enum을 사용한 싱글턴이 효과적일까요?? 

 

1. 자동 직렬화 보장

Enum은 기본적으로 직렬화가 안전하게 처리되므로, 싱글턴 인스턴스가 직렬화된 상태로 복원될 때 새로운 인스턴스가 생성되지 않아요. 

직렬화는 객체를 바이트 스트림으로 변환하여 저장하거나 네트워크를 통해 전송할 때 사용됩니다. 

기본적으로 자바의 Serializable 인터페이스를 구현한 클래스는 직렬화할 수 있지만, 싱글턴 패턴을 사용하고 있을 때는 직렬화 시 문제가 생길 수 있어요. 아래 예시 코드를 한번 보겠습니다 🤔

 

import java.io.*;

public class Singleton implements Serializable {
    private static final Singleton INSTANCE = new Singleton();

    private Singleton() {}

    public static Singleton getInstance() {
        return INSTANCE;
    }

    private Object readResolve() {
        return INSTANCE;
    }
}

 

 

Singleton 클래스가 직렬화 가능하도록 Serializable으로 구현되어 있습니다. 그러나 직렬화 과정에서 새로운 인스턴스가 생성될 위험이 있어요. 이 문제를 방지하기 위해 readResolve 메서드를 오버라이드하여 직렬화된 객체가 읽어질 때 항상 기존의 INSTANCE를 반환하도록 해줍니다. 

 

public enum Singleton {
    INSTANCE;

    // 다른 메서드와 필드들
}

 

 

Enum을 사용할 경우, 자바는 직렬화와 역직렬화 과정을 자동으로 처리해 줍니다. 따라서 Enum 인스턴스는 직렬화와 역직렬화 후에도 동일한 인스턴스가 보장돼요. 별도로 readResolve 메서드를 구현할 필요가 없는 거죠. 👍

 

2. 리플렉션 방어

리플렉션은 자바에서 클래스의 메타데이터를 읽거나 조작할 수 있는 기능이라고 합니다. 리플랙션을 사용해 Singleton 클래스의 인스턴스를 생성할 수 있지만, 이를 방지하는 것이 중요합니다. 

import java.lang.reflect.Constructor;

public class Singleton {
    private static final Singleton INSTANCE = new Singleton();

    private Singleton() {}

    public static Singleton getInstance() {
        return INSTANCE;
    }
}

public class Test {
    public static void main(String[] args) throws Exception {
        Singleton instance1 = Singleton.getInstance();
        
        Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        Singleton instance2 = constructor.newInstance();
        
        System.out.println(instance1 == instance2); // false
    }
}

 

위 예시에서는 리플렉션을 사용해 Singleton 인스턴스를 추가로 생성합니다. 하지만 constuctor.newInstance()로 생성된 인스턴스는 기존의 INSTANCE와 다른 인스턴스가 되죠.

public enum Singleton {
    INSTANCE;
    
    // 다른 메서드와 필드들
}

 

Enum을 사용하면 리플렉션을 사용해도 인스턴스를 추가로 생성할 수 없어요. 자바는 Enum의 리플렉션을 방지하여 오직 정의된 인스턴스만 존재하게 하죠 👍


스프링 부트를 사용하는 환경에서는 싱글턴 패턴이 기본적으로 제공되기 때문에, 자바에서 직접 싱글턴을 구현할 필요는 적지만, 여전히 패턴의 이해는 중요하다고 느꼈습니다. 문득 스프링 부트한테 감사하게 되네요 😆 

스프링 프레임워크 감사해요. Bean 감사해요.