본문 바로가기
Develop/Spring (이론)

@Configuration과 싱글톤

by 보보트레인 2023. 8. 17.

Spring 코드를 보다보면 이상한 점이 있다.

내가 작성한 AppConfig.java 코드를 보자.

 

package hello.core;

import hello.core.discount.DiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
import hello.core.member.MemberRepository;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemoryMemberRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AppConfig {

    @Bean
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }
    @Bean
    public MemberService memberService() {
        return new MemberServiceImpl(memberRepository());
    }
    @Bean
    public DiscountPolicy discountPolicy() {
       //여기서 간단하게 정률 할인으로 할것이냐, 고정 할인으로 할것이냐 선수교체 간단히 할 수 있음 -> 객체화 good!
       //return new FixDiscountPolicy();
       return new RateDiscountPolicy();
    }
    @Bean
    public OrderService orderService() {
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }
}

 

 

1. memberService를 만드는 Bean 코드를 보자.

▶ memberRepository()를 실행하면 → new MemoryMemberRepository()를 실행한다.

 

2. orderService를 만드는 Bean 코드를 보자.

▶ orderService()를 실행하면 → new OrderServiceImpl()를 실행 → memberRepository()를 실행

→  new MemoryMemberRepository()를 실행한다.

 

결과적으로 각각 다른 2개의 MemoryMemberRepository가 생성되면서 싱글톤이 깨지는 것 처럼 보인다.

스프링 컨테이너는 이 문제를 어떻게 해결할지 실험 코드를 짜보자.

 


<실험 코드 예제>

1. 테스트를 위해 MemberRepository를 조회할 수 있는 기능을 추가한다.

public class MemberServiceImpl implements MemberService {

	private final MemberRepository memberRepository;
  
	//테스트 용도
	public MemberRepository getMemberRepository() {
		return memberRepository;
	}
}


public class OrderServiceImpl implements OrderService {
    private final MemberRepository memberRepository;
    
    //테스트 용도
    public MemberRepository getMemberRepository() {
    	return memberRepository;
    }
}

 

 

2. 테스트코드에 다음과 같은 코드를 추가한다.

package hello.core.singleton;

import hello.core.AppConfig;
import hello.core.member.MemberRepository;
import hello.core.member.MemberServiceImpl;
import hello.core.order.OrderServiceImpl;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class ConfigurationSingletonTest {

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

        //Bean객체 구체방식으로 끌어오기
        MemberServiceImpl memberService = ac.getBean("memberService", MemberServiceImpl.class);
        OrderServiceImpl orderService = ac.getBean("orderService", OrderServiceImpl.class);
        MemberRepository memberRepository = ac.getBean("memberRepository", MemberRepository.class);

        //객체에 담기
        MemberRepository memberRepository1 = memberService.getMemberRepository();
        MemberRepository memberRepository2 = orderService.getMemberRepository();

        //이게 같은 것인지 다른 것인지 조회해보자 -> 세개 다 같은 참조값임을 확인할 수 있음
        System.out.println("memberService -> memberRepository1 = " + memberRepository1);
        System.out.println("orderService -> memberRepository2 = " + memberRepository2);
        System.out.println("memberRepository = " + memberRepository);

        //로직이 여러 객체를 통해 실행되더라도 같은인스턴스를 끌어옴을 알 수 있다. -> 싱글톤 방식 준수

        Assertions.assertThat(memberService.getMemberRepository()).isSameAs(memberRepository);
        Assertions.assertThat(orderService.getMemberRepository()).isSameAs(memberRepository);



    }
}

 

<실행결과>

다음과 같이 같은 참조값을 가리키는 것을 확인할 수 있다. → 스프링 컨테이너는 싱글톤 레지스트리다.

왜 이것이 가능할까?


 

@Configuration과 바이트코드 조작

어노테이션 : Configuration

스프링은 클래스의 바이트코드를 조작하는 라이브러리를 사용하기 때문이다.

package hello.core.singleton;

import hello.core.AppConfig;
import hello.core.member.MemberRepository;
import hello.core.member.MemberServiceImpl;
import hello.core.order.OrderServiceImpl;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class ConfigurationSingletonTest {

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

        //AppConfig도 springBean으로 등록이 되는 상태
        AppConfig bean = ac.getBean(AppConfig.class);

        //AppConfig안에 있는  클래스 정보가 출력됨.
        System.out.println("bean = " + bean.getClass());
    }
}

다음의 테스트 코드를 작성했을 때, 

 

<나의 예상>

class hello.core.AppConfig 이 출력

 

<결과>

<Why?>

클래스 명에 xxxCGLIB가 붙는다. 

이것은 내가 만든 클래스가 아니라 스프링이 CGLIB라는 바이트코드 조작 라이브러리를 사용하여 

AppConfig 클래스를 상속받은 임의의 다른 클래스를 만들고, 그 다른 클래스를 스프링 빈으로 등록한 것이다.

 

참조 구조도

따라서 한개의 클래스를 공유하기 때문에 싱글톤 방식이 가능한 것이다.

 

+@ 김영한쌤의 CGLIB예상코드 ( 실제로는 매우 복잡하게 구현되어 있을 것)

@Bean
public MemberRepository memberRepository() {
	if (memoryMemberRepository가 이미 스프링 컨테이너에 등록되어 있으면?) {
		return 스프링 컨테이너에서 찾아서 반환;
	} else { //스프링 컨테이너에 없으면
		기존 로직을 호출해서 MemoryMemberRepository를 생성하고 스프링 컨테이너에 등록
		return 반환
	}
}

@Configuration을 적용하지 않고, @Bean 만 적용하면 어떻게 될까?

`@Configuration` 을 붙이면 바이트코드를 조작하는 CGLIB 기술을 사용해서 싱글톤을 보장하지만, 

만약 @Bean만 적용하면 어떻게 될까?

 

 

 

<나의 예측>

 

→ @Bean만 사용해도 스프링 빈으로 등록되지만 싱글톤을 보장하지는 않는다.

왜냐하면 memberRepository()처럼 의존관계 주입이 필요해서 

(예시)

메서드를 직접 호출할 때 싱글톤을 보장하지 않는다.

 

<예제 코드>

//@Configuration을 삭제하여 Tdd를 실행해보자.

//@Configuration
public class AppConfig {

    @Bean
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }
    @Bean
    public MemberService memberService() {
        return new MemberServiceImpl(memberRepository());
    }
    @Bean
    public DiscountPolicy discountPolicy() {
       //여기서 간단하게 정률 할인으로 할것이냐, 고정 할인으로 할것이냐 선수교체 간단히 할 수 있음 -> 객체화 good!
       //return new FixDiscountPolicy();
       return new RateDiscountPolicy();
    }

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



}

<결과>

 

다음과 같이 참조값이 서로 다른 두개의 객체가 생성됨을 확인할 수 있다 → 싱글톤 x

당연히 assertionThat 테스트 유효성 검사도 통과하지 못해 오류가 발생한다. 

 

 


결론

스프링 설정정보는 항상 @Configuration을 통해 할 필요성이 있다. 

반응형