메일로 다음과 같은 문의를 받았습니다.
"@Autowired가 필드 이름과 빈의 식별자가 같으면 인젝션이 되는데, 님(저)이 필드 이름을 이용해서 의존 주입하는 것을 선호하지 않는 이유는 무엇인가요?"
"그리고, 이 경우 이름이 일치하도록 하면 @Qualifier를 사용할 필요가 없는 거 아닌가요?"
이 외에도 몇 가지 질문이 있었지만, 위 질문에 대한 답을 장문의 메일로 쓰기가 쉽지 않아, 블로그에 글로 남깁니다. (이후부터는 높임을 하지 않고 편하게 적습니다.)
시작은 의존 설계
게시글에 대한 검색 기능을 구현한다고 해 보자. 아직 게시글 검색 기능을 구현하지 않았을 뿐만 아니라 어떤 기술을 사용할지 결정된 바도 없다. 꼭 TDD를 좋아하지 않더라도 게시글 검색 기능과 연동되는 클래스를 테스트하려면(또는 검색 기능과 연동되는 클래스를 구현하려면), 다음처럼 검색 기능을 인터페이스로 정의하고 해당 인터페이스를 사용하도록 구현할 것이다.
public interface SearchService {
public List<Document> search(String query);
}
public class ArticleController {
@Autowired
private SearchService searchService; // 인터페이스를 사용
public String search(String query) {
...
}
}
컨텐츠 검색 처리를 위한 ContentController도 검색 기능이 필요하기에 다음과 같이 SearchService를 이용해서 코드 구현을 시작했다고 하자.
public class ContentController {
@Autowired
private SearchService searchService; // 인터페이스를 사용
...
}
ArticleController를 구현하는 과정에서 Lucene을 이용한 SearchService의 구현이 완료되었다.
public class LuceneSearchService implement SearchService { ... }
그리고, 게시글이 아닌 다른 컨텐츠는 외부에서 제공하는 검색 서비스를 이용해서 구현하기로 했다.
public class QueryJetSearchService implements SearchService { ... }
즉, 객체 간의 관계는 다음과 같이 연결(wiring)이 된다.
- ArticleController 객체 --> LuceneSearchService 객체
- ContentController 객체 --> QueryJetSearchService 객체
스프링 설정에서 빈의 이름은?
각 클래스의 구현이 마무리 되었다. 스프링 설정은 어떻게 만들까?
<!-- autowired 관련 설정이 되었다고 가정 -->
<bean id="articleController" class="ArticleController" />
<bean id="searchController" class="ContentController" />
<bean id="???" class="LuceneSearchService" />
<bean id="???" class="QueryJetSearchService" />
위 설정에서 보면 LeceneSearchService와 QueryJetSearchService의 이름을 어떻게 줘야 하나? 구현 기술을 따라 다음과 같이 이름을 주었다고 해 보자.
- luceneSearchService
- queryJetSearchService
이렇게 이름을 줄 경우, @Autowired가 적용된 ArticleController와 ContentController의 두 searchService 필드는, luceneSearchService 빈과 queryJetSearchService 빈 중 어떤 걸 의존 연결 대상으로 선택해야 할지 알 수 없기 때문에 스프링이 빈 객체를 생성하는 과정에서 익셉션을 발생시키게 된다.
이름을 이용해서 의존 객체를 찾는 방식으로 이 문제를 해결하려면 아래 코드처럼 빈의 이름과 동일한 필드 이름을 갖도록 자바 코드를 변경하면, 일단은 된다.
<bean id="luceneSearchService" class="LuceneSearchService" />
<bean id="queryJetSearchService" class="QueryJetSearchService" />
public class ArticleController {
@Autowired
private SearchService luceneSearchService; // 이름 변경 발생!
...
}
public class ContentController {
@Autowired
private SearchService queryJetSearchService; // 이름 변경 발생!
}
필드 이름에 lecene이나 queryJet과 같은 구현 기술에 종속된 이름이 나오는 건 코드의 의미를 전달하기에 적합하지 않으므로, 의미가 잘 드러나도록 각각 articleSearchService, contentSearchService로 이름을 변경할 수도 있을 것이다.
<bean id="articleSearchService" class="LuceneSearchService" />
<bean id="contentSearchService" class="QueryJetSearchService" />
public class ArticleController {
@Autowired
private SearchService articleSearchService; // 이름 변경 발생!
...
}
public class ContentController {
@Autowired
private SearchService contentSearchService; // 이름 변경 발생!
}
뭐,아직까지는 나빠보이지 않는다. 그런데, Lucene을 이용한 검색에서 외부의 QueryJet을 이용한 검색으로 이관하기로 했다고 하자. 그럼, 이 경우에는 어떻게 해야 하나? 이름을 기준으로 의존 객체를 찾으면 ArticleController의 코드가 다음과 같이 바뀐다.
public class ArticleController {
@Autowired
private SearchService contentSearchService; // 이름 변경 발생!
...
}
contentSearchService라니? Article도 함께 검색할 수 있으므로, 뭔가 이름이 어울리지 않는다. 그래서 다시 스프링 빈의 이름과 필드 이름을 바꾼다.
<bean id="articleSearchService" class="LuceneSearchService" />
<bean id="externalSearchService" class="QueryJetSearchService" /> <!-- 이름 변경 발생 -->
public class ArticleController {
@Autowired
private SearchService externalSearchService; // 이름 변경 발생!
...
}
public class ContentController {
@Autowired
private SearchService externalSearchService; // 이름 변경 발생!
}
게다가 이름을 이용한 자동 의존 설정 방식을 사용하면, 역으로 필드 이름을 변경해야 하면, 스프링 설정의 이름도 변경해 줘야 한다.
왜 이런 일이....??
왜 이렇게 이름을 바꿔줘야 하는 상황이 발생하는지 좀 생각해보면, 단위 테스트 역량과 설계 역량이 쌓일수록 의존하는 부분을 인터페이스로 정의하기 때문인 듯 하다. 이런 역량이 쌓이면, 앞서 ArticleController의 경우처럼, 아직 SearchService의 상세 구현이 없는 상태에서 SearchService를 사용하는 ArticleController를 구현할 수 있게 된다. 비슷하게 ContentController도 실제 사용할 SearchService의 컨크리트 클래스 없이 구현할 수 있게 된다. 이 상황에서 나중에 스프링 설정에 사용할 빈 이름을 미리 고려해서 각자 사용할 필드의 이름을 다르게 정할 이유가 없다. 그냥 searchService라는 필드 이름으로 충분할 것이다.
// SearchService의 실제 구현이 뭐가 될지 모르는 상황에서
// 필드 이름을 미리 특정 구현 기술이나(luceneSearchService나 qjSearchService 등)
// 빈의 이름을 미리 에측해서 (internalSerachSvc, extSearchSvc 등) 정할 이유가 없다!
public class ArticleController {
private SearchService searchService;
...
}
public class ContentController {
private SearchService searchService;
...
}
이런 상태에서 자연스럽게 구현이 진행되고 그러다보면 두 개의 서로 다른 SearchService가 출현할 수 있는 것이다.
@Qualifier는?
같은 타입 또는 같은 인터페이스를 상속받은 서로 다른 클래스를 이용해서 두 개의 빈을 정의할 경우, @Autowired는 필드 이름과 일치하는 빈을 찾지 못하면 익셉션을 발생시킨다. 이런 경우에 필드의 이름을 변경하는 방법은, 결국 필드 이름이 특정 구현 기술이나 의존하는 빈 객체의 이름에 영향을 받게 되므로 그다지 좋은 방법이 아니다. 게다가 필드 이름을 바꾸면 (리팩토링 도구가 알아서 해 준다 하더라도) 그 필드를 사용하는 코드도 함께 변경을 하게 된다.
이럴 때, 변화를 최소화할 수 있는 방법이 @Autowired와 @Qualifer를 함께 사용하는 것이다. 예를 들어, 스프링 설정을 다음과 같이 했다고 해 보자.
<bean id="articleSearchService" class="LuceneSearchService">
<qualifier value="internal" />
</bean>
<bean id="contentSearchService" class="QueryJetSearchService">
<qualifier value="external" />
</bean>
이 경우, @Qualifier를 사용하면 이미 구현한 클래스의 필드 이름 변경 없이 자동으로 의존 연결을 처리할 수 있게 된다.
public class ArticleController {
@Qualifier("internal")
private SearchService searchService;
...
}
public class ContentController {
@Qualifier("external")
private SearchService searchService;
...
}
ArticleController가 사용할 SearchService를 "contentSearchService" 빈으로 변경해야 하는 상황이 발생해도, 필드 이름을 변경할 필요는 없다. 단지 @Qualifier의 값만 바꿔주면 된다.
public class ArticleController {
@Qualifier("external") // 요기만 변경
private SearchService searchService;
...
}
뭔가 바꾼거니까 필드 이름 바꾸는 거나 별 차이가 없지 않냐라고 할 수도 있지만, 필드 이름을 바꾸면 그 필드를 사용하는 모든 코드가 영향을 받지만, @Qualifier를 사용하는 경우에는 이 태그의 값만 바꿔주면 된다. 그 만큼 변경의 여파가 작은 것이다.
정리하며
다시, 존칭 모드로 돌아와서, 이 글이 질문을 보내신 분에게 약간이나마 '제가 왜 이름을 이용한 자동 의존 설정'을 선호하지 않는지에 대한 답이 되었으면 합니다.