본문 바로가기

Language/Java Plus

Effective JAVA - Item3 : private 생성자나 열거 타입으로 싱클턴임을 보증하라.

 

결론 : " 싱글턴 사용시 여러가지 방식중 장단점을 고려해 사용을 하되, 열거타입을 가장 추천한다. 하지만 다른 방법들 또한 단점을 보완하는 해결책이 있으니 그 점을 유의해서 사용하자 "

 

싱글턴 정리

  • 정의 : 인스턴스를 오직 하나만 만들 수 있는 클래스를 의미한다.
  • 예시 : 함수와 같은 무상태 객체, 설계상 유일해야 하는 시스템 컴포넌트(자바에서 빈을 생각)
  • 문제점 : 클래스를 싱글턴으로 만들면 이를 사용하는 클라이언트를 테스트하기 어려워질 수 있다.
  • -> 타입을 인터페이스로 정의하고 그 인터페이스를 구현하여 만든 클래스가 아니라면 싱글턴 인스턴스를 가짜 구현으로 대체하기 어렵기 때문이다. 예를 들어, Spring에서 테스트시 클래스들을 가짜 객체로 만들어 테스팅한다. 하지만 인터페이스로 구현되지 않은 클래스라면 이러한 가짜 객체를 만들기 어렵다.

 

싱글턴 만들기

  • 방식 1. : private 생성자 + public static final 필드
  • 설명 : 생성자는 private으로 감추고 public static 멤버를 하나 마련해 둔 구조 즉, public static final로 해당 클래스 내부에 해당 클래스에 대한 인스턴스를 만들어 두고 프로그램 시작시 private 생성자로 생성된다. 이 후. 해당 필드는 전역 필드이기 때문에  외부에서는 같은 패키지 내라면 패키지 명을 제외하고 반대라면 포함하고 클래스명을 통해 접근이 가능하다. 또한 final 처리를 통해 외부에서 인스턴스에 직접 접근후 변경이 불가능하다.
  • 다만, 권한이 있는 클라이언트는 리플렉션 API인 AccessibleObject.setAccessible을 사용해 private 생성자를 호출가능. 이러한 공격을 방어하려면 생성자를 수정하여 두번째 인스턴스가 생성되려고 할 때, 예외처리를 한다.
  • 장점
  • - 해당 클래스가 싱글턴임을 명확히 표시하며 간결하다. 
  • 단점
  • - 싱글턴을 사용하는 클라이언트가 테스트하기 어렵다
  • - 역직렬화 시 새로운 인스턴스가 생길 수 있다
  • - 리플렉션으로 private 생성자를 호출 가능하다.

 

  • 방식 2 : private 생성자 + private static final 필드 + 정적 팩터리 메서드
  • 설명 : 마찬가지로 생성자르 감추고 private static final 로 인스턴스를 생성하고 static 메서드를 통해 다른 클래스에 전달하는 방식, 리플렉션에 대한 예외는 마찬가지로 존재한다. 또한 private 처리를 통해 외부에서 인스턴스에 직접 접근하는것은 불가능하다.
  • 장점
  • - API를 변경하지 않고도 싱글턴이 아니게 변경할 수 있다.
  • - 정적 팩터리를 제네릭 싱글턴 패턴으로 만들 수 있다.
  • - 정적 팩터리의 메서드 참조를 공급자로 사용할 수 있다.
  • 단점
  • - 싱글턴을 사용하는 클라이언트가 테스트하기 어렵다
  • - 역직렬화 시 새로운 인스턴스가 생길 수 있다
  • - 리플렉션으로 private 생성자를 호출 가능하다.

 

 

방식2 장점 상세 설명

1. API를 변경하지 않고도 싱글턴이 아니게 변경할 수 있다.

- 클라이언트 코드를 변경하지 않고도 클래스 내부에서 return INSTANCE; -> return new Class(); 변경시 싱글턴이 아니게 변경이 가능하다. 이를 통해, 스레드 별로 다른 인스턴스를 반환하게 끔 클라이언트 API를 변경하지 않아도 가능하게 한다.

 

2. 정적 팩터리를 제네릭 싱글턴 패턴으로 만들 수 있다.

- 아래와 같이 작성시를 예시로 들 수 있다.

- 인스턴스 생성시 같은 인스턴스이지만 서로 다른 타입을 활용하는 인스턴스를 만들 수 있다.(제네릭)

public class sendMessage<T>{
    private static final sendMessage<?> INSTANCE = new sendMessage<>();

    private sendMessage(){
    
    }
    
    public static <T> sendMessage<T> getInstance(){
    	return (sendMessage<T>) INSTANCE;
    }
    
    public void send(T message){
    	System.out.println(message);
    }
}


// 사용
sendMessage<String> instance1 = sendMessage.getInstance();
sendMessage<Integer> instance2 = sendMessage.getInstance();

 

3. 정적 팩터리의 메서드 참조를 공급자로 사용할 수 있다.

- 아래와 같이 작성시 해당 장점의 예시가 될 수 있다.

- 즉, 정적 팩터리의 메서드를 공급자로 전달이 가능하다는 의미이다.

// Main
Test test = new Test();
test.go(() -> sendMessage.getInstance());
// Or
test.go(sendMessage::getInstance());


// Test
public class Test{
    public void(Supplier<sendMessage> supplier){
    	sendMessage = supplier.get();
        sendMessage.send("test");
    }
}


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

    private sendMessage(){
    
    }
    
    public static sendMessage getInstance(){
    	return INSTANCE;
    }
    
    public void send(String message){
    	System.out.println(message);
    }
}

 

- 하지만 위 3가지의 장점을 굳이 활용할 일이 없다면 public 필드 방식이 좋다.

 

 

 

방식 1, 2 단점 상세 설명

1. 싱글턴을 사용하는 클라이언트가 테스트하기 어렵다

- 예를들어, 싱글턴 패턴을 사용하여 DB와 커넥션을 갖는 인스턴스를 사용한다고 가정하고 이를 테스팅한다고 하였을 때, 원하는 것은 실제 데이터가 DB에 들어가는 것이 아닌 동작 여부에 대한 테스팅을 목적을 둔다고 가정했을 때, 위와 같이 싱글턴을 만들어 사용시 실제 데이터를 삽입하지 않고 테스팅만 하는 것은 불가능하다.

 

해결 : 인터페이스 생성 -> Mock클래스, 실제클래스 해당 인터페이스를 구현 -> 사용시 다운캐스팅해서 테스팅

TestInterface realClass = realClass.getInstance();

TestInterface mockClass = mockClass.getInstance();

-> Mock클래스 생성시 RealClass에서 비용이 드는 부분을 수정하여 생성하여 테스팅시 비용이 드는 부분을 없애고 인터페이스 구현을 통해 의존성을 해소하여 사용한다.

 

 

 

2. 역직렬화 시 새로운 인스턴스가 생길 수 있다

직렬화 : 객체를 바이트 스트림으로 변환하여 파일에 저장하거나 네트워크를 통해 전송

역질렬화 : 바이트 스트림으로 이루어진 값을 다시 객체로 변경해서 APP에서 사용

 

해결 : 필드값 노출(클래스의 필드를 private transient로 설정하여 노출 방지), 클래스 내에 readResolve()메서드를 생성해 사용하여 인스턴스 재생성 방지, 이름은 readResolve()여야 함.

 

두 방식으로 Serializable을 구현한다고 선언하는 것만으로는 직렬화를 하는데 부족하다. 모든 필드를 일시적(transient)로 설정하고 readReslove를 제공해야한다. 이렇게 하지 않으면 역직렬화시 새로운 인스턴스가 생성된다.

private Object readResolve(){
    return INSTANCE;
}

 

 

 

3. 리플렉션으로 private 생성자를 호출 가능하다.

- 리플렉션 : 생성자를 가져와 접근 지정자를 변경하여 사용하여 공격하는 경우

 

해결 : private 생성자 안에 인스턴스 존재 여부를 체크하고 새로운 인스턴스 생성 호출시 throw new UnsupportedOperationException("message"); 를 통해 새로운 인스턴스 생성을 억제 가능

 

 

추가 설명

- 추가적으로 인스턴스에 final 처리를 하지않고 메서드에서 null 임을 체크하며 인스턴스를 반환할 시에는 다른 인스턴스를 반환하는 불완전함이 있기에 싱글턴임을 보장할 수 없다. (thread-safe 하지 않음)

 

 

 

방식 3 : 열거타입을 사용하라.

- 아래 해시코드 체크시 동일한 결과를 가짐을 확인할 수 있다.

- 또한 위의 장점들을 갖고 있고 대부분 상황에서는 원소가 하나뿐인 열거 타입이 싱글턴을 만드는 가장 좋은 방법이다.

- 단, 만드려는 싱글턴 클래스가 Enum외의 클래스를 상속해야한다면 사용할 수 없다. 

public enum EnumSingleTon implements TestInterface{
    INSTANCE;
    
    @Override
    public void send(String message){
    	System.out.println("message");
    }
}


// 사용
EnumSingleTon test1 = EnumSingleTon.INSTANCE;
EnumSingleTon test2 = EnumSingleTon.INSTANCE;
EnumSingleTon test3 = EnumSingleTon.INSTANCE;

System.identityHashCode(test1);
System.identityHashCode(test2);
System.identityHashCode(test3);

 

 

방식 4 : Holder-Idiom

- 위에서 설명한 장점들을 대부분 가지고 있다.

- 위의 예시에서는 추가적인 단점으로 사용하지 않는 인스턴스도 만들어져 존재해야 하는데, 이는 호출시 처음 인스턴스가 생긴다는 장점이 있다. 다시 말해, Lazy Loading을 할 수 있다는 장점이 있다.

 

public class singleTon{
    private sigleTon(){
    
    }
	
    public static singleTon getSingleTon(){
    	return Holder.INSTANCE:
    }
    
    public static class Holder{
    	static final singleTon.INSTANCE = new singleTon();
    }
}

 

 

 

 

코드 정리

https://github.com/mokjaemin/EffectiveJAVA

 

GitHub - mokjaemin/EffectiveJAVA: Study Files Of Effective JAVA

Study Files Of Effective JAVA. Contribute to mokjaemin/EffectiveJAVA development by creating an account on GitHub.

github.com