
현대적인 파이썬 애플리케이션 환경에서 오래된 라이브러리나 레거시 시스템을 수정 없이 재사용하는 '어댑터 패턴(Adapter Pattern)'의 실무적 적용 전략을 다룹니다.
1. 서론: 레거시 코드의 늪에서 벗어나는 기술
현업 개발자에게 가장 고통스러운 순간 중 하나는 "동작은 하지만 수정할 수 없는" 레거시 코드를 새로운 시스템에 통합해야 할 때입니다. 코드를 직접 수정하는 것은 사이드 이펙트(Side Effect)의 위험이 크고, 그렇다고 새로 작성하기에는 비용과 시간이 너무 많이 소요됩니다. 이때 구원투수 역할을 하는 것이 바로 어댑터 패턴(Adapter Pattern)입니다. 어댑터 패턴은 서로 호환되지 않는 인터페이스를 가진 클래스들이 함께 작동할 수 있도록 연결하는 구조적 디자인 패턴입니다. 파이썬의 동적 특성을 활용하면 '객체 어댑터'와 '클래스 어댑터'를 유연하게 구현하여 시스템 간의 결합도를 낮추고 유지보수성을 극대화할 수 있습니다.
2. 어댑터 패턴의 유형별 구조적 차이 및 특징 비교
어댑터 패턴을 설계할 때 고려해야 할 두 가지 주요 방식의 차이점과 특징을 표로 정리하였습니다.
| 비교 항목 | 객체 어댑터 (Object Adapter) | 클래스 어댑터 (Class Adapter) |
|---|---|---|
| 구현 방식 | 위임(Delegation)을 사용 (HAS-A 관계) | 다중 상속(Inheritance)을 사용 (IS-A 관계) |
| 유연성 | 높음 (런타임에 다양한 레거시 객체 수용 가능) | 낮음 (특정 레거시 클래스에 고정됨) |
| 파이썬 적합도 | 매우 높음 (일반적인 파이썬 스타일) | 보통 (다중 상속 활용 시 사용) |
| 해결 방법 | 레거시 객체를 인스턴스 변수로 감싸서 호출 | 인터페이스와 레거시 클래스를 동시 상속 |
| 복잡도 | 낮음 (직관적인 래퍼 구조) | 중간 (MRO 순서 고려 필요) |
3. 실무 중심의 레거시 통합 Sample Examples (7선)
실제 현업에서 마주칠 수 있는 7가지 레거시 통합 시나리오와 해결 코드를 제안합니다.
Example 1: 구형 XML 로깅 시스템을 현대적 JSON 형식으로 변환
기존 시스템은 XML 출력을 기대하지만, 새로운 인프라는 JSON을 원할 때 사용하는 어댑터입니다.
# Legacy: 수정 불가
class XMLDataGenerator:
def get_xml(self):
return "<user><id>1</id><name>Kim</name></user>"
# Target Interface
class DataConsumer:
def fetch_json(self):
pass
# Adapter
import xmltodict
import json
class XMLToJsonAdapter(DataConsumer):
def __init__(self, xml_gen):
self.xml_gen = xml_gen
def fetch_json(self):
xml_str = self.xml_gen.get_xml()
dict_data = xmltodict.parse(xml_str)
return json.dumps(dict_data)
# Usage
adapter = XMLToJsonAdapter(XMLDataGenerator())
print(adapter.fetch_json())
Example 2: 3rd-party 구형 PG(결제창) 라이브러리 통합
결제 모듈이 바뀌었으나 인터페이스 명칭이 다를 때 결합도를 낮추는 방법입니다.
class OldPaymentSystem:
def make_payment(self, amount_usd):
print(f"Processing ${amount_usd} via Old System")
class NewPaymentInterface:
def pay(self, amount_krw):
pass
class PaymentAdapter(NewPaymentInterface):
def __init__(self, old_system):
self.old_system = old_system
def pay(self, amount_krw):
# 환율 계산 로직 포함 가능
amount_usd = amount_krw / 1300
self.old_system.make_payment(amount_usd)
Example 3: 파일 시스템 접근 방식의 차이 해결
로컬 파일 시스템(Legacy)에서 S3(Modern) 환경으로의 마이그레이션 중간 단계에서 유용합니다.
class LocalFileReader:
def read_from_disk(self, path):
return f"Content of {path}"
class CloudStorageInterface:
def download(self, bucket, key):
pass
class StorageAdapter(CloudStorageInterface):
def __init__(self, local_reader):
self.local_reader = local_reader
def download(self, bucket, key):
# 버킷 경로를 로컬 경로로 매핑하여 레거시 함수 호출
local_path = f"/tmp/{bucket}/{key}"
return self.local_reader.read_from_disk(local_path)
Example 4: 클래스 어댑터를 활용한 다중 상속 기반 통합
파이썬의 다중 상속 기능을 활용하여 두 클래스의 기능을 하나로 묶는 예제입니다.
class LegacyCalculator:
def calculate_old(self, a, b):
return a + b
class TargetApp:
def sum(self, val1, val2):
pass
class CalcAdapter(TargetApp, LegacyCalculator):
def sum(self, val1, val2):
return self.calculate_old(val1, val2)
Example 5: 데이터베이스 드라이버 호환성 어댑터
execute()와 run_query()처럼 명칭이 다른 DB 커넥터를 통일합니다.
class LegacyDB:
def run_query(self, q):
return "Legacy Results"
class ModernApp:
def __init__(self, db_adapter):
self.db = db_adapter
def execute_logic(self):
return self.db.execute("SELECT *")
class DBAdapter:
def __init__(self, legacy_db):
self.legacy_db = legacy_db
def execute(self, sql):
return self.legacy_db.run_query(sql)
Example 6: 비표준 통신 프로토콜의 표준화
소켓 기반의 원시 데이터를 HTTP API 인터페이스로 변환합니다.
class RawSocketClient:
def send_raw(self, bytes_data):
print("Sending raw bytes over socket")
class ApiAdapter:
def __init__(self, socket_client):
self.client = socket_client
def post_data(self, json_payload):
raw_data = json_payload.encode('utf-8')
self.client.send_raw(raw_data)
Example 7: 사용자 권한 시스템의 브릿지 역할
정수형 권한(Legacy)을 문자열 역할(Modern) 기반으로 변환하여 매핑합니다.
class LegacyAuth:
def get_user_level(self):
return 1 # 1: Admin, 2: User
class AuthAdapter:
def __init__(self, legacy_auth):
self.legacy_auth = legacy_auth
def get_role(self):
level = self.legacy_auth.get_user_level()
mapping = {1: "ADMIN", 2: "USER"}
return mapping.get(level, "GUEST")
4. 결론: 어댑터 패턴 적용 시 주의사항
어댑터 패턴은 레거시 통합의 만능 해결사이지만, 남용할 경우 코드의 간접 층(Indirection)이 너무 많아져 디버깅이 어려워질 수 있습니다. 다음과 같은 해결 원칙을 지키는 것이 중요합니다.
- 단일 책임 원칙(SRP): 어댑터는 오직 '변환' 작업에만 집중해야 합니다. 비즈니스 로직을 어댑터 내부에 섞지 마십시오.
- 명확한 인터페이스 정의: 타겟 인터페이스가 무엇인지 명확히 정의하여 어댑터가 올바른 방향으로 구현되도록 하십시오.
- 마이그레이션 전략: 어댑터는 영구적인 해결책이 아니라, 레거시를 현대화하기 위한 '디딤돌'로 인식하고 점진적으로 레거시를 교체하는 계획을 세워야 합니다.
'Artificial Intelligence > 60. Python' 카테고리의 다른 글
| [PYTHON] TDD 적용 시 코드 구조 설계를 최적화하는 3가지 방법과 실무적 차이점 분석 (0) | 2026.03.29 |
|---|---|
| [PYTHON] unittest와 pytest의 5가지 차이점 분석 및 pytest가 대세가 된 해결 방법 (0) | 2026.03.29 |
| [PYTHON] 외부 API 테스트를 위한 Mocking과 Patching의 3가지 차이점과 해결 방법 (0) | 2026.03.29 |
| [PYTHON] @dataclass와 NamedTuple, 일반 클래스의 용도 차이 해결 방법과 7가지 실무 사례 (0) | 2026.03.29 |
| [PYTHON] 인터페이스 규약 강제를 위한 NotImplementedError 활용 방법 3가지와 구조적 차이점 (0) | 2026.03.29 |