쌓고 쌓다

@Configuration, 싱글톤 방식의 주의점 본문

프로그래밍/spring

@Configuration, 싱글톤 방식의 주의점

승민아 2023. 7. 12. 20:39

AppConfig

@Configuration
public class AppConfig {

    @Bean
    public MemberService memberService() {
        return new MemberServiceImpl(memberRepository());
    }

    @Bean
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }

    @Bean
    public OrderService orderService() {
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }

    @Bean
    public DiscountPolicy discountPolicy() {
        return new RateDiscountPolicy();
    }
}

memberService 빈을 만들때 "new MemoryMemberRepository()"를 호출한다.

orderService 빈을 만들때 "new MemoryMemberRepository()"를 호출한다.

각 다른 MemoryMemberRepository가 생성되어 싱글톤이 깨지는것 같다.

 

확인해보자.

 

MemberServiceImpl, OrderServiceImpl에 memberRepository의 인스턴스를 확인하기 위해

아래의 메소드를 추가한다.

    public MemberRepository getMemberRepository() {
        return memberRepository;
    }

 

이제 테스트 코드를 작성해서 돌려보자.

public class ConfigurationSingletonTest {

    @Test
    void configurationTest() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);


        // 인터페이스가 아닌 구현체로 꺼내야 구현체에 @Override하지 않은 메소드 사용 가능
        MemberServiceImpl memberService = ac.getBean("memberService", MemberServiceImpl.class);
        OrderServiceImpl orderService = ac.getBean("orderService", OrderServiceImpl.class);

        MemberRepository memberRepository1 = memberService.getMemberRepository();
        MemberRepository memberRepository2 = orderService.getMemberRepository();
        MemberRepository memberRepository = ac.getBean("memberRepository", MemberRepository.class);

        //인스턴스 출력
        System.out.println("memberRepository1 = " + memberRepository1);
        System.out.println("memberRepository2 = " + memberRepository2);
        System.out.println("memberRepository = " + memberRepository);

        //검증
        Assertions.assertThat(memberService.getMemberRepository()).isSameAs(memberRepository);
        Assertions.assertThat(orderService.getMemberRepository()).isSameAs(memberRepository);

    }
}

 

출력 결과

각 Service가 가지는 memoryMemberRepository는 동일한 하나의 인스턴스이다.

 

AppConfig - 스프링 빈 등록시 호출 횟수를 찍어보자

@Configuration
public class AppConfig {

    @Bean
    public MemberService memberService() {
        System.out.println("Call AppConfig.memberService"); // 추가
        return new MemberServiceImpl(memberRepository());
    }

    @Bean
    public MemberRepository memberRepository() {
        System.out.println("Call AppConfig.memberRepository"); // 추가
        return new MemoryMemberRepository();
    }

    @Bean
    public OrderService orderService() {
        System.out.println("Call AppConfig.orderService"); // 추가
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }
}

 

출력 결과

memberRepository는 여러번 호출하지 않고 한번만 호출되어 스프링 빈들을 등록한다.

 

 

싱글톤 방식의 주의점

싱글톤 패턴, 스프링 컨테이너를 사용할때 여러 클라이언트가 하나의 객체를 공유하여 사용하기에

싱글톤 객체는 상대를 유지하는 Stateful하게 설계하면 안된다.

 

StatefulService

public class StatefulService {

    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;
    }
}

order이 들어오면 price 필드는 갱신된다.

이 싱글톤 객체는 상대를 유지한다. price 필드는 상태를 유지한다.

StatefulService를 스프링 빈으로 등록하여 아래의 로직을 보자.

 

로직

class StatefulServiceTest {

    @Test
    void statefulServiceSingleton() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
        StatefulService statefulService1 = ac.getBean("statefulService", StatefulService.class);
        StatefulService statefulService2 = ac.getBean("statefulService", StatefulService.class);
        
        // 두 클라이언트가 10000, 20000원 주문
        statefulService1.order("userA", 10000); // A 클라이언트
        statefulService2.order("userB", 20000); // B 클라이언트

        // A 클라이언트의 주문 가격
        int price = statefulService1.getPrice();
        System.out.println("price = " + price);
        
        Assertions.assertThat(price).isEqualTo(20000);

    }

}

A 클라이언트는 10000원을 주문했지만 getPrice로 price를 가져와보면 20000원이 출력된다.

=> 공유 필드를 조심하여 무상태(stateless)로 설계해야 한다.

 

스프링 빈 등록시 어떻게 중복된 호출을 피하는것일까?

스프링 컨테이너에 AppConfig 또한 빈으로 등록이된다. 이 빈의 getClass를 호출해보면 

내가 만든 순수 클래스가 아닌 것을 확인할 수 있다.

스프링이 CGLIB라는 바이트 조작 라이브러리를 사용해서 AppConfig를 상속 받은 임의의 클래스를 만들고

그 다른 클래스를 스프링 빈으로 등록한 것이다.

AppConfig@CGLIB에는 아마 이미 등록된 빈이라면 찾아서 반환하고 없으면 기존 우리가 작성한 로직을 수행하는

코드를 동적으로 만들 것 이다.

 

@Configuration 을 사용하지 않고, @Bean 만 적용하면?

스프링 빈으로 등록은 되나 CGLIB 기술이 적용되지 않아 싱글톤 패턴이 보장되지 않는다.

-> memberRepository()처럼 의존 관계 주입을 위해 메소드를 직접 호출할때 싱글톤 보장하지 않는다.

각각 다른 MemoryMemberRepository를 가질 것이다.

스프링 설정 정보에는 그냥 항상 @Configuration을 붙이자.

 

 


+ 인터페이스 레퍼런스로 Override 되지 않은 메소드 호출?

MemberService 인터페이스
MemberService의 구현체 MemoryMemberRepository

인터페이스 레퍼런스로 인터페이스의 구현체에 Override하지 않은 메소드는 사용할 수 없다.

Comments