코드네임 JY

[스프링 핵심] 싱글톤 패턴 본문

백엔드 공부

[스프링 핵심] 싱글톤 패턴

영재임재영 2023. 1. 4. 11:52

 

미리보기

✅ 싱글톤 패턴에 대해 알아보자!

✅ 싱글톤 컨테이너에 대해 알아보자!

✅ @Configuration의 비밀에 대해 파헤쳐보자!


🍔 싱글톤 패턴 (Singleton Pattern)

싱글톤 패턴은 "클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는 디자인 패턴" 이다.

public class Singleton {

    // 1. static 설정으로 변수는 메모리에 딱 한 번만 할당 (= 같은 주소값을 사용)
    private static final Singleton INSTANCE = new Singleton();
    
    // 2. 생성자 private 선언으로 외부에서 new를 통한 객체 생성 방지
    private Singleton() {}

    // 3. 객체 인스턴스가 필요할 시, 오직 이 메소드를 통해서만 조회 가능
    public static Singleton getInstance() {
        return INSTANCE;
    }
}

간단하게 코드로 구현해보면, 이런 식으로 작성할 수 있다.

 

핵심은 static 변수가 붙은 객체 인스턴스 생성 부분에서 볼 수 있는데, 변수는 메모리에 딱 한 번만 할당될 수 있다.

또한 생성자 부분을 private 선언으로 지정 해놓음으로써, 외부에서 new를 통한 새로운 객체 생성을 막을 수 있다.

따라서 클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는 것이다!

 

하지만 스프링에서 위 코드를 사용해서 싱글톤을 구현하고 싶다면? 음.. 그건 쉽지 않다.. 🤔🤔

일단 싱글톤 패턴을 구현하는 코드 자체를 써야한다. 내부의 속성을 변경하거나 초기화하기도 어렵다.

또한 위 코드는 구체적인 '클래스'기 때문에, 위 코드를 사용하는 것은 DIP나 OCP에 위반되는 행위이다.

 

🍟 싱글톤 컨테이너 (Singleton Container)

싱글톤을 구현하기 위해, 위 코드 자체를 스프링에서 구현하기에는 어려움이 있다고 했다.

따라서 스프링에서 싱글톤 방식이 구현되기 위해 '싱글톤 컨테이너' 라는 개념이 있다.

 

'싱글톤 컨테이너' 라는 것이 따로 있는 것은 아니고, 스프링 컨테이너는 싱글톤 컨테이너 역할을 한다.

스프링 컨테이너는 따로 싱글턴 패턴을 적용하지 않아도, 객체 인스턴스(= 스프링 빈)를 싱글톤으로 관리한다!

 

여러 클라이언트에게 요청이 들어와도, 동일한 객체 인스턴스(= 스프링 빈)를 반환해준다는 것이다!

따라서 이미 만들어진 하나의 객체를 공유하여 효율적으로 재사용할 수 있는 장점이 있다!

 

🥤 @Configuration 통한 싱글톤 컨테이너 구현

설정 정보 파일에는 @Configuration 어노테이션을 붙여야한다고 이전 포스팅에서 공부했는데, 비밀이 하나 있다.

일단 다음과 같은 클래스 다이어그램을 구현한다고 해보자!

이들의 Config 파일을 코드로 작성하면 다음과 같이 나오게 된다.

@Configuration
public class Config {
    @Bean
    public ReservationService reservationService() {
        // System.out.println("Config.reservationService");
        return new ReservationServiceImpl(ticketReserve(), memberRepository());
    }

    @Bean
    public TicketReserve ticketReserve() {
        // System.out.println("Config.ticketReserve");
        return new TicketReserveImpl();
    }

    @Bean
    public MemberRepository memberRepository() {
        // System.out.println("Config.memberRepository");
        return new JPAMemberRepository();
    }
}

그리고 ApplicationContext를 통해 스프링 컨테이너에 실제로 빈들을 올려보는데, 체크해 볼 것이 한 가지 있다.

ApplicationContext ac = new AnnotationConfigApplicationContext(Config.class);
System.out.println(ac.getBean(Config.class));
hello.core.singleton.ConfigurationSingletonContainerTest$Config$$EnhancerBySpringCGLIB$$953e0baf@704f1591

 

음? 뭐 이렇게 길지? 일단 비교를 위해 Config 파일 내에 빈들도 어떻게 나오는지 출력해보자!

ReservationService reservationService = ac.getBean(ReservationService.class);
TicketReserve ticketReserve = ac.getBean(TicketReserve.class);
MemberRepository memberRepository = ac.getBean(MemberRepository.class);

System.out.println("reservationService = " + reservationService);
System.out.println("ticketReserve = " + ticketReserve);
System.out.println("memberRepository = " + memberRepository);
reservationService = hello.core.singleton.ReservationServiceImpl@58fb7731
ticketReserve = hello.core.singleton.TicketReserveImpl@13e547a9
memberRepository = hello.core.singleton.JPAMemberRepository@3fb6cf60

뭔가 다르긴 다르다. 스프링 빈들은 일반 클래스처럼 출력되는데, 설정 정보 파일인 Config는 그렇지 않다.

 

다시 Config 빈의 결과값에 대해 살펴보면 'EnhancerBySpringCGLIB' 라고 쓰여 있는 부분을 찾아볼 수 있는데,

이를 직관적으로 해석해보면, "스프링에 의해 CGLIB으로 향상" 정도로 볼 수 있다.

 

여기서 'CGLIB'는 'Byte Code Generation Library' 라는 것인데, 스프링이 바이트 코드를 조작하여 프록시를 생성한 것이다.

프록시 생성으로 인해 만들어진 @CGLIB이 붙은 클래스가 바로 싱글톤을 보장해주는 비밀인데, 다음 그림을 보자.

 

Config 빈에 대해서 출력해보니, 일반 Config 클래스가 출력되는 것이 아닌 @CGLIB이 붙은 클래스가 출력되었는데,

프록시 생성으로 인해 위와 같은 관계를 가지고 있게 되었다. 이는 스프링이 바이트 코드를 조작해서 새로운 클래스를 만든 것이다.

 

우리가 만든 Config 파일을 다시 들여다보면,

@Configuration
public class Config {
    @Bean
    public ReservationService reservationService() {
        // System.out.println("Config.reservationService");
        return new ReservationServiceImpl(ticketReserve(), memberRepository());
    }

    @Bean
    public TicketReserve ticketReserve() {
        // System.out.println("Config.ticketReserve");
        return new TicketReserveImpl();
    }

    @Bean
    public MemberRepository memberRepository() {
        // System.out.println("Config.memberRepository");
        return new JPAMemberRepository();
    }
}

reservationService 빈을 가져올 때, 구현체를 반환하는데 ticketReserve 빈과 memberRepository 빈도 같이 생성한다.

그렇다면 ticketReserve 빈을 가져올 때, 다시 return new 해서 또 다른 ticketReserve 빈을 생성하는 것일까?

 

그렇지 않다! 위 코드만 보면 그렇게 동작하는 것으로 보이지만, 실제 동작하는 코드는 @CGLIB가 붙은 코드가 돌아간다.

스프링이 바이트 코드를 조작해서 새로운 클래스를 만든 것이라고 했는데, 이 코드에는 빈이 싱글톤으로 관리되도록 구현해놓았다.

 

뭐 예를 들어서, 만약 기존 빈이 생성되어 있다면, 새로 빈을 생성하는 것이 아니라 기존 빈을 반환해주는 로직이 있을 것이다!

그런 식으로 기존 Config 클래스를 Override 해서 만들어 놓았을 것이라고 예상이 된다.

 

정리하면, CGLIB 프록시 객체가 스프링 빈을 반환할 때 싱글톤이 보장되게 해주는 것이다.

어쨌든! 결론은 스프링 설정 정보 파일을 작성할 때는 항상 @Configuration 사용하자는 것이다!

 

Comments