주요글: 도커 시작하기
반응형

메일로 다음과 같은 문의를 받았습니다.

"@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를 사용하는 경우에는 이 태그의 값만 바꿔주면 된다. 그 만큼 변경의 여파가 작은 것이다.


정리하며


다시, 존칭 모드로 돌아와서, 이 글이 질문을 보내신 분에게 약간이나마 '제가 왜 이름을 이용한 자동 의존 설정'을 선호하지 않는지에 대한 답이 되었으면 합니다.


+ Recent posts