오늘도 새로운 패턴들을 배우며 조금 더 깊게 익혀보는 시간을 가져보았다.
서로 호환되지 않는 객체를 중간에서 연결해주는 패턴이다.
쉽게 말하면 형식이 다른 것을 변환해서 연결해주는 것이다.
나쁜 예시
// 결제 처리기 인터페이스
interface PayProcessor {
void processPay();
}
// 기본 결제 처리자
class DefaultPayProcessor implements PayProcessor {
public void processPay() {
System.out.println("결제요청 처리중");
System.out.println("처리 완료");
}
}
// 외부 결제 API -> 바꿀 수 없음
class PayPalAPI {
public void handlePay(String apiKey) {
System.out.println("API키 확인중: " + apiKey);
System.out.println("페이팔 결제요청 처리중");
System.out.println("처리 완료");
}
}
// 주문 처리 객체
class OrderService {
PayProcessor payProcessor;
// 페이팔 의존성이 추가됨
PayPalAPI payPal;
OrderService(PayProcessor payProcessor, PayPalAPI payPal) {
this.payProcessor = payProcessor;
this.payPal = payPal;
}
void processOrder(String payMethods) { // payMethods: 사용자가 선택한 결제수단
System.out.println("주문 접수");
System.out.println("결제를 시작합니다");
// 기본 결제 수단은 똑같이 처리합니다
if ("default-pay".equals(payMethods)) {
payProcessor.processPay();
// 페이팔인 경우
} else if ("paypal".equals(payMethods)) {
// 발급받은 API 키를 적용합니다
String apiKey = "secret-123";
// 결제 처리
payPal.handlePay(apiKey);
} else {
System.out.println("알 수 없는 결제수단");
return;
}
System.out.println("주문 완료");
}
}
public class Main {
public static void main(String[] args) {
PayProcessor payProcessor = new DefaultPayProcessor();
PayPalAPI payPal = new PayPalAPI();
// 의존성 주입
OrderService orderService = new OrderService(payProcessor, payPal);
// 페이팔로 결제 요청이 들어왔습니다
orderService.processOrder("paypal");
// 기본 결제수단으로 결제 요청이 들어옴
orderService.processOrder("default-pay");
}
}
이 코드가 나쁜 예시인 이유는 앞서 했던 예제들과 비슷하게 OrderService가 너무 많은 것을 알고있기 때문이다.
if문을 사용해 현재 OrderService가 기본 결제 처리 방식, 페이팔 처리 방식, API 키처리, 어떤 결제수단인지 판단하는 것까지 전부 담당하고 있다.
그렇다면 새로운 결제수단이 추가될 때마다 OrderService를 계속 수정해야 한다는 문제가 생긴다.
그렇다면 좋은 코드는 어떻게 쓰면 될까?
좋은 예시
// 결제 처리기 인터페이스
interface PayProcessor {
void processPay();
}
// 기본 결제 처리자
class DefaultPayProcessor implements PayProcessor {
public void processPay() {
System.out.println("결제요청 처리중");
System.out.println("처리 완료");
}
}
// 외부 결제 API -> 바꿀 수 없음
class PayPalAPI {
public void handlePay(String apiKey) {
System.out.println("API키 확인중: " + apiKey);
System.out.println("페이팔 결제요청 처리중");
System.out.println("처리 완료");
}
}
// 페이팔용 어뎁터
class PayPalAdapter implements PayProcessor {
// 페이팔 API를 포함합니다
PayPalAPI payPalAPI;
// API 키
String apiKey;
PayPalAdapter(PayPalAPI payPalAPI, String apiKey) {
this.payPalAPI = payPalAPI;
this.apiKey = apiKey;
}
public void processPay() {
// 내부적으로는 페이팔API를 호출합니다
this.payPalAPI.handlePay(apiKey);
}
}
// 주문 처리 객체
class OrderService {
PayProcessor payProcessor;
OrderService(PayProcessor payProcessor) {
this.payProcessor = payProcessor;
}
void processOrder() {
System.out.println("주문 접수");
System.out.println("결제를 시작합니다");
payProcessor.processPay();
System.out.println("주문 완료");
}
}
public class Main {
public static void main(String[] args) {
String payRequest = "paypal";
PayProcessor payProcessor;
if ("default-pay".equals(payRequest)) {
payProcessor = new DefaultPayProcessor();
} else if ("paypal".equals(payRequest)) {
PayPalAPI payPalAPI = new PayPalAPI();
String apiKey = "secret-123";
payProcessor = new PayPalAdapter(payPalAPI, apiKey);
} else {
System.out.println("알 수 없는 결제 요청");
return;
}
OrderService orderService = new OrderService(payProcessor);
orderService.processOrder();
}
}
이 코드는 어댑터(Adapter) 패턴을 잘 적용한 좋은 예시라고 볼 수 있다.
가장 핵심은 OrderService가 더 이상 페이팔 API를 직접 알지 못한다는 점이다.
이전 나쁜 예시에서는 IrderService가 내부에서 if문을 사용해 직접 페이팔 API를 호출하고 있었다.
즉 주문처리 객체가 결제 방식의 세부구현까지 모두 알고 있어야했다.
하지만 지금 구조에서는 PayProcessor라는 인터페이스만 의존하고 있다.
즉 OrderService는 payProcessor.processPay();만 호출하면 된다.
실제로 내부에서 어떤 결제 API가 동작하는지는 전혀 모른다.
여기서 핵심 역할을 하는 것이 바로 PayPalAdapter이다.
PayPalAdapter는 PayProcessor인터페이스를 구현하면서 내부적으로는 PayPalAPI를 사용하는 중간 변환 객체 역할을 한다.
즉 시스템이 기대하는 방식은 processPay()인데 페이팔 API는 jandlePay(apiKey)를 사용하기 때문에 서로 구조가 맞지 않는다.
그래서 어댑터가 중간에서 processPay() -> handlePay(apiKet)로 변환해주는 것이다.
덕분에 OrderService는 외부 API 구조를 몰라도 되고 새로운 결제 시스템이 추가되어도 기존 코드를 거의 수정하지 않아도 된다.
예를들어 KakaoPayAdapter, NaverPayAdapter... 같은 어댑터만 새로 만들면 바로 확장이 가능하다.
즉 이 코드의 핵심 장점은 외부 API와의 강한 결합을 제거하고 확장성을 증가시키는 것이라고 볼 수 있다.
결국 어댑터패턴은 호환되지 않는 외부 API를 현재 시스템 구조에 맞게 연결해주는 패턴이라는 것을 보여주는 좋은 예시라고 볼 수 있다.
220v 충전기와 110v용 충전기를 한국에서 쓰려면?
나쁜 예시
// 220v 충전기 인터페이스
interface ChargerStandard220V {
void connect220V();
}
// 220v 충전기 생성용 클래스
class Charger220V implements ChargerStandard220V {
public void connect220V() {
System.out.println("220V 소켓에 연결되었습니다");
}
}
// 110V 충전기 생성용 클래스 -> 수정할 수 없음
class Charger110V {
public void connect110V() {
System.out.println("110V 소켓에 연결되었습니다");
}
}
// 충전기를 사용하려는 사람
class Person {
ChargerStandard220V chargerStandard220V;
Charger110V charger110V;
Person(ChargerStandard220V chargerStandard220V, Charger110V charger110V) {
this.chargerStandard220V = chargerStandard220V;
this.charger110V = charger110V;
}
// 충전하기
void charge(String chargerToUse) { // chargerToUse: 사용할 충전기 타입
System.out.println("충전해야지");
// 220V 충전기는 바로 사용가능
if ("charger-220V".equals(chargerToUse)) {
chargerStandard220V.connect220V();
// 110V 충전기는 사용할 수 없음
} else if ("charger-110V".equals(chargerToUse)) {
System.out.println("110V용 소켓이 없습니다");
return;
} else {
System.out.println("알 수 없는 규격");
return;
}
System.out.println("충전중!\n");
}
}
public class Main {
public static void main(String[] args) {
ChargerStandard220V chargerStandard220V = new Charger220V();
Charger110V charger110v = new Charger110V();
Person person = new Person(chargerStandard220V, charger110v);
person.charge("charger-220V");
person.charge("charger-110V");
}
}
이 코드 또한 Person이 110V 충전기를 직접 처리하고 있기 때문에 나쁜 코드라고 볼 수 있다.
현재 Person 클래스안에 if문 같은 조건이 들어가 있어서 충전기 규격에 따라 직접 판단하고 있다.
즉 새로운 규격이 추가될 때마다 Person 코드를 계속 수정해야 한다는 문제가 생긴다.
또 110V 충전기는 connect110V를 사용해 현재 시스템 인터페이스인 connerct220V()와 호환되지 않는다.
즉 호환되지 않는 객체를 연결하지 못하는 구조인 것이다.
그렇다면 좋은 코드는 어떻게 작성하면 될까?
좋은 예시
// 220v 충전기 인터페이스
interface ChargerStandard220V {
void connect220V();
}
// 220v 충전기 생성용 클래스
class Charger220V implements ChargerStandard220V {
public void connect220V() {
System.out.println("220V 소켓에 연결되었습니다");
}
}
// 220V 연결용 어뎁터
class Adapter implements ChargerStandard220V {
Charger110V charger110V;
Adapter(Charger110V charger110V) {
this.charger110V = charger110V;
System.out.println("220V 어뎁터가 연결되었습니다");
}
public void connect220V() {
// 내부적으로 110V 충전기
charger110V.connect110V();
}
}
// 110V 충전기 생성용 클래스 -> 수정할 수 없음
class Charger110V {
public void connect110V() {
System.out.println("110V 소켓에 연결되었습니다");
}
}
// 충전기를 사용하려는 사람
class Person {
ChargerStandard220V chargerStandard220V;
Person(ChargerStandard220V chargerStandard220V) {
this.chargerStandard220V = chargerStandard220V;
}
// 충전하기
void charge() {
System.out.println("충전해야지");
chargerStandard220V.connect220V();
System.out.println("충전중!\n");
}
}
public class Main {
public static void main(String[] args) {
String chargerToUse = "charger-110V";
ChargerStandard220V chargerStandard220V;
if ("charger-220V".equals(chargerToUse)) {
chargerStandard220V = new Charger220V();
} else if ("charger-110V".equals(chargerToUse)) {
chargerStandard220V = new Adapter(new Charger110V());
} else {
System.out.println("알 수 없는 규격");
return;
}
Person person = new Person(chargerStandard220V);
person.charge();
}
}
이 코드는 어댑터 패턴을 잘 적용한 좋은 예시라고 볼 수 있다.
interface라는 공통 규격을 만든 후 Person이 ChargerStandard220V만 알고 있다는데 이는 Person은 오직 220V 구격만 사용하는 구조가 된 것이다.
핵심은 Adapter 클래스이다.
class Adapter implements ChargerStandard220V 이 부분이 바로 어댑터 패턴의 핵심이다.
원래 Charger110V는 connect110V() 밖에 없어서 기존 시스템과 호환되지 않는다.
하지만 Adapter가 중간에서 110V를 220V 구격처럼 변환해주고 있다.
즉 Person 입장에서는 connect220V만 호출했는데 실제로 내부에서는 charger110V.connect110V();가 실행되는 것이다.
또한 Main에서 chargerStandard220V = new Adapter(new Charger110V());를 통해 외부의 110V 충전기를 기존 시스템 규격에 맞게 연결하고 있다.
즉 기존 코드를 수정하지 않고 기능을 확장한 것이다.
즉 호환되지 않는 객체를 중간 어댑터로 연결한 전형적인 Adapter Pattern 구조라고 볼 수 있다.
오늘은 어댑터 패턴(Adapter Pattern)에 대해 공부하며 기존 시스템과 외부 객체를 어떻게 연결하는지 배워보는 시간을 가졌다.
처음에는 단순히 “호환되지 않는 객체를 연결한다” 정도로만 이해했는데, 직접 예제를 만들어보면서 왜 이런 패턴이 필요한지 조금 더 체감할 수 있었다.
특히 OrderService가 외부 PayPal API를 직접 처리하던 구조를 어댑터로 분리했을 때 결합도가 낮아지고 유지보수가 쉬워지는 부분이 굉장히 인상적이었다.
또 220V와 110V 충전기 예제를 통해 실제 현실에서도 어댑터 패턴이 어떻게 사용되는지 쉽게 이해할 수 있었다.
기존 코드를 수정하지 않고 중간 어댑터만 추가해서 기능을 확장한다는 점이 가장 핵심이라는 것도 알게 되었다.
패턴들을 공부할수록 단순히 “코드가 돌아가게 만드는 것”과 “유지보수가 쉬운 구조로 만드는 것”은 완전히 다르다는 것을 느끼고 있다.
오늘도 직접 나쁜 예시와 좋은 예시를 비교하면서 왜 객체지향 설계가 중요한지 조금 더 이해할 수 있었던 것 같다.
아직은 익숙하지 않은 부분도 많지만, 계속 직접 코드를 작성하고 구조를 비교해보면서 조금씩 익숙해지고 있는 느낌이다.
앞으로도 다양한 디자인 패턴들을 공부하면서 더 좋은 구조를 만드는 연습을 계속 해봐야겠다.