Framework/Spring

[TIL] 같은 타입의 Bean이 2개라면 발생하는 문제 해결 (feat. @Primary, @Qualifier)

쉬지마 이굥진 2024. 8. 6. 01:39

오늘은, Spring 환경에서 같은 타입으로 등록된 Bean이 2개 이상일 때 발생할 수 있는 문제를 알아보고 어떻게 해결할 수 있을지까지 학습해보도록 하겠다 😀

 

환경

food 라는 패키지 아래에 클래스 세 개가 있다. Food라는 인터페이스에, 이 인터페이스를 구현하는 클래스 Chicken Pizza가 있는 구조이다.

 

1. Food 인터페이스

package com.study.springauth.food;

public interface Food {
    void eat();
}

 

2. Food를 구현하는 Chicken과 Pizza 클래스

여기선 @Component 애너테이션을 사용해서 Bean으로 등록하고 있다.

package com.study.springauth.food;
import org.springframework.stereotype.Component;

@Component
public class Chicken implements Food {
    @Override
    public void eat() {
        System.out.println("치킨을 먹습니다.");
    }
}
package com.study.springauth.food;
import org.springframework.stereotype.Component;

@Component
public class Pizza implements Food {
    @Override
    public void eat() {
        System.out.println("피자를 먹습니다.");
    }
}

 

👉 즉, 다시 말하면 Food 타입의 Bean 객체 Chicken, Pizza를 등록한 것!

👉 = 하나의 타입으로 두 개의 구현체를 만든 다음 Bean으로 '등록'은 할 수 있다.

 

테스트

@SpringBootTest
public class BeanTest {

    @Autowired
    Food food;
    
}

이제 이 같은 타입의 빈들이 어떻게 동작하는지 테스트를 위해 테스트코드를 만들어줬다. 하지만 여기서 에러 발생!

 

문제 발생?!

위의 테스트 코드에서, Food food; 필드에 @Autowired를 통 Bean 객체를 주입하려고 했다. 하지만 주입을 할 수 없다는 에러 메세지가 나타난 것을 볼 수 있다. (Could not autowire. There is more than one bean of 'Food' type)

 

에러 메세지를 해석해보면 'Food 타입의 빈 객체가 하나 이상 있어서 주입이 안돼용' 이라고 알려주고 있다 😉

 

👉 즉, food 필드에 Bean을 주입해줘야 하는데 같은 타입의 Bean 객체가 하나 이상이라 (chicken, pizza / 에러 메세지에서도 알려주고 있음) 인텔리제이가 어떤 Bean을 등록해야 할지 몰라서 오류가 발생한 것이다.

 

어떻게 해결할 수 있을까?

요기서 이 글의 제목에 명시해줬던 @Primary, @Qualifier 애너테이션이 나온다! 저 두 가지 애너테이션을 포함해 이 상황을 해결할 수 있는 방법들을 알아보자.

 

1. 등록된 Bean의 '이름'을 명시해주기

▪️참고) 등록된 Bean의 이름
Chicken Class를 Bean으로 등록 → chicken으로 등록
Pizza Class를 Bean으로 등록 → pizza로 등록
@SpringBootTest
public class BeanTest {

    @Autowired
    Food pizza;
    
    @Autowired
    Food chicken;
    
}

 

이렇게 등록된 Bean 이름 pizza, chicken을 정확하게 명시해주면 이 문제를 해결할 수 있다.

 

👉 여기서 우리가 알 수 있는 점은 @Autowired가 기본적으로는 Bean Type(Food)으로 DI를 지원하며, 연결이 되지않을 경우 Bean Name(pizza, chicken)으로 찾는다는 것 까지 알고 가면 금상첨화!

 

[전체 테스트코드/결과]

@SpringBootTest
public class BeanTest {

    @Autowired
    Food pizza;
    
    @Autowired
    Food chicken;
    
    @Test
    @DisplayName("테스트")
    void test1() {
    	pizza.eat();		// 피자를 먹습니다.
        chicken.eat();		// 치킨을 먹습니다.
    }		// => 정확하게 해당하는 구현체를 주입해준다는 걸 확인할 수 있다.
    
}

 

 

2. @Primary 사용하기

Chicken 클래스에 @Primary를 추가해준다.

 

@Component
@Primary  // 기존 Chicken 클래스에 추가
public class Chicken implements Food {
    @Override
    public void eat() {
        System.out.println("치킨을 먹습니다.");
    }
}

 

추가해 준 후, 아까 에러가 났었던 테스트 코드로 가보니 빨간 줄이 사라진 것을 볼 수 있었다.

@SpringBootTest
public class BeanTest {
    @Autowired
    Food food;	// 에러 없이 정상 작동
}

 

👉 즉! @Primary가 추가되면 같은 타입의 Bean이 여러 개 있더라도 @Primary가 설정된 Bean 객체를 '우선으로' 주입해준다는 걸 알 수 있었다.

 

[전체 테스트코드/결과]

@SpringBootTest
public class BeanTest {
    @Autowired
    Food food;
    
    @Test
    @DisplayName("테스트")
    void test1() {
    	food.eat();	 // 치킨을 먹습니다.
    } // => Food를 주입해줬는데 "치킨을 먹습니다."가 출력됨 -> @Primary 잘 작동됨을 알 수 있음
}

 

 

3. @Qualifier 사용하기

이번에는 Pizza 클래스에 @Qualifier("pizza")를 추가해주자. ("" 사이는 별칭으로 원하는 값으로 주면 된다)

@Component
@Qualifier("pizza")  // 기존 Pizza 클래스에 추가
public class Pizza implements Food {
    @Override
    public void eat() {
        System.out.println("피자를 먹습니다.");
    }
}

 

추가해준 후, 주입하고자 하는 필드에도 @Qualifier("pizza")를 추가해주면 해당 Bean 객체가 주입된다.

@SpringBootTest
public class BeanTest {

    @Autowired
    @Qualifier("pizza") // 주입하고자 하는 필드에도 @Qualifier("pizza") 추가
    Food food;
}

 

🎇 여기서 주의할 점

위에서 적용한 단계들에 의해서 현재 Chicken 클래스에는 @Primary 애너테이션이 적용 되어 있는 상태이다. 즉, Food 타입의 Bean들엔 Qualifier와 Primary가 동시에 적용되어있는 상태라는 것이다.

 

👉 동시에 적용되어있다면 Qualifier의 우선순위가 더 높다!

👉 하지만, 위 코드에서 봤다시피 Qualifier는 적용하기 위해서 주입 받고자하는 곳에 해당 Qualifier를 반드시 추가해주어야 한다.

 

테스트코드와 그 결과를 확인해보면서 눈으로 이 애너테이션들의 상관관계를 봐보자.

[전체 테스트코드/결과]

@SpringBootTest
public class BeanTest {

    @Autowired
    @Qualifier("pizza")
    Food food;

    @Test
    @DisplayName("Primary 와 Qualifier 우선순위 확인")
    void test1() { 
    
        food.eat(); // 피자를 먹습니다.
        
        // => 현재 Chicken 은 Primary 가 적용된 상태, Pizza는 Qualifier가 추가된 상태임
        // '피자를 먹습니다' 결과값을 보면 Qualifier가 더 우선순위가 높음을 확인할 수 있다.
    }
}

 

결론

따라서 같은 타입의 Bean이 여러 개 있을 때는, 범용적으로 사용되는 Bean 객체에는 @Primary를 설정하고 지엽적으로 사용되는 Bean 객체에는 @Qualifier를 사용하는 것이 좋다.

 

ex) 

우리 식당에서 전체 주문 중 Chicken 주문이 95%다! (범용적 사용)👉 @Primary 사용

우리 식당에서 전체 주문 중 Pizza 주문이 5%다! (지엽적 사용) 👉 @Qualifier 사용 (조금 주문이 되기 때문에 Qualifier 달아주는 수고로움을 줄일 수 있다)

▪️참고) Spring 설정에서 우선순위를 논할 때
Spring에서는 이런식으로 설정에 우선순위를 논할 때가 상당히 많다. 기본적으로 Spring에서 우선순위를 설정할 때는 큰 범위가 우선순위가 더 낮다. 즉, 좁은 범위의 설정이 우선순위가 더 높다는 것이다. (위의 사례에선 큰 범위가 Primary, 작은 범위가 Qualifier인 것)
Spring에서는 이 두개의 애너테이션처럼 다른 쪽에서도 중복되는 설정이 있을 때 좁은 범위의 설정을 더 우선으로 친다는 것을 숙지해두고 개발하면 좋을 것 같다.