인프런/스프링핵심원리(기본)

16 ★★★ (의존관계주입시)조회한 빈이 모두 필요할때, (의존관계주입)자동, 수동의 올바른 실무 운영 기준 , 빈 생명주기 콜백

backend dev 2022. 12. 30.

조회한 빈이 모두 필요할때   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를 빈 등록하는것보다 테스트니까

스프링컨테이너를 생성할때 전달인자로 줘서 빈 등록하는게 편해서 이렇게 한거 같다.

 

결과

Map,List 둘다 잘 들어간것을 확인 가능하다.

가져온 빈중 하나를 선택해서 할인을 적용하는 테스트

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의 저장된 빈 객체를 가져와 원하는대로 사용하는 로직이다.

Map은 <빈이름(String),빈객체> 형식이다.

 

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스캔이 있는것처럼 스프링부트는 컴포넌트스캔을 기본으로 사용한다.

 

자동을 이용할시 해결 가능한 단점들

그러면 수동 빈 등록은 언제 사용하는게 좋을까?

 

업무 로직 빈 : 지금까지 공부하면서 등록했던 오더서비스,메모리멤버리포지토리..등
프로젝트 최상단에 위치하는 구성설정파일(ex.AppConfig)를 딱보면 어떤 기술 지원 빈이 등록되서 사용중이구나를 파악할 수 있어야 한다.

기술지원 로직애들을 컴포넌트 스캔으로 넣으려면 메뉴얼에 넣어놓거나 따로 위치를 지정해줘야한다.(번거롭다) 

 

 

비즈니스 로직중 수동빈 등록을 사용하면 좋은경우?

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을 넣은것은 출력이 되지않을것이다.

 

스프링 빈 라이프 사이클

자동 빈 생성할때 @Component로 인해 생성되고 , @Autowired로 주입되고 / 수동 빈 생성할때는 @Bean으로 생성되고, 파라미터로 인해 주입되고

 

초기화 작업이란? -> 생성하는 작업을 말하는게 아닌 객체안에 필요한값들이 다 연결이 되어있어서 일을 시작할 수있게끔 만드는것, 위의 예제에서는 NetworkClient에다가 url를 설정하고 connect()해주는 일련의 과정을 초기화라고한다.

 

초기화작업은 빈이 생성되고, 의존관계주입까지 다 끝나고 난후에 진행되어야한다.

그래서 스프링은 의존관계 주입이 완료되면 스프링 빈에게 콜백 메서드를 통해서 초기화 시점을 알려준다.

 

 

 

그냥 NetworkClient 생성자에서 파라미터로 다 받아서 초기화까지 해버리면 안되나?

생성자는 객체를 생성하는데 집중해야하고, 초기화는 그 객체를 사용하는것이기 때문에 둘을 분리하는게 좋다.

간단한 작업은 생성자에서 처리할 수 있지만 ,외부연결이라던지 무거운 초기화 작업들은 분리한다.(편한 유지보수를 위해)

 

 

 

 

 

댓글