조회한 빈이 모두 필요할때 List , Map
실무적 예를 들면)
할인 서비스를 제공하는데 클라이언트가 할인의 종류를 선택할 수 있을때를 가정해보자.
스프링을 사용하면 소위 말하는 전략패턴을 매우 간단하게 구현할 수 있다.
빈들 Map,List로 가져오기 테스트
public class AllBeanTest {
@Test
void findAllBean() {
ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class,
DiscountService.class); //전달인자로 넣은것은 빈으로 등록된다.(DiscountService는 스프링컨테이너 생성할때 전달인자로 넣어서 직접 빈 등록해준거다.)
// 전달인자 클래스중 그안에 @Bean 또는 @ComponentScan이 있다면 그것도 진행해서 빈을 추가 등록해준다. 물론 구성설정 클래스는 @Configuration이 필수다.
}
static class DiscountService{ // 기존 오더서비스를 손대면 복잡해지니 테스트용으로 하나 만든다.
private final Map<String, DiscountPolicy> policyMap;
private final List<DiscountPolicy> policies;
//@RequiredArgsConstructor로 대체가능 , 이 테스트에는 출력할거라 직접 생성.
@Autowired
public DiscountService(Map<String, DiscountPolicy> policyMap,
List<DiscountPolicy> policies) {
this.policyMap = policyMap;
this.policies = policies;
//의존관계 주입 잘 됬는지 출력
System.out.println("policyMap = " + policyMap);
System.out.println("policies = " + policies);
}
}
}
ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class,
DiscountService.class);
스프링컨테이너를 생성할때 구성설정 클래스말고도 빈으로 등록할 일반 클래스도 전달할 수 있다.
전달인자들은 다 빈으로 등록되고, 전달인자중 @Bean 또는 @ComponentScan이 포함된 클래스가 있다면 그것들도 실행해서 추가로 빈 등록을 해준다.
AutoAppconfig를 넣은 이유는 @ComponentScan을 진행해서 DiscountPolicy의 구현체들을 스프링빈으로 등록하기 위함이고
DiscountService를 넣은 이유는 DiscountService를 빈으로 등록해야 , 다른 빈을 주입 받을 수 있기 때문이다.
(의존관계 주입은 빈에만 받을 수있다 -> 빈끼리의 의존관계를 설정하는것이기 때문)
기존에 있던 구성설정파일을 수정하거나 , 임시로 새로 하나 만들어서 DiscountService를 빈 등록하는것보다 테스트니까
스프링컨테이너를 생성할때 전달인자로 줘서 빈 등록하는게 편해서 이렇게 한거 같다.
결과
가져온 빈중 하나를 선택해서 할인을 적용하는 테스트
public class AllBeanTest {
@Test
void findAllBean() {
ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class,
DiscountService.class); //전달인자로 넣은것은 빈으로 등록된다.(DiscountService는 스프링컨테이너 생성할때 전달인자로 넣어서 직접 빈 등록해준거다.)
// 전달인자 클래스중 그안에 @Bean 또는 @ComponentScan이 있다면 그것도 진행해서 빈을 추가 등록해준다. 물론 구성설정 클래스는 @Configuration이 필수다.
DiscountService discountService = ac.getBean(DiscountService.class);
Member userA = new Member(1L, "userA", Grade.VIP);
int discountPrice = discountService.discount(userA, 10000, "fixDiscountPolicy");
assertThat(discountPrice).isEqualTo(1000);
}
static class DiscountService{ // 기존 오더서비스를 손대면 복잡해지니 테스트용으로 하나 만든다.
private final Map<String, DiscountPolicy> policyMap;
private final List<DiscountPolicy> policies;
//@RequiredArgsConstructor로 대체가능 , 이 테스트에는 출력할거라 직접 생성.
@Autowired
public DiscountService(Map<String, DiscountPolicy> policyMap,
List<DiscountPolicy> policies) {
this.policyMap = policyMap;
this.policies = policies;
//의존관계 주입 잘 됬는지 출력
System.out.println("policyMap = " + policyMap);
System.out.println("policies = " + policies);
}
int discount(Member member, int price, String discountCode) {
//할인코드를 빈 이름이랑 매칭시킨다.
DiscountPolicy discountPolicy = policyMap.get(discountCode); //가져온 빈값들중 해당 빈이름의 스프링빈을 가져온다.
return discountPolicy.discount(member, price); //가져온 DiscountPolicy의 메소드를 이용해서 할인 금액을 리턴해준다.
}
}
}
DiscountService 안에서 List 또는 Map으로 관련된 모든빈을 받아 저장하고 , 메소드를 생성해서 파라미터로 빈 이름을 받아 , 해당 빈 이름으로 Map 또는 List의 저장된 빈 객체를 가져와 원하는대로 사용하는 로직이다.
private final Map<String, DiscountPolicy> policyMap;
private final List<DiscountPolicy> policies;
@Autowired
public DiscountService(Map<String, DiscountPolicy> policyMap,
List<DiscountPolicy> policies) {
this.policyMap = policyMap;
this.policies = policies;
}
스프링을 이용해서 빈들을 받고,
DiscountPolicy discountPolicy = policyMap.get(discountCode);
다형성을 활용해서 받고, 유연한 전략패턴을 활용한다.
동적으로 빈을 선택해야할때 , Map 또는 List로 받아 사용하는게 좋다.
자동, 수동의 올바른 실무 운영 기준
스프링 실행시키는 어플리케이션
@SpringBootTest
class DemoApplicationTests {
@Test
void contextLoads() {
}
}
에서 @SpringBootTest안에 @Component스캔이 있는것처럼 스프링부트는 컴포넌트스캔을 기본으로 사용한다.
그러면 수동 빈 등록은 언제 사용하는게 좋을까?
기술지원 로직애들을 컴포넌트 스캔으로 넣으려면 메뉴얼에 넣어놓거나 따로 위치를 지정해줘야한다.(번거롭다)
비즈니스 로직중 수동빈 등록을 사용하면 좋은경우?
Map또는 List에 어떤 빈들이 주입되는지는 코드만 보고는 잘모르니까 다른 코드를 더 찾아봐야하는 번거로움이 생긴다.
(DiscountPolicy 에서 ctrl + alt + b 를 해서 구현체가 어떤게 있는지 체크해봐야함.)
이 부분을 별도의 설정정보파일을 만들고 수동으로 등록하면 다음과 같다.
@Configuration
public class DiscountPolicyConfig {
@Bean
public DiscountPolicy rateDiscountPolicy() {
return new RateDiscountPolicy();
}
@Bean
public DiscountPolicy fixDiscountPolicy() {
return new FixDiscountPolicy();
}
}
다형성을 적극 사용하는 부분에서는 이렇게 따로 설정파일을 구성해서, 한눈에 보기 쉽고, 추가,수정하기 쉽게 해두는게 좋다.
자동 등록을 사용해도 되긴하는데 그렇다면 패키지를 따로 만들어서 모아두는게 더 보기 편할것이다.
결론 : 수동을 쓰던, 자동을 쓰던 핵심은 딱 보고 이해가 되야한다.
빈 생명주기 콜백
테스트
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);
}
}
테스트용으로 네트워크 클라이언트 클래스를 만들고 생성자할때 어떤 url과 연결됬는지 출력하게끔 하였다.
public class BeanLifeCycleTest {
@Test
public void lifeCycleTest() {
ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(
LifeCycleConfig.class);
NetworkClient client = ac.getBean(NetworkClient.class);
ac.close(); //스프링컨테이너를 닫는 명령어인데 ConfigurableApplicationContext인터페이스에 있음, 그걸 구현한 AnnotationConfigApplicationContext에도 있음
}
@Configuration
static class LifeCycleConfig {
@Bean
public NetworkClient networkClient() {
NetworkClient networkClient = new NetworkClient();
networkClient.setUrl("http://hell-spring.dev"); //스프링빈으로 등록하기전에 url 설정해준다.
return networkClient;
}
}
}
네트워크클라이언트를 생성하고, setUrl해주고, 스프링 빈을 등록해주었다.
당연히 생성자에서 url를 호출하게 하였으니 , 생성하고 난후 url을 넣은것은 출력이 되지않을것이다.
스프링 빈 라이프 사이클
초기화 작업이란? -> 생성하는 작업을 말하는게 아닌 객체안에 필요한값들이 다 연결이 되어있어서 일을 시작할 수있게끔 만드는것, 위의 예제에서는 NetworkClient에다가 url를 설정하고 connect()해주는 일련의 과정을 초기화라고한다.
초기화작업은 빈이 생성되고, 의존관계주입까지 다 끝나고 난후에 진행되어야한다.
그래서 스프링은 의존관계 주입이 완료되면 스프링 빈에게 콜백 메서드를 통해서 초기화 시점을 알려준다.
그냥 NetworkClient 생성자에서 파라미터로 다 받아서 초기화까지 해버리면 안되나?
생성자는 객체를 생성하는데 집중해야하고, 초기화는 그 객체를 사용하는것이기 때문에 둘을 분리하는게 좋다.
간단한 작업은 생성자에서 처리할 수 있지만 ,외부연결이라던지 무거운 초기화 작업들은 분리한다.(편한 유지보수를 위해)
댓글