6. 어댑터 패턴 (Adapter)
호환되지 않는 인터페이스를 맞춰서 연결해주는 어댑터 패턴을 알아봅니다.
한 줄 요약
모양이 맞지 않는 객체를 우리 약속에 맞는 껍질로 감싸서, 원래 코드를 고치지 않고도 함께 쓸 수 있게 해 주는 패턴이에요.
문제: 약속이 다른 남의 클래스
배달 가게의 결제 프로그램을 만든다고 해 볼게요. 우리 가게에는 결제 수단이 지킬 약속이 있어요.
- 결제 수단이라면
<결제하기>를 할 줄 알아야 하고, 금액은 원으로 받아요.
카드 결제는 이 약속을 잘 따라요. 그런데 해외 손님을 위해 다른 회사가 만든 글로벌페이를 붙이기로 했어요. 글로벌페이는 남이 만든 클래스(패키지)라서 우리가 고칠 수 없고, 모양이 전혀 달라요.
- 메서드 이름이
<결제하기>가 아니라<송금>이에요. - 인자 순서가 달라서, 통화를 먼저 받고 금액을 나중에 받아요.
- 금액의 단위가 원이 아니라 달러예요. 여기서는 간단하게 1000원을 1달러로 칠게요.
가장 먼저 떠오르는 방법은 결제하는 곳마다 “글로벌페이라면 이렇게 부르자”를 따지는 거예요.
(참고: 우리 가게의 결제 약속이에요)
[결제]를 규정하자:
<결제하기>가 있어야 한다 ([숫자]인 '원')
[결제]를 따르는 [카드결제]를 설계하자:
<결제하기>를 만들자 ([숫자]인 '원'):
틀"[카드] {'원'}원 결제"를 출력하자
(참고: 다른 회사가 만든 결제 도구라서 고칠 수 없어요. 이름도, 순서도, 단위도 달라요)
[글로벌페이]를 설계하자:
<송금>을 만들자 ([문자열]인 '통화', [숫자]인 '금액'):
틀"[글로벌페이] {'금액'} {'통화'} 송금"을 출력하자
(참고: 결제하는 곳마다 '글로벌페이라면...'을 따져요)
<주문결제>를 만들자 ('결제수단', [숫자]인 '원'):
만약 ('결제수단'이 [글로벌페이]의 일종이다) 라면:
'결제수단'의 <송금>("USD", '원' / 1000)을 실행하자
그렇지 않다면:
'결제수단'의 <결제하기>('원')를 실행하자
<배달비결제>를 만들자 ('결제수단', [숫자]인 '원'):
만약 ('결제수단'이 [글로벌페이]의 일종이다) 라면:
'결제수단'의 <송금>("USD", '원' / 1000)을 실행하자
그렇지 않다면:
'결제수단'의 <결제하기>('원')를 실행하자
<주문결제>(새로운 [카드결제](), 15000)를 실행하자
<주문결제>(새로운 [글로벌페이](), 15000)를 실행하자
<배달비결제>(새로운 [글로벌페이](), 3000)을 실행하자
실행 결과
[카드] 15000원 결제
[글로벌페이] 15 USD 송금
[글로벌페이] 3 USD 송금
잘 돌아가요. 그런데 결제하는 곳이 늘어나면 아파져요.
- 결제하는 곳이 글로벌페이의 사정을 다 알아야 해요.
<주문결제>는 “돈을 받는다”만 알면 되는데, 글로벌페이의 메서드 이름, 인자 순서, 달러 계산까지 알고 있어요. - 같은 변환이 복사돼요.
<주문결제>와<배달비결제>에 똑같은만약이 들어 있어요. 환불, 정기 결제가 생기면 또 복사해야 하고, 환율이 바뀌면 모든 곳을 찾아 고쳐야 해요. - 약속으로 지켜 줄 수가 없어요.
'결제수단'에[결제]타입을 붙이면 글로벌페이를 넘길 수 없어서, 아무 타입이나 받게 열어 둬야 해요. 엉뚱한 것이 들어와도 하리가 막아 주지 못해요. - 결제 수단이 하나 더 늘면
만약도 하나 더 늘어요. “간편결제 B”가 또 다른 모양이라면, 결제하는 곳마다 갈래가 하나씩 더 생겨요.
해결책: 맞지 않는 객체를 맞는 껍질로 감싸기
어댑터 패턴은 남의 클래스를 고치지 않고, 우리 약속을 따르는 껍질을 하나 만들어 감싸요.
- 우리 약속(
[결제])은 그대로 둬요. - 어댑터를 만들어요. 어댑터는
[결제]를 따르고, 안에 글로벌페이를 하나 품고 있어요. - 어댑터의
<결제하기>는 원을 받아서 이름, 인자 순서, 단위를 바꾼 뒤 글로벌페이의<송금>을 불러요. 변환은 이곳 한 군데에만 있어요. - 결제하는 곳은 이제
[결제]하나만 알아요. 카드든 글로벌페이든 똑같이<결제하기>를 불러요.
비유: 여행용 플러그 어댑터
해외여행에서 노트북 충전기를 꽂으려는데 콘센트 모양이 달라요. 충전기를 뜯어 고치지도, 호텔 벽을 바꾸지도 않죠. 대신 변환 플러그를 하나 끼워요. 한쪽은 충전기에 맞고, 다른 쪽은 벽에 맞아요. 충전기는 평소처럼 꽂기만 하면 돼요. 이 변환 플러그가 바로 “어댑터”예요.
누가 무엇을 하나요?
| 역할 | 이 예제에서 | 하는 일 |
|---|---|---|
| 우리 약속 | [결제] (규정) |
우리 코드가 기대하는 모양이에요 |
| 맞지 않는 객체 | [글로벌페이] |
쓸모는 있지만 모양이 달라요. 고칠 수 없어요 |
| 어댑터 | [글로벌페이어댑터] |
우리 약속을 따르고, 안에서 글로벌페이의 모양으로 바꿔 불러요 |
| 쓰는 곳 | <주문결제>, <배달비결제> |
[결제]만 알고, 누구든 똑같이 불러요 |
하리로 만들기
같은 결제 프로그램을 어댑터 패턴으로 다시 만들어 볼게요. [글로벌페이]는 한 글자도 고치지 않았어요.
(참고: 우리 가게의 결제 약속이에요)
[결제]를 규정하자:
<결제하기>가 있어야 한다 ([숫자]인 '원')
[결제]를 따르는 [카드결제]를 설계하자:
<결제하기>를 만들자 ([숫자]인 '원'):
틀"[카드] {'원'}원 결제"를 출력하자
(참고: 다른 회사가 만든 결제 도구라서 고칠 수 없어요. 이름도, 순서도, 단위도 달라요)
[글로벌페이]를 설계하자:
<송금>을 만들자 ([문자열]인 '통화', [숫자]인 '금액'):
틀"[글로벌페이] {'금액'} {'통화'} 송금"을 출력하자
(참고: 글로벌페이를 감싸서 우리 약속에 맞춰 주는 어댑터예요)
[결제]를 따르는 [글로벌페이어댑터]를 설계하자:
'글로벌'을 [글로벌페이]인 비어있음으로 정하여 숨기자
처음 만들어질 때 ([글로벌페이]인 '글로벌') 다음과 같이 하자:
'나'의 '글로벌'을 '글로벌'로 정하자
<결제하기>를 만들자 ([숫자]인 '원'):
(참고: 이름, 인자 순서, 단위를 여기서 한 번만 바꿔 줘요)
'나'의 '글로벌'의 <송금>("USD", '원' / 1000)을 실행하자
(참고: 결제하는 곳은 이제 [결제] 하나만 알아요)
<주문결제>를 만들자 ([결제]인 '결제수단', [숫자]인 '원'):
'결제수단'의 <결제하기>('원')를 실행하자
<배달비결제>를 만들자 ([결제]인 '결제수단', [숫자]인 '원'):
'결제수단'의 <결제하기>('원')를 실행하자
'해외손님결제'를 [결제]인 새로운 [글로벌페이어댑터](새로운 [글로벌페이]())로 정하자
<주문결제>(새로운 [카드결제](), 15000)를 실행하자
<주문결제>('해외손님결제', 15000)를 실행하자
<배달비결제>('해외손님결제', 3000)을 실행하자
실행 결과
[카드] 15000원 결제
[글로벌페이] 15 USD 송금
[글로벌페이] 3 USD 송금
결과는 리팩터링 전과 같아요. 하지만 <주문결제>와 <배달비결제>는 이제 한 줄씩이에요.
하나씩 다시 보기
- 우리 약속은 그대로.
[결제]도[카드결제]도 바뀌지 않았어요. 어댑터 패턴은 이미 잘 돌아가는 쪽을 건드리지 않아요. - 남의 클래스도 그대로.
[글로벌페이]도 한 글자도 바뀌지 않았어요. 패키지를 새 버전으로 올려도 우리가 고친 부분과 부딪칠 일이 없어요. - 변환은 어댑터 한 군데에만. 메서드 이름(
<결제하기>→<송금>), 인자 순서(통화 먼저), 단위(원 → 달러)를[글로벌페이어댑터]의<결제하기>한 곳에서 바꿔요. 환율이 바뀌면 여기만 고치면 돼요. - 이제 약속으로 지켜 줘요.
<주문결제>가[결제]인 '결제수단'만 받게 바꿀 수 있었어요. 어댑터를 거치지 않은 글로벌페이를 그대로 넘기면 하리가 그 자리에서 막아요.
어댑터와 데코레이터는 둘 다 “감싸는” 거 아닌가요?
맞아요, 둘 다 객체를 감싸요. 다만 감싸는 목적이 달라요. 어댑터는 모양(약속)을 바꾸려고 감싸요. 글로벌페이는<송금>을 할 줄 알지만, 감싸면<결제하기>를 하는 객체가 돼요. 데코레이터는 모양은 그대로 두고 기능을 더하려고 감싸요. 커피를 감싸도 여전히 커피예요.
새 결제 수단을 더해 보면
모양이 또 다른 “간편결제 B”를 붙인다고 해 볼게요.
- 앞의 방식: 결제하는 모든 곳에
그렇지 않고 만약 ('결제수단'이 [간편결제B]의 일종이다) 라면:을 넣어야 해요. - 어댑터 패턴:
[결제]를 따르는 [간편결제B어댑터]하나만 만들면 돼요. 결제하는 곳은 그대로예요.
직접 해 보기
위 코드에[포인트몰]이라는 남의 클래스를 더해 보세요.<포인트차감>([숫자]인 '포인트')라는 메서드가 있고, 1포인트는 10원이에요. 그리고[포인트몰어댑터]를 만들어서<주문결제>(새로운 [포인트몰어댑터](새로운 [포인트몰]()), 15000)이 1500포인트를 차감하게 해 보세요.<주문결제>는 고칠 필요가 없어요.
언제 쓰면 좋을까요?
- 쓰고 싶은 클래스가 있는데, 모양(메서드 이름, 인자, 단위)이 우리 약속과 다를 때
- 그 클래스를 고칠 수 없거나 고치고 싶지 않을 때 (남의 패키지, 오래된 코드, 다른 팀의 코드)
- 모양이 다른 여러 객체를 하나의 약속으로 똑같이 다루고 싶을 때
- 남의 클래스가 바뀌어도 우리 코드는 한 군데만 고치고 싶을 때
좋은 점과 아쉬운 점
| 좋은 점 | 아쉬운 점 |
|---|---|
| 우리 코드도, 남의 코드도 고치지 않아요 | 클래스(어댑터)가 하나 더 생겨요 |
| 변환이 어댑터 한 군데에 모여 있어요 | 한 번 더 거쳐 부르니, 따라 읽을 때 한 단계가 더 있어요 |
| 쓰는 곳은 우리 약속 하나만 알면 돼요 | 두 모양이 너무 다르면(필요한 정보가 아예 없으면) 어댑터로도 맞추기 어려워요 |
| 약속(타입)으로 잘못된 것을 막을 수 있어요 |
너무 이른 패턴은 독이에요.
남의 클래스를 딱 한 곳에서 한 번만 부른다면, 그 자리에서 바로 맞춰 불러도 충분해요. 같은 변환을 두 번째로 복사하려는 순간이 어댑터를 만들 때예요. 그리고 고칠 수 있는 우리 코드끼리 모양이 안 맞는 거라면, 어댑터보다 한쪽 이름을 바로 고치는 게 더 깔끔할 때가 많아요.