0+ 스프링/0+ 스프링 Core

[스프링 Core] @Configuration를 사용하면 싱글톤을 유지할 수 있는 이유(CGLIB)

힘들면힘을내는쿼카 2023. 6. 2. 18:01
728x90
반응형

[스프링 Core] @Configuration를 사용하면 싱글톤을 유지할 수 있는 이유(CGLIB)

 

미리 보는 결론

  • @Configuration은 싱글톤을 위해서 존재합니다.
  • @Bean만 사용해도 스프링 빈으로 등록이 가능
  • @Configuration을 사용하면 바이트 코드를 조작하는 CGLIB를 사용하여 클래스를 상속받는 프록시 객체를 만들어서 스프링 빈을 싱글톤으로 관리할 수 있게 해준다.
  • 스프링 설정 정보는 반드시 @Configuration을 사용하자!

 

 

@Configuration 예시

스프링 컨테이너는 기본값으로 모든 인스턴스를 싱글톤으로 관리합니다.

그런데 아래와 같은 코드를 보면 좀 이상하지 않나요?

 

ApplicationConfig

@Configuration
public class ApplicationConfig {
    @Bean
    public MemberRepository memberRepository() {
        return new MemberRepository();
    }

    @Bean
    public PencilRepository pencilRepository() {
        return new PencilRepository();
    }

    @Bean
    public MemoService memoService() {
        // memberRepository()는 new MemberRepository()을 반환합니다.
        return new MemoServiceImpl(memberRepository(), pencilRepository());
    }

    @Bean
    public UserService userService() {
        // memberRepository()는 new MemberRepository()을 반환합니다.
        return new UserServiceImpl(memberRepository());
    }
}

 

memoService 빈을 생성하는 코드를 보면 memberRepository()를 호출 합니다.
memberRepository()new MemberRepository()를 반환합니다.

 

userService 빈을 생성하는 코드를 보면 memberRepository()를 호출 합니다.
memberRepository()new MemberRepository()를 반환합니다.

 

각각 다른 2개의 new MemberRepository()를 반환하여 싱글톤이 아닌 것처럼 보입니다…..?

 

이러면 싱글톤이 깨져야 정상 아닌가요?

 

싱글톤 유지 테스트

직접 테스트 해봅시다.

memberRepository를 반환하는 getMemberRepository() 메소드를 작성했습니다.

 

MemoServiceImpl

public class MemoServiceImpl implements MemoService {
    private final MemberRepository memberRepository;
    private final PencilRepository pencilRepository;

    public MemoServiceImpl(MemberRepository memberRepository, PencilRepository pencilRepository) {
        this.memberRepository = memberRepository;
        this.pencilRepository = pencilRepository;
    }

    public MemberRepository getMemberRepository() {
        return memberRepository;
    }

    public PencilRepository getPencilRepository() {
        return pencilRepository;
    }
}

 

UserServiceImpl

public class UserServiceImpl implements UserService {
    private final MemberRepository memberRepository;

    public UserServiceImpl(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    public MemberRepository getMemberRepository() {
        return memberRepository;
    }
}

 

ApplicationConfigTest

public class ApplicationConfigTest {

    @Test
    @DisplayName("싱글톤이 깨지는가?")
    void applicationConfigTest() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(ApplicationConfig.class);
        UserServiceImpl userService = ac.getBean("userService", UserServiceImpl.class);
        MemoServiceImpl memoService = ac.getBean("memoService", MemoServiceImpl.class);
        MemberRepository memberRepository = ac.getBean("memberRepository", MemberRepository.class);

        System.out.println("userService 의 memberRepository="+ userService.getMemberRepository());
        System.out.println("memoService 의 memberRepository="+ memoService.getMemberRepository());
        System.out.println("memberRepository 의 memberRepository="+ memberRepository);

        Assertions.assertEquals(userService.getMemberRepository(), memberRepository);
        Assertions.assertEquals(memoService.getMemberRepository(), memberRepository);
    }
}

 

결과

 

결과를 보면 memberRepository모두 같은 인스턴스가 공유되고 있다는 것을 확인할 수 있습니다.
(싱글톤이 유지 되고 있습니다!!)


ApplicationConfig를 보면 분명히 두번 memberRepository()를 호출하여 다른 인스턴스가 생성되어야할 것 같은데….


어떻게 이러한 결과가 발생할 걸까요? 🤔

 

ApplicationConfig 에서 호출 안되는거 아냐? 🫨

정말로 두 번 호출되는지ApplicationConfig에 로그를 남겨봅시다.

 

ApplicationConfig

@Configuration
public class ApplicationConfig {
    @Bean
    public MemberRepository memberRepository() {
        System.out.println("memberRepository()");
        return new MemberRepository();
    }

    @Bean
    public PencilRepository pencilRepository() {
        return new PencilRepository();
    }

    @Bean
    public MemoService memoService() {
        System.out.println("memoService()");
        // new MemberRepository()
        return new MemoServiceImpl(memberRepository(), pencilRepository());
    }

    @Bean
    public UserService userService() {
        System.out.println("userService()");
        // new MemberRepository()
        return new UserServiceImpl(memberRepository());
    }
}

 

ApplicationConfigTest를 실행하면 다음과 같은 결과가 출력 됩니다.

 

엇...?
memberRepository()가 1번만 호출됩니다…?


예상대로라면 memoService(), userService()를 호출할 때 memberRepository()가 호출되어야 합니다..

 

무슨일이 발생한 걸까요? 🤔

 

@Configuration과 바이트코드 조작 마법

스프링 컨테이너는 싱글톤 레지스트리 입니다.
이말은 스프링 빈이 싱글톤이 유지되도록 보장해줘야 한다는 의미 입니다.


그런데 스프링이 자바 코드까지 컨트롤하기는 어렵습니다.

ApplicationConfig 코드를 보면 분명 memberRepository가 3번 호출 되어야 합니다.
그래서 스프링은 클래스의 바이트 코드를 조작하는 라이브러리를 사용하여 이러한 문제를 해결합니다.

 

참고
바이트 코드(.class)란 자바 클래스 파일(.java)가 자바 컴파일러(javac)를 통해 컴파일된 코드입니다.
이 바이트 코드가 런타임 환경을 통해 실행됩니다.

 

ApplicationConfig를 출력해봅시다.

 

ConfigurationDeep

public class ConfigurationDeep {

    @Test
    void configurationDeep() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(ApplicationConfig.class);
        ApplicationConfig bean = ac.getBean(ApplicationConfig.class);
        System.out.println("bean = " + bean);
    }
}

 

결과

 

AnnotationConfigApplicationContext에서 넘긴 값은 스프링 빈으로 등록 됩니다.
그래서 ApplicationConfig도 스프링 빈으로 등록 됩니다.

 

순수한 클래스라면 seaung.springstudy.config.ApplicationConfig가 출력되어야 합니다.
그런데 출력 결과를 보면 CGLIB가 붙어 있는 것을 확인할 수 있습니다.


이것 저희가 작성한 클래스가 아니고 스프링이 바이트코드 조작을 위해 사용한 라이브러리 입니다.
ApplicationConfig를 상속받은 ApplicationConfig@CGLIB를 생성하고 이를 스프링 빈으로 등록한 것 입니다.

 

ApplicationConfig@CGLIB 가 하는 역할은
@Bean이 붙은 메서드마다 이미 스프링 빈이 존재하면 존재하는 빈을 반환하고,
스프링 빈이 없으면 생성해서 스프링 빈으로 등록하고 반환하는 코드가 동적으로 만들어 스프링 빈을 싱글톤으로 관리하게 됩니다.

 

참고
CGLIB클래스 기반으로 바이트 코드를 조작하여 프록시를 생성하는 방식 입니다.
프록시 객체를 생성할 때 일반적으로 인터페이스를 상속 받거나 클래스를 상속 받거나 하는 2가지 방식을 사용합니다.
CGLIB클래스를 상속받아 바이트 코드를 조작하여 프록시 객체를 생성합니다.

 

@Configuration이 없으면? 🤔

@Configuration을 붙이면 바이트 코드를 조작하는 CGLIB(프록시 객체)를 생성하여 스프링 빈을 싱글톤으로 관리할 수 있게 해줍니다.

 

정말로 그런지 @Configuration을 제거해봅시다.

 

ApplicationConfig
@Configuration을 주석처리

// @Configuration
public class ApplicationConfig {
    @Bean
    public MemberRepository memberRepository() {
        System.out.println("memberRepository()");
        return new MemberRepository();
    }

    @Bean
    public PencilRepository pencilRepository() {
        return new PencilRepository();
    }

    @Bean
    public MemoService memoService() {
        System.out.println("memoService()");
        // new MemberRepository()
        return new MemoServiceImpl(memberRepository(), pencilRepository());
    }

    @Bean
    public UserService userService() {
        System.out.println("userService()");
        // new MemberRepository()
        return new UserServiceImpl(memberRepository());
    }
}

 

ConfigurationDeep

public class ConfigurationDeep {

    @Test
    void configurationDeep() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(ApplicationConfig.class);
        ApplicationConfig bean = ac.getBean(ApplicationConfig.class);
        System.out.println("bean = " + bean);
    }
}

 

결과

 

먼저 @Bean만 사용해도 ApplicationConfig가 스프링 빈으로 등록되는 것을 확인할 수 있습니다.

또한, memberRepository가 3번 호출되는 것을 확인할 수 있고, CGLIB가 생성되지 않은 것을 확인할 수 있습니다!!

 

 

정리

  • @Bean만 사용해도 스프링 빈으로 등록이 가능
  • @Configuration을 사용하면 바이트 코드를 조작하는 CGLIB를 사용하여 클래스를 상속받는 프록시 객체를 만들어서 스프링 빈을 싱글톤으로 관리할 수 있게 해준다.
  • 스프링 설정 정보는 반드시 @Configuration을 사용하자!

 

 

싱글톤 주의사항 🚨

싱글톤을 사용할 때 주의해야하는 사항이 있습니다.
따라서 싱글톤 객체를 사용하는 경우 상태를 유지(stateful)하게 설계하면 안됩니다.


무상태(stateless)로 설계해야 합니다.
싱글톤 패턴은 객체 인스턴스를 하나만 생성해서 공유하는 방식 이기 때문입니다.

 

상태를 유지하게 설계할 경우 어떤 문제가 발생할까요?

 

OrderService
OrderService 인스턴스가 price라는 변수를 소유하고 있습니다.

public class OrderService {

    private int price;

    public void order(String name, int price) {
        System.out.println("name = " + name + ", price = " + price);
        this.price = price; // 상태 저장
    }

    public int getPrice() {
        return price;
    }
}

 

AppConfig
빈 등록

@Configuration
public class AppConfig {

    @Bean
    public OrderService orderService() {
        return new OrderService();
    }
}

 

OrderServiceTest

public class OrderServiceTest {

    @Test
    void orderServiceServiceSingleton() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
          // 빈 조회
        OrderService orderService1 = ac.getBean("orderService", OrderService.class);
        OrderService orderService2 = ac.getBean("orderService", OrderService.class);

        orderService1.order("user1", 1000);
        orderService2.order("user2", 2000);

        int price1 = orderService1.getPrice();
        int price2 = orderService2.getPrice();
        System.out.println("user1의 price = " + price1);
        System.out.println("user2의 price = " + price2);

        assertAll(
                () -> assertEquals(1000, price1),
                () -> assertEquals(2000, price2)
        ); 
    }
}

 

결과

 

uesr1price2000으로 변경된것을 확인할 수 있습니다.
user2price2000으로 변경했기 때문에 발생한 문제 입니다.

 

이처럼 공유 필드는 싱글톤을 사용할 때는 사용해서는 안됩니다.

항상 무상태(stateless)로 설계 해야합니다.

 

참고

 

스프링 핵심 원리 - 기본편 - 인프런 | 강의

스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., - 강의 소개 | 인프런

www.inflearn.com

 

 

 

728x90
반응형