어댑터 패턴(Adapter Pattern) ; OCP, 추상화

한 클래스의 인터페이스를 클라이언트에서 사용하고자 하는 다른 인터페이스로 변환한다.
어댑터를 이용하면 인터페이스 호환성 문제 때문에 같이 쓸 수 없는 클래스들을 연결해서 사용할 수 있다.

객체 어댑터 패턴 vs 클래스 어댑터 패턴

객체 어댑터 패턴

‘구성(Composition)’ 을 사용하여 인터페이스를 변경하는 경우

  • 구성을 사용하므로, Adapter 에서 변환 로직이 복잡한 경우, 다소 코드가 길어질 수 있음

클래스 어댑터 패턴

‘상속(Inheritance)’ 을 사용하여 인터페이스를 변경하는 경우

  • 유연성이 떨어지지만, 상속의 장점인 ‘중복제거’ 측면에서 생산성은 높을 수 있음

어떤 것을 사용?

경우에 따라 다 다르다.

다만, 유연성 vs 코드중복 관점에서는 대체로 유연성이 더 좋은 설계를 선호하기 때문에 객체 어댑터 패턴으로 구현하는 것이 좋다.

어댑터 패턴 장단점

장점

  • Adapter 의 가장 큰 장점은 제공자 / 클라리언트 둘 다 코드를 변경하지 않아도 된다는 점
    • Adapter만 새로 작성하면 됨
  • 재활용성이 높음(Adapter 규격에 맞는 객체는 모두 사용 가능)

단점

  • 구성요소를 위해 클래스를 증가시켜야 하기 때문에 복잡도가 증가할 수 있음
  • 클래스 어댑터 패턴을 사용한다면, 상속을 사용하기 때문에 유연성이 떨어짐

일본의 전자제품을 한국에서 사용하게 변경해주는 Adapter 설계

책의 예제가 조금 이해하기 난해해서, 실생활을 추상화하여 예시를 하나 설계해보았다.
일본에서 산 전자제품(110v 정격전압)을 한국(220v 정격전압)에서 사용할 수 있도록 해주는 어뎁터를 실제 설계해보았다.

JPElectronicProduct Interface

일본의 전자제품 연결을 위한 Interface

1
2
3
public interface JPElectronicProduct {
void connect();
}

JPAirConditioner Class

일본에서 구매한 에어컨 클래스, JPElectronicProduct 를 구현하고 있다.

1
2
3
4
5
6
public class JPAirConditioner implements JPElectronicProduct {
@Override
public void connect() {
System.out.println("일본에서 구매한 에어컨입니다. 110v 규격과 연결됩니다.");
}
}

KRElectronicProduct Interface

한국의 전자제품 연결을 위한 Interface

1
2
3
public interface KRElectronicProduct {
void connect();
}

KRRefrigerator Class

한국에서 구매한 냉장고 클래스, KRElectronicProduct 를 구현하고 있다.

1
2
3
4
5
6
public class KRRefrigerator implements KRElectronicProduct {
@Override
public void connect() {
System.out.println("한국에서 구매한 냉장고입니다. 220v 와 연결됩니다.");
}
}

Adapter 클래스

일본에서 구매한 전자제품을 한국의 정격전압과 연결하게 해주는 Adapter 클래스

1
2
3
4
5
6
7
8
9
10
11
12
13
public class AdapterForKoreaElectronicProduct implements KRElectronicProduct {
private final JPElectronicProduct jpElectronicProduct;

public AdapterForKoreaElectronicProduct(JPElectronicProduct jpElectronicProduct) {
this.jpElectronicProduct = jpElectronicProduct;
}

@Override
public void connect() {
System.out.println("어댑터입니다. 외부에서 220v 전압을 받아 110v로 변경합니다.");
jpElectronicProduct.connect();
}
}

테스트 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class AdapterTest {
// 결과
/*
한국 정격전압은 220v을 사용합니다.
어댑터입니다. 외부에서 220v 전압을 받아 110v로 변경합니다.
일본에서 구매한 에어컨입니다. 110v 규격과 연결됩니다.
한국 정격전압은 220v을 사용합니다.
한국에서 구매한 냉장고입니다. 220v 와 연결됩니다.
*/
@Test
void adapterTest() throws Exception {
// given
JPElectronicProduct airConditioner = new JPAirConditioner();
KRElectronicProduct refrigerator = new KRRefrigerator();

// when
KRElectronicProduct adapterWithJapanElectronicProduct = new AdapterForKoreaElectronicProduct(airConditioner);

// then
connectWithKoreaElectric(adapterWithJapanElectronicProduct);
connectWithKoreaElectric(refrigerator);

assertThat(adapterWithJapanElectronicProduct).isInstanceOf(KRElectronicProduct.class);
}

public void connectWithKoreaElectric(KRElectronicProduct product) {
System.out.println("한국 정격전압은 220v을 사용합니다.");
product.connect();
}
}

Spring 속 어댑터 패턴 - MultiValueMapAdapter

Spring의 util 패키지에 (Key, List) 로 구성되는 MultiValueMap 이란 자료구조가 존재한다.

MultiValueMapAdapter 는 JavaSE 의 java.util.Map 컬렉션 을 MultiValueMap으로 사용하게 끔 도와주는 어댑터이다.
따라서 Map<K, List<V>> 구조의 Map을 MultiValueMap 타입으로 변환하여 사용이 가능하게 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package org.springframework.util;

// ...

/**
* Adapts a given {@link Map} to the {@link MultiValueMap} contract.
*
* @author Arjen Poutsma
* @author Juergen Hoeller
* @since 5.3
* @param <K> the key type
* @param <V> the value element type
* @see CollectionUtils#toMultiValueMap
* @see LinkedMultiValueMap
*/
@SuppressWarnings("serial")
public class MultiValueMapAdapter<K, V> implements MultiValueMap<K, V>, Serializable {

private final Map<K, List<V>> targetMap;


/**
* Wrap the given target {@link Map} as a {@link MultiValueMap} adapter.
* @param targetMap the plain target {@code Map}
*/
public MultiValueMapAdapter(Map<K, List<V>> targetMap) {
Assert.notNull(targetMap, "'targetMap' must not be null");
this.targetMap = targetMap;
}

// MultiValueMap implementation

@Override
@Nullable
public V getFirst(K key) {
List<V> values = this.targetMap.get(key);
return (values != null && !values.isEmpty() ? values.get(0) : null);
}

...
}

생각해보자

데코레이터 vs 어댑터

앞서 다루었던 데코레이터 패턴도 어느정도 어댑터 패턴과 공통점이 있는 것 같아서 헷갈리기 시작했다.
따라서 어댑터 패턴과 데코레이터 패턴의 정의로부터 목적을 다시 상기하며 비교해보았다.

공통점

  • 둘 다 어떤 관점으로든 특정 객체를 Wrapping 하고 있다.

차이점

  • 어댑터 : 특정 인터페이스로 변환하기 위한 목적이다.
  • 데코레이터 : 특정 기능이나 속성을 추가하기 위한 목적이다.

변화와 책임부여 관점에서 바라 본 디자인 패턴

디자인 패턴을 계속 공부하다보니 대부분의 경우
‘변화할 가능성이 있는 부분을 별도 객체로 분리하고, 그 객체에 변화에 대한 책임을 부여한다.’ 라는 차원에서 접근한다.
그리고 그 방법에 따라 여러 디자인 패턴으로 나눌 수 있는 것 같다. 이런 사실 때문에 디자인 패턴은 단독으로 사용되는 경우가 드물며, 여러 디자인 패턴을 결합해서 사용하는 경우도 많은 것 같다.

또한, 어떤 코드를 봤을 때 ‘아 이건 OOO 디자인 패턴을 사용했다.’ 라고 단정지을 수 있는 경우가 많지 않다.
따라서, 어떤 디자인 패턴을 학습할 때 조심해야 되는 것은 ‘어떤 기술과 구현 방법을 썼기 때문에 이 디자인 패턴은 OOO 다’ 라고 단정지으면 안된다는 생각이 들었다.
가장 중요한 것은 ‘어떤 문제가 발생했고, 이 문제를 어떻게 해결하는 것이 더 좋은 설계인 지’ 관점에서 디자인 패턴을 학습해야 한다.

참고자료