[Item3] private 생성자나 열거타입으로 싱글턴임을 보증하라 (feat. 싱글톤은 안티패턴)

Sera Lee·2022년 2월 21일
0

EffactiveJava

목록 보기
2/9
post-thumbnail

싱글턴을 만드는 방법 1 : static final

public static 멤버가 final 필드인 방식

public class Elvis {
	public static final Elvis INSTANCE = new Elvis();
	private Elvis() {...}
	
	public void leaveTeBuilding() {
	}
}

외부에서 Elvis.INSTANCE 로 객체를 가져올 수 있다.

장점

  • 해당 클래스가 싱글턴임이 API 에 드러난다.
  • 간결하다

문제점

  • 리플렉션을 이용하면 객체를 2개 이상 만들 수 있다
  • 결과

    com.maxst.maxwork.remote.Elvis@25865fca
    com.maxst.maxwork.remote.Elvis@4c32aa7e

  • 해결방법
    - Class 내부에 count 변수를 두고, 객체가 생성되면 count 값이 증가하도록 하여, 객체가 두번 생성되려고 하면 예외를 던진다.
    예시는 다음과 같다.
    예제코드1 을 실행시키면 다음과 같이 IllegalStateException이 발생하며, 객체 2개 이상 생성을 막을 수 있다.

싱긑턴을 만드는 방법2 : factory method

  • 정적 팩터리 메서드를 public static 멤버로 제공
public class Elvis {
	private static final Elvis INSTANCE = new Elvis();
	private Elvis() {...}
	public Elvis getInstance() { return INSTANCE;}
	
	public void leaveTeBuilding() {
	}
}

외부에서 Elvis.getInstance() 로 객체를 가져올 수 있다.

장점

  • API 를 바꾸지 않고 싱글턴이 아니게 변경할 수 있다. 그러므로 클라이언트 쪽 코드에 영향이 전혀 가지 않는다.
public Elvis getInstance() { return new Elvis();}
  • 정적 팩터리를 제네릭 싱글턴 팩터리 로 만들 수 있다.
  • 정적 팩터리의 메서드 참조를 공급자로 사용할 수 있다
    • Elvice::getInstance 를 Supplier로 사용

      Supplier<Elvis> supplier = (Supplier<Elvis>) Elvis.getInstance();
      Elvis elvis = supplier.get();

❗ 싱글턴을 만드는 방법 1 vs 2 뭐가 좋나,

2번방식의 장점이 필요하지 않다면 첫번째 방식으로 하는 것이 좋다.

❓︎ 직렬화 <-> 역직렬화를 거쳐고 싱글톤 속성을 유지하는 방법

  • 직렬화 → 역직렬화를 거치게 된 객체는 직렬화를 거치기 전 객체와 같은 인스턴스일 수 없다.
  • 싱글톤이더라도 직렬화 → 역직렬화를 하게 되면 예외일 수 없다.
  • 직렬화 → 역직렬화를 거치더라도 싱글톤 속성 그대로 유지하도록 하려면 어떻게 해야할까?

방법

  • 멤버 필드에 transient 키워드를 선언한다.
    • 직렬화가 가능한 클래스의 멤버필드에 transient 키워드를 선언하면, 키워드를 붙인 멤버필드는 직렬화에서 제외된다는 속성을 이용하였다.
  • readResolve()에서 싱글톤 인스턴스를 리턴하게 해 준다.
    • transient 를 모든 멤버변수에 붙이는 것은 번거롭다.

참고한 사이트
https://100100e.tistory.com/342
https://madplay.github.io/post/what-is-readresolve-method-and-writereplace-method
https://madplay.github.io/post/java-serialization
https://madplay.github.io/post/what-is-readobject-method-and-writeobject-method

Enum으로 Thread-save 한 Singleton 구현하기

  • 직렬화/역직렬화 문제도 없고, 리플렉션으로private constructor가 호출되는 문제도 고민할 필요가 없는 방법이다.

예제코드

public enum ElvisEnum {
    INSTANCE;
    
    String name;
    int age;

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setAge(int age) {
        this.age = age;
    }
}
ElvisEnum singleton = ElvisEnum.INSTANCE;
System.out.println(singleton.getName());
singleton.setAge(2);

enum 으로 싱글턴을 구현하는 방법이 과연 좋은 방법일까

enum 은 프로그래밍 언어에 국한 되지않고 공통적으로 명명된 상수들의 집합, 즉 열거를 의미한다. enum 이라는 네이밍에 열거 라는 구현의 의도가 명확하게 드러난다. 과연 enum을 싱글턴으로 썼을 때 목적을 쉽게 파악할 수 있을까라는 의문도 생겼다.
분명 enum을 싱글톤으로 사용했을 때의 장점은 크다.
Thread-Safe 하고, 리플렉션 공격에도 안전하고, Serialization 도 보장하는 Singleton 이 가지는 모든 단점을 커버한다.
Joshua Bloch 는 책에서도 enum 방식이 널리 채택되어지지 않았다는 것을 인지하지만 싱글턴을 구현하는 최적의 방법이 enum 이라고 한다.
enum 으로 싱글턴을 구현하는 방법이 널리 쓰이지 않는데는, java 진영은 대부분 Spring 을 사용하고 있고, Spring은 싱글턴패턴이 안티패턴이므로 컨테이너를 통해 직접 빈들을 싱글톤으로 관리하도록 개발할 수 있도록 제공하기 때문이라고 생각한다. 굳이 싱글톤을 사용할 필요가 없다.
enum 으로 싱글턴을 구현하는 것을 극도로 싫어하는 사람이 있는데, 이해는 간다. 하지만 정말 자원을 한정적으로 전역자원으로 써야할 필요가 있다면, 그야말로 Minimum Effort, Maximum Effect 이 아닐까. 극도로 싫어하는 사람도 있지만, 소수 enum 으로 구현하기를 선호하는 사람도 있다.

Moking 하기(WIP)


예제코드

public interface SingletonInterface {
  int getNum();
}

-----------------------------------------------

public enum SingletonObject implements SingletonInterface {
    INSTANCE;
    private int num;

    protected void setNum(int num) {
        this.num = num;
    }

    @Override
    public int getNum() {
        return num;
    }
}
----------------------------------------------------
@Test
public void test() {
  SingletonInterface singleton = Mockito.mock(SingletonInterface.class);
  when(singleton.getNum()).thenReturn(1); //does work
}

싱글턴을 사용하는 이유

한정된 자원 안에서 인스턴스를 남용하지 않고, 하나의 자원으로 모두가 공유해서 사용해야하는 경우 유용한 방법이 될 수 있다.

결론

  • Singleton Pattern 은 안티패턴이다
    • 테스트가 어렵다
    • 싱글톤이 한개임을 보장하지 못한다.
    • 전역상태는 누구든지 접근할 수 있고, 아무나 수정될 수 있으므로 객체지향 프로그래밍에서는 권장되지 못한다.
  • 한정적으로 리소스를 사용할 수 있다는 가치와 Singleton Pattern 사용하기 위한 effort는 trade-off 되어야한다.
  • 정말, 무조건 Singleton 을 사용해야한다면 enum 으로 구현하는 법도 고려할 필요가 있다.
  • Singleton 뿐 아니라 전역으로 사용되는 변수들은 Thread-safe 하도록 더 신경 써야겠다.

0개의 댓글