Configuration과 싱글톤
스프링컨테이너가 빈을 잘 생성하는지 직접 확인
@Configuration으로 등록한 AppConfig 코드를 보면
memberService 빈이 만들어질때 new로 memberRepository 객체를 생성한다.
orderService 빈이 만들어질때도 new로 memberRepository 객체를 생성한다.
자바 코드를 보면 그렇다.
그러면 스프링 컨테이너가 싱글톤을 유지시켜 주는지 아닌지 궁금할 것이다.
자바 코드를 보면 MemoryMemberRepository가 2개 생성될 것만 같다..!!
직접 테스트 코드로 확인을 해보면 제대로 알 수 있을 것이다.
확인을 해보니 완전히 같은 것을 알 수 있다.
어째서 이게 가능할까?
스프링 컨테이너가 단순히 내가 AppConfig에서 정의한 메서드를 호출하는 것이라면 이게 가능할 리가 없다.
좀 더 자세히 확인하기 위해 AppConfig에 전부 로그를 남겨보자.
이렇게 로그를 찍고, 나타날 예상 결과는 아래와 같다.
call AppConfig.memberService
call AppConfig.memberRepository
call AppConfig.memberRepository
call AppConfig.orderService
call AppConfig.memberRepository
이제 실행하고 로그를 확인해보면?
Configuration과 바이트 코드 조작의 마법
스프링 컨테이너는 싱글톤 레지스트리다. 따라서 스프링 빈이 싱글톤이 되도록 보장해주어야 한다. 그런데 스프링이 자 바 코드까지 어떻게 하기는 어렵다. 저 자바 코드를 보면 분명 3번 호출되어야 하는 것이 맞다. 그래서 스프링은 클래스의 바이트코드를 조작하는 라이브러리를 사용한다. 모든 비밀은 @Configuration
을 적용한 AppConfig
에 있다.
다음의 코드를 보자.
@Test
void configurationDeep() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
AppConfig bean = ac.getBean(AppConfig.class);
System.out.println("bean = " + bean.getClass());
}
이렇게 테스트 코드를 실행했을 때 결과물은 다음과 같다.
클래스 명에 xxxCGLIB가 붙어있음을 알 수 있다. 이 말은 내가 만든 AppConfig.class가 아니라는 것이다.
스프링이 CGLIB이라는 바이트 코드 조작 라이브러리를 사용해서 AppConfig를 상속받은 다른 클래스를 만들고
그걸 빈으로 등록한다.
아마도 다른 클래스는 다음과 같이 생겼을 것이다.
@Bean public MemberRepository memberRepository() {
if (memoryMemberRepository가 이미 스프링 컨테이너에 등록되어 있으면?) {
return 스프링 컨테이너에서 찾아서 반환; }
else { //스프링 컨테이너에 없으면
기존 로직을 호출해서 MemoryMemberRepository를 생성하고 스프링 컨테이너에 등록
return 반환
Configuration은 CGLIB을 사용하여 싱글톤을 보장한다.
그러면 @Configuration을 달지 않고 @Bean만 달면 어떻게 될까?
그렇게 해보고 실행해보자.
헐... CGLIB를 사용하지 않아서 memberRepository가 3번이나 호출이 되었다.
싱글톤이 깨져버린 것이다..!!!!!!!!!!
꼭 @Configuration를 달아야 한다는 교훈을 얻게 되었다.
스프링 설정 정보 관련된 건 항상 @Configuration
을 사용하라는게 강사님의 말씀이다.
(물론 Appconfig에서 @Autowired로 공통 필드 만들어서 반환하면 싱글톤을 유지할 수 있기는 할 것이다.
하지만 @Configuration은 싱글톤을 자동으로 보장해준다는 점에서 좀 더 안전하다.)
컴포넌트 스캔
지금까지는 @Bean으로 설정 정보에 등록한 빈을 등록했다.
하지만 실무에서는 이렇게 등록해야 할 빈이 수십 수백개가 되고, 누락하는 문제가 발생할 수 있다.
그래서 스프링은 설정 정보가 없어도 자동으로 스프링 빈을 등록하는 컴포넌트 스캔이라는 기능을 제공한다.
그리고 자동으로 의존관계를 주입하는 @Autowired 기능도 제공한다.
@ComponentScan은 @Component가 붙은 클래스를 빈으로 등록한다.
AppConfig, AppAutoConfig 생성, excludeFilters, basePackages
- 자 이제, 이 기능을 활용해볼 것이다.
- 바로 진행해보기 전에 우리는 @Configuration이 붙은 AppConfig를 만들었었다.
- 이제 @ComponentScan으로 AppAutoConfig를 만들 것이다.
- 하지만 그러면 왠지 충돌이 날 것 같지 않은가?
- @ComponentScan은 @Component 붙은 걸 전부 읽는다고 했다.
- 그러면, @Configuration도 읽을까?
- @Configuration을 타고 들어가서(ctrl + click) 내부 로직을 보면 @Component가 달려있다.
- 그래서 충돌이 날 것이다.
- 그래서 다음과 같이 exclude를 해준다.
보통 설정 정보를 컴포넌트 스캔 대상에서 제외하지는 않지만, 기존 예제 코드를 최대한 남기고 유지하기 위해서 이 방법을 선택했다.
@Configuration
@ComponentScan(
excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION,classes = Configuration.class)
)
public class AutoAppConfig {
}
그리고 MemberServiceImpl, RateDiscountPolicy,orderServiceImpl에 @Component 를 붙여보자.
그런데 기존에는 의존관계 주입 로직이 AppConfig에 있었다.
지금은 없다. 그러면 의존관계 주입은 어떻게 하느냐? @Autowired로 한다.
@Autowired 하면 내부적으로 ac.getBean(MemberRepository.class)를 해서 주입해줄 것이다.
orderServiceImpl에도 @Component를 붙이고 생성자에는 @Autowired를 붙여주자.
@Component
public class MemberServiceImpl implements MemberService{
/**
* 회원 가입하고 조회하려면 뭐가 필요하지? 저장소가 필요하다.
*/ private final MemberRepository memberRepository;
@Autowired
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Override
public void join(Member member) {
memberRepository.save(member);
}
@Override
public Member findMember(Long memberId) {
return memberRepository.findById(memberId);
}
//테스트용도
public MemberRepository getMemberRepository() {
return memberRepository;
}
}
그리고 테스트 코드를 돌려보면
public class AutoAppConfigTest {
@Test
void basicScan() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class);
MemberService memberService = ac.getBean(MemberService.class);
}
}
다음과 같은 결과가 나온다.
ClassPathBeanDefinitionScanner가 추가되었음을 확인할 수 있다..!
basePackage를 지정할 수도 있다.
basePackage를 지정하지 않으면 package hello.core;
여기부터 시작해서 다 뒤진다.
basePackageClasses는 뭘까?
basePackageClasses는 지정한 클래스의 패키지를 탐색 시작 위치로 지정한다.
package hello.core;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
@Configuration
@ComponentScan(
basePackages = "hello.core.member",
basePackageClasses = AutoAppConfig.class,
excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION,classes = Configuration.class)
)
public class AutoAppConfig {
}
그렇다면 뭘 사용할까?
권장하는 방법
패키지 위치를 지정하지 않고, 설정 정보 클래스의 위치를 프로젝트 최상단에 두는 것 이다. 최근 스프링 부트도 이 방법을 기본으로 제공한다. 이렇게 하면 내 프로젝트의 위치를 전부 뒤질 것이고, basePackage를 지정하지 않아도 될 것이다.
스프링부트는 기본적으로 ComponentScan으로 다 돌아간다.
@SpringBootApplication에도 ComponentScan이 붙어있다.
스프링 부트의 대표 시작 정보인 @SpringBootApplication 를 프로젝트 시작 위치에 두는 것이 관례이다.
(스프링부트 프로젝트 구성하면 기본적으로 그렇게 되어있음)
단축키
soutm : 메서드 명 바로 sout
'백엔드 > 김영한 스프링 기본편' 카테고리의 다른 글
김영한 스프링 핵심 원리 기본 편 복습 - 4 (1) | 2025.01.09 |
---|---|
김영한 스프링 핵심 원리 기본 편 복습 - 2 (1) | 2025.01.02 |
김영한 스프링 핵심 원리 기본 편 복습 - 1 (0) | 2024.12.30 |