지난 시간에 우리는 @Component를 써서 스프링이 알아서 객체(Bean)를 만들게 했습니다. 그런데 여기서 호기심이 생깁니다.
"만약 게임이 팩맨(Pacman) 하나가 아니라, 마리오(Mario)도 있다면 어떻게 될까?"
스프링에게 "게임기 가져와!"라고 시켰는데, 게임기가 두 대라면 스프링은 과연 알아서 골라올까요? 오늘은 이 상황에서 발생하는 에러와 두 가지 해결책을 알아보겠습니다.
우리는 PacmanGame에 @Component를 붙여뒀습니다. 이번엔 MarioGame에도 똑같이 @Component를 붙여보겠습니다.
@Component
public class MarioGame implements GamingConsole { ... }
@Component
public class PacmanGame implements GamingConsole { ... }
그리고 실행(Run)을 하면... 펑! 💥 에러가 발생합니다.
에러 메시지:
NoUniqueBeanDefinitionException
해석:
스프링이 GameRunner를 만들려고 보니 GamingConsole이 필요한데, 후보자가 마리오와 팩맨 둘 다 있어서 누구를 골라야 할지 모르겠다는 뜻입니다. 스프링은 결정장애가 오면 그냥 멈춰버립니다.
@Primary첫 번째 해결책은 '우선순위'를 정해주는 것입니다. 여러 후보가 있을 때, "별말 없으면 그냥 얘를 써!"라고 정해두는 것이죠. 이것이 바로 @Primary 어노테이션입니다.
MarioGame에 왕관(@Primary)을 씌워줍시다.
@Component
@Primary // <-- "여러 개면 나를 먼저 선택해!"
public class MarioGame implements GamingConsole { ... }
결과:
이제 스프링은 팩맨과 마리오를 둘 다 발견하지만, @Primary가 붙은 마리오 게임을 선택해서 실행합니다. 팩맨은 아쉽지만 선택받지 못했네요.
@Qualifier자, 이제 상황을 더 복잡하게 만들어 볼까요?
PacmanGame (@Component)MarioGame (@Component, @Primary) -> 현재 1순위SuperContraGame (새로 추가, @Component)여기서 제가 "마리오가 1순위인 건 알겠는데, 이번엔 특별히 '슈퍼 콘트라'를 하고 싶어!"라고 한다면 어떻게 해야 할까요?
이때 사용하는 것이 @Qualifier (한정자)입니다. 별명을 붙여서 콕 집어 부르는 방식이죠.
@Component
@Qualifier("SuperContraGameQualifier") // <-- "내 별명은 이거야"
public class SuperContraGame implements GamingConsole { ... }
게임을 실행하는 GameRunner에게 가서 "그냥 게임기 말고, 꼭 저 별명을 가진 애를 가져와"라고 말해줍니다.
@Component
public class GameRunner {
private GamingConsole game;
// 생성자 주입
public GameRunner(@Qualifier("SuperContraGameQualifier") GamingConsole game) {
this.game = game;
}
// ...
}
결과:
MarioGame이 @Primary라는 강력한 우선권을 가지고 있음에도 불구하고, @Qualifier로 아주 구체적으로 지목당한 슈퍼 콘트라 게임이 실행됩니다.
스프링 컨테이너 안에 똑같은 타입의 객체(Bean)가 여러 개 있을 때, 스프링은 혼란에 빠집니다(NoUniqueBeanDefinitionException). 이를 해결하는 방법은 두 가지입니다.
승자 판정: @Primary보다 @Qualifier가 더 강력합니다. (넓은 범위의 기본값보다 구체적인 지시가 이깁니다.)
코드를 작성할 때, 평소에 가장 많이 쓰는 것에는 @Primary를 붙여두고, 가끔 특별한 상황에 써야 하는 것에는 @Qualifier를 사용하면 아주 깔끔하게 코드를 짤 수 있습니다!
다음 시간에는 스프링의 더 깊은 개념들(의존성 주입의 종류 등)을 알아보겠습니다.