이번시간에는 애너테이션으로 하는 방식에 대하여 공부해보았다.

 

package hello.core.lifecycle;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

public class NetworkClient {

    private String url;

    public NetworkClient(){
        System.out.println("생성자 호출, url = " + url);
    }

    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);
    }

    
    @PostConstruct
    public void init() {
        System.out.println("NetworkClient.init");
        connect();
        call("초기화 연결 메시지");
    }
    
    @PreDestroy
    public void close() {
        System.out.println("NetworkClient.close");
        disconnect();
    }
}

init()에는 @PostContruct 애너테이션을 달아주고

close()에는 @PreDestroy 애너테이션을 달아주었다.

 

package hello.core.lifecycle;

import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

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
        public NetworkClient networkClient(){
            NetworkClient networkClient = new NetworkClient();
            networkClient.setUrl("http://hello-spring.dev");
            return networkClient;
        }
    }
}

설정 정보는 초기의 코드와 같다.

 

 

@PostConstruct, @PreDestroy 애너테이션 특징

- 최신 스프링에서 가장 권장하는 방법이다.

- 애너테이션 하나만 붙이면 되므로 매우 편리하다.

- 패키지를 보면 javax.annotation.PostConstruct이다. 스프링에 종속적인 기능이 아니라는 것이다.

- 컴포넌트 스캔과 잘 어울린다.

- 유일한 단점은 외부 라이브러리에는 적용하지 못한다는 것이다. 왜냐하면 코드를 고쳐야하기 때문이다. 외부 라이브러리를

초기화, 종료해야한다면 @Bean의 기능을 사용해야한다.

 

@PostConstruct, @PreDestroy 애너테이션을 사용하자.

 

이번에는 빈을 등록하는 시점에 초기화, 소멸이라고 지정하는 방법에 대하여 알아보았다.

 

설정 정보에 @Bean(initMethod = '"init" , destroyMethod = "close") 처럼 초기화, 소멸 메서드를 지정할 수 있다.

 

package hello.core.lifecycle;

public class NetworkClient {

    private String url;

    public NetworkClient(){
        System.out.println("생성자 호출, url = " + url);
    }

    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.init");
        connect();
        call("초기화 연결 메시지");
    }

    public void close() {
        System.out.println("NetworkClient.close");
        disconnect();
    }
}

이번에는 init() , close() 메서드를 만들어 놓고

 

package hello.core.lifecycle;

import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

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;
        }
    }
}

@Bean(initMethod = '"init" , destroyMethod = "close")  이렇게 설정해주고 돌려보면 

만족하는 결과가 나왔다.

 

설정 정보 사용 특징은 

- 메서드 이름을 자유롭게 줄 수 있다.

- 스프링 빈이 스프링 코드에 의존하지 않는다.

- 코드가 아니라 설정 정보를 사용하기 때문에 코드를 고칠 수 없는 외부라이브리에도 초기화, 종료 메서드를 적용할 수 있다.

 

종료 메서드 추론

-@Bean의 destroyMethod 속성에는 아주 특별한 기능이 있다.

-라이브러리는 대부분 close, shutdown 이라는 이름의 종료 메서드를 사용한다.

-@Bean의 destroyMethod는 기본값이 (inferred) (추론)으로 등록되어 있다.

-이 추론 기능은 close,shutdown 이라는 이름의 메서드를 자동으로 호출해준다. 이름 그대로 종료 메서드를 추론해서

 호출해준다.

-따라서 직접 스프링 빈으로 등록하면 종료 메서드는 따로 적어주지 않아도 잘 동작한다.

-공백으로 지정하면 추론기능이 동작하지 않는다.

먼저 인터페이스로 초기화랑 소멸전 콜백을 받는 방법을 공부해보았다.

 

package hello.core.lifecycle;

import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;

public class NetworkClient implements InitializingBean, DisposableBean {

    private String url;

    public NetworkClient(){
        System.out.println("생성자 호출, url = " + url);
    }

    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);
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("NetworkClient.afterPropertiesSet");
        connect();
        call("초기화 연결 메시지");
    }

    @Override
    public void destroy() throws Exception {
        System.out.println("NetworkClient.destroy");
        disconnect();
    }
}

이전 코드와 달라진 점은  InitializingBean, DisposableBean 인터페이스들은 implements 하고 있고

 

afterProperisesSet() 는 초기화를 지원한고

destroy() 는 소멸을 지원한다.

 

 

초기화, 소멸 인터페이스 단점

 

이 인터페이스는 스프링 전용 인터페이스이다. 해당 코드가 스프링 전용 인터페이스에 의존한다.

초기화, 소멸 메서드의 이름을 변경할 수 없다

내가 코드를 고칠 수 없는 외부 라이브러리에 적용할 수 없다.

 

이런 단점들에 의해서 거의 사용하지 않는 방법이다.

이번에는 빈 생명주기 콜백에 대하여 공부해보았다.

 

스프링 빈이 생성되거나 죽기 직전에 스프링이 빈 안에있는 메서드를 호출해 줄 수 있는 기능이다.

생성되고나서 초기화할때 호출하고 빈이 사라지기 직전에 안전하게 사라지게 하는 메서드를 호출하는 내용이다.

 

3가지 방식이 있고

각 방식에 대한 특징이 있다.

 

예를 들어 데이터베이스 커넥션 풀이나 네트워크 소켓처럼 애플리케이션 시작 시점에 필요한 연결이 미리 해두고,

애플리케이션 종료 시점에 연결을 모두 종료하는 작업을 진행하려면, 객체의 초기화와 종료 작업이 필요하다.

 

스프링을 통해 이러한 초기화 작업과 종료가 어떻게 진행될까??

 

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);
    }
}

가짜 네트워크 클래스를 만들었다.

 

package hello.core.lifecycle;

import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

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
        public NetworkClient networkClient(){
            NetworkClient networkClient = new NetworkClient();
            networkClient.setUrl("http://hello-spring.dev");
            return networkClient;
        }
    }
}

LifeCycleConfig라는 설정정보를 가지고

스프링 빈을 생성하여 테스트를 돌려보았다.

 

null??? 예상한 값과 달리 나온것을 알 수 있었다.

생성자 부분에 url 정보 없이 connect가 호출되는 것을 확인할 수 있다.

객체를 생성하는 단계에서는 url이 없고, 객체를 생성한 다음에 외부에서 수정자 주입을 통해서  setUrl() 이 호출되어야

url이 존재하게 된다.

 

스프링 빈은 간단하게 다음과 같은 라이프 사이클을 가진다.

객체 생성 -> 의존관계 주입

생성자 주입같은경우에는 예외이다.

 

스프링 빈은 객체를 생성하고, 의존관계 주입이 다 끝난 다음에 필요한 데이터를 사용할 수 있는 준비가 완료된다.

따라서 초기화 작업은 의존관계 주입이 끝나고 난 다음에 호출해야한다.

 

초기화 작업이란 객체를 생성하는 것이 아니라 외부랑 연결하여 처음 일을 시작하는 작업이다.

 

그런데 개발자가 의존관계 주입이 끝난 시점을 어떻게 알 수 있을까?

 

스프링은 의존관계 주입이 완료되면 스프링 빈에게 콜백 메서드를 통하여 초기화 시점을 알려주는 다양한 기능을 제공한다.

또한 스프링은 스프링 컨테이너가 종료되기 직전에 소멸 콜백을 준다. 따라서 안전하게 종료 작업을 할 수 있다.

 

스프링 빈의 이벤트 라이프사이클

스프링 컨테이너 생성 -> 스프링 빈 생성 -> 의존관계 주입 -> 초기화 콜백 -> 사용 -> 소멸 전 콜백 -> 스프링 종료

(싱글톤)

 

스프링은 다양한 방식으로 생명주기 콜백을 지원한다.

 

최대한 생성자에서 하는게 나은거 아닌가?

 

사실은 객체의 생성과 초기화를 분리해야한다. 그 이유는

기본적으로 객체를 생성하는 것은 객체를 생성하는것에 집중해야 한다. 

실제 객체의 초기화 작업을 한다는 것은 동작한다는 것이다. 그래서 객체 생성은 메모리 할당하는 것 까지만

하는 것이 유지보수 관점에서 좋다.

 

스프링은 크게 3가지 방법으로 빈 생명주기 콜백을 지원한다.

- 인터페이스

- 설정 정보에 초기화 메서드, 종료 메서드 지정

- @PostConstruct, @PreDestory 애너테이션 지원

자동과 수동의 올바른 실무 운영 기준에 대한 공부를 해보았다.

 

먼저 편리한 자동 기능을 기본으로 사용하는 것이 좋다.

 

결론부터 말하자면 스프링이 나오고 시간이 갈 수록 자동을 선호하는 추세이다.

스프링은 @Component 뿐만 아니라 @Controller @Service @Repository 처럼 

계층에 맞추어 일반적인 애플리케이션 로직을 자동으로 스캔할 수 있도록 지원한다. 거기에 더해서

최근 스프링 부트는 컴포넌트 스캔을 기본으로 사용하고, 스프링 부트의 다양한 스프링 빈들도 조건이 맞으면

자동으로 등록하도록 설계 하였다.

 

수동으로 등록하게되면 번거롭다.

또 관리할 빈이 많아지면 관리하는 자체가 부담이 된다.

결정적으로 자동 빈 등록을 사용해도  OCP, DIP를 지킬 수 있다.

 


 

수동은 언제?

 

애플리케이션은 크게 업무 로직과 기술 지원 로직으로 나뉜다.

 

업무 로직 빈 : 웹을 지원하는 컨트롤러, 핵심 비즈니스 로직이 있는 서비스, 등등 비즈니스 요구사항을 개발할 때 추가되거나 변경됨

기술 지원 빈 : 기술적인 문제나 공통 관심사를 처리할 때 주로 사용. 업무 로직을 지원하기 위한 하부 기술이나 공통 기술들이다.

 

업무 로직에는 자동 기능을 적극 사용하는 것이 좋다. 문제가 발생해도 어떤 곳에서 문제가 발생하였는지 명확하게 파악하기 쉽기때문.

 

기술 로직은 매우 수가 적지만 애플리케이션 전반에 걸쳐 광범위하게 영향을 미치고 어디서 문제가 발생하였는지 파악하기 어렵다.

그래서 수동 빈 등록을 사용하는 것이 좋다.

 

애플리케이션에 광범위하게 영향을 미치는 기술 지원 객체는 수동 빈으로 등록하여 

설정 정보에 바로 나타나게 하는 것이 유지보수하기 좋다.

 

비즈니스 로직 중에서도 다형성을 적극 활용할때 ?

 

  static class DiscountService{
        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;
            System.out.println("policyMap = " + policyMap);
            System.out.println("polices = " + policies);
        }

        public int discount(Member member, int price, String discountCode){
            DiscountPolicy discountPolicy = policyMap.get(discountCode);
            return discountPolicy.discount(member, price);
        }
    }
}

이 코드를 보는 순간 DiscountPolicy에 뭐가 들어오는지 한눈에 볼 수 없다.

 

이런 경우 수동 빈으로 등록하거나 또는 자동으로하면 특정 패키지에 같이 묶어 두는 것이 좋다. 

 

@Configuration
public class DiscountPolicyConfig{

@Bean
public DiscountPolicy rateDiscountPolicy(){
	return new RateDiscountPolicy();
	}

@Bean
public DiscountPolicy fixDiscountPolicy(){
	return new FixDiscountPolicy():
	}
}

이 설정 정보만 봐도 한눈에 빈의 이름과, 어떤 빈이 주입될지 파악할 수 있다. 그래도 자동 빈 등록을 사용하고 싶다면

파악하기 좋게 DiscountPolicy의 구현 빈들만 따로 모아서 특정 패키지에 모아두자

이번에는 조회한 빈이 모두 필요할때 한번에 조회하는 방법에 대해 공부해보았다.

 

스프링 빈이 다 필요한 경우가 있다.

예를 들어 할인과 관련된 서비스를 제공할 때 클라이언트가 할인의 종류를 선택할 수 있는것이다.

그렇게 되면 스프링을 사용하면 전략 패턴을 매우 간단하게 구현할 수 있다.

 

package hello.core.autowired.allben;

import hello.core.AutoAppConfig;
import hello.core.discount.DiscountPolicy;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import java.util.List;
import java.util.Map;

public class AllBeanTest {

    @Test
    void findAllBean() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class, DiscountService.class);
    }

    static class DiscountService{
        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;
            System.out.println("policyMap = " + policyMap);
            System.out.println("polices = " + policies);
        }
    }
}

테스트 코드 하나를 작성하고 클래스를 작성하였다.

 

policyMap = {fixDiscountPolicy=hello.core.discount.FixDiscountPolicy@55562aa9, rateDiscountPolicy=hello.core.discount.RateDiscountPolicy@655ef322}
polices = [hello.core.discount.FixDiscountPolicy@55562aa9, hello.core.discount.RateDiscountPolicy@655ef322]

이렇게 확인할 수 있었다.

 

package hello.core.autowired.allben;

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.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import java.util.List;
import java.util.Map;

public class AllBeanTest {

    @Test
    void findAllBean() {
        ApplicationContext 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");

        Assertions.assertThat(discountService).isInstanceOf(DiscountService.class);
        Assertions.assertThat(discountPrice).isEqualTo(1000);

        int rateDiscountPrice = discountService.discount(member, 20000,"rateDiscountPolicy");
        Assertions.assertThat(rateDiscountPrice).isEqualTo(2000);

    }

    static class DiscountService{
        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;
            System.out.println("policyMap = " + policyMap);
            System.out.println("polices = " + policies);
        }

        public int discount(Member member, int price, String discountCode){
            DiscountPolicy discountPolicy = policyMap.get(discountCode);
            return discountPolicy.discount(member, price);
        }
    }
}

DisCountService는 Map으로 모든 DiscountPolicy를 주입받는다.

이때 fix..와 rate...모두가 주입된다.

discount() 메서드는 discountCode로 fixDiscountPolicy가 넘어오면 map에서 fixDiscountPolicy 스프링 빈을 찾아서

실행한다. 물론 rateDiscountPolicy가 넘어오면 rateDiscountPolicy 스프링 빈을 찾아서 실행한다.

 

굉장히 편하게 스프링이 빈에 등록된 것들을 불러와서

유연한 전략패턴을 발휘할 수 있게 된다.

 

이번에는 직접 애너테이션을 직접 만들어보았다.

 

@Qualifier("mainDiscountPolicy") 여기에는 문제가 한가지 있다.

문자는 컴파일시 타입 체크가 안된다. 이 문제를 애너테이션을 만들어서 해결할 수 있다.

 

package hello.core.annotation;

import org.springframework.beans.factory.annotation.Qualifier;

import java.lang.annotation.*;

@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Qualifier("mainDiscountPolicy")
public @interface MainDiscountPolicy {
}

기존 @Qualifier에 들어있는 애너테이션들을 가져오고 추가로 @Qualifier를 붙여주면 완성이다.

 

package hello.core.discount;

import hello.core.annotation.MainDiscountPolicy;
import hello.core.member.Grade;
import hello.core.member.Member;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;

@Component
@MainDiscountPolicy
public class RateDiscountPolicy implements DiscountPolicy{

    private int discountPercent = 10;

    @Override
    public int discount(Member member, int price) {
        if(member.getGrade() == Grade.VIP){
            return price * discountPercent / 100;
        } else{
            return 0;
        }
    }
}

그리고 구현클래스에 보기 깔끔하게 붙여주었다.

 

    @Autowired
    public OrderServiceImpl(MemberRepository memberRepository, @MainDiscountPolicy DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }

이렇게 가져다가 쓰는 클래스의 생성자 파라미터에 붙여주면 깔끔하게 쓸 수 있다.

 

애너테이션은 상속이라는 개념이 없다. 이렇게 여러 애너테이션을 모아서 사용하는 기능은 스프링이 제공하는 기능이다.

https://w2as2l.tistory.com/148

 

39. @Autowired 필드 명, @Qualifier, @Primary

이번에는 여러개의 빈이 선택될때 3가지 방법을 알아보았다. 1. @Autowired 필드 명 매칭 2. @Qualifier -> @Qualifier -> 빈 이름 매칭 3. @Primary 사용 @Autowired 필드 명 매칭 @Autowired 필드 명 매칭이란..

w2as2l.tistory.com

 

빈이 두개 이상일때 문제를 해결하는 해결법에 대해서 공부하던 와중

기존에 구현코드를 변경하는 로직이 있으므로 OCP를 지키지 못하는것 아닌가?

그럼 지금까지 OCP를 지키기 위해 했던 방법들은??? 무슨 의미가 있을까??

라는 궁금증이 생겼다.

 

일단은 클라이언트 코드를 고쳐야했기때문에 OCP를 지키지 못하는 것이맞다.

ㅎㄷㄷ....

 

이 부분에대해서 

@Quilifier 나 @Primary 애너테이션을 붙이기 위해 구현클래스를 찾아가서 변경해야 하는점이 자꾸 걸렸다.

 

기존 구현 클래스의 애너테이션을 변경하지 않으면 좋겟지만 이 부분에대해서는 컴포넌트 스캔의 한계라고 한다.

@Bean을 사용하면 확실하게 되지만 불편함이 따라온다. 어느 것을 얻으려면 반드시 다른 것을 희생해야 한다.

트레이드오프 현상이라고 볼 수 있고, 두마리 토끼를 다 잡기란 쉽지가 않다.

이번에는 여러개의 빈이 선택될때 3가지 방법을 알아보았다.

 

1. @Autowired 필드 명 매칭

2. @Qualifier -> @Qualifier -> 빈 이름 매칭

3. @Primary 사용

 


 

@Autowired 필드 명 매칭

 

@Autowired 필드 명 매칭이란 무슨 말이지?

@Autowired는 특이한 기능이 있다. @Autowired는 타입 매칭을 시도하고,

이때 여러 빈이 있으면 필드 이름, 파라미터 이름으로 빈 이름을 추가 매칭한다.

 

기존 코드

@Autowired
private DiscountPolicy discountPolicy

 

필드 명을 빈 이름으로 변경

@Autowired
private DiscountPolicy rateDiscountPolicy

 

2개의 빈 중에 필드 명이나 파라미터 이름을 보고 2개의 빈 중 빈이름이 같은 것이 있다면 찾아온다.

 

@Autowired 매칭 정리를 하자면

1. 타입 매칭

2. 타입 매칭의 결과가 2개 이상일때 필드 명 또는 파라미터 명으로 빈 이름 매칭

 


@Qualifier 사용

 

@Qualifier는 추가 구분자를 붙여주는 방법이다. 주입시 추가적인 방법을 제공하는 것이지 빈 이름을 변경하는 것은 아니다.

 

빈 이름 등록시 @Qualifier를 붙여준다.

@Component
@Qualifier("mainDiscountPolicy")
public class FixDiscountPolicy implements DiscountPolicy {}
@Component
@Qualifier("FixDiscountPolicy")
public class FixDiscountPolicy implements DiscountPolicy{}

 

    @Autowired
    public OrderServiceImpl(MemberRepository memberRepository, @Qualifier("mainDiscountPolicy")DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }

이런식으로 쓸 수 있다.

@Qualifier 로 주입할 때 @Qualifier("mainDiscountPolicy")를 못찾으면 어떻게 될까? 그러면

mainDiscountPolicy라는 이름의 스프링 빈을 추가로 찾는다. 

 

직접 빈 등록시에도 @Qualifier를 사용 가능하다.

 

@Qualifier 정리

1. @Qualifier끼리 매칭

2. 빈 이름 매칭

3. NoSuchBeanDefinitionException 예외 발생

 


@Primary 사용

@Primary는 우선순위를 지정하는 방법이다. @Autowired 시에 여러 빈이 매칭되면 @Primary가 붙은 클래스가 우선권을 가진다.

 

package hello.core.discount;

import hello.core.member.Grade;
import hello.core.member.Member;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;

@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy{

    private int discountPercent = 10;

    @Override
    public int discount(Member member, int price) {
        if(member.getGrade() == Grade.VIP){
            return price * discountPercent / 100;
        } else{
            return 0;
        }
    }
}

 

매우 심플하기때문에 자주 쓰인다고 한다.

 

여기까지 보면 @Primary와 @Qualifier 중 어떤것을 사용하면 좋을지 고민이 될 것인데 

@Qualifier의 단점은 주입 받을 시에 모든 코드에 @Qualifier를 붙여주어야 한다는 점이다.

 

반면에 @Primary 를 사용하면 @Qualifier를 붙일 필요가 없다.

 

우선순위에 있어서는

@Primary 보다는 @Qualifier가 더 높다 

스프링은 자동보다는 수동이 , 넓은 범위의 선택권 보다는 좁은 범위의 선택권이 우선 순위가 높듯이

이 문제 또한 마찬가지로 @Qualifier가 더 높다 

이번에는 조회 빈이 2개 이상인 문제에 대하여 공부하였다.

 

@Autowired는 기본적으로 타입으로 조회를 한다.

 

    @Autowired
	private DiscountPolicy discountPolicy

 

타입으로 조회하기 때문에, 마치 다음 코드와 유사하게 동작한다.

ac.getBean(DiscountPolicy.class)

 

스프링 빈 조회에서 학습했듯이 타입으로 조회하면 선택된 빈이 2개이상일때 문제가 발생한다.

 

기존에  RateDiscountPolicy에 @Component를 붙이고

FixDiscountPolicy는 붙이지 않았다 이제 FixDiscountPolicy에도 붙여주면? 

 

NoUniqueBeanDefinitionException 오류가 발생한다.!

 

하나의 빈을 기대했지만 RateDiscountPolicy,FixDiscountPolicy 2개가 발생했다고 알려준다.

 

이때 하위타입을 지정할 수 도 있지만, 하위 타입으로 지정하는것은 DIP를 위반하고 유연성이 떨어진다.

이름만 다르고 완전히 같은 타입의 스프링 빈이 2개 있을때 해결이 안된다.

스프링 빈을 수동 등록한다면 문제가 해결되지만, 의존 관계 자동 주입에서 해결하는 여러 방법이 있다.

 

 

+ Recent posts