Adapter Pattern
Adapter Pattern 은 기존의 인터페이스 (Adaptee) 를 클라이언트가 기대하는 인터페이스 (Target) 로 변환하는 구조 패턴이다. 어댑터 (Adapter) 는 Target 인터페이스를 구현하고 Adaptee 객체를 감싸 (합성) 거나 상속하여, 클라이언트가 요구하는 방식으로 기능을 제공한다. 이를 통해 기존 코드 수정 없이 다양한 외부 시스템, 레거시 코드, 라이브러리와의 통합이 가능하며, 코드의 재사용성과 유지보수성을 크게 높일 수 있다.
배경
현재 사용하고 있는 라이브러리가 더 이상 요구에 부합하지 않아 재작성하거나 다른 라이브러리를 사용해야 할 때, 어댑터 패턴을 이용해 기존 코드를 가능한 적게 변경하면서 새로운 라이브러리로 교체할 수 있다.
기존에 있는 시스템에 새로운 써드파티 라이브러리가 추가되거나 레거시 인터페이스를 새로운 인터페이스로 교체하는 경우에 코드의 재사용성을 높일 수 있다.
목적 및 필요성
주요 목적:
- 인터페이스 불일치 해결: 호환되지 않는 인터페이스 간의 연결
- 코드 재사용성 향상: 기존 코드 수정 없이 새로운 환경에서 활용
- 시스템 통합성 확보: 이질적인 시스템들의 원활한 협업
- 유지보수성 개선: 변경 영향 범위 최소화
필요성:
- 레거시 시스템 현대화 프로젝트
- 서드파티 라이브러리 통합
- 마이크로서비스 아키텍처에서 서비스 간 통신
- API 버전 관리 및 호환성 유지
핵심 개념
어댑터 패턴은 호환되지 않는 인터페이스를 가진 클래스들이 함께 동작할 수 있도록 중간에 어댑터를 두어 인터페이스를 변환해주는 구조적 디자인 패턴이다.
기본 개념
- 인터페이스 변환 (Interface Translation): 클래스의 인터페이스를 사용자가 기대하는 다른 인터페이스로 변환하는 것
- Wrapper 개념: 어댑터가 기존 객체를 감싸서 새로운 인터페이스를 제공하는 방식으로, Wrapper 패턴이라고도 불림
- 합성 vs 상속: 클래스 어댑터 (상속) 와 객체 어댑터 (합성) 두 가지 구현 방식
- 중간 계층 (Intermediary Layer): 코드와 레거시 클래스, 타사 클래스 간의 변환기 역할을 하는 중간 레이어
의도 (Intent)
한 클래스의 인터페이스를 클라이언트에서 사용하고자 하는 다른 인터페이스로 변환한다. 어댑터를 이용하면 인터페이스 호환성 문제 때문에 같이 쓸 수 없는 클래스들을 연결해서 쓸 수 있다.
다른 이름 (Also Known As)
- Wrapper Pattern(래퍼 패턴)
동기 (Motivation / Forces)
- 기존 코드나 라이브러리의 인터페이스가 클라이언트 요구와 다를 때, 수정 없이 통합하고 싶을 때
- 외부 시스템, 레거시 코드, 써드파티 라이브러리와의 연동
적용 가능성 (Applicability)
- 기존 클래스를 사용하고 싶지만 그 인터페이스가 나머지 코드와 호환되지 않을 때
- 부모 클래스에 추가할 수 없는 어떤 공통 기능들이 없는 여러 기존 자식 클래스들을 재사용하려는 경우
- 써드파티 라이브러리 통합 시 인터페이스 불일치 해결이 필요한 경우
- 레거시 코드를 새로운 시스템에 통합할 때
패턴이 해결하고자 하는 설계 문제
인터페이스 불일치 문제
- 서로 다른 시스템이나 라이브러리 간의 인터페이스 호환성 부족
- 메서드 이름, 매개변수, 반환값 형식의 차이
레거시 시스템 통합 문제
- 기존 시스템을 수정하지 않고 새로운 시스템과 연동 필요
- 코드 재작성 비용과 리스크 최소화
의존성 관리 문제
- 클라이언트 코드가 특정 구현체에 강하게 결합되는 문제
- 시스템 간 결합도 증가로 인한 유지보수 어려움
문제를 해결하기 위한 설계 구조와 관계
graph TB subgraph "Problem Space" P1[인터페이스 불일치] P2[레거시 시스템 통합] P3[의존성 관리] end subgraph "Solution Structure" S1[Target Interface] S2[Adapter Class] S3[Adaptee Class] end subgraph "Design Relationships" R1[Inheritance/Implementation] R2[Composition/Aggregation] R3[Delegation Pattern] end P1 --> S1 P2 --> S2 P3 --> S3 S1 --> R1 S2 --> R2 S3 --> R3
설계 구조 상세:
계층적 분리 (Layered Separation)
- 클라이언트 계층과 서비스 계층 간의 명확한 분리
- 각 계층의 독립적 발전 가능
인터페이스 추상화 (Interface Abstraction)
- 구체적 구현으로부터 클라이언트 보호
- 다형성을 통한 유연한 구현체 교체
위임 메커니즘 (Delegation Mechanism)
- Adapter 가 실제 작업을 Adaptee 에게 위임
- 책임의 명확한 분리
실무 구현 연관성
인터페이스 호환성 (Interface Compatibility)
- 서로 다른 인터페이스를 가진 클래스들 간의 호환성 확보
- 실무에서는 API 통합, 레거시 시스템 연동 시 필수적 개념
구조적 변환 (Structural Transformation)
- 인터페이스 구조의 변환을 통한 시스템 통합
- 실무에서는 데이터 형식 변환, 프로토콜 변환에 활용
위임 (Delegation)
- Adapter 가 실제 작업을 Adaptee 에게 위임하는 메커니즘
- 실무에서는 Wrapper 클래스 구현 시 핵심 원리
투명성 (Transparency)
- 클라이언트가 Adapter 의 존재를 인식하지 못하는 투명한 연동
- 실무에서는 시스템 간 결합도 최소화에 기여
개념 | 실무 구현 측면 | 연관성 |
---|---|---|
인터페이스 호환성 | API Gateway, Service Mesh | 마이크로서비스 아키텍처에서 서비스 간 통신 표준화 |
구조적 변환 | Data Mapper, Message Translator | ETL 프로세스, 메시지 큐 시스템에서 데이터 변환 |
위임 | Proxy Pattern, Decorator Pattern | AOP(Aspect-Oriented Programming) 구현 |
투명성 | Interface Segregation, Dependency Injection | SOLID 원칙 준수, IoC 컨테이너 설계 |
주요 기능 및 역할
핵심 기능:
인터페이스 변환 (Interface Translation)
- 클라이언트가 요구하는 인터페이스로 변환
- 메서드 시그니처, 파라미터 형식 등 조정
데이터 변환 (Data Transformation)
- 입력/출력 데이터 형식 변환
- 프로토콜 간 데이터 구조 매핑
예외 처리 (Exception Handling)
- Adaptee 의 예외를 Target 인터페이스에 맞게 변환
- 오류 상황에 대한 적절한 대응
주요 역할:
- 브리지 (Bridge): 서로 다른 시스템 간의 연결점
- 번역기 (Translator): 인터페이스 언어 간의 번역
- 중재자 (Mediator): 시스템 간 통신 중재
특징
특징 | 설명 | 실무 적용 예시 |
---|---|---|
투명성 | 클라이언트가 Adapter 존재를 모름 | REST API Gateway 에서 내부 gRPC 서비스 호출 |
단방향성 | 특정 방향으로만 변환 수행 | XML to JSON 변환기 |
조합성 | 여러 Adapter 연쇄 사용 가능 | 프로토콜 스택에서 다중 변환 |
확장성 | 새로운 Adaptee 추가 용이 | 결제 시스템에 새 PG 사 추가 |
핵심 원칙
단일 책임 원칙 (Single Responsibility Principle)
- Adapter 는 오직 인터페이스 변환만 담당
- 비즈니스 로직 추가 금지
개방 - 폐쇄 원칙 (Open-Closed Principle)
- 새로운 Adaptee 추가 시 기존 코드 수정 불필요
- 확장에는 열려있고 수정에는 닫혀있음
의존성 역전 원칙 (Dependency Inversion Principle)
- 고수준 모듈이 저수준 모듈에 의존하지 않음
- 추상화에 의존하는 구조
인터페이스 분리 원칙 (Interface Segregation Principle)
- 클라이언트가 사용하지 않는 메서드에 의존하지 않음
- 필요한 인터페이스만 노출
주요 원리
Adapter 가 중간자 역할, 메서드 호출을 받으면 내부에서 Adaptee 의 메서드 호출 흐름으로 변환
classDiagram Client --> Target Target <|.. Adapter Adapter o-- Adaptee Client --> Adapter: request() Adapter --> Adaptee: specificRequest()
Adapter 는 내부에서 interface call → adaptee conversion → method dispatch 흐름을 수행
작동 원리 및 방식
- 클라이언트 → Adapter 호출 (
Target
메서드) - Adapter 내부에서 Adaptee 변환 로직 수행
- Adaptee 메서드 호출
- Adapter 가 결과를 클라이언트에 반환
구성 요소
구성요소 | 분류 | 설명 | 주요 역할 | 특징 |
---|---|---|---|---|
Target | 필수 | 클라이언트가 기대하는 인터페이스 정의 | 어댑터와의 계약 (클라이언트가 호출할 인터페이스) | 인터페이스 또는 추상 클래스 형태로 정의됨 |
Adapter | 필수 | Target 을 구현하며, 요청을 Adaptee 에 맞게 변환 | 중재자 역할 수행 | Target 을 구현하고 Adaptee 를 포함하거나 상속 |
Adaptee | 필수 | 기존 구현된 클래스 (기능 보유) | 실질적인 처리 담당 | 외부 라이브러리 또는 변경이 어려운 코드 |
Client | 필수 | Target 을 통해 Adapter 에 요청을 보내는 사용자 | 간접적으로 기능 사용 | Adapter 존재를 알지 못하며, Target 만 의존 |
ConcreteTarget | 선택 | Target 의 기본 구현체 (필요 시 사용) | Target 기본 기능 구현 | 추상 Target 을 직접 사용할 수 없는 경우 사용됨 |
Multiple Adaptees | 선택 | 하나의 어댑터가 여러 Adaptee 인스턴스를 처리 | 다양한 Adaptee 를 공통 Target 에 맞게 변환 | 어댑터 내부에 전략 또는 매핑 로직 포함됨 |
Object Adapter 방식
sequenceDiagram participant Client participant Adapter participant Adaptee Client->>Adapter: request() Adapter->>Adapter: validateInput() Adapter->>Adaptee: specificRequest() Adaptee->>Adapter: result Adapter->>Adapter: transformResult() Adapter->>Client: transformedResult
Adapter Pattern 의 구조는 다음과 같은 핵심 구성요소들로 이루어진다:
classDiagram class Target { <<interface>> +request() } class Adapter { +request() -adaptee : Adaptee } class Adaptee { +specificRequest() } class Client { -target : Target +execute() } class ConcreteTarget { +request() } Target <|-- Adapter Adapter --> Adaptee Client --> Target Target <|-- ConcreteTarget
Target
: 클라이언트가 의존하는 추상 인터페이스Adapter
:Target
을 구현하고, 내부적으로Adaptee
를 참조Adaptee
: 기존 시스템 또는 외부 라이브러리의 클래스Client
:Target
을 통해Adapter
를 사용하며, 실제로는Adaptee
의 기능을 사용ConcreteTarget
: 필요 시Target
의 기본 구현체로 사용 가능
Class Adapter 방식
classDiagram class Target { <<interface>> +request() } class Adaptee { +specificRequest() } class Adapter { +request() } class Client { -target : Target +execute() } Adapter <|-- Target Adapter <|-- Adaptee Client --> Target
- Adapter는
Target
인터페이스를 구현하고 동시에Adaptee
를 상속받음 - 상속 기반으로,
Adaptee
의 메서드를 직접 호출하여Target
의 메서드를 구현 - 장점: 성능 및 접근 속도 측면에서 효율적이며, 모든
Adaptee
기능에 접근 가능 - 제약: **다중 상속이 가능한 언어 (C++, Python 등)**에서만 사용 가능
패턴 적용의 결과와 트레이드오프
긍정적 결과:
- 코드 재사용성 향상: 80% 이상의 기존 코드 재활용 가능
- 시스템 유연성 증대: 새로운 구현체 추가 용이성
- 테스트 용이성: Mock 객체를 통한 독립적 테스트
트레이드오프:
- 성능 오버헤드: 추가 계층으로 인한 5-10% 성능 저하
- 복잡성 증가: 클래스 수 증가로 인한 설계 복잡도 상승
- 메모리 사용량: 추가 객체 생성으로 인한 메모리 사용량 증가
구현 기법
구현 기법 | 핵심 개념 | 구성 요소 | 특징 및 장점 | 주요 사용 언어 | 대표 사용 사례 |
---|---|---|---|---|---|
Object Adapter | 합성 (Composition) 기반 | Adapter 가 Target 인터페이스 구현 - Adaptee 를 내부 변수로 포함 | - 단일 상속 언어에서 유리 - 유연한 Adaptee 교체 - 테스트 용이 | Java, Python, JS | 외부 API 어댑터, 마이크로서비스 연동 |
Class Adapter | 상속 (Inheritance) 기반 | Adapter 가 Target , Adaptee 를 모두 상속 | - 메모리 효율적 - Adaptee 의 모든 기능 직접 사용 가능 - 다중 상속 필요 | C++, Python | DB 드라이버 어댑터, 그래픽 인터페이스 |
Two-way Adapter | 양방향 변환 가능 | Adapter 가 Target , Adaptee 인터페이스 모두 구현 - 양방향 변환 메서드 포함 | - 서로 다른 시스템 간 상호 변환 가능 - 통신 양방향 대응 | Python, C++ | Protocol ↔ API 변환, Dual API 어댑터 |
Interface Adapter | 부분 구현 | - 추상 클래스를 기반으로 일부 메서드만 오버라이딩 Default 메서드 제공 가능 | - 인터페이스가 너무 클 때 유리 - 필요한 기능만 선택적으로 구현 | Java, Python | 이벤트 핸들러, 미디어 플레이어 |
Adapter Registry / Factory | 동적 인스턴스 관리 및 검색 | Adapter 인스턴스를 저장하고 키 기반으로 제공 - 다형성 및 전략 패턴과 혼용 가능 | - 런타임 어댑터 선택 - 다양한 Adaptee 관리 가능 OCP 준수 가능 | Java, Python | 멀티 PG 연동, 멀티 클라우드 SDK 연동 |
- Object Adapter: 가장 일반적, 유지보수와 테스트에 유리함.
- Class Adapter: 성능상 유리하나, 유연성이 떨어지고 다중 상속이 필요.
- Two-way Adapter: 양방향 프로토콜 변환 시 필요. 구조가 복잡해질 수 있음.
- Interface Adapter: 이벤트 핸들러처럼 메서드가 많은 인터페이스에 유용.
- Adapter Registry/Factory: 어댑터를 전역적으로 관리할 수 있어 대규모 시스템에 적합.
Object Adapter (객체 어댑터)
- 핵심: Adapter 가
Target
인터페이스를 구현하고, 내부에Adaptee
객체를 포함(Composition) - 장점: 유연한 교체, 테스트 용이, 단일 상속 언어에서 사용 가능
- 용도: 외부 API, 마이크로서비스 연동, 오픈소스 통합
|
|
Class Adapter (클래스 어댑터)
- 핵심: Adapter 가
Target
,Adaptee
를 모두 상속 - 장점: Adaptee 의 기능 직접 활용 가능
- 제약: Python 에서는 다중 상속 가능하지만, Java 등에서는 제한 있음
|
|
Two-Way Adapter (양방향 어댑터)
- 핵심: 하나의 어댑터가
Target
,Adaptee
역할 모두 수행 - 장점: 서로 다른 시스템 간 양방향 통신 가능
- 용도: Protocol ↔ API 변환, 구형 ↔ 신형 API 상호 연동
|
|
Interface Adapter (인터페이스 어댑터)
- 핵심: 인터페이스가 너무 클 때, 필요한 메서드만 오버라이딩
- 장점: 간편한 구현, 코드 중복 감소
- 용도: 이벤트 핸들러, UI 컴포넌트, 미디어 처리
|
|
Adapter Factory / Registry
- 핵심: 여러 어댑터들을 동적으로 생성하고 등록하여 관리
- 장점: 전략 패턴과 결합 시 어댑터 구성 변경이 용이
- 용도: 다수 PG/DB 연동, SDK 자동 매핑 등
|
|
요약 비교
구현 기법 | 방식 | 특징 요약 | 실무 사용 예시 |
---|---|---|---|
Object Adapter | 합성 기반 | 유연성 높고 일반적인 방식 | 외부 API, SOAP 연동 |
Class Adapter | 다중 상속 | 성능 우수, Adaptee 기능 직접 활용 가능 | C++ 드라이버, 그래픽 렌더링 |
Two-way Adapter | 양방향 변환 | 상호 변환 가능, 컨텍스트 의존 | 양방향 통신, SDK ↔ REST 호환 |
Interface Adapter | 부분 구현 | 필요한 메서드만 구현 가능 | 이벤트 리스너, UI 처리 |
Factory/Registry | 런타임 관리 | Adapter 동적 선택 및 관리 가능 | PG 연동, 클라우드 API 변환 |
장점
구분 | 항목 | 설명 | 기여 요인 / 설계 원칙 |
---|---|---|---|
호환성 | 인터페이스 호환성 | 서로 다른 인터페이스를 가진 시스템이나 클래스가 함께 동작 가능 | 어댑터의 인터페이스 변환 기능 |
재사용성 | 기존 코드 재사용 | 검증된 Adaptee 로직을 수정 없이 재활용 가능 | 기존 클래스 유지, 중복 제거 |
캡슐화 | 클라이언트 코드 변경 최소화 | 클라이언트는 어댑터를 통해 동작하므로 기존 코드 수정 없이 새 기능 통합 가능 | 인터페이스 기반 설계 |
유연성 | 다양한 Adaptee 연동 지원 | 다양한 구현체나 외부 시스템을 동일한 인터페이스로 통합 가능 | 다형성, 어댑터 확장성 |
확장성 | 신규 Adaptee 확장 용이 | 어댑터만 추가하면 새로운 외부 시스템도 통합 가능 | 개방 - 폐쇄 원칙 (OCP), 구성 기반 확장 |
테스트 용이성 | Mock 어댑터 지원 | 어댑터를 Mock 객체로 대체하여 단위 테스트 수행 가능 | 테스트 격리, 유닛 테스트 강화 |
설계 원칙 준수 | 단일 책임 원칙 (SRP) | 어댑터는 변환 로직만 담당하여 인터페이스와 비즈니스 로직을 분리 | 책임 분리 |
설계 원칙 준수 | 개방 - 폐쇄 원칙 (OCP) | 어댑터 추가만으로 기능 확장 가능하며 기존 로직 수정 불필요 | 모듈화 및 인터페이스 기반 설계 |
구조적 이점 | 시스템 분리성 | 클라이언트와 서비스 구현체 간의 결합도 감소, 변경 전파 최소화 | 중간 계층을 통한 추상화 |
사용자 관점 편의 | 투명성 | 클라이언트는 어댑터 존재를 의식하지 않고 기존 방식대로 사용 가능 | 인터페이스 일치로 인한 무중단 통합 |
단점 및 문제점
단점
항목 | 설명 | 해결 방안 |
---|---|---|
클래스 수 증가 | 어댑터 클래스가 많아지면 구조가 복잡해지고 유지보수가 어려워짐 | 명확한 네이밍 규칙, 패키지 분리, 모듈화 설계 적용 |
성능 오버헤드 | 중간 계층을 추가함으로써 호출 체인이 길어져 실행 성능이 저하될 수 있음 | 불필요한 로직 제거, 핵심 기능만 래핑, 캐싱 및 배치 처리 도입 |
디버깅 어려움 | 여러 계층을 거친 호출로 인해 로그 추적과 디버깅이 복잡해짐 | 로그 체계 강화, 분산 트레이싱 도입 (OpenTelemetry 등), 단계별 검증 삽입 |
강결합 유도 | Adaptee 에 대한 직접적인 의존도가 높아지면 유연성이 감소할 수 있음 | 인터페이스 기반 설계, 의존성 주입 (DI) 적용, 느슨한 결합 원칙 준수 |
인터페이스 제약 | Target 인터페이스가 Adaptee 의 모든 기능을 감싸지 못할 수 있음 | 다중 인터페이스 분리, Facade 패턴과 조합하여 세분화된 기능별 분리 설계 |
과도한 사용 | 모든 호환 문제에 무조건 어댑터를 적용하면 오히려 설계가 비효율적이 될 수 있음 | 필요성 검토 후 제한적 적용, 통합 전략과 설계 목적 명확화 |
문제점
항목 | 원인 | 영향 | 탐지 및 진단 | 예방 방법 | 해결 방법 및 기법 |
---|---|---|---|---|---|
메모리 누수 | Adapter 가 Adaptee 를 장기 참조하고 GC 대상이 되지 않음 | 메모리 사용량 증가, 성능 저하 | Heap 분석, 메모리 프로파일러 | 명시적 해제 로직 구현, 약한 참조 사용 (WeakReference ) | 객체 수명 주기 관리, 자동 자원 해제 (contextlib , with ) |
순환 의존성 | Adapter 와 Adaptee 가 서로 참조하여 GC 되지 않는 순환 구조 발생 | 메모리 누수, StackOverflow, 교착 가능성 | 의존성 분석, 정적 분석 도구 사용 | 방향성 있는 의존 설계, 중간 이벤트 버스나 중계자 사용 | 이벤트 기반 구조 (Observer, Mediator) 로 전환 |
타입 안전성 부족 | 동적 캐스팅 또는 비정형 객체 사용으로 인한 런타임 오류 | TypeError , ClassCastException , 기능 오작동 | 정적 분석, 타입 검사기 (Mypy, TypeScript) 사용 | 제네릭 및 타입 힌트 적용, 강타입 설계 | 타입 가드 도입, 입력 검증 로직 추가 |
설정 불일치 | 어댑터와 어댑티 간 설정 또는 인코딩 방식 등이 달라 발생 | 실행 오류, 결과 왜곡, 예외 발생 | 통합 테스트, 설정 유효성 검사 추가 | 기본값 정의, 명시적 설정 명세 사용 | 설정 매핑 테이블, 체크포인트 기반 설정 검증 |
동기화 문제 | 멀티스레드 환경에서 어댑터 내부 공유 자원 접근 시 상태 불일치 발생 | 데이터 손실, Race condition, 교착상태 | 동시성 테스트, Thread Dump 분석 | 불변 객체 사용, Thread-safe 구조 설계 | Lock , Queue , Atomic , Concurrent 자료구조 활용 |
다중 어댑터 관리 어려움 | 다양한 외부 시스템을 연결할 때 어댑터 구성 및 유지가 복잡해짐 | 유지보수 비용 증가, 시스템 확장성 저하 | 구성 변경 시 테스트 실패 및 관리 혼란 | 어댑터 등록/관리 메타데이터화, 명세 기반 관리 도입 | Adapter Factory 또는 Registry 패턴 도입 |
로직 중복 | 여러 Adapter 에 유사한 변환 로직이 반복 구현됨 | 유지보수성 악화, 코드 중복 | 코드 리뷰, 중복 검사 도구 사용 | 공통 인터페이스/변환 계층 도출 | Abstract Adapter 또는 유틸 기반 공통 모듈화 |
호환성 오류 | Adaptee 인터페이스 변경 후 Adapter 비호환 발생 | 시스템 통합 실패, 예외 발생 | 리그레션 테스트, 타입 시스템 적용 | 변경 감지 테스트 도입, 계약 기반 설계 적용 | 단위 테스트 강화, Wrapper 수정/리팩토링 |
도전 과제 및 해결책
카테고리 | 도전 과제 | 원인 | 영향 | 예방 방법 | 해결 방안 및 기법 |
---|---|---|---|---|---|
설계/구조 | 다양한 외부 인터페이스 대응 | 서로 다른 외부 시스템/라이브러리 연동 필요 | 어댑터 수 증가, 구조 복잡화 | 추상화 수준 재정의, 중간 변환 계층 도입, 공통 인터페이스 설계 | 어댑터 팩토리/레지스트리 적용, 계층화 설계, 전략 어댑터 사용 |
상속 기반 어댑터 한계 | 클래스 상속 기반은 언어 제한 존재 (ex. Java 다중 상속 불가) | 유연성 제한, 테스트 어려움 | 상속보다 합성 (Composition) 우선 | 객체 어댑터 방식 적용, 인터페이스 기반 설계로 전환 | |
복잡성 관리 | 어댑터 과다 사용 | 무분별한 적용 또는 비즈니스 로직 포함 | 유지보수성 저하, 코드 중복, 테스트 어려움 | SRP(단일 책임 원칙) 준수, 필요한 경우에만 도입, 설계 단계에서 명확한 기준 수립 | 어댑터 전용 추상 계층 구성, 코드 생성 도구 사용, 문서화 강화 |
레거시 시스템 연동 | 기존 시스템 전체 교체 어려움 | 기술 부채 증가, 장애 복원력 저하 | 점진적 마이그레이션 전략 (Strangler Fig Pattern), 경계 기반 리팩토링 | 이벤트 소싱 + CQRS 기반 전환, 중간 어댑터 계층 분리 | |
성능 최적화 | 계층 오버헤드 및 데이터 변환 비용 | 어댑터 계층 추가로 인한 호출/변환 오버헤드 | 응답 지연, 처리량 감소, 리소스 과소 활용 | 프로파일링 기반 설계 검토, 경량 구조 설계 | 캐싱 적용, 비동기 처리 도입, Lazy Initialization 전략 도입 |
실시간 데이터 스트리밍 변환 | 대용량 실시간 데이터 처리 및 포맷 변환 요구 | 지연시간 증가, 실시간 처리 실패 가능성 | 스트리밍 아키텍처 설계, 처리 방식 단순화 | Apache Kafka Streams / Flink 기반 변환 처리 | |
테스트 전략 | 어댑터 내부 호출의 테스트 어려움 | Adaptee 호출이 숨겨져 있어 테스트 불가능 또는 어려움 | 테스트 커버리지 부족, 결함 검출 지연 | 인터페이스 주입 (DI), 인터페이스 기반 설계 | Mock/Stub 도입, 테스트 더블 기반 단위 테스트 구성 |
다양한 어댑터 구성의 테스트 복잡도 증가 | 시스템 통합이 많은 경우 각 어댑터별 테스트 환경 요구 | 테스트 자동화 난이도 상승, 검증 누락 가능성 | 테스트 계층 분리, 계약 기반 테스트 도입 | CI 파이프라인 연동, 시나리오 기반 테스트 자동화 | |
운영/모니터링 | 인터페이스 변경의 영향 파급 | 외부 API 또는 라이브러리가 변경될 가능성 존재 | Adapter 실패, 시스템 장애 | 인터페이스 변경 감지 도구 도입, 계약 기반 테스트 적용 | 버전 명시 및 관리, 자동화 테스트 도입, 모의 (Mock) Adaptee 구성 |
장애 추적 어려움 | 어댑터 계층 내부 처리가 로깅/트레이싱되지 않음 | 원인 진단 지연, 장애 확산 가능성 | 분산 트레이싱 (예: OpenTelemetry), 어댑터 내부 로깅 표준화 적용 | 메트릭 기반 알림 설정, APM 연계, 구조적 로깅 적용 | |
아키텍처 통합 | 서비스 메시 환경에서 통신 복잡도 증가 | 마이크로서비스 간 이기종 통신 방식 존재 | 통신 비용 증가, 유지보수 어려움 | 표준화된 API 계약 정의, 공통 프로토콜 지정 | API Gateway + Service Mesh 도입, Protocol Buffers 기반 통합 |
분류 기준에 따른 종류 및 유형
분류 기준 | 종류/유형 | 특징 | 적용 상황 |
---|---|---|---|
구현 방식 | Object Adapter | 합성 사용, 런타임 유연성 | 단일 상속 언어, 다중 Adaptee 지원 |
구현 방식 | Class Adapter | 상속 사용, 컴파일타임 결정 | 다중 상속 지원 언어, 양방향 변환 |
방향성 | 단방향 Adapter | 한 방향으로만 변환 | API 통합, 데이터 형식 변환 |
방향성 | 양방향 Adapter | 양방향 변환 지원 | 시스템 간 상호 운용성 |
범위 | 인터페이스 Adapter | 전체 인터페이스 변환 | 완전한 시스템 통합 |
범위 | 메서드 Adapter | 특정 메서드만 변환 | 부분적 기능 통합 |
데이터 처리 | 동기 Adapter | 즉시 처리 및 응답 | 실시간 요구사항 |
데이터 처리 | 비동기 Adapter | 지연 처리, 콜백 사용 | 대용량 데이터, 네트워크 통신 |
실무 사용 예시
분야 | 예시 / 적용 대상 | 설명 | 구현 방식 |
---|---|---|---|
외부 API 연동 | 결제 (PG), 인증, 메시징 API 어댑터 | 서로 다른 외부 API 인터페이스를 내부 시스템에 맞게 통합 | 객체 어댑터, 전략 어댑터 적용 |
레거시 시스템 통합 | OldService → NewService, 레거시 DB/서비스 래퍼 | 기존 시스템을 신규 인터페이스에 맞게 마이그레이션 또는 연동 | 추상 어댑터 + 어댑터 팩토리 조합 |
데이터 변환 | XML ↔ JSON, Kafka ↔ AWS SNS/SQS | 포맷 및 메시지 프로토콜 간 변환 수행 | 메시지 어댑터, Serializer 어댑터 |
클라우드 및 플랫폼 | AWS SDK, Azure SDK, GCP 클라이언트 | 멀티 클라우드 환경에서 벤더 종속성 제거 및 표준화 | 플랫폼 추상화 어댑터 |
마이크로서비스 아키텍처 | API Gateway, gRPC ↔ REST | 마이크로서비스 간 통신 방식 변환 (REST ↔ gRPC 등) | 마이크로서비스용 네트워크 어댑터 |
로깅 시스템 | SLF4J ↔ Log4j, 외부 로깅 → 내부 Logger | 다양한 로깅 백엔드를 표준 인터페이스로 통합 | 인터페이스 기반 어댑터 |
프론트엔드/모바일 | React Props 어댑터, RecyclerView Adapter | 비동기 응답 데이터를 UI 컴포넌트에 맞게 변환 | 함수형 어댑터, 추상 클래스 상속 |
파일 및 스트림 처리 | InputStream → Reader, BufferedReader | 입출력 포맷이나 방식의 변환을 위한 래퍼 클래스 사용 | 다중 레벨 어댑터 |
Java 표준 API | InputStreamReader, Collections.enumeration() | 자바 내장 어댑터 예시로, 객체 간 인터페이스 변환 수행 | 내부 클래스, 표준 어댑터 구현 |
UI/그래픽 컴포넌트 | 호환되지 않는 위젯 통합, 뷰 모델 매핑 | 다양한 프레임워크 또는 디자인 시스템 간 UI 컴포넌트 연동 | 컴포넌트 매핑 어댑터 |
데이터베이스 연동 | JDBC ↔ ORM (JPA), Connection Pool 어댑터 | DB 벤더 간 호환성 확보 및 마이그레이션 유연성 확보 | ORM 어댑터, 커넥션 추상화 계층 |
통합 메시징 | Event Bus ↔ MQ ↔ Kafka | 서로 다른 메시지 시스템 간의 메시지 포맷/전송 방식 통합 | 메시지 브로커 어댑터 |
인증/보안 연동 | OAuth2, SAML, JWT 기반 IdP 어댑터 | 다양한 인증 제공자를 하나의 인터페이스로 추상화 | 인증 토큰 어댑터, 인터페이스 래핑 |
활용 사례
사례 1: 레거시 지불 시스템 연동
배경:
- 신규 e-commerce 플랫폼에서는
PaymentProcessor
인터페이스를 사용하지만, 레거시 시스템은 LegacyPaymentService.processOldPayment()` 메서드만 제공
어댑터 구성:
|
|
Workflow:
- 사용자는
PaymentProcessor
를 통해 결제 요청 PaymentAdapter
가 요청을 변환LegacyPaymentService
가 처리
사례 2: 외부 메일 발송 솔루션 교체
시스템 구성:
- 기존 MailSenderA(Adaptee), 신규 MailSenderB(Adaptee)
- Target: 기존 시스템에서 사용하는 메일 발송 인터페이스
- Adapter: MailSenderB → Target 변환
- Client: 기존 시스템
Workflow:
- 기존 시스템은 Target 인터페이스만 사용
- MailSenderB 를 Adapter 로 감싸 Target 인터페이스로 변환
- 시스템 코드 수정 없이 신규 솔루션 적용
|
|
사례 3: 대형 전자상거래 플랫폼의 결제 시스템 통합
시스템 구성
Frontend Application Layer
- 사용자 인터페이스: React 기반 SPA
- 장바구니 서비스: Node.js 기반 마이크로서비스
Backend Services Layer
- 결제 서비스: Spring Boot 기반 핵심 비즈니스 로직
- 결제 어댑터: 다양한 PG 사 API 통합을 위한 Adapter Pattern 구현
External Payment Gateways
- 카드결제 PG: 신용카드/체크카드 처리
- 간편결제 PG: 카카오페이, 네이버페이 등
- 계좌이체 PG: 실시간 계좌이체
- 은행 API: 직접 은행 연동
graph TB subgraph "Frontend Application" UI[사용자 인터페이스] Cart[장바구니 서비스] end subgraph "Backend Services" PaymentService[결제 서비스] PaymentAdapter[결제 어댑터] end subgraph "External Payment Gateways" PG1[카드결제 PG] PG2[간편결제 PG] PG3[계좌이체 PG] Bank[은행 API] end UI --> Cart Cart --> PaymentService PaymentService --> PaymentAdapter PaymentAdapter --> PG1 PaymentAdapter --> PG2 PaymentAdapter --> PG3 PaymentAdapter --> Bank classDef frontend fill:#e1f5fe classDef backend fill:#f3e5f5 classDef external fill:#fff3e0 class UI,Cart frontend class PaymentService,PaymentAdapter backend class PG1,PG2,PG3,Bank external
Workflow:
sequenceDiagram participant User as 사용자 participant UI as 프론트엔드 participant PaymentService as 결제서비스 participant PaymentAdapter as 결제어댑터 participant PG as PG사 User->>UI: 결제 요청 UI->>PaymentService: 결제 처리 요청 PaymentService->>PaymentService: 주문 검증 PaymentService->>PaymentAdapter: 결제 실행 PaymentAdapter->>PaymentAdapter: PG사 선택 및 데이터 변환 PaymentAdapter->>PG: PG사별 API 호출 PG->>PaymentAdapter: 결제 결과 응답 PaymentAdapter->>PaymentAdapter: 응답 데이터 표준화 PaymentAdapter->>PaymentService: 표준화된 결과 반환 PaymentService->>PaymentService: 결제 결과 처리 PaymentService->>UI: 결제 완료 응답 UI->>User: 결제 결과 표시
Adapter Pattern 의 역할:
인터페이스 통합
- 각 PG 사의 서로 다른 API 스펙을 표준 인터페이스로 통합
- 결제 서비스는 하나의 인터페이스만 알면 모든 PG 사 이용 가능
데이터 변환
- 주문 정보를 각 PG 사가 요구하는 형식으로 변환
- 응답 데이터를 표준 형식으로 변환
오류 처리 표준화
- 각 PG 사의 서로 다른 오류 코드를 표준 오류 형식으로 변환
- 일관된 오류 처리 로직 제공
PG 사마다 다른 API 제공. 시스템 내부는BillingService
인터페이스만 사용함.
Adapter 구조:
classDiagram BillingService <|.. PG1Adapter BillingService <|.. PG2Adapter PG1Adapter --> PG1API PG2Adapter --> PG2API Client --> BillingService
Adapter 유무에 따른 차이점
- Adapter 패턴 적용 전:
- 각 PG 사별로 별도의 결제 로직 필요
- 새로운 PG 사 추가 시 핵심 서비스 코드 수정 필요
- 코드 중복 및 유지보수 비용 증가
- 테스트 복잡도 증가
- Adapter 패턴 적용 후:
- 단일 결제 인터페이스를 통한 모든 PG 사 이용
- 새로운 PG 사 추가 시 Adapter 만 구현하면 됨
- 핵심 비즈니스 로직과 PG 사 연동 로직 분리
- 독립적인 테스트 및 배포 가능
구현 예시
Javascript
|
|
JavaScript: 내부 결제 시스템
|
|
Python
|
|
이 코드는 클라이언트는
BillingService
만 알면 되고, PG API 변경은PG1Adapter
만 수정하면 되는 구조로, Adapter 패턴의 구조적 이점을 잘 보여준다.
Python: 결제 서비스
|
|
Java
|
|
Java: GraphQL → REST Adapter
기존 REST API(GET /users/{id}
) 를 사용하는 시스템을 GraphQL API 를 통해 통합 제공해야 함.
GraphQL 스키마 정의
구성도
REST API 응답 예시
Java (Spring Boot + GraphQL) 어댑터 구현
1 2 3 4 5 6 7 8 9 10 11 12
// 어댑터: REST API → GraphQL DTO 변환 @Component public class UserAdapter { public UserDTO fromRest(RestUserResponse restUser) { UserDTO dto = new UserDTO(); dto.setId(String.valueOf(restUser.getUserId())); dto.setName(restUser.getUsername()); dto.setEmail(restUser.getEmailAddress()); return dto; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
// GraphQL Resolver @Component public class UserQueryResolver implements GraphQLQueryResolver { @Autowired private RestTemplate restTemplate; @Autowired private UserAdapter adapter; public UserDTO user(Long id) { String url = "http://internal-service/api/v1/users/" + id; RestUserResponse restUser = restTemplate.getForObject(url, RestUserResponse.class); return adapter.fromRest(restUser); } }
Adapter vs. Facade vs. Decorator
구분 | Adapter (어댑터) | Facade (퍼사드) | Decorator (데코레이터) |
---|---|---|---|
목적 | 인터페이스 호환 | 복잡한 서브시스템을 단순화 | 객체 기능 확장 |
구조 | 클라이언트 ↔ Adapter ↔ Adaptee | 클라이언트 ↔ Facade ↔ 복잡한 하위 컴포넌트 | 객체 ↔ 여러 데코레이터 체인 |
동작 | 요청 변환 후 기존 객체에 위임 | 여러 객체를 묶어 하나의 API 처럼 제공 | 기존 객체에 기능을 동적으로 추가 |
사용 사례 | 레거시 코드 연동, 호환성 해결 | 서브시스템 단순화, 외부 API 감싸기 | 기능 추가 (로깅, 캐싱 등) |
변경 여부 | 인터페이스 변경 없이 사용 | 내부 구현 감추고 단순화 | 기존 클래스를 수정하지 않음 |
예시 | SLF4J ↔ Log4j 어댑터 | Spring RestTemplate 내부 구현 | Java IO 의 BufferedReader, GzipInputStream |
- Adapter: " 호환되지 않는 인터페이스를 맞춰줌 "
- Facade: " 복잡한 로직을 간단하게 감싸줌 "
- Decorator: " 기존 객체의 기능을 유연하게 확장함 "
공통 주제: Audio Player
- 다양한 오디오 형식 (mp3, wav 등) 을 재생
- Adapter: 기존 클래스 호환을 위해
- Facade: 복잡한 하위 시스템 단순화
- Decorator: 기능을 확장 (예: 이펙트, 로그, 필터)
항목 | Adapter | Facade | Decorator |
---|---|---|---|
목적 | 기존 클래스를 호환 가능한 인터페이스로 변환 | 복잡한 서브시스템을 단순한 API 로 감쌈 | 기능을 동적으로 추가, 확장 |
구조 | Adaptee → Adapter → Target | 여러 하위 시스템 → Facade → 사용자 | Component → Decorator → 확장 기능 |
적용 대상 | 레거시 시스템 또는 외부 API | 라이브러리, 프레임워크 초기화 또는 멀티 구성 단계 | 기능 확장이 필요한 모듈형 시스템 |
기능 추가 | ❌ (기능 변경 없음) | ❌ (단순화만, 기능 추가 없음) | ✅ (기능 확장에 적합, 체이닝 가능) |
예시 키워드 | 호환성, 포맷 변환, 레거시 래핑 | 간편 API, 복잡도 은닉, 사용성 향상 | 로그, 이펙트, 필터, 권한 체크 등 기능 체이닝 |
- Adapter: " 이거 형식이 안 맞는데 맞춰 써야 해!" → 호환 목적
- Facade: " 너무 복잡해서 못 쓰겠어! 한 줄로 끝내줘 " → 단순화 목적
- Decorator: " 여기에 기능 하나만 더 붙여보자 " → 기능 확장 목적
Adapter Pattern
기존 클래스의 인터페이스를 변경 없이 다른 인터페이스에 맞춰주는 패턴
→ 예전 라이브러리를 최신 시스템에 맞춰 사용
코드 예시
|
|
Facade Pattern
복잡한 서브 시스템을 단순화된 API 로 감싸서 제공
→ 사용자에게 쉬운 사용 경험 제공
코드 예시
|
|
Decorator Pattern
원래 객체를 감싸면서 기능을 확장
→ 런타임에 효과, 필터, 로깅 등을 추가
코드 예시:
|
|
실무에서 효과적으로 적용하기 위한 고려사항 및 주의할 점
카테고리 | 고려사항 | 설명 | 권장사항 | |
---|---|---|---|---|
설계 | 인터페이스 명세 정의 | 클라이언트 요구와 Target 인터페이스의 계약 정의 필요 | Target 인터페이스 우선 설계, 명확한 명세 문서화, 도메인 전문가와 협업 필요 | |
클래스 vs 객체 어댑터 선택 | 언어 특성에 따라 클래스 어댑터 (상속) 사용이 제한될 수 있음 | 객체 어댑터 (합성) 방식 우선 적용, 특히 Java 등 다중 상속 제한 언어에서 추천 | ||
책임 분리 | 어댑터에 도메인 로직이 섞이지 않도록 설계 필요 | 로직 최소화, SRP(단일 책임 원칙) 준수 | ||
인터페이스 일관성 | 다양한 Adaptee 관리 시 인터페이스 일관성 필요 | 공통 Target 인터페이스 정의, 표준화된 계약 사용 | ||
중복 방지 | 다양한 시스템과 연동 시 어댑터 계층이 중복될 수 있음 | 추상 어댑터/팩토리 패턴 도입, 공통화 구조 설계 | ||
성능 | 변환 오버헤드 | 어댑터 계층에서의 데이터 변환 비용 존재 | 병목 구간 프로파일링, 캐싱 및 배치 처리 적용 | |
계층 오버헤드 | 계층 과도화로 인한 응답 시간 증가 | 계층 최소화 또는 직접 통합 검토, 경량 어댑터 설계 | ||
성능 테스트 필요성 | 고빈도 호출 시 어댑터 성능 영향 확인 필수 | 성능 프로파일링 수행, 시나리오 기반 부하 테스트 | ||
테스트 | 단위 테스트 가능성 | Adaptee 내부 호출 시 테스트 곤란 가능성 존재 | DI 기반 설계, Mock 또는 Stub 활용, 어댑터 단위 테스트 강화 | |
통합 테스트 복잡도 | 다수 시스템과의 연동으로 테스트 복잡도 증가 | 테스트 환경 분리, 단계별 시나리오 테스트, 테스트 자동화 도구 활용 | ||
운영 | 예외 처리 | Adaptee 의 예외를 클라이언트에 맞게 매핑 필요 | 의미 있는 예외 메시지 변환, Target 인터페이스의 표준 에러 설계 적용 | |
모니터링 및 추적 | 장애 추적과 성능 모니터링이 어려울 수 있음 | 분산 트레이싱 (OpenTelemetry 등) 도입, 메트릭 기반 알림 구성 | ||
유지보수 | 버전 호환성 | Adaptee 변경 시 Target 과의 호환성 고려 필요 | 계약 테스트 적용, 버전 명시 및 점진적 롤아웃 전략 적용 | |
복잡성 증가 | 어댑터 수 증가 시 유지보수 난이도 증가 | 모듈화, 어댑터 레지스트리 적용, 코드 생성 도구 활용 | ||
문서화 | 어댑터 내부 로직과 변환 기준이 불명확할 수 있음 | 변환 규칙과 예외 처리 기준을 포함한 명세 문서 작성 | ||
보안 | 민감 데이터 처리 | 인증 정보 또는 개인정보 등 처리 시 주의 필요 | 암호화, 토큰화 적용 및 보안 감사 로그 구축 |
테스트 전략
단위 테스트 전략
- 목표:
CardPaymentAdapter
클래스의 동작을 외부 시스템의 영향 없이 독립적으로 검증 - 대상: 어댑터 계층이 올바르게 요청을 전달하고, 응답을 변환하며, 예외를 처리하는지 확인
- 접근법: 외부 의존성 (
CardPaymentGateway
) 을 Mock으로 대체하여 어댑터의 책임만 테스트
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 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
import unittest from unittest.mock import Mock, MagicMock class TestPaymentAdapter(unittest.TestCase): """결제 어댑터 단위 테스트""" def setUp(self): self.mock_gateway = Mock(spec=CardPaymentGateway) self.adapter = CardPaymentAdapter(self.mock_gateway) def test_successful_payment(self): """성공적인 결제 테스트 성공 케이스를 Mock 응답으로 설정하고, 어댑터가 `SUCCESS` 상태를 반환하는지 검증""" # Given self.mock_gateway.charge_card.return_value = { "result_code": "SUCCESS", "tx_ref": "TX_12345", "approval_number": "APP_67890" } # When result = self.adapter.process_payment(100.0, "USD", "card", "user_001") # Then self.assertEqual(result.status, PaymentStatus.SUCCESS) self.assertEqual(result.transaction_id, "TX_12345") self.mock_gateway.charge_card.assert_called_once() def test_failed_payment(self): """실패한 결제 테스트 결제 실패 응답 시 어댑터가 적절한 실패 상태 및 메시지를 반환하는지 검증 """ # Given self.mock_gateway.charge_card.return_value = { "result_code": "FAIL", "error_message": "Insufficient funds", "tx_ref": "TX_12346" } # When result = self.adapter.process_payment(100.0, "USD", "card", "user_001") # Then self.assertEqual(result.status, PaymentStatus.FAILED) self.assertIn("Insufficient funds", result.message) def test_exception_handling(self): """예외 처리 테스트 예외 발생 시 어댑터가 **Fail-safe** 방식으로 예외를 처리하고, 실패 상태로 응답하는지 확인 """ # Given self.mock_gateway.charge_card.side_effect = Exception("Network error") # When result = self.adapter.process_payment(100.0, "USD", "card", "user_001") # Then self.assertEqual(result.status, PaymentStatus.FAILED) self.assertIn("Network error", result.message)
- 목표:
통합 테스트 전략
- 목표: 어댑터와 실제 비즈니스 로직, 백엔드 시스템 간의 전체 연동 흐름을 검증
- 대상:
PaymentService
전체 흐름 (process
,verify
,cancel
) 이 일관성 있게 동작하는지 확인 - 접근법: 어댑터가 연결된 실제 서비스 또는 Stub 을 통해 종단 간 흐름을 검증
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
class TestPaymentIntegration(unittest.TestCase): """결제 시스템 통합 테스트""" def setUp(self): self.payment_service = PaymentService() def test_end_to_end_payment_flow(self): """전체 결제 플로우 테스트""" # Given payment_method = "card" amount = 100.0 currency = "USD" user_id = "test_user" # When - 결제 처리 # `process_payment()` 호출 → 어댑터 호출 및 결제 요청 전달 payment_result = self.payment_service.process_payment( payment_method, amount, currency, user_id ) # Then - 결제 성공 확인 self.assertEqual(payment_result.status, PaymentStatus.SUCCESS) transaction_id = payment_result.transaction_id # When - 결제 검증 # `verify_payment()` 호출 → 트랜잭션 ID 기반 상태 확인 verify_result = self.payment_service.verify_payment( payment_method, transaction_id ) # Then - 검증 성공 확인 self.assertEqual(verify_result.status, PaymentStatus.SUCCESS) # When - 결제 취소 # `cancel_payment()` 호출 → 결제 취소 로직 검증 cancel_result = self.payment_service.cancel_payment( payment_method, transaction_id ) # Then - 취소 성공 확인 self.assertEqual(cancel_result.status, PaymentStatus.CANCELLED)
성능 테스트 전략
- 목표: 다수의 동시 요청에 대해 어댑터 및 결제 서비스가 지속적으로 안정적인 성능을 제공하는지 검증
- 대상:
PaymentService
의 응답 시간, 성공률, 동시성 처리 능력 - 접근법:
ThreadPoolExecutor
로 병렬 요청을 시뮬레이션하여 부하 상황을 재현
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
import time import threading from concurrent.futures import ThreadPoolExecutor class TestPaymentPerformance(unittest.TestCase): """결제 시스템 성능 테스트""" def test_concurrent_payments(self): """동시 결제 처리 성능 테스트""" payment_service = PaymentService() # 동시에 전송할 요청 수 (100건 등) concurrent_requests = 100 # max_workers: 실제 동시 스레드 수 (10개 스레드로 분산 실행) def process_payment(): return payment_service.process_payment( "card", 10.0, "USD", f"user_{threading.current_thread().ident}" ) start_time = time.time() with ThreadPoolExecutor(max_workers=10) as executor: futures = [executor.submit(process_payment) for _ in range(concurrent_requests)] results = [future.result() for future in futures] end_time = time.time() processing_time = end_time - start_time # 성능 검증 # 전체 요청을 5초 이내에 처리할 수 있는지 측정 → 성능 기준치 설정 self.assertLess(processing_time, 5.0) # 5초 이내 처리 success_count = sum(1 for r in results if r.status == PaymentStatus.SUCCESS) self.assertGreaterEqual(success_count / concurrent_requests, 0.95) # 95% 이상 성공률
리팩토링 전략
레거시 코드 점진적 리팩토링
- 목표: 전체 시스템을 한 번에 바꾸는 것이 어려울 때, 기존 시스템 (Legacy) 과 새 시스템 (Modern) 을 공존시키며 점진적으로 전환
- 전략: 요청 트래픽을 일정 비율로 모던 시스템에 분산 라우팅하여 리스크를 줄이는 방식 (Strangler Fig 패턴 기반)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
class LegacyRefactoringStrategy: """레거시 시스템 점진적 리팩토링 전략""" def __init__(self, legacy_system, modern_interface): self.legacy_system = legacy_system self.modern_interface = modern_interface self.migration_percentage = 0.0 def migrate_traffic(self, percentage: float): """트래픽 점진적 마이그레이션""" self.migration_percentage = min(percentage, 100.0) def process_request(self, request): """요청을 레거시/모던 시스템으로 라우팅""" import random if random.random() * 100 < self.migration_percentage: # 모던 시스템으로 라우팅 return self.modern_interface.process(request) else: # 레거시 시스템으로 라우팅 return self.legacy_system.process(request)
어댑터 성능 최적화
- 목표: 어댑터 계층에서 발생하는 불필요한 반복 호출과 리소스 사용을 줄여 성능을 최적화
- 전략:
- 결과를 LRU 캐시에 저장해 반복 호출을 회피
- 외부 시스템 호출 시 커넥션 풀을 활용해 연결 비용 절감
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
class OptimizedAdapter(PaymentProcessor): """성능 최적화된 어댑터""" def __init__(self, adaptee, cache_size=1000): self.adaptee = adaptee self.result_cache = LRUCache(cache_size) self.connection_pool = ConnectionPool(max_connections=10) def process_payment(self, amount, currency, payment_method, user_id): # 캐시 키 생성 cache_key = f"{amount}_{currency}_{payment_method}_{user_id}" # 캐시 확인 if cache_key in self.result_cache: return self.result_cache[cache_key] # 연결 풀에서 연결 가져오기 with self.connection_pool.get_connection() as conn: # 실제 처리 result = self._process_with_connection(conn, amount, currency, payment_method, user_id) # 결과 캐싱 self.result_cache[cache_key] = result return result
활용 시 흔한 실수
인터페이스 설계 실수
잘못된 예:올바른 예:
과도한 어댑터 사용
문제점: 단순한 매핑도 어댑터로 처리
해결책: 설정 기반 매핑이나 간단한 변환 함수 사용예외 처리 누락
잘못된 예:올바른 예:
1 2 3 4 5 6 7 8
def process_payment(self, amount, currency, payment_method, user_id): try: result = self.adaptee.charge(amount * 100) return PaymentResult(PaymentStatus.SUCCESS, result.id) except AdapteeException as e: return PaymentResult(PaymentStatus.FAILED, "", str(e)) except Exception as e: return PaymentResult(PaymentStatus.FAILED, "", f"Unexpected error: {e}")
성능 최적화를 위한 고려사항
카테고리 | 항목 | 설명 | 권장사항 |
---|---|---|---|
설계 최적화 | 위임 구조 최소화 | Adapter 내부의 단순 위임 계층이 과도하면 성능 오버헤드 발생 | 단순 위임은 제거하거나 통합 고려, Adapter 내 관심사 분리 |
인터페이스 기반 설계 | 다양한 시스템 간 유연한 연결 및 테스트 가능성 확보 | 인터페이스 중심 설계 및 DI 적용 | |
Lazy 초기화 | 무거운 Adaptee 객체는 초기 호출 시점까지 생성 지연 | Provider, Lazy 주입 또는 Proxy 지연 초기화 활용 | |
불필요 계층 제거 | 단순한 통합에도 불필요한 Adapter 계층 도입 시 오히려 복잡성 증가 | 사전 분석을 통해 직접 통합 또는 경량화 설계 | |
리소스 관리 | 객체 생성 비용 | Adapter 또는 Adaptee 객체 생성이 잦을 경우 리소스 낭비 | 싱글톤, 객체 풀, 플라이웨이트 패턴 활용 |
메모리 관리 | 어댑터가 Adaptee 를 장기 참조하거나 캐싱 시 누수 발생 가능 | WeakReference , 주기적 정리, 수명 주기 관리 적용 | |
캐싱 전략 | 변환 결과나 외부 호출 결과의 반복 호출 시 성능 저하 | 불변 객체 기반 캐싱, Guava Cache, Spring Cache 등 적용 | |
실행 최적화 | 배치 처리 | 대량 데이터를 1 건씩 처리하면 성능 저하 발생 | Stream API, 일괄 처리 전략 (Batch Adapter) 적용 |
비동기 처리 | 네트워크 호출, 외부 API 연동 등에서 블로킹 발생 가능 | CompletableFuture , async/await , Reactive Adapter 도입 | |
네트워크 호출 최소화 | Adaptee 가 외부 시스템일 경우 불필요한 호출로 인한 지연 시간 증가 | 호출 결과 캐싱, 연결 풀 활용, 요청 합치기 등 적용 | |
테스트 전략 | 테스트 자동화 | 다양한 조합에서의 Adapter 기능 테스트 필요 | Mock Adapter, 테스트 더블 패턴 사용 |
성능 모니터링 | Adapter 계층의 성능 병목 원인 추적 필요 | APM 도구 (New Relic, Datadog 등) 통한 트레이싱 및 메트릭 수집 | |
운영 안정성 | 스레드 안전성 | 멀티스레드 환경에서 공유 객체 상태가 변경되면 동시성 문제 발생 | 불변 객체 설계, 락 -Free 구조, 동기화 처리 (Lock , Thread-safe Collection ) |
장애 격리 | Adaptee 장애가 전체 시스템에 영향 미치는 경우 | Circuit Breaker, 타임아웃 설정, 대체 로직 구성 | |
데이터 일관성 | 멀티 호출 시 동일한 결과 일관성 보장 필요 | 상태 분리, 동기화 구조 적용 또는 외부 상태는 별도 전달 | |
확장성과 유연성 | 수평 확장 고려 | Adapter 가 병목이 되지 않도록 무상태 구조 권장 | Stateless 설계, Auto Scaling 및 Load Balancing 적용 |
런타임 어댑터 관리 | 다양한 Adapter 를 런타임에 유연하게 관리하는 구조 필요 | Adapter Registry 또는 Plugin 기반 동적 로딩 구조 활용 | |
모듈화 설계 | 기능별 Adapter 를 분리하여 유지보수 및 재사용 용이 | 기능 중심으로 Adapter 분리, 각기 다른 도메인에 특화된 Adaptee 인터페이스 설계 적용 |
주제와 관련하여 주목할 내용
카테고리 | 주제 | 항목 | 설명 |
---|---|---|---|
설계 원칙 | Adapter | 호환성 | 서로 다른 인터페이스를 가진 클래스들이 함께 동작 가능하게 함 |
기존 코드 보존 | 기존 코드를 수정하지 않고 새로운 인터페이스에 적응 | ||
Object vs Class Adapter | 구현 방식 | 객체 위임 기반 vs 클래스 상속 기반 (다중 상속 지원 여부에 따라 선택) | |
SRP/OCP | 단일 책임, 개방/폐쇄 원칙 적용 | 어댑터를 통해 변경에 닫히고 확장에 열려 있는 구조 구현 | |
실무 활용 | 외부 API 연동 | 외부 API 어댑터 | 외부 시스템의 응답 형식을 내부 시스템 형식으로 변환하여 통합 |
레거시 통합 | 레거시 어댑터 | 레거시 시스템의 인터페이스를 변경하지 않고 신 시스템에 통합 | |
데이터베이스 | Database Adapter | 다중 벤더/종류의 DB 를 하나의 인터페이스로 추상화하여 통합 | |
메시징 시스템 | Protocol Adapter | 서로 다른 메시징 프로토콜 (MQTT, Kafka 등) 을 중계 | |
아키텍처 연계 | Microservices | API Gateway | REST/gRPC 등 이질적 프로토콜 간 어댑팅 |
Kubernetes | Service Mesh | 서비스 간 통신 시 TLS, gRPC, HTTP 등 프로토콜 자동 변환 | |
데이터 통합 | Data Mapper | 다양한 데이터 소스 간 포맷/스키마 변환 어댑터 | |
RESTful API | API Versioning | 버전 간의 호환성을 위한 요청/응답 변환 계층 | |
성능 및 최적화 | Adapter 계층 | 성능 오버헤드 | 계층 증가로 인한 지연 발생 가능 → 주의 필요 |
캐싱/비동기 처리 | 최적화 어댑터 | 비동기 변환 처리 및 캐싱을 통한 성능 향상 (Reactive Adapter 등) | |
비동기 어댑터 | Reactive Streams | CompletableFuture, RxPy 등 기반 비동기 어댑터 구현 | |
보안/관찰성 | 인증 통합 | Identity Adapter | OAuth, SAML, JWT 등 인증 체계 간 변환 |
모니터링 통합 | Metrics Adapter | 서로 다른 메트릭 수집 도구 (Prometheus, Datadog 등) 통합 | |
테스트 전략 | 테스트 더블 | Mock 어댑터 | 테스트용 대체 어댑터로 테스트 더블/Stub/Fake 구현 지원 |
DI 기반 테스트 | 인터페이스 기반 설계 | 의존성 주입을 활용한 테스트 가능성 확보 | |
확장성 관리 | Adapter Registry | 런타임 동적 어댑터 로딩 | 플러그인 시스템 등에서 런타임에 어댑터 자동 로딩 및 관리 |
프로그래밍 패턴 | 함수형 어댑터 | 고차 함수 기반 | 함수 커링 및 조합으로 인터페이스 어댑팅 구현 |
제네릭 어댑터 | 타입 안전성 | TypeScript, Kotlin 등의 제네릭 기반으로 타입 안정성과 재사용성 확보 | |
패턴 비교/연계 | 래퍼 패턴 (Wrapper) | 대표 구현 형태 | Adapter 는 Wrapper 의 대표적 활용 사례 |
데코레이터/프록시 패턴 | 구조적 유사성 | Adapter 는 목적이 인터페이스 호환, Decorator/Proxy 는 기능 확장/통제 |
추가 학습 필요 내용
카테고리 | 주제 | 항목/키워드 | 설명 |
---|---|---|---|
디자인 패턴 | 어댑터 패턴 (Adapter Pattern) | 핵심 구조 / 역할 | Target, Adapter, Adaptee 간 인터페이스 변환 구조 |
관련 패턴 비교 | Bridge / Decorator / Proxy / Facade | 목적과 구현 방식의 차이 분석 | |
고급 패턴 확장 | 체인 어댑터 / 전략 어댑터 / 팩토리 어댑터 | 전략, 생성, 연결 기능이 결합된 고급 어댑터 구성 | |
아키텍처 설계 | 아키텍처 패턴 적용 | 헥사고날 아키텍처 / 클린 아키텍처 | 포트와 어댑터 구조에서의 계층 설계와 의존성 역전 구현 |
ACL (Anti-Corruption Layer) | 외부 시스템 캡슐화 계층 | 시스템 경계를 보호하며 어댑터의 확장된 역할 수행 | |
마이크로서비스 통합 구조 | API Gateway / Service Mesh | 엔드포인트와 내부 시스템 간의 어댑터 역할 수행 | |
레거시 및 외부 시스템 통합 전략 | 외부 API 어댑터 / 서드파티 라이브러리 | 직접 수정 불가한 시스템과 연동을 위한 어댑터 활용 | |
프레임워크 활용 | 어댑터 적용 예제 | Spring HandlerAdapter / ViewResolver | 프레임워크 내부에서의 어댑터 구현 사례 파악 |
서버리스 아키텍처 내 어댑터 | AWS Lambda / Azure Functions | 입력/출력 포맷 변환, 이벤트 처리기 인터페이스 조정 등 | |
구현 전략 | 구현 방식 | 객체 어댑터 (합성) / 클래스 어댑터 (상속) | 위임과 상속의 구조적 차이와 상황별 적용 전략 |
객체 생명주기 최적화 | Lazy Loading / Adapter Pooling | 성능을 위한 어댑터 생성 비용 절감 전략 | |
동적 선택 전략 | 전략 어댑터 | 실행 환경 또는 입력 조건에 따라 어댑터 인스턴스를 런타임에 선택 | |
테스트 기법 | 테스트 설계 | Mock Adapter / Stub / Integration Test | 의존성 격리 및 시스템 통합 검증을 위한 테스트 방식 |
테스트 전략 | Mock 기반 단위 테스트 | 어댑터 계층의 기능 단위 테스트와 시나리오 테스트 구성 | |
성능 최적화 | 실행 효율 | LRU Cache / Lazy Initialization | 변환 결과의 캐싱 및 지연 초기화를 통한 리소스 최적화 |
오버헤드 최소화 | 경량 어댑터 설계 / 불필요 호출 제거 | 성능 병목 제거와 효율적인 변환 처리 방식 설계 | |
설계 원칙 적용 | SOLID 원칙 | SRP / OCP / ISP / DIP | 객체 책임 분리, 확장성 확보, 인터페이스 분리, 추상화 의존성 적용 등 |
의존성 역전 | 고수준 모듈 ↔ 저수준 모듈 간 추상화 연결 | 클린 아키텍처의 핵심 설계 원칙 반영 | |
통합 및 자동화 | 자동 생성 도구 활용 | 어댑터 코드 생성기 / 인터페이스 매핑 도구 | 반복적인 어댑터 생성/관리 자동화로 생산성 향상 |
메시지/데이터 변환 패턴 적용 | Message Translator / ETL Adapter | 형식이 다른 메시지 또는 데이터 스트림을 변환하는 어댑터 응용 사례 | |
학습 보조 개념 | 내부 동작 이해 | 위임 / 인터페이스 변환 / 변환 전략 | 어댑터 내부 로직과 설계 원리 이해를 위한 기반 지식 |
Wrapper 개념 | 기존 객체 감싸기 구조 패턴 | 인터페이스 재정의 또는 확장 구조로서의 어댑터 이해 | |
Thread Safety / Immutable 객체 | 동시성 고려 설계 | 멀티스레드 환경에서 어댑터 객체의 안정성 확보 전략 |
용어 정리
카테고리 | 용어 | 설명 |
---|---|---|
패턴 구성 요소 | Target | 클라이언트가 기대하는 표준 인터페이스 |
Adaptee | 기존 시스템/서드파티 라이브러리 등, 호환되지 않는 인터페이스를 가진 클래스 | |
Adapter | Target 인터페이스를 구현하고, 내부적으로 Adaptee 를 호출하여 인터페이스를 변환하는 중개 클래스 | |
Client | 비즈니스 로직을 실행하며 Target 인터페이스를 통해 Adapter 를 사용하는 객체 | |
Wrapper (래퍼) | 어댑터 패턴의 또 다른 이름으로, 기존 객체를 감싸 인터페이스를 변환 | |
Anti-Corruption Layer | 외부 시스템과의 의존성을 줄이기 위해 경계 역할을 수행하는 어댑터 계층 | |
구현 방식 | Object Adapter | 합성 (composition) 을 사용하여 Adaptee 인스턴스를 포함하는 방식 |
Class Adapter | 다중 상속을 통해 Target 과 Adaptee 를 모두 상속받아 구현하는 방식 | |
Two-way Adapter | 양방향 변환이 가능한 어댑터로, Adaptee 와 Target 역할을 모두 수행 | |
설계 원칙 | Single Responsibility Principle (SRP) | 클래스는 하나의 책임만 가져야 한다는 객체지향 설계 원칙 |
Open-Closed Principle (OCP) | 확장에는 열려 있고, 변경에는 닫혀 있어야 한다는 원칙 | |
Interface Segregation Principle (ISP) | 클라이언트가 사용하지 않는 인터페이스에 의존하지 않도록 분리해야 한다는 원칙 | |
Dependency Inversion Principle (DIP) | 고수준 모듈이 저수준 모듈에 의존하지 않고 추상화에 의존해야 한다는 원칙 | |
기술 개념 | Interface Translation | 서로 다른 인터페이스의 메서드 호출을 변환하는 과정 |
Delegation (위임) | 어댑터가 포함하고 있는 객체 (Adaptee) 에게 기능 수행을 위임하는 구조 | |
Object Composition (객체 합성) | 상속 대신 객체를 포함하여 기능을 구성하는 방식 | |
소프트웨어 배경 | Legacy System | 오래된 기술로 구축되어 있고, 직접 변경이 어려운 기존 시스템 |
Third-party Library | 외부에서 제공된 라이브러리로, 직접 수정이 어렵고 어댑터를 통해 통합되는 경우가 많음 | |
아키텍처 요소 | API Gateway | 외부 클라이언트의 요청을 내부 서비스로 라우팅하는 진입점 역할 수행 (Adapter 역할과 유사한 목적) |
Service Mesh | 서비스 간 통신을 추상화하여 관리하는 인프라 계층 (Adapter 역할 포함 가능) | |
통합 및 변환 | Message Translator | 서로 다른 메시지 형식을 변환해주는 구성 요소 (Adapter 의 일종으로 사용됨) |
ETL (Extract, Transform, Load) | 데이터를 추출, 변환, 적재하는 통합 프로세스로, Adaptee ↔ Target 데이터 흐름의 구현 예시 | |
보조 개념 | Mock Object | 테스트에서 실제 객체 대신 사용하는 가짜 객체 (어댑터 테스트 시 사용됨) |
Connection Pooling | 재사용 가능한 연결을 유지하여 성능을 최적화하는 기법 | |
LRU Cache | 가장 오래된 항목부터 제거하는 캐싱 알고리즘 | |
Circuit Breaker | 외부 시스템 장애가 발생했을 때 시스템 보호를 위한 패턴 | |
Bulkhead Pattern | 장애가 전체 시스템에 영향을 주지 않도록 격리하는 구조 | |
Serialization | 객체를 바이트 스트림으로 변환하는 과정 | |
Marshalling | 객체를 JSON, XML 등 포맷으로 변환하는 과정 | |
IdP (Identity Provider) | 사용자 인증을 제공하는 시스템 | |
Distributed Tracing | 요청 흐름을 추적하여 분산 시스템에서 문제를 진단하는 기술 | |
SLA (Service Level Agreement) | 서비스 제공자와 사용자 간의 품질 보장 합의 |
참고 및 출처
공식 및 이론 기반
튜토리얼 및 실용 예제
- adapter pattern | yuni‑q 블로그
- [Design Pattern] 어댑터 패턴, Adapter Pattern - nnnyeong (Velog)
- [디자인 패턴 - 구조] Adapter Pattern - bae_mung (Velog)
- [Java] 디자인 패턴 - 어댑터 패턴(Adapter Pattern) - seek316 (네이버 블로그)
- 디자인패턴 - 어댑터 패턴 - yaboong
- [Design Pattern] Adapter 패턴 - kscory
- Chapter 13. 어댑터 패턴(Adapter Pattern) - ansohxxn
- (번역) 리액트에서 어댑터(Adapter) 패턴을 사용하는 방법 - superlipbalm (Velog)
- ones1kk: 어댑터 패턴 (GoF) 정리
아키텍처·프레임워크 관련
- Spring Framework - DispatcherServlet (Adapter Pattern 응용)
- Martin Fowler - Anti‑Corruption Layer (어댑터 계층 아키텍처)
심화·최신 분석
- 마이크로서비스 구축을 위한 API Gateway 패턴 사용하기 - NGINX STORE
- 지속 가능한 소프트웨어 설계 패턴: 포트와 어댑터 아키텍처 적용하기 - LINE Engineering Blog