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

이주오·2021년 10월 13일
0

도서

목록 보기
15/15

싱글턴

  • 인스턴스를 오직 하나만 생성할 수 있는 클래스를 말한다.
    • ex) 함수와 같은 무상태 객체, 설계상 유일해야 하는 시스템 컴포넌트
  • 클래스를 싱글턴으로 만들면 이를 사용하는 클라이언트를 테스트하기가 어려워질 수 있다.
    • 타입을 인터페이스로 정의한 다음 해당 인터페이스를 구현해서 만든 싱글턴이 아니라면 싱글턴 인스턴스를 가짜(mock) 구현으로 대체할 수 없기 때문이다.
  • 싱글턴 만드는 방식
    • 두 방식 모두 생성자는 private로 감춰두고, 유일한 인스턴스에 접근할 수 있는 수단으로 public static 멤버를 하나 마련해둔다.

1. public static 멤버가 final 필드인 방식

  • private 생성자는 public static final 필드인 Elvis.INSTANCE를 초기화할 때 한번만 호출된다.
  • public, protected 생성자가 없으므로 Elvis 클래스가 초기화될 때 만들어진 인스턴스가 전체 시스템에서 하나뿐임이 보장된다.
    • 예외: 권한이 있는 클라이언트는 리플렉션 api인 AccessibleObject.setAccessible을 사용해 private 생성자를 호출할 수 있다.
    • 이러한 공격을 방어하려면 생성자를 수정하여 두 번째 객체가 생성되려 할 때 예외를 던지도록 한다.
  • 장점
    • 해당 클래스가 싱글턴임이 API에 명백히 드러난다
    • public static 필드가 final 이니 절대로 다른 객체를 참조할 수 없다
    • 간결하다
public class Elvis {
    public static final Elvis INSTANCE = new Elvis();
    private Elvis() { ... }

    public void leaveTheBuilding() { ... }
}

2. 정적 팩터리 메서드를 public static 멤버로 제공

  • Elvis.getInstance는 항상 같은 객체의 참조를 반환하므로 제2의 Elvis 인스턴스는 만들어지지 않다
    • 리플렉션을 통한 예외는 똑같이 적용
  • 장점
    • API를 바꾸지 않고도 싱글턴이 아니게 변경할 수 있다.
    • 정적 팩터리를 제네릭 싱글턴 팩터리(아이템 30)를 만들 수 있다.
    • 정적 팩터리의 메서드 참조를 공급자(supplier)로 사용할 수 있다.
      • Elvis::get Instance → Supplier
    • 이런한 장점들이 굳이 필요하지 않는다면 public 필드 방식이 좋다.
public class Elvis {
	 	private static final Elvis INSTANCE = new Elvis();
    private Elvis() { }
    public static Elvis getInstance() { return INSTANCE; }

    public void leaveTheBuilding() {...}
}
  • 두 가지 방식으로 만든 싱글턴 클래스를 직렬화하는 경우 Serializable을 구현하는 것만으로 부족
    • 모든 인스턴스 필드를 일시적(transient)이라고 선언하고 readResolve 메서드를 제공해야 한다.
    • 그렇지 않으면 직렬화된 인스턴스를 역직렬화할 때 마다 새로운 인스턴스가 생성된다
public Object readResolve() {
    // 진짜 Elvis를 반환하고, 가짜 Elvis는 가비지 컬렉터에 맡긴다.
    return INSTANCE;
  }
}

Singleton은 왜 안티패턴이라 불리는가?

  • SOLID 원칙의 대부분은 인터페이스 설계와 관련이 되어있다. 의존성을 concrete class(구현 클래스)가 아닌 Interface에 두면, 실제 concrete class의 구현이 변경되어도 이를 사용한 코드는 큰 영향을 받지 않는다. 그렇기 때문에 SOLID원칙(OCP, LSP, ISP, DIP등)을 지키기 위해서는 인터페이스로 설계를 해야한다.
  • 하지만 싱글톤을 이용하는 경우 대부분 인터페이스가 아닌 콘크리트 클래스의 객체를 미리 생성해놓고 정적 메소드를 이용하여 사용하게 된다. 이는 여러 SOLID원칙을 위반할 수 있는 가능성을 열어둠과 동시에, 싱글톤을 사용하는 곳과 싱글톤 클래스 사이에 의존성이 생기게 된다. 클래스 사이에 강한 의존성, 즉 높은 결합이 생기게 되면 수정, 단위테스트의 어려움 등 다양한 문제가 발생한다.

1. private 생성자를 갖고 있어 상속이 불가능하다.

  • 싱글톤은 자신만이 객체를 생성할 수 있도록 생성자를 private으로 제한한다. 하지만 상속을 통해 다형성을 적용하기 위해서는 다른 기본생성자가 필요하므로 객체지향의 장점을 적용할 수 없다. 또한 싱글톤을 구현하기 위해서는 객체지향적이지 못한 static 필드와 static 메소드를 사용해야 한다.

2. 테스트하기 힘들다.

  • 싱글톤은 테스트하기가 힘드며 테스트 방법에 따라 불가능할 수 있다. 싱글톤은 생성 방식이 제한적이기 때문에 Mock 객체로 대체하기가 어려우며, 동적으로 객체를 주입하기도 힘들다.
  • 테스트는 개발의 핵심인데, 테스트 코드를 작성할 수 없다는 것은 큰 단점이 된다.

3. 서버 환경에서는 싱글톤이 1개만 생성됨을 보장하지 못한다.

  • 서버에서 클래스 로더를 어떻게 구성하느냐에 따라 싱글톤 클래스임에도 불구하고 1개 이상의 객체가 만들어질 수 있다. 따라서 Java 언어를 이용한 싱글톤 기법은 서버 환경에서 싱글톤이 꼭 보장된다고 볼 수 없다. 또한 여러 개의 JVM에 분산돼서 설치되는 경우에도 독립적으로 객체가 생성된다.
  • 생성자를 private하게 두었어도 reflection을 통해 하나 이상의 오브젝트가 만들어질 수 있다. 또한 여러개의 JVM에 분산돼서 설치가 되는 경우에도 각각 독립적으로 오브젝트가 생기기 때문에 싱글톤으로서의 가치가 떨어진다.

4. 전역 상태를 만들 수 있기 때문에 바람직하지 못하다.

  • 싱글톤의 스태틱 메소드를 이용하면 언제든지 해당 객체를 사용할 수 있고, 전역 상태(Global State)로 사용되기 쉽다. 아무 객체나 자유롭게 접근하고 수정하며 공유되는 전역 상태는 객체지향 프로그래밍에서 권장되지 않는다.
  • 싱글톤 패턴은 객체를 1번 생성하고 재사용할 수 있다는 장점이 있다. 하지만 다른 단점들이 너무 크기 때문에 활용이 쉽지 않았는데, Spring에서는 컨테이너를 통해 직접 객체(빈)들을 싱글톤으로 관리함으로써 객체를 재사용함과 동시에 객체지향스로운 개발을 할 수 있도록 해주었다.

3. 원소가 하나인 열거 타입을 선언

  • public 필드 방식과 비슷하지만 더 간결하고, 직렬화 가능하고 좋다
  • 단, 만들려는 싱글턴이 Enum 외의 클래스를 상속해야 한다면 사용 불가
public enum Elvis {
  INSTANCE;

  public void leaveTheBuilding() {...}
}

참고 출처

profile
동료들이 같이 일하고 싶어하는 백엔드 개발자가 되고자 합니다!

0개의 댓글