BeanDefinition
은 빈 설정 메타정보라고 한다.웹 애플리케이션은 고객 요청이 많다.
우리가 만들었던 스프링 없는 순수한 DI 컨테이너인 AppConfig는 요청을 할 때마다 객체를 새로 생성한다.
public class SingletonService {
// 1. 자바가 실행되며 초기에 static 변수로 딱 한번 instance를 생성한다.
private static final SingletonService instance = new SingletonService();
// 2. 생성한 instance를 가져올 수 있도록 instance를 리턴하는 static 메서드를 만든다.
public static SingletonService getInstance() {
return instance;
}
// 3. 외부에서 생성자에 접근하지 못하도록 private으로 접근 제어한다.
private SingletonService(){}
public void logic(){
System.out.println("싱글톤 객체 로직 호출");
}
}
// ////////////////////////// 외부 클래스에서 테스트 ///////////////////////////////////////
@Test
@DisplayName("싱글톤 패턴을 적용한 객체 사용")
void singletonServiceTest(){
//싱글톤 패턴으로 정의한 클래스의 객체 가져오기
SingletonService singletonService1 = SingletonService.getInstance();
SingletonService singletonService2 = SingletonService.getInstance();
//참조값이 같은 것을 확인
System.out.println("singletonService1 = " + singletonService1);
System.out.println("singletonService2 = " + singletonService2);
// isSameAs: == 비교 (물리적 주소)
// isEqual: equal() 비교 (논리적 주소)
assertThat(singletonService1).isSameAs(singletonService2);
}
** ThreadLocal: JDK 1.2부터 제공된 오래된 클래스, 스레드 단위로 로컬 변수를 사용할 수 있기 때문에 마치 전역변수처럼 여러 메서드에서 활용할 수 있다
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;
}
}
// /////////////////////////////////////////// TEST //////////////////////////////////////////////////
@Test
void statefulServiceSingleton(){
ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
StatefulService statefulService1 = ac.getBean(StatefulService.class);
StatefulService statefulService2 = ac.getBean(StatefulService.class);
// Thread A: A사용자가 10,000원 주문
statefulService1.order("userA", 10000);
// Thread B: B사용자가 20,000원 주문
statefulService2.order("userB", 20000);
// Thread A: 사용자A가 주문 금액 조회
// expexted = 10,000 < - > but 20,000 (자원을 공유해버림)
int price1 = statefulService1.getPrice();
System.out.println("price1 = " + price1);
Assertions.assertThat(statefulService1.getPrice()).isEqualTo(20000);
}
new MemoryMemberRepository()
를 두번 호출한다. @Test
void configurationTest(){
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
// ✨✨ 분명 각각 생성자를 호출하여 MemberRepository를 생성했는데.. 같은 값이 할당된다 ! ! ✨✨
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 -> memberRepository = " + memberRepository1);
System.out.println("orderService -> memberRepository = " + memberRepository2);
System.out.println("memberRepository = " + memberRepository);
assertThat(memberService.getMemberRepository()).isSameAs(memberRepository);
assertThat(orderService.getMemberRepository()).isSameAs(memberRepository);
}
싱글톤을 보장할 수 없게 된다.
컴포넌트 스캔을 사용하려면 @ComponentScan을 설정 정보 클래스에 붙여주면 된다.
기존의 AppConfig와 달리 @Bean으로 등록한 클래스가 하나도 없다.
@ComponentScan은 이름 그대로 @Component
애노테이션이 붙은 클래스를 스캔해서 스프링 빈으로 등록시킨다!
이 때, 생성자 파라미터는 어떻게 처리될까?? 기존 AppConfig에서는 @Bean으로 등록해서 넣어줬었는데..
@ComponentScan에 (basePackages= "") 를 작성하여, 탐색할 패키지의 시작 위치를 지정할 수 있다.
따로 지정하지 않는다면 @ComponentScan이 붙은 설정 정보 클래스의 패키지가 시작 위치가 된다.
권장 방식은 패키지 위치를 지정하지 않고, 설정 정보 클래스의 위치를 프로젝트 최 상단에 두는 것. (스프링 부트도 그렇게 하고 있음)
@SpringBootApplication
를 프로젝트 시작 위치에 두는 것이 관례이다. (@SpringBootApplication 안에 @ComponentScan이 이미 들어있다.)컴포넌트 스캔 기본 대상
// 어노테이션의 타입은 @interface
@Target(ElementType.TYPE) // TYPE -> class 레벨에 붙음
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyIncludeComponent {
}
// 만들어준 어노테이션을 붙인 클래스
@MyIncludeComponent
public class BeanA {
}
// //////////////////////////////////////////////Test 파일/////////////////////////////////////////////////////////////////
@Test
void filterScan(){
ApplicationContext ac = new AnnotationConfigApplicationContext(ComponentFilterAppConfig.class);
BeanA beanA = ac.getBean("beanA", BeanA.class);
assertThat(beanA).isNotNull();
assertThrows(
NoSuchBeanDefinitionException.class,
() -> ac.getBean("beanB", BeanB.class));
}
@Configuration
@ComponentScan(
// includeFilters, excludeFilters 사용법.
includeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = MyIncludeComponent.class),
excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = MyExcludeComponent.class)
)
static class ComponentFilterAppConfig{
}
자동 빈 등록 시 메서드명이 중복되는 일만 피해주면 된다.
수동 빈 등록과 자동 빈 등록 과정에서 충돌되면 어떻게 될까?
단 스프링 부트에서는 수동 빈 등록과 자동 빈 등록이 충돌될 경우 오류를 내기로 방식을 바꿨다.
spring.main.allow-bean-definition-overriding=true
방식
생성자 주입
불변, 필수
의존 관계에 사용한다. (주로)수정자 주입 (setter 주입)
필드 주입
일반 메서드 주입
❗❗ 의존 관계 자동 주입은 스프링 컨테이너가 관리하는 스프링 빈이어야 동작한다!
✨✨✨ 결론: 생성자를 통한 의존관계 주입
이 가장 좋은 방식임을 알 수 있다. (실제로 스프링 진영에서도 생성자 주입 방식을 권장한다.)