어댑터 패턴(Adapter Pattern)
예제에 대한 전체 코드는 https://github.com/minseokLim/practice/tree/main/design-pattern에서 확인할 수 있다.
어댑터 패턴(Adapter Pattern)은 클라이언트가 요구하는 인터페이스와 사용하려는 모듈의 인터페이스가 일치하지 않을 때 사용할 수 있는 디자인 패턴이다. 어댑터 패턴의 주요 구성요소는 다음과 같다.
- 타겟 인터페이스(Target Interface): 클라이언트가 사용할 인터페이스이다.
- 어댑터(Adapter): 타겟 인터페이스를 구현하고, 어댑티(Adaptee)의 인터페이스를 타겟 인터페이스에 맞게 변환한다.
- 어댑티(Adaptee): 사용하려는 모듈이다.
예제를 통해 좀 더 자세히 이 패턴에 대해 알아보자. 예제는 코틀린으로 작성하였다.
아래의 코드에서 SearchService는 타겟 인터페이스이고, WebSearchRequestHandler는 이 인터페이스를 사용하는 클라이언트 코드이다.
interface SearchService {
fun search(keyword: String): SearchResult
}
class WebSearchRequestHandler(
private val searchService: SearchService
) {
fun handleSearchRequest(keyword: String): SearchResult {
return searchService.search(keyword)
}
}
원래는 DB에서 검색 결과를 반환하는 서비스를 제공했다고 생각해보자. 아래는 이를 구현한 DBSearchService 예시이다.
class DBSearchService(
private val db: Set<String>
) : SearchService {
override fun search(keyword: String): SearchResult {
return SearchResult(db.filter { it.contains(keyword) })
}
}
그런데 DB 데이터가 많아지면서 like 검색을 통한 DB 검색의 성능이 떨어지는 현상이 생겼다고 생각해보자. 그래서 별도의 검색 엔진 도입이 필요하다는 요구 사항이 주어졌다. 새로 도입할 검색 엔진은 TolrClient로 아래와 같은 인터페이스를 제공한다. 이 인터페이스는 기존의 인터페이스와는 형태가 맞지 않는데, 이 때 도입할 수 있는 패턴이 바로 어댑터 패턴이다.
class TolrClient(
private val tolr: Set<String>
) {
fun query(query: TolrQuery): TolrResponse {
return TolrResponse(tolr.filter { it.contains(query.keyword) }.toSet())
}
}
아래 코드는 어댑터 패턴을 적용한 어댑터 클래스 예시이다.
class SearchServiceTolrAdapter(
tolr: Set<String>
) : SearchService {
private val tolrClient = TolrClient(tolr)
override fun search(keyword: String): SearchResult {
val query = TolrQuery(keyword)
val response = tolrClient.query(query)
return SearchResult(response.contents.toList())
}
}
어댑터 클래스는 기존의 SearchService 인터페이스를 구현하면서 TolrClient 사용을 가능하게 해준다. 이로써 클라이언트 코드는 수정될 필요가 없이 변경된 어댑터 클래스를 주입해주면 된다.
class WebSearchRequestHandlerTest {
@Test
fun handleSearchRequest() {
// given
val db = setOf("apple", "banana", "cherry")
val webSearchRequestHandler1 = WebSearchRequestHandler(DBSearchService(db))
// when
val result1 = webSearchRequestHandler1.handleSearchRequest("a")
// then
assert(result1.contents.contains("apple"))
assert(result1.contents.contains("banana"))
assert(!result1.contents.contains("cherry"))
// given
val tolr = setOf("apple", "banana", "cherry")
val webSearchRequestHandler2 = WebSearchRequestHandler(SearchServiceTolrAdapter(tolr))
// when
val result2 = webSearchRequestHandler2.handleSearchRequest("a")
// then
assert(result2.contents.contains("apple"))
assert(result2.contents.contains("banana"))
assert(!result2.contents.contains("cherry"))
}
}
어댑터 패턴이 적용된 예로는 Slf4j라는 로깅 API가 있다. Slf4j는 단일 로깅 API를 사용하면서 자바 로깅, log4j, logback 등의 로깅 프레임워크를 선택적으로 사용할 수 있도록 해주는데, 이 때 Slf4j가 제공하는 인터페이스와 각 로깅 프레임워크를 맞춰 주기 위한 어댑터를 사용하고 있다.
어댑터 패턴은 개방 폐쇄 원칙을 따를 수 있도록 도와준다. Slf4j를 예로 들면, Slf4j에서 제공하는 인터페이스를 통해 로깅 프레임워크를 사용해왔다면, 로깅 프레임워크를 log4j -> logback으로 클라이언트 코드 수정 없이 변경할 수 있다.
이 글은 "객체 지향과 디자인 패턴 - 최범균" 책을 기반으로 작성되었다.