스프링 핵심 원리 - 기본편 복습 2편

voidmelody·2022년 8월 31일
0

싱글톤 컨테이너

싱글톤

웹 애플리케이션은 보통 여러 고객이 동시에 요청을 한다.

public class SingletonTest {

    @Test
    @DisplayName("스프링 없는 순수한 DI 컨테이너")
    void pureContainer(){
            AppConfig appConfig = new AppConfig();
            //1. 조회 : 호출할 때마다 객체 생성
            MemberService memberService1 = appConfig.memberService();

            //2. 조회: 호출할 때마다 객체 생성
            MemberService memberService2 = appConfig.memberService();

            //참조값이 다름
            System.out.println("memberService1 = " + memberService1);
            System.out.println("memberService2 = " + memberService2);

        // memberService1 != memberService2
        Assertions.assertThat(memberService1).isNotSameAs(memberService2);
    }
}

우리가 만들었던 스프링 없는 순수 컨테이너인 AppConfig는 요청을 할 때마다 새로운 객체를 생성한다.
이렇게 되면 메모리 낭비가 심하기에, 해당 객체를 딱 1개만 생성하고 공유하도록 설계하면 된다. 이를 싱글톤 패턴이라 한다.

public class SingletonService {

	// static 영역에 객체를 딱 1개만 생성한다.
    private static final SingletonService instance = new SingletonService();

	// public으로 열어서 객체 인스턴스가 필요하면 이 static 메서드를 통해서만 조회하도록 설정했다.
    public static SingletonService getInstance(){
        return instance;
    }
	// 생성자를 private로 선언해서 외부에서 new 키워드를 사용한 객체 생성을 못하게 막는다.
    private SingletonService(){
    }

    public void logic(){
        System.out.println("싱글톤 객체 로직 호출");
    }
}

private로 new 키워드를 막아두었다.
그렇기에 호출할 때마다, 같은 객체 인스턴스를 getInstance()메서드를 통해서 반환한다.
하지만 싱글톤 패턴의 문제점들이 있다.

  • 싱글톤 패턴을 구현하는 코드 자체가 많이 들어간다.
    -> 생성자 private으로 막아야하고, getInstance()함수 구현해야하는 등
  • 의존관계상 클라이언트가 구체 클래스에 의존한다.
    -> getInstance를 활용해야하다보니 구체 클래스에 의존하게 된다.
    -> 그렇기에 DIP, OCP 위반을 하게 된다.
  • 테스트하기 어렵다.
    -> 처음에 instance를 만들고 시작하다보니 새로운 test를 하기가 쉽지 않다.

하지만 스프링 컨테이너는 싱글톤 패턴의 문제점을 해결하면서, 객체 인스턴스를 싱글톤으로 관리한다.
지금까지 우리가 배운 스프링 빈이 싱글톤으로 관리된다.

싱글톤 컨테이너

스프링 컨테이너는 객체 인스턴스를 싱글톤으로 관리한다.
전에 컨테이너 생성 과정을 생각해보면 컨테이너는 객체를 하나만 생성했다.
그렇다보니, 싱글톤 패턴을 위한 지저분한 코드를 넣지 않아도 되고
private 생성자로부터 자유롭게 싱글톤을 사용할 수 있다.

@Test
@DisplayName("스프링 컨테이너와 싱글톤")
void springContainer(){
	ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
    
    MemberService memberService1 = ac.getBean("memberService", MemberService.class);
    MemberService memberService2 = ac.getBean("memberService", MemberService.class);
    
    // 싱글톤이라 같다.
    assertThat(memberService1).isSameAs(memberService2);

여기서 스프링에서 assertion을 쓸 때 isSameAs()와 isEqualTo()를 쓰는데 차이점을 짚고 넘어가려한다.

isSameAs() vs isEqualTo()

  • isSameAs()
    -> 주소값을 비교한다.(참조)
String a = "apple";
String b = a;
//주소값이 같은지 검증
assertions.assertThat(a).isSameAs(b);
  • isEqualTo()
    -> 대상의 내용 자체를 비교한다.
String a = "apple";
String b = "apple";
// 1. 내용이 같은지 검증 -> 성공!
assertions.assertThat(a).isEqualTo(b);
// 2. 주소값이 같은지 검증 -> 실패
assertions.assertThat(a).isSameAs(b);

아무튼 싱글톤 컨테이너 덕분에 이미 만들어진 객체를 공유해서 효율적으로 재사용할 수 있다.

스프링 빈의 기본 빈 등록 방식은 싱글톤이지만, 요청할 때마다 새로운 객체를 생성해서 반환하는 기능도 제공한다. 하지만 대다수는 싱글톤을 사용한다.

싱글톤 방식의 주의점

싱글톤 패턴이든, 싱글톤 컨테이너든 객체 인스턴스를 하나만 생성해서 공유하는 싱글톤 방식은 여러 클라이언트가 하나의 같은 객체 인스턴스를 공유하기 때문에 싱글톤 객체는 상태를 유지하게(stateful) 설계하면 안된다.
즉, 무상태(stateless)로 설계해야한다.

  • 특정 클라이언트에 의존적인 필드 X
  • 특정 클라이언트가 값을 변경할 수 있는 필드 X
  • 가급적 읽기만 가능하게 설정
  • 필드 대신에 자바에서 공유되지 않는 지역변수, 파라미터, ThreadLocal 등을 사용
// 상태가 유지될 경우 발생하는 문제점 예시
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;
    }
}
public class StatefulServiceTest{

	static class TestConfig{
    	@Bean
        public StatefulService statefulService(){
        	return new StatefulService();
        }
    }
	@Test
    void statefulServiceSingleton(){
    	ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
        StatefulService statefulService1 = ac.getBean("statefulService", statefulService.class);
        StatefulService statefulService2 = ac.getBean("statefulService", statefulService.class);
        
        //Thread A : A 사용자 주문
        statefulService1.order("userA", 10000);
        //Thread B : B 사용자 주문
        statefulService2.order("userB", 20000);
        
        //ThreadA : A 사용자 주문 금액 조회
        int price = statefulService1.getPrice();
        // ThreadA: A 사용자는 10000원이 나올것을 예상했지만 실제론 20000원이 나옴.
        System.out.println("price = " + price);
 }

StatefulService의 price필드가 공유되는 필드인데, 값을 변경하기 때문에 이러한 문제가 발생했다.
공유필드를 조심해야한다. 스프링 빈은 항상 무상태(stateless)로 설계해야한다.

@Configuration
public class AppConfig{
	
    @Bean
    public MemberService membserService(){
    	return new MemberServiceImpl(memberRepository());
    }
    
    @Bean
    public OrderService orderService(){
    	return new OrderServiceImpl(memberRepository(), discountPolicy());
    }
    
    @Bean
    public MemberRepository memberRepository(){
    	return new MemoryMemberRepository();
    }
    ...
}

memberService 코드를 보면 memberRepository()를 호출한다.
또 orderService 코드를 보면 동일하게 memberRepository()를 호출한다.
결과적으로 각각 다른 2개의 MemoryMemberRepository 객체를 생성하면서 싱글톤이 깨지는 것처럼 보인다. 스프링은 이를 어떻게 해결할까?

먼저 테스트를 진행해보자.

@Configuration
public class AppConfig{
	@Bean
    public MemberService memberService(){
    	//1번
        System.out.println("call AppConfig.memberService");
        return new MemberServiceImpl(memberRepository());
    }
    
    @Bean
    public OrderService orderService(){
    	//1번
        System.out.println("call AppConfig.orderService");
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }
    
    @Bean
    public MemberRepository memberRepository(){
    	// 2번? 3번?
        System.out.println("call AppConfig.memberRepository");
        return new MemoryMemberRepository();
    }
    
    @Bean
    public DiscountPolicy discountPolicy(){
    	return new RateDiscountPolicy();
    }
}   

현재 코드를 보면 memberRepository()는 총 몇번 호출되어야할까?
스프링 컨테이너가 각각 @Bean을 호출해서 스프링 빈을 생성하기 때문에
1. @Bean이 붙어있는 memberRepository()를 호출
2. memberService에서 호출
3. OrderService에서 호출
3번 호출될 것 같다.

그런데 실제로는 한번만 호출된다.

@Configuration과 바이트코드 조작

스프링 컨테이너는 싱글톤 컨테이너이기 때문에, 스프링 빈이 싱글톤이 되도록 보장해야한다. 그렇다고 스프링이 자바 코드를 조작하긴 어렵다.
그래서 스프링은 클래스의 바이트코드를 조작하는 라이브러리를 사용한다.
핵심은 @Configuration에 있다.

@Test
void configurationDeep(){
	ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
    
    // AppConfig도 스프링 빈으로 등록.
    AppConfig bean = ac.getBean(AppConfig.class);
    
    System.out.println("bean = " + bean.getClass());
    // 출력 : bean = class.hello.core.AppConfig$$.........

AppConfig를 스프링 빈으로 등록해서 클래스 정보를 출력했다.
순수한 클래스라면 다음과 같이 출력되어야한다.
class.hello.core.AppConfig
하지만 실제론 뒤에 ....무엇이 더 붙는다.
이것은 내가 만든 것이 아니라, 스프링이 바이트코드 조작을 해서 AppConfig클래스를 상속받은 임의의 다른 클래스를 만들고, 그 다른 클래스를 스프링 빈에 등록한 것이다..!!!

@Configuration을 적용하지 않고, @Bean만 적용하면 순수하게 스프링 빈에 등록된다.
그렇기에 3번 호출된다.

정리하자면,
@Bean만 사용해도 스프링 빈으로 등록되지만, 싱글톤을 보장하지 않는다.
그렇기에 싱글톤을 보장하기 위해서는, @Configuration을 사용해야 한다.

컨테이너 스캔

지금까지 우리가 스프링 빈을 등록할 때는 일일이 @Bean을 붙여주었다.
하지만 그 빈의 갯수가 많아지면 우리가 일일이 @Bean을 붙여주기엔 시간소모가 크다.
이걸 해결하기 위해, 스프링은 설정 정보가 없어도 자동으로 스프링 빈을 등록하는 컴포넌트 스캔 기능을 제공한다.
의존관계를 자동으로 주입하는 @Autowired 기능도 제공한다.

@Configuration
@ComponentScan
public class AutoAppConfig{
}

//이제 스캔 대상이 될 클래스들에 @Component를 붙여주면 된다.
@Component
public class MemoryMemberRepository implements MemberRepository{}

@Component
public class RateDiscountPolicy implements DiscountPolicy{}

@Component
public class MemberServiceImpl implements MemberService{
	
    private final MemberRepository memberRepository;
    
    // 의존관계가 명시되어있지 않기 때문에 생성자에 의존관계자동주입
    @Autowired
    public memberServiceImpl(MemberRepository memberRepository){
    	this.memberRepository = memberRepository;
    }
}

@Component
public class OrderServiceImpl implements OrderService{
	
    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;
    
    // 의존관계 자동 주입
    @Autowired
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy){
    	this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }
}
  1. @ComponentScan

    @ComponentScan은 @Component가 붙은 모든 클래스를 빈으로 등록한다.
    이 때 빈의 기본이름은 클래스 이름을 사용하되 맨 앞 글자만 소문자로 바꾼다.
    ex) MemberServiceImpl 클래스 -> 빈 이름 : memberServiceImpl
    만약 빈 이름을 지정하고 싶으면 @Component("지정이름") 식으로 부여하면된다.

  2. @Autowired 의존관계 자동 주입
    생성자에 @Autowired를 지정하면 스프링 컨테이너가 자동으로 빈을 찾아서 주입한다.
    기본 조회는 타입을 기준으로 같은 타입을 찾아서 주입한다.
    마치 getBean(MemberRepository.class)와 동일하다.

탐색 위치와 기본 스캔 대상

모든 자바 클래스를 다 컴포넌트 스캔을 하게 되면 시간이 너무 오래 걸린다.
그렇기에, 꼭 필요한 위치부터 탐색하도록 지정할 수 있다.

@ComponentScan{
	backPackages = "hello.core",
}

만약 지정하지 않으면, @ComponentScan이 붙은 설정 정보 클래스의 패키지가 시작 위치가 된다.
최근에 권장되는 방법은 패키지 위치를 따로 지정하지 않고, 설정 정보 클래스의 위치를 프로젝트 최상단에 두는 것이다.

예를 들어 프로젝트가

  • com.hello
  • com.hello.service
  • com.hello.repository
    로 되어 있다면
    com.hello가 프로젝트 시작 루트이니 여기에 메인 설정 정보를 두고
    @ComponentScan을 붙여준다.
    그렇게 되면 com.hello를 포함한 하위 모두가 컴포넌트 스캔 대상이 된다.

컴포넌트 스캔은 @Component뿐만 아니라 다른 내용들도 대상에 추가 포함된다.

  • @Component : 컴포넌트 스캔
  • @Controller : 스프링 MVC 컨트롤러에서 사용
  • @Service : 스프링 비즈니스 로직(개발자들이 핵심 비즈니스 로직이 여기에 있음을 암시)
  • @Repository : 스프링 데이터 접근 계층으로 인식하고, 데이터 계층의 예외를 스프링 예외로 변환
  • @Configuration : 스프링 설정 정보, 스프링 빈을 싱글톤으로 유지처리

해당 기능들의 소스코드를 보면 다 @Component를 포함하고 있다.

@Component
public @interface Controller{
}

@Component
public @interface Service{
}

@Component
public @interface Configuration{
}

중복 등록과 충돌

  • 자동 빈 등록 vs 자동 빈 등록
    컴포넌트 스캔에 의해서 자동으로 스프링 빈이 등록이 되는데, 이름이 같은 경우 스프링은 오류를 발생한다.

  • 수동 빈 등록 vs 자동 빈 등록

@Component
public class MemoryMemberRepository implements MemberRepository{}

@Configuration
@ComponentScan(
	excludeFilters = @Filter(type = FilterType.ANNOTATION, classes = Configuration.class)
)
public class AutoAppConfig{

	@Bean(name = "memoryMemberRepository")
    public MemberRepository memberRepository(){
    	return new MemoryMemberRepository();
    }
}

이 경우 수동 빈 등록이 우선권을 가진다.
(수동 빈이 자동 빈을 오버라이딩한 경우다.)
하지만 현실은 개발자가 의도적으로 설정해서 하기 보다는 여러 설정들이 꼬여서 오류가 나는 경우가 많다.
그래서 최근 스프링부트에서는 수동 빈 등록과 자동 빈 등록이 충돌나면 오류가 발생하도록 기본 값을 바꾸었다.

의존관계 자동 주입

의존관계는 크게 4가지 방법이 있다.

  • 생성자 주입
    생성자 호출 시점에 딱 1번만 호출되는 것이 보장된다.
    불편, 필수 의존관계에서 사용한다.
@Component
public class OrderServiceImpl implements OrderService{
	
    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;
    
    @Autowired
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy){
    	this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }
}

만약, 생성자가 딱 하나만 있다면 @Autowired를 생략해주어도 된다!

  • 수정자(setter)주입
    선택, 변경 가능성이 있는 의존관계에 사용한다.
@Component
public class OrderServiceImpl implements OrderService{
	
    private MemberRepository memberRepository;
    private DiscountPolicy discountPolicy;
    
    @Autowired
    public void setMemberRepository(MemberRepository memberRepository){
    	this.memberRepository = memberRepository;
    }
    
    @Autowired
    public void setDiscountPolicy(DiscountPolicy discountPolicy){
    	this.discountPolicy = discountPolicy;
    }
}
  • 필드 주입
    필드에 바로 주입하는 방법이다.
    코드가 간결해서 유용해보이지만, 외부에서 변경이 불가능해서 테스트하기가 힘들다는 치명적인 단점이 있다.
    DI 프레임워크가 없다면 아무것도 할 수 없기에, 실제로는 사용하지 않는다.
    애플리케이션의 실제 코드와 관련없는 테스트 코드에서 보통 사용한다.
@Component
public class OrderServiceImpl implements OrderService{
	
    @Autowired
    private MemberRepository memberRepository;
    @Autowired
    private DiscountPolicy discountPolicy;
    
}
  • 일반 메서드 주입
    일반 메서드를 통해 주입한다.
    한 번에 여러 필드를 주입받을 수 있다.
    하지만 일반적으로 잘 사용하지 않는다.
@Component
public class OrderServiceImpl implements OrderService{
	
    private MemberRepository memberRepository;
    private DiscountPolicy discountPolicy;
    
    @Autowired
    public void init(MemberRepository memberRepository, DiscountPolicy discountPolicy){
    	this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }
}

옵션 처리

주입할 스프링 빈이 없어도 동작해야할 때가 있다.
하지만 @Autowired의 required의 기본 옵션이 true이기 때문에 대상이 없으면 오류가 뜬다.

이를 옵션으로 처리하는 방법은 크게 3가지이다.

  • @Autowired(required = false) : 자동 주입할 대상이 없으면 수정자(setter)메서드 자체가 호출이 안됨.
  • org.springframework.lang.@Nuallable : 자동 주입할 대상이 없으면 null이 입력된다.
  • Optional<> : 자동주입할 대상이 없으면 Optional.empty가 입력됨.
//호출 안됨 X
@Autowired(required = false)
public void setNoBean1(Member member){
	System.out.println("setNoBean1 = " + member);
}
// null 호출
@Autowired
public void setNoBean2(@Nullable Member member){
	System.out.println("setNoBean2 = " + member);
}
//Optional.empty 호출
@Autowired
public void setNoBean3(Optional<Member> member){
	System.out.println("setNoBean3 = " + member);
}	

우리는 Member를 스프링 빈으로 등록하지 않은 상태이다.
그런 경우에 @Autowired(required = false)를 해놓으면 호출 자체가 안된다.
하지만 @nullable이나 Optional<>을 활용하면 null값이나 empty가 들어가기 때문에 호출이 가능하다.

결국엔, 생성자 주입을 선택하자.

과거에는 수정자 주입과 필드 주입을 많이 사용했지만 최근에는 많은 DI 프레임워크 대부분이 수정자 주입을 권하고 있다. 왜 그럴까?

불변

우리가 의존관계 설명하면서 연극과 기획자, 배우를 예시로 들었다.
그런데 기획자가 배우를 설정한 다음에, 바뀌는 경우가 잘 있을까?
대부분의 의존관계 주입은 한 번 일어나면 애플리케이션 종료 시점까지 의존관계를 변경할 일이 잘 없다.
우리가 예를 들어 수정자 주입을 사용하면, set함수를 public으로 열어야하기 때문에 누군가가 실수로 사용해서 오류를 만들 수 있기 때문에 좋은 방법은 아니다.
생성자 주입은 객체를 생성할 때, 딱 1번만 호출되므로 그 이후에 호출 될 일이 없어 불변하게 설계할 수 있다.

누락

프레임워크 없이 순수 자바코드로 테스트를 한다 가정해보자.
현재 코드는 수정자 의존관계일 때이다.

public class OrderServiceImpl implements OrderService{
	
    private MemberRepository memberRepository;
    private DiscountPolicy discountPolicy;
    
    @Autowired 
    public void setMemberRepository(MemberRepository memberRepository){
    	this.memberRepository = memberRepository;
    }
    @Autowired
    public void setDiscountPolicy(DiscountPolicy discountPolicy){
    	this.disocuntPolicy = discountPolicy;
    }
@Test
void createOrder(){
	OrderServiceImpl orderService = new OrderServiceImpl();
    orderService.createOrder(1L, "itemA", 10000);
}

이 경우 NullPointException이 발생한다. 왜 그럴까?
현재 memberRepository와 discountPolicy의 의존관계 주입이 다 누락되었기 때문이다.
그러면 생성자 주입을 사용하면 어떨까?

@Component
public class OrderServiceImpl implements OrderService{
	
    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;
    
    @Autowired
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy){
    	this.memberRepository = memberRepository;
    }

우선 생성자 주입을 사용하면 필드에 final을 붙여줄 수 있다.
그렇기에 값을 생성자에서 넣어주지 않으면 컴파일 오류를 통해 미리 방지할 수 있다. 지금 코드를 보면 생성자에서 discountPolicy에 대해 값을 설정하지 않았기에 컴파일 오류가 나는 것이다.
컴파일 오류는 가장 빠르고, 좋은 오류이다.
final 키워드를 붙일 수 있는 방법은 오직 생성자 주입 뿐이다.
우리가 가져야할 태도는
기본으로 생성자 주입을 사용하고, 필수 값이 아닌 경우에는 수정자 주입 방식을 옵션으로 부여하면 된다.
필드 주입의 경우 프레임워크가 없으면 사용이 불가능하므로 사용하지 않는 편이 좋다.

롬복과 최신 트렌드

보통 필드에 경우에 대부분이 다 불변이다보니 final 키워드를 사용하게 된다.
하지만 생성자도 만들어야하고, 주입 받은 값을 대입하는 코드도 만들어줘야한다. 이처럼 말이다.

@Component
public class OrderServiceImpl implements OrderService{
	
    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;
    
    @Autowired
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy){
    	this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }
}

우선, 생성자가 딱 1개만 있다면, @Autowired를 생략할 수 있다.

@Component
public class OrderServiceImpl implements OrderService{
	
    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;
    
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy){
    	this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }
}

이제 롬복 라이브러리를 사용해보자.
@RequiredArgsConstructor 기능을 사용하면 final이 붙은 필드를 모아서 생성자를 자동으로 만들어준다.

@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService{
	
    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;
    
}

@RequiredArgsConstructor가 해당 코드를 그냥 만들어준 것이다.

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

조회 빈이 2개 이상일 때 - 문제 발생

@Autowired는 타입(Type)으로 조회한다.

@Autowired
private DiscountPolicy discountPolicy;

그렇기에 마치 해당 코드처럼 작동한다.

ac.getBean(DisocuntPolicy.class);

그런데 타입으로 조회하면 타입이 같은 빈이 2개 이상일 때 문제가 발생한다.

@Component
public class FixDiscountPolicy implements DisocuntPolicy{}

@Component
public class RateDiscountPolicy implements DiscountPolicy{}

이렇게 한 후에 의동관계 자동 주입을 실행하면 오류가 난다.
이 경우 하위 타입으로 지정해서 해결할 수도 있고, 스프링빈을 직접 수동 등록해서 해결할 수도 있지만, 자동 주입에서 해결하는 방법들이 있다.

@Autowired 필드 명 매칭

@Autowired는 타입 매칭을 시도하고, 이 때 여러 빈이 있다면 필드 이름, 파라미터 이름으로 빈 이름을 추가 매칭한다.
즉, 아까 기존 코드인

@Autowired
private DiscountPolicy discountPolicy

에서 필드명을 원하는 빈 이름으로 변경하는 것이다.

private DiscountPolicy rateDiscountPolicy

필드명이 rateDiscountPolicy이기 때문에, 해당 이름을 가진 빈이 정상주입된다.
즉 @Autowired의 매칭 순서는
1. 타입으로 매칭해본다
2. 타입 매칭의 결과가 2개 이상일 경우에 필드 명, 파라미터 명으로 빈 이름 매칭

@Qualifier 사용

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

@Component
@Qualifer("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy{}

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

주입시에 @Qualifier를 부여주고 등록한 이름을 적어준다.

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

@Qualifier로 주입할 때 @Qualifier("mainDiscountPolicy")를 못찾으면 mainDiscountPolicy라는 이름의 스프링 빈을 추가로 찾는다.
하지만 @Qualifier는 @Qualifier를 찾는 용도로만 사용하는 것이 명확해서 좋다.
직접 빈 수동 등록에도 @Qualifier를 동일하게 사용할 수 있다.

@Bean
@Qualifier("mainDiscountPolicy")
public DiscountPolicy discountPolicy(){
	return new ...
}

@Primary 사용

@Primary는 우선순위를 정하는 방법이다. @Autowired 시에 여러 빈이 매칭되면 @Primary가 우선순위를 가진다.

@Component
@Primary // 얘 먼저
public class RateDiscountPolicy implements DiscountPolicy{}

@Component
public class FixDiscountPolicy implements DiscountPolicy{}
//생성자
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy){
	this.memberRepository = memberRepository;
    this.discountPolicy = discountPolicy;
}

//수정자
@Autowired
public DiscountPolicy setDiscountPolicy(DiscountPolicy discountPolicy){
	this.discountPolicy = discountPolicy;
}

@Primary, @Qualifier 활용

코드에서 자주 사용하는 메인 데이터베이스를 획득하는 스프링 빈이 있고
특별한 기능으로 가끔 사용하는 서브 데이터베이스의 커넥션을 획득하는 스프링 빈이 있다 해보자.
메인 데이터베이스의 경우엔 @Primary를 적용해서 조회하는 곳에서 @Qualifier 지정 없이 편리하게 조회하고,
서브 데이터베이스를 획득할 때는 @Qualifier를 지정해서 명시적으로 획득하는 방식으로 진행하면 깔끔하게 코드를 유지할 수 있다.
물론 메인 데이터베이스의 스프링 빈을 등록할 때 @Qualifier를 지정해주는 것은 상관없다.

우선순위

@Primary는 기본값처럼 동작하고, @Qualifier는 매우 상세하게 동작한다.
스프링은 자동보다는 수동이, 넓은 범위보다는 좁은 범위의 선택권이 우선순위가 높다.(사실 모든 프로그래밍 언어가..)
그렇기에 @Qualifier가 우선순위가 높다.

조회한 빈이 모두 필요할 때, List, Map

의도적으로 해당 타입의 스프링 빈이 모두 필요한 경우가 있다.
예를 들어 할인 서비스를 제공하는데 클라이언트가 할인의 종류(rate,fix)를 선택할 수 있다고 가정해보자.

public class AllBeanTest{
	
    @Test
    void findAllBean(){
    	ApplicationContext ac = new AnnotaitonConfigApplicationContext(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");
   }
   
   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;
        }
        
        public int discount(Member member, int price, String discountCode){
        	DiscountPolicy discountPolicy = policyMap.get(discountCode);
            return discountPolicy.discount(member,price);
        }
 }

자동, 수동의 올바른 실무 운영 기준

편리한 자동 기능을 기본으로 사용하자
그러면 어떠한 경우에 컴포넌트 스캔과 자동 주입을 사용하고,
어떤 경우에 설정 정보를 통해서 수동으로 빈을 등록하고, 의존관계도 수동으로 주입해야할까?

결론은, 스프링이 나오고 시간이 갈수록 자동을 선호하는 추세다
스프링은 @Component뿐만 아니라 @Controller, @Service, @Repository처럼 계층에 맞추어 일반적인 애플리케이션 로직을 자동으로 스캔할 수 있도록 지원하고 있다.
스프링 부트는 컴포넌트 스캔을 기본으로 사용하고, 다양한 스프링 빈들도 조건이 맞으면 자동으로 등록하도록 설계했다.

그러면 수동 빈 등록은 언제 사용해야할까?

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

  • 업무 로직 빈 : 컨트롤러, 핵심 비즈니스 로직인 서비스, 데이터 계층의 로직을 처리하는 리포지토리 등이 모두 업무 로직이다.
    비즈니스 요구사항을 개발할 때 추가 또는 변경된다.

  • 기술 지원 빈 : 기술적인 문제나 공통관심사(AOP)를 처리할 때 주로 사용된다. 데이터베이스 연결이나 공통 로그 처리처럼 업무 로직을 지원하기 위한 하부 기술이나 고통 기술들이다.

업무 로직은 숫자도 매우 많고, 한 번 개발하면 유사한 패턴이 있기에 자동 기능을 적극 사용하는 것이 좋다. 문제가 발생해도 어떤 곳에서 문제가 발생했는지 명확하게 판단하기 쉽다.

기술 지원 로직은 업무 로직과 비교해서 그 수가 매우 적고, 애플리케이션 전반에 걸쳐서 광범위하게 영향을 미친다.
업무 로직은 어디가 문제가 발생했는지 명확하게 잘 드러나지만, 기술 지원 로직은 적용이 잘 되고 있는지 아닌지 조차 알기 어려운 경우가 많다. 그래서 이러한 기술 지원 로직은 가급적 수동 빈 등록을 사용해서 명확히 드러내는 것이 좋다.
애플리케이션에 광범위하게 영향을 미치는 기술 지원 객체는 수동 빈으로 등록해서 설정 정보에 나타나게 하는 것이 유지보수에 좋다.

profile
어제보다 오늘 더 나은 삶을 위해

0개의 댓글