Programming/Java

[Youtube][이팩티브 자바] #3 싱글톤을 만드는 여러가지 방법 그중에 최선은?

bisi 2020. 5. 6. 20:32

백기선님의 유투브 강의 내용을 정리하였습니다.

 

백기선님 Github 바로가기

Youtube 바로가기

 

 

강의내용 필기 


주제 3: private 생성자 또는 enum 타입을 사용해서 싱글톤으로 만들 것.

 

오직 한 인스턴스만 만드는 클래스를 싱글톤이라 부른다. 보통 함수 같은 Stateless 객체 또는 본질적으로 유일한 시스템 컴포넌트를 그렇게 만든다. 

 

싱글톤은 패턴의 이름. 애플리케이션을 통틀어서 패턴에 인스턴스가 하나만 사용되는 것.

 

싱글톤을 사용하는 클라이언트 코드를 테스트 하는게 어렵다. 싱글톤이 인터페이스를 구현한게 아니라면 mock으로 교체하는게 어렵기 때문이다. 

싱글톤으로 만드는 두가지 방법이 있는데, 두 방법 모두 생성자를 prirvate으로 만들고 public static 멤버를 사용해서 유일한 인스턴스를 제공한다. 

 

첫번째 방법: final 필드 사용

 

Singleton1 은 한번 불러오면 동일한 인스턴스를 사용한다. 

public class Singleton1 {

    public static final Singleton1 instance = new Singleton1();

    private Singleton1() {
     
    }

}
public class SingleTest {

    public static void main(String[] args) throws NoSuchMethodException {
     	Singleton1 singletone1 = Singleton1.instance;
        Singleton1 singletone2 = Singleton1.instance;
    }
}

 

하지만 리플렉션을 사용해서 private 생성자를 호출하는 방법을 제외하면 ( 그 방법을 막고자 생성자 안에서 카운팅 하거나 flag를 이용해서 예외를 던지게 할 수도 있지만) 생성자는 오직 최초 한번만 호출되고 Singleton1은 싱글톤이 된다.

 

카운팅 예시 

public class Singleton1 {

    public static final Singleton1 instance = new Singleton1();

    int count;

    private Singleton1() {
        count++;
        if (count != 1) {
            throw new IllegalStateException("this object should be singleton");
        }
    }
}

이런 코드를 안하면 리플렉션해서 싱글톤이 아닌게 호출될 수 있다. 

장점은 이런 API 사용이 static 팩토리 메소드를 사용하는 방법에 비해 더 명확하고 더 간단하다.

 

두번째 방법 : Static 팩토리 메소드

public 이 아니고 private으로 선언

public class Singleton2 implements Serializable {

    private static final Singleton2 instance = new Singleton2();

    private Singleton2() { //밖에서 호출 할 수 없다.

    }

    public static Singleton2 getInstance() {
        return instance;
    }
}
public class SingleTest {

    public static void main(String[] args) throws NoSuchMethodException {
     	 Singleton2 singletone1 = Singleton2.getInstance();
         Singleton2 singletone2 = Singleton2.getInstance();
    }
}

 

이 방법의 장점은 API를 변경하지 않고도 싱글톤으로 쓸지 안쓸지 변경할 수 있다. 

처음엔 싱글톤으로 쓰다가 나중에 쓰레드랑 새 인스턴스를 만든다는 등 클라이언트 코드를 고치지 않고도 변경할 수 있다. 

 

만약 아래와 같이 new로 호출해 버리면 더이상 Singleton이 아니게 된다. 

public class Singleton2 implements Serializable {

    private static final Singleton2 instance = new Singleton2();

    private Singleton2() { //밖에서 호출 할 수 없다.

    }

    public static Singleton2 getInstance() {
        return new Singleton2();
    }
}

 

두번째 방법은 클라이언트 코드를 변경하지 않아도 싱글톤 패턴을 바꿀수 있지만, 

첫번째 방법은 클라이언트 코드 수정이 필수로 해야한다. 

 

필요하다면 Generic 싱글톤 팩토리를 만들 수 있다. 

 

static 팩토리 메소드를 Supplier<Singleton1> 에 대한 메소드 레퍼런스로 사용할 수 있다. 

Supplier는 자바 8부터 들어갔다. (람다도!) 메소드 하나만 가지고 있는 인터페이스 이다.  아무타입이나 리턴하는 인터페이스, 이 인터페이스에 걸린다. -> 즉  Supplier의 구현체 처럼 쓰일 수 있다. 

public class SingleTest {

    public static void main(String[] args) throws NoSuchMethodException {
     	 Supplier<Singleton2> s2supplier = Singleton2::getInstance;         
    }
}

따라서 Supplier타입을 필요로하는 곳에 사용가능 하다.

 

세번째 방법 : 직렬화 (Serialization)

위에서 살펴본 두 방법 모두, 직렬화에 사용한다면 역직렬화 할때 같은 타입의 인스턴스가 여러개 생길 수 있다. 그 문제를 해결하려면 모든 인스턴스 필드에 transient를 추가 (직렬화 하지 않겠다는 뜻)하고 readResolve 메소드를 다음과 같이 구현하면 된다. (객체 직렬화 API의 비밀 참고)

 

직렬화 할때 숨겨져 있는 메소드가 있다. 자바의 Serializable

 

네번째 방법 : Enum

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

enum은 메소드도 있고, 필드도 다 가지고 있음. 제일 간단한 방법 

public enum Singleton3 {

    INSTANCE;

    public String getName() {
        return "keesun";
    }
}
public class SingleTest {

    public static void main(String[] args) throws NoSuchMethodException {
     	 String name = Singleton3.INSTANCE.getName();
    }
}

 

코드는 좀 불편하게 느껴지지만 싱글톤을 구현하는 최선의 방법이다.

유일한 단점이라면 Enum 말고 다른 상위 클래스를 상속해야 한다면 사용할 수 없다. (하지만 인터페이스는 구현할 수 있다.) 

 

이제는 현실적인 이야기

 

Bean으로 등록해서 가져다가 쓰는것 모두 싱글톤이다. Spring안에 들어가는 빈은 기본 Scope 모두 싱글톤이다. 

@Repository
public class UserRepository  {
}
@Service
public class UserService implements Serializable {

    @Autowired
    UserRepository userRepository;

}

싱글톤이라는 말은 SpringApplicationContext안에서의 싱글톤이라는 것이지, 

UserService라는 어플리케이션 전반에 걸쳐서 타입 인스턴스가 오직 하나다 그런 이야기는 아니다.

 

여러개 만드는것도 가능

public class SingleTest {

    public static void main(String[] args) throws NoSuchMethodException {
     	 UserService userService1 = new UserService();
    }
}

 

다만 ApplicationContext를 활용해서 만든다면 아래와 같이 설정하면 싱글톤으로 사용가능하다. 

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan(basePackageClasses = Config.class)
public class Config {
}
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class SingleTest {

    public static void main(String[] args) throws NoSuchMethodException {
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(Config.class);
        // 몇번을 꺼내든 같은 오브젝트
        UserService userService1 = applicationContext.getBean(UserService.class);
        UserService userService2 = applicationContext.getBean(UserService.class);

        System.out.println(userService1 == userService2);
    }
}

 

그렇다고 항상 싱글톤이 되는 것은 아니고, @Scope 어노테이션을 통해 싱글톤의 scope을 줄 수 있다. 

@Service
@Scope("prototype")
public class UserService implements Serializable {

    @Autowired
    UserRepository userRepository;

}

만약 이렇게 해놓으면 꺼낼때마다 다른 객체가 된다.