구조 패턴 한눈에 보기: Adapter부터 Flyweight까지 언제 무엇을 고르나
Adapter, Bridge, Composite, Decorator, Facade, Flyweight, Proxy 등 GoF 구조 패턴을 목적별로 묶어 비교하고 상황별 선택 기준을 정리한다.
2026-08-13 · 최초 발행 2025-10-14
구조 패턴이 푸는 문제
객체 지향 설계에서 구조(Structural) 패턴은 클래스와 객체를 더 큰 구조로 유연하게 조합하는 방법론이다. GoF(Design Patterns)의 구조 패턴군은 Adapter, Bridge, Composite, Decorator, Facade, Flyweight, Proxy를 포함하며, 복잡도 제어, 확장성 향상, 레거시 통합, 성능·운영 비용 최적화의 핵심 수단으로 쓰인다. 대규모 시스템, 마이크로서비스, SDK·라이브러리, UI·그래픽스 등에서 재사용성과 변경 용이성을 확보하는 데 기여한다.
패턴군은 목적별로 갈린다
목적은 객체 간 관계를 구조적으로 재조합해 결합도를 줄이고 응집도를 높이며, 인터페이스를 적응시키고 리소스 공유를 최적화하는 데 있다. 이 목적을 기준으로 패턴을 묶어보면 갈래가 보인다.
인터페이스 적응 계열인 Adapter와 Facade는 외부·레거시 인터페이스를 캡슐화하고 단순화한다. 구성 분리 계열인 Bridge와 Composite는 추상과 구현을 분리하거나 부분-전체 트리 구조를 모델링한다. 행동 확장 계열인 Decorator와 Proxy는 런타임에 기능을 확장하고 접근 제어·원격·캐시·로깅을 주입한다. 자원 최적화 계열인 Flyweight는 공유 가능한 내재 상태를 분리해 메모리를 아낀다. 핵심 개념을 관통하는 원칙은 상속보다 합성을 우선하고, 경계(Facade)와 계층(Bridge)을 명확히 하며, 공통성은 공유하고(Flyweight) 가변성은 외부화한다(Adapter/Decorator/Proxy)는 것이다.
이 중 Adapter는 인터페이스 적응 계열의 대표 격으로, 이질적인 인터페이스를 통일해 호출자 변경을 최소화하고 벤더 교체나 버전 변화의 영향 범위를 격리하는 역할을 맡는다. 개별 패턴으로서 Adapter의 구조와 구현, 트레이드오프를 깊이 다루려면 별도의 글이 필요할 만큼 내용이 많다.
요청이 통과하는 경로
입력은 클라이언트 요청이고, 처리 단계에서 Adapter가 변환하고 Decorator가 정책을 주입하며 Proxy가 접근 제어·캐시·원격 호출을 담당한다. 출력은 응답 반환이며, 예외가 발생하면 재시도·폴백·회로 차단을 수행한다. 원격 호출 재시도는 멱등 연산을 우선하고, 트랜잭션 경계는 Facade나 애플리케이션 서비스 레이어에서 명확히 하는 것이 일관성을 지키는 방법이다.
도입 절차: 문제 진단부터 운영 회고까지
먼저 결합도 과다, 인터페이스 불일치, 과다 객체, 횡단 관심사 누수 같은 문제를 진단한다. 그다음 목표에 맞는 후보 패턴을 도출한다 — 적응이 필요하면 Adapter/Facade, 확장이 필요하면 Decorator/Bridge, 보호가 필요하면 Proxy, 공유가 필요하면 Flyweight, 구조화가 필요하면 Composite다. 설계 단계에서는 공개 인터페이스, 책임 배분, 상태의 내재·외재 분해, 실패·경합·락 시나리오를 정의한다. 검증 단계에서는 단위·통합 테스트와 함께 부하·메모리 프로파일링, 캐시 히트율·지연 시간·GC 지표를 수집한다.
도입은 점진적으로 하는 편이 안전하다. Facade/Adapter로 첫 경계를 세운 뒤, 내부 구조를 Bridge/Composite로 정리하고, Proxy/Decorator로 비기능 요구를 더한다. 운영에 들어가면 장애 패턴을 분석하고, 캐시 TTL이나 회로 차단 임계값 같은 정책을 튜닝하며, 코드 복잡도와 운영 비용의 균형을 다시 맞춘다.
레거시 통합부터 원격 자원 보호까지
레거시 API를 통합할 때는 레거시 SOAP를 REST로 변환하는 계층을 Adapter로 구현해, 상위 애플리케이션이 통일된 REST 인터페이스를 쓰도록 만든다. Facade로 비즈니스 시퀀스를 단순화하고 트랜잭션 경계를 한곳에 모을 수도 있다.
대규모 UI나 그래픽 트리에서는 Composite로 장면 그래프를 구성해 노드 단위로 render·update 같은 공통 연산을 제공한다. Flyweight로 텍스처·폰트를 공유하면 메모리 사용량이 크게 줄어든다.
스트리밍 파이프라인에서는 Decorator로 압축·암호화·관측(메트릭/트레이싱) 기능을 레이어로 조합한다. 기능을 토글하거나 정책을 바꿀 때 재배포 없이 런타임에서 구성을 변경할 수 있다.
원격 리소스를 보호할 때는 Proxy로 캐시, 서킷브레이커, 레이트리미터를 적용해 SLA를 지키고, IAM 연동과 감사 로깅을 경계에서 강제한다.
코드로 보는 Adapter와 Decorator
전제는 Java 17, Python 3.10 기준이며 로컬에서 바로 실행할 수 있는 예시다.
레거시 사각형을 공통 Shape 인터페이스로 적응시키는 Adapter 예시다.
// Java 17
interface Shape {
int area();
}
class LegacyRectangle {
public int getWidth() { return 10; }
public int getHeight() { return 20; }
}
class RectangleAdapter implements Shape {
private final LegacyRectangle rect;
RectangleAdapter(LegacyRectangle rect) { this.rect = rect; }
@Override public int area() { return rect.getWidth() * rect.getHeight(); }
}
public class Main {
public static void main(String[] args) {
Shape s = new RectangleAdapter(new LegacyRectangle());
System.out.println(s.area()); // 200
}
}
지연 측정과 재시도를 주입하는 Decorator 예시다.
# Python 3.10
import time
from functools import wraps
def timed_retry(retries=2, delay=0.05):
def deco(fn):
@wraps(fn)
def wrapper(*args, **kwargs):
last = None
for i in range(retries + 1):
start = time.perf_counter()
try:
res = fn(*args, **kwargs)
elapsed = (time.perf_counter() - start) * 1000
print(f"ok in {elapsed:.2f} ms")
return res
except Exception as e:
last = e
time.sleep(delay)
raise last
return wrapper
return deco
count = {"n": 0}
@timed_retry(retries=1, delay=0.01)
def flaky():
count["n"] += 1
if count["n"] < 2:
raise RuntimeError("transient")
return "done"
print(flaky()) # 첫 호출 실패 후 재시도 성공
Decorator 레이어가 로깅·메트릭·재시도·서킷·트레이싱까지 5단으로 쌓이는 경우, 층당 0.2ms의 오버헤드를 가정하면 약 1ms의 추가 지연이 생긴다. 기본 호출이 10ms라면 10% 정도 늘어나는 셈이라, 필요하면 비동기 처리나 버퍼링으로 최적화할 수 있다.
적용했을 때 달라지는 수치
Proxy 캐싱을 예로 들면, 백엔드 응답이 50ms이고 캐시 응답이 2ms일 때 히트율이 60%라면 기대 지연은 0.6×2 + 0.4×50 = 21.2ms로, 약 57.6% 개선된다. Flyweight를 예로 들면, 1천만 개 객체를 공유해 객체당 60바이트를 절감하면 약 6억 바이트, 즉 572MB 정도가 절약된다. Bridge/Facade로 인터페이스를 불변 조건으로 유지하면서 구현을 교체할 때는 호출부 변경을 0건으로 만드는 것을 목표로 삼을 수 있고, 그만큼 코드 변경 범위도 국소화된다. 다만 구체적인 지표는 워크로드와 구현 품질에 따라 달라지므로 사전 벤치마크가 권장된다.
패턴별로 지표를 비교하면
| 패턴 | 성능(지연/오버헤드) | 확장성 | 일관성 | 안정성 | 운영 편의 |
|---|---|---|---|---|---|
| Adapter | 낮음(변환 비용만) | 높음 | 높음(무상태 권장) | 높음 | 높음 |
| Facade | 낮음 | 높음 | 높음(단일 경계) | 높음 | 높음(관찰·제어 용이) |
| Bridge | 낮음 | 높음(독립 배포) | 높음 | 높음 | 중간 |
| Composite | 중간(트리 순회) | 중~높음 | 중간(트리 불변성) | 중간 | 중간 |
| Decorator | 중간(레이어 누적) | 높음 | 중간(상태 주의) | 중간 | 중간(레이어 관리) |
| Flyweight | 낮음(GC 감소) | 높음 | 중간(공유 상태) | 중간 | 중간(풀 관리) |
| Proxy | 가변(원격/캐시 영향) | 높음 | 가변(캐시 일관) | 중~높음(회로차단) | 중간~낮음(정책 튜닝) |
경계를 강화할 때 고려할 점
Proxy/Facade 계층에서 인증·인가·TLS 종료·레이트리미팅을 한곳에 모으면 보안 정책을 집중 관리하고 감사하기 쉬워지지만, 단일 장애점(SPOF) 위험이 생기므로 수평 확장과 헬스체크가 필요하다.
Flyweight의 공유 상태는 불변으로 설계하고 외재 상태는 명시적으로 전달해야 한다. 메모리는 절감되지만 API 복잡도가 늘고 스레드 안전성도 함께 고려해야 한다.
Decorator로 로깅·메트릭·트레이싱을 주입하고 상관관계 ID를 전파하면 가시성이 좋아지지만, 레이어가 쌓일수록 오버헤드도 늘어나므로 샘플링이나 비동기 처리로 상쇄하는 편이 낫다.
전체적으로는 Facade/Adapter로 단순화하고, Proxy/Decorator로 정책을 강화하며, Bridge/Composite로 구조화하는 조합이 역할을 명확히 나눈다. 다만 추상화를 과도하게 쌓으면 이해 비용이 늘어나므로, 규칙과 도식을 문서화해두는 것이 필수적이다.