인프런 스프링 핵심 원리-기본편 #5 싱글톤 컨테이너 / 의존관계 자동 주입
스프링 컨테이너에서 싱글톤은 기본적으로 동작한다.
@Test
@DisplayName("스프링 컨테이너와 싱글톤")
void springContainer(){
// 1. 조회 : 호출할 떄마다 객체 생성
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
// 2. 조회 : 호출할 때마다 객체 생성
MemberService memberService1 = ac.getBean("memberService", MemberService.class);
MemberService memberService2 = ac.getBean("memberService", MemberService.class);
// 참조 값이 다른 것을 확인
System.out.println("memberService1 = " + memberService1);
System.out.println("memberService2 = " + memberService2);
// memberService != memberService2
assertThat(memberService1).isNotSameAs(memberService2);
}
따라서 이 코드에서 memberService1, 2는 같은 객체로 생성되므로 assertThat에서 오류가 발생하게 된다.
이렇게 싱글톤 컨테이너로 인해 발생하는 문제가 또 있다.
바로 같은 객체임을 이용해 다음과 같이 간단한 Servive의 Test를 구현해보면,
package hello.core.singletone;
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
void statefulServiceSingleton(){
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
StatefulService statefulService1 = ac.getBean(StatefulService.class);
StatefulService statefulService2 = ac.getBean(StatefulService.class);
// ThreadA : A 사용자가 10000원 주문
statefulService1.order("userA", 10000);
// ThreadB : B 사용자가 20000원 주문
statefulService2.order("userB", 20000);
// ThreadA : A 사용자가 주문 금액 조회
int price = statefulService1.getPrice();
System.out.println("price = " + price); // 20000
Assertions.assertThat(statefulService1.getPrice()).isEqualTo(20000);
}
같은 객체를 공유하고 있기 때문에 price도 다르게 설정됨을 알 수 있다.
@Test
void configurationTest(){
AnnotationConfigApplicationContext 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("memberService -> memberRepository = " + memberRepository1);
System.out.println("orderService -> memberRepository = " + memberRepository2);
System.out.println("memberRepository = " + memberRepository);
Assertions.assertThat(memberService.getMemberRepository()).isSameAs(memberRepository);
Assertions.assertThat(orderService.getMemberRepository()).isSameAs(memberRepository);
}
이 Test의 결과로 출력된 세 가지 객체가 모두 동일한 것을 알 수 있다.
자바 코드의 @Bean이나 XML의 <bean>을 통해서 스프링 빈을 등록함
=> 자동으로 스프링 빈을 등록하는 컴포넌트 스캔이라는 기능을 사용
프로젝트의 시작 부분에 있는 Config에 @ComponentScan을 입력하면 하위 디렉토리에 존재하는 모든 클래스의 @Component가 붙은 클래스를 모두 스프링 컨테이너에서 빈으로 관리하게 된다.
package hello.core.scan.filter;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import java.lang.annotation.Annotation;
public class ComponentFilterAppConfigTest {
@Test
void filterScan(){
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(ComponentFilterAppConfig.class);
BeanA beanA = ac.getBean("beanA", BeanA.class);
Assertions.assertThat(beanA).isNotNull();
ac.getBean("beanB", BeanB.class); // 존재하지 않는다.
org.junit.jupiter.api.Assertions.assertThrows(
NoSuchBeanDefinitionException.class,
() -> ac.getBean("beanB", BeanB.class)
);
}
@Configuration
@ComponentScan(
includeFilters = @ComponentScan.Filter(type= FilterType.ANNOTATION, classes = MyIncludeComponent.class),
excludeFilters = @ComponentScan.Filter(type= FilterType.ANNOTATION, classes = MyExcludeComponent.class)
)
static class ComponentFilterAppConfig {
}
}
ComponentScan의 Filter를 이용해, 특정 조건을 만족하는 클래스만을 스캔하도록 설정한다.
수정자 주입은 Setter이라는 필드의 값을 변경하는 수정자 메서드를 통해서 의존관계를 주입하는 방법이다.
필드 주입
@Configuration 같은 곳에서만 특별한 용도로 사용한다.
일반 메서드 주입
일반 함수를 만들어서 의존관계 주입
롬복을 사용한 생성자 주입 간결화
@RequiredArgsConstructor
어노테이션을 클래스 상단에 붙이면, final로 선언된 모든 멤버 변수에 대한 생성자가 자동 생성되고, 생성자 주입이 이루어지게 된다.
조회 빈이 2개 이상인 경우
FixDiscountPolicy, RateDiscountPolicy 두 클래스에 모두 @Component를 붙이면 생기는 문제점,
조회 빈이 2개가 되어 OrderServiceImpl에서 하위 타입 두 개 중 무엇을 선택할지 에러가 발생한다 .
그래서 필드 이름으로 이를 지정해주면 되는데,
이렇게 rateDiscountPolicy 혹은 fixDiscountPolicy로 이름을 설정해주면
정상적으로 돌아가는 것을 알 수 있다.
@Quilifier 은 추가 구분자를 설정하는 것.
구분자를 설정해 그때 그때 다르게 할인율을 적용할 수 있다.
혹은 Primary 어노테이션을 붙여서, 두 개 이상의 클래스 중 우선권을 정해 의존관계를 주입한다.
Primary보다 Qualifer의 우선순위가 더 높다.
어노테이션 직접 만들기
Qualifer의 안에 있는 어노테이션을 가져와, @Inteface 형식인 MainDiscountPolicy에 붙인다.
Qualifer의 사용 없이 어노테이션을 만들어 컴파일시 타입 체크를 가능하게 할 수 있다.
문자는 컴파일 시간에 체크가 되지 않기 때문이다.
조회와 빈이 모두 필요할 때
클라이언트가 rate, fix 중 선택하게끔 설계하려면?
package hello.core.autowired;
import hello.core.AutoAppConfig;
import hello.core.discount.DiscountPolicy;
import hello.core.member.Grade;
import hello.core.member.Member;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.*;
public class AllBeanTest {
@Test
void findAllBean(){
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class, DiscountService.class);
DiscountService discountService = ac.getBean(DiscountService.class);
Member member = new Member(1L, "userA", Grade.VIP);
int discountPrice = discountService.discount(member, 10000, "fixDiscountPolicy");
assertThat(discountService).isInstanceOf(DiscountService.class);
assertThat(discountPrice).isEqualTo(1000);
int rateDiscountPolicy = discountService.discount(member,10000, "rateDiscountPolicy");
}
static class DiscountService{
private final Map<String, DiscountPolicy> policyMap;
private final List< DiscountPolicy> policies;
public DiscountService(Map<String, DiscountPolicy> policyMap, List<DiscountPolicy> policies) {
this.policyMap = policyMap;
this.policies = policies;
System.out.println("policyMap = " + policyMap);
}
public int discount(Member member, int price, String discountCode) {
DiscountPolicy discountPolicy = policyMap.get(discountCode);
return discountPolicy.discount(member, price);
}
}
}
discount 함수에서 discountCode에 어떤 문자열이 들어가냐에 따라 다르게 할인을 적용할 수 있다.
빈 생명주기 콜백
데이터베이스 커넥션 풀, 네트워크 소켓 등 필요한 연결을 미리 해두고 종료 시점에서 연결을 모두 종료하는 작업을 진행
package hello.core.lifecycle;
public class NetworkClient {
private String url;
public NetworkClient() {
System.out.println("생성자 호출, url = " + url);
connect();
call("초기화 연결 메세지");
}
public void setUrl(String url){
this.url = url;
}
// 서비스 시작
public void connect(){
System.out.println("connect : " + url);
}
public void call(String message){
System.out.println("call : " + url + " message = " + message);
}
// 서비스 종료 시 호출
public void disconnect(){
System.out.println("close : " + url);
}
public void init() {
System.out.println("NetworkClient.afterPropertiesSet");
connect();
call("초기화 연결 메세지 ");
}
public void close() {
System.out.println("NetworkClient.destroy");
disconnect();
}
}
url 이라는 변수를 가지며, init, close 함수를 가지고 있는 NetworkClient 클래스를 설계하였다.
package hello.core.lifecycle;
import org.junit.jupiter.api.Test;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.Lifecycle;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.lang.annotation.Annotation;
public class BeanLifeCycleTest {
@Test
public void lifeCycleTest(){
ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(LifeCycleConfig.class);
NetworkClient client = ac.getBean(NetworkClient.class);
ac.close();
}
@Configuration
static class LifeCycleConfig{
@Bean(initMethod = "init", destroyMethod = "close")
public NetworkClient networkClient(){
NetworkClient networkClient = new NetworkClient();
networkClient.setUrl("http://hello-spring.dev");
return networkClient;
}
}
}
이렇게 init, close 함수에 @PostConstruct, @PreDestroy 어노테이션을 달면
Test시 @Bean의 옵션에 initMethod와 destoryMethod를 따로 지정하지 않아도 된다.