Home Item 3 - private 생성자나 열거 타입으로 싱글턴임을 보증하라
Post
Cancel

Item 3 - private 생성자나 열거 타입으로 싱글턴임을 보증하라

  • 싱글턴: 객체의 인스턴스가 오직 1개만 생성되는 패턴을 의미
    ex) 함수와 같은 stateless Object(무상태 객체), 설계상 유일해야 하는 시스템 컴포넌트
    • stateless Object(무상태 객체): 인스턴스 변수가 없는 객체
      1
      2
      3
      4
      5
      
        public class Car {
            void Car() {
                System.out.println("I'm a car!");
            }
        }
      

      아래와 같이 컴파일 타임에 정의되어 있고 변경되지 않는 상수도 stateless Object이다.

      1
      2
      3
      4
      5
      6
      7
      
      public class Car {
            static final String MESSAGE = "I'm a car!";
                 
            void Car() {
                System.out.println(MESSAGE);
            }
        }
      

      stateless Object와 연관된 헷갈릴 수 있는 개념으로 Immutable Object가 있다. 객체 지향 프로그래밍에서 Immutable Object란 상태를 바꿀 수 없는 객체이다.

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      
      public class Car {
            private static String message;
                 
            void Car(String message) {
                this.message = message;
            }
                 
            public void printMessage() {
                System.out.println(message);
            }
        }
      

      이와 같이 불변 객체는 상태가 한번 지정되면 바뀔 수 없지만 컴파일 시점에 값이 정의되는 것이 아니기때문에 stateless Object는 아니다.

클래스를 싱글턴으로 만들면 클라이언트 코드를 테스트하기가 어렵다. 싱글톤이 인터페이스를 구현한게 아니라면 싱글턴 인스턴스를 mock 객체 구현으로 대체할 수 없기 때문이다.

  • Mock 객체: 실제 객체를 다양한 조건으로 인해 제대로 구현하기 어려울 경우 가짜 객체를 만들어 사용하는데, 이를 Mock 객체라 한다.
    Mock 객체가 필요한 경우,
    • 테스트 작성을 위한 환경 구축이 어려운 경우
    • 테스트가 특정 경우나 순간에 의존적인 경우
    • 시간이 걸리는 경우

싱글턴을 만드는 방식은 두가지가 있으며 공통점이 있다.

  • 생성자는 private으로 감춰둔다.
  • 유일한 인스턴스에 접근할 수 있는 수단으로 public static 멤버를 마련해둔다.

1. public static final 필드 방식의 싱글턴

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package item03;

public class Singleton1 {

    //instance는 static이니까 클래스가 로딩될 시점에 한번만 초기화됨.
    //초기화될 때 new를 사용하기 때문에 그때 처음 만들어진 instance가 계속해서 재사용하게됨
    public static final Singleton1 instance = new Singleton1();

    static int cnt;

    private Singleton1() {
        cnt++;
        if(cnt != 1){//리플렉션을 막기 위한 방법
            throw new IllegalStateException("this object should be singleton");
        }
    }

}

public static final 필드인 Singleton1.instance 초기화 할 때 private 생성자는 딱 한번만 호출되고 Singleton1은 싱글턴이 된다.
예외로는 리플렉션 API를 사용해 private 생성자를 호출할 수 있지만 생성자를 수정하여 두 번째 객체가 생성되려 할 때 예외를 던지면 이러한 공격에 대한 방어가 가능하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package item03;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class SingleTest {
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        //private이므로 Singleton1 밖에서 생성 불가
        //Singleton1 singleton1 = new Singleton1();
        Singleton1 singleton2 = Singleton1.instance;

        System.out.println(singleton2); //item03.Singleton1@4eec7777 - 주소 잘 가져옴

        //아래와 같은 방식으로 리플렉션 API를 이용하여 private 생성자 호출 가능. But
        Constructor<Singleton1> constructor = Singleton1.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        Singleton1 singleton3 = constructor.newInstance(); //Caused by: java.lang.IllegalStateException: this object should be singleton - 에러발생

        System.out.println(singleton3);
    }
}

public static final인 instance를 호출하는 singleton2는 정상적으로 객체를 잘 가져오지만,
리플렉션 API를 활용하여 private 생성자를 직접 호출 하는 singleton3는 private 생성자에 추가된 리플렉션 방어 로직에 의하여 this object should be singleton에러가 발생한다.

장점

  1. static 팩토리 메소드를 사용하는 방식보다 간단하다.
  2. 해당 클래스가 싱글턴임이 API에 명백히 드러난다.

2. static 팩토리 메소드 방식의 싱글턴

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package item03;

public class Singleton2 {

    private static final Singleton2 instance = new Singleton2();

    private Singleton2(){
    }

    public static Singleton2 getInstance(){
        return instance;
    }

}

장점

  1. public static factory 메소드를 호출하는 클라이언트 코드를 바꾸지 않고도 싱글턴이 아니게 변경할 수 있다.
    • 처음엔 싱글턴을 쓰다가 나중에 호출하는 스레드별로 다른 인스턴스를 넘겨주게 할 수 있다.
  2. static factory를 Generic 싱글턴 팩토리로 만들 수 있다.
  3. static factory method 참조를 Supplier<Singleton2>와 같이 공급자로 사용할 수 있다.
    • public static Singleton2 getInstance() 자체가 Supplier 인터페이스의 구현체처럼 쓰일 수 있는 메소드다
interface Supplier

JAVA 8부터 등장한 개념인 Supplier란 메서드를 ‘get()’ 하나만 가지고 있고 아무타입이나 리턴하는(T) 인터페이스이다.

1
2
3
4
5
6
7
public class supplierExam {
    public static void main(String[] args){
        final Supplier<String> helloSupplier = () -> "Hello";

        System.out.println(helloSupplier.get() + "World!");
    }
}

위는 supplier 예제 소스코드이다. 스트링 타입의 Supplier를 helloSupplier라는 이름으로 생성하여 “Hello” 값을 담는다.
그리고 나중에 “Hello”라는 값을 얻고 싶을 때 helloSupplier.get()메소드를 사용하여 호출 할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package item03;

import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;

public class supplierExam {
    public static void main(String[] args){
        long start = System.currentTimeMillis();
        printIfValidIndex(0, ()-> getVeryExpensiveValue());//3초
        printIfValidIndex(-1, ()-> getVeryExpensiveValue());//0초
        printIfValidIndex(-2, ()-> getVeryExpensiveValue());//0초
        System.out.println("It took "+((System.currentTimeMillis() - start) / 1000) + " seconds."); //9초 -> 3초
    }

    //아래구문이 많은 CPU 연산을 필요로 한다고 가정해보자!
    private static String getVeryExpensiveValue(){
        try{
            TimeUnit.SECONDS.sleep(3);
        }catch (InterruptedException e){
           e.printStackTrace();
        }
        return "Kevin";
    }

    private static void printIfValidIndex(int number, Supplier<String> valueSupplier){
        if(number>=0){
            System.out.println("The Value is"+valueSupplier.get()+".");
        }else{
            System.out.println("Invalid");
        }
    }

}

getVeryExpensiveValue가 굉장히 많은 CPU 연산을 필요로 한다고 가정해보자. main에서 printIfValidIndex 메소드 호출 시 첫 번쨰 number가 0인 경우를 제외하고 -1, -2인 경우는 getVeryExpensiveValue를 수행할 필요가 없다.
왜냐? number가 0 이상일 때에만 valueSupplier 값이 필요하기 때문이다.

getVeryExpensiveValue가 많은 연산을 필요로 하지 않는다면 불필요하게 호출된다해도 큰 차이는 없겠지만, 그 반대의 경우 Supplier를 활용하여 매우 유용하게 쓸 수 있다.

  1. int형의 number, valueSupplier는 빈 값을 주어 printIfValidIndex 호출 (Supplier는 입력 매개변수가 필요 없음)
  2. printIfValidIndex 메소드 내부 로직 수행
    2-1. number가 0인 경우, valueSupplier.get()을 통해 getVeryExpensiveValue 수행됨.
    2-2. number가 0미만인 경우, 출력 후 종료 (getVeryExpensiveValue 수행하지 않음)

3. Enum 방식의 싱글턴

직렬화/역직렬화 할 때 코딩으로 문제를 해결할 필요도 없고, 리플렉션으로 호출되는 문제도 고민할 필요도 없고, 코드도 간결한 방법이 있다.

1
2
3
4
5
6
7
public enum Singleton3 {
    INSTANCE;

    public String getNmae(){
        return "Ara";
    }
}

위 처럼 선언한 뒤 객체를 가져올 때 아래와 같이 가져다 쓰면 된다.

1
String name = Singleton3.INSTANCE.getNmae();

코드는 좀 불편하게 느껴지지만 싱글턴을 구현하는 최선의 방법이다. 하지만 이 방법은 Enum말고 다른 상위 클래스를 상속해야한다면 사용할 수 없다. 그러나 인터페이스는 구현할 수 있다.

참고

This post is written by PRO.

Item 2 - 생성자에 매개변수가 많다면 빌더를 고려하라

Item 4 - 인스턴스화를 막으려거든 private 생성자를 사용하라