본문 바로가기

도서/이펙티브 자바

[이펙티브 자바] 3. private 생성자나 열거 타입으로 싱글턴임을 보증하라

싱글턴을 만드는 가장 안전한 방법은 Enum 을 사용하는 것이다.

 

싱글턴이란?

인스턴스를 오직 하나만 생성할 수 있는 클래스

  • 무상태 클래스
  • 설계상 유일해야하는 시스템 컴포넌트

 

싱글턴 생성 방법

1. public static final 필드 사용

생성자 private, 접근 수단으로 public static final 멤버 제공 (초기화 할 때만 호출되어 싱글턴 보장) 

 

장점

  • 싱글턴임이 명백하게 드러남
  • 간결한 코드

문제점

  • 리플렉션 API인 AccessibleObejct.setAccessible을 사용하여 private 생성자를 호출하는 공격 가능
  • 따라서 생성자에 두 번째 객체가 생성될 때 예외를 던지게끔 수정 필요
public class Elvis {
    private static final Elvis INSTANCE = new Elvis(); // 초기화시 한 번 생성
    private Elvis(){ ... }
}

 

2. 정적 팩터리 메서드 사용

생성자 private, 접근 수단으로 getInsance() 정적 팩터리 메서드 제공 (final 필드를 반환)

 

장점

  • 필요시 API를 바꾸지 않아도 싱글턴이 아니게 변경 가능
  • 원하는 경우 정적 팩터리를 제너릭 싱글턴 팩터리로 생성 가능
  • 정적 팩터리의 메소드 참조를 공급자로 사용 가능

문제점

  • 1번의 리플렉션 공격 가능 
public class Elvis {
    private static final Elvis INSTANCE = new Elvis(); // 초기화시 한 번 생성
    
    // private 생성자 
    private Elvis(){ ... } 
    
    // 정적 팩터리 메서드
    public staic Elvis getInstance() { return INSTANCE; }
}

 

 

1, 2 방식의 문제점

역직렬화 할 때마다 새로운 객체가 생성되어 싱글턴이 깨진다. 

역직렬화할 때 생성자를 거치지 않고 새로운 객체를 만들기 때문이다.

 

직렬화: 객체 → 바이트 스트림

역직렬화: 바이트 스트림 → 객체

 

이를 해결하기 위해서는 readResolve() 메서드를 추가해야한다.

readResolve(): 역직렬화 시 새 객체를 만드는 대신 기존 객체를 반환

import java.io.Serializable;

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

    private Singleton() {}

    // 역직렬화 시 싱글턴 유지
    protected Object readResolve() {
        return INSTANCE;
    }
}

 

3. enum 타입 사용

  • 가장 안전하고, 좋은 방법 
  • 역직렬화시에도 새 객체 생성하지 않음 readResolve() 필요 없음
  • 리플렉션 공격 시 생성자 강제 호출 불가   새 객체 안만들어짐
public enum Elvis {
	INSTANCE;
}

 

만들려는 싱글턴이 Enum 외 상속을 해야하는 경우에는 사용할 수 없다.

 

반응형