[Spring] 5-3. @Configuration과 싱글톤

송광호·2024년 1월 3일

[Spring]

목록 보기
23/41
post-thumbnail

Spring 시리즈는 혼자 공부하며 기록으로 남기고, 만약 잘못 학습 한 지식이 있다면 공유하며 피드백을 받고자 작성합니다.
스프링에 대해 깊게 공부해보고자 인프런의 김영한 강사님께서 강의를 진행하시는 (스프링 핵심 원리 - 기본편) 강의를 수강하며 정리하는 글입니다.
혹여나 글을 읽으시며 잘못 설명된 부분이 있다면 지적 부탁드리겠습니다.


@Configuration과 싱글톤

  • 싱글톤은 객체인스턴스를 하나로 관리하여주는 기술이다.
  • 근데 AppConfig의 코드를 보면 뭔가 이상하지 않은가?
@Configuration
public class AppConfig {

    //@Bean memberService -> new MemoryMemberRepository()
    //@Bean orderService -> new MemoryMemberRepository(), new RateDiscountPolicy()
    //MemoryMemberRepository 두번 호출되는데... 싱글톤이 깨지는건 아닌가?

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

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

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

    @Bean
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }
}
  • 위 코드의 자바코드 흐름대로 가게되면 MemoryMemberRepository가 총 3번 생성된다.
  • 각각 다른 객체 인스턴스가 생성되면서 싱글톤이 깨지는것처럼 보이는데 스프링 컨테이너는 어떻게 문제를 해결할까?

검증 용도의 코드 추가

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;
    }
}
  • 검증을 위해 MemberRepositoy를 조회할 수 있는 코드를 추가한다.

테스트

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

        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("memberRepository1 = " + memberRepository1);
        System.out.println("memberRepository2 = " + memberRepository2);
        System.out.println("memberRepository = " + memberRepository);

        assertThat(memberRepository1).isSameAs(memberRepository);
        assertThat(memberRepository2).isSameAs(memberRepository);

    }
}

결과

  • 총 3개가 호출되었고, 마지막에 memberReposoty는 모두 같은 인스턴스를 리턴하는것을 볼 수 있다.
  • AppConfig는 순수 자바코드인데 어떻게 하나의 인스턴스만 생성하여 공유할 수 있는걸까?

@Configuration과 바이트코드 조작

  • 스프링 컨테이너는 싱글톤 레지스트리다. 따라서 스프링빈이 싱글톤이 되도록 보장되어야한다.
  • 하지만 자바 코드까지 어떻게 하지는 못한다.
  • 그래서 스프링은 클래스의 바이트코드를 조작하는 라이브러리를 사용한다.
  • 모든 비밀은 @Configuration 애노테이션을 적용한 AppConfig에 있다.

@Configuration이 적용된 AppConfig 클래스명

클래스명 확인 테스트 코드

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

        System.out.println("bean = " + bean.getClass());
    }
}

출력
bean = class hello.core.AppConfig$$SpringCGLIB$$0

  • AnnotationConfigApplicationContext의 파라미터로 넘긴 값도 스프링 빈으로 등록된다.
  • 순수한 클래스라면 AppConfig 까지만 나오는게 맞겠지만 스프링이CGLIB라는 바이트코드 조작 라이브러리를 사용하여 AppConfig를 상속받은 임의의 다른 클래스를 만들어 스프링 빈으로 등록한다.

그림

  • 임의로 만들어진 다른 클래스가 싱글톤이 보장되도록 해준다. 아마 추측컨데 다음과 같은 코드로 내부가 작성되어있지 않을까 한다.(실제로는 엄청 복잡하다)

AppConfig@CGLIB 예상 코드

@Bean
public MemberRepository memberRepository() {
	if (memoryMemberRepository가 이미 스프링 컨테이너에 등록되어 있으면?) { 
    	return 스프링 컨테이너에서 찾아서 반환;
	} else { //스프링 컨테이너에 없으면
    	기존 로직을 호출해서 MemoryMemberRepository를 생성하고 스프링 컨테이너에 등록 
    	return 반환
	}
}
  • @Bean 이 붙은 메서드마다 이미 스프링 빈이 존재하면 존재하는 빈을 반환한다.
  • 빈이 등록되어 있지 않으면 생성해서 빈으로 등록하고 반환하는 코드가 작동한다.

참고 AppConfig@CGLIB는 AppConfig의 자식타입으로, AppConfig 타입으로 조회가 가능하다.

만약 @Configuration을 적용하지 않으면?

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

        System.out.println("bean = " + bean.getClass());
    }
}

결과

call AppConfig.memberService
call AppConfig.memberRepository
call AppConfig.orderService
call AppConfig.memberRepository
call AppConfig.memberRepository
bean = class hello.core.AppConfig
  • memberRepository가 3번 불러지는 걸 볼 수 있다.

또 다른 테스트 코드

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

        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("memberRepository1 = " + memberRepository1);
        System.out.println("memberRepository2 = " + memberRepository2);
        System.out.println("memberRepository = " + memberRepository);

        assertThat(memberRepository1).isSameAs(memberRepository);
        assertThat(memberRepository2).isSameAs(memberRepository);

    }
}
  • 작성된 테스트 코드를 돌려보면 다음과 같이 3개의 memberRepository 가 서로 다른 객체인스턴스인것을 확인할 수 있다.

결과

memberRepository1 = hello.core.member.MemoryMemberRepository@35229f85
memberRepository2 = hello.core.member.MemoryMemberRepository@6d3c5255
memberRepository = hello.core.member.MemoryMemberRepository@b1712f3

다음 학습

  • 컴포넌트 스캔과 의존관계 자동주입을 알아볼 예정이다.

0개의 댓글