1. 디자인 패턴이란 무엇인가?
1-1. 소프트웨어 설계의 재사용 가능한 해법
디자인 패턴은 단순한 코드 템플릿이 아니다. 그것은 반복되는 문제에 대해 검증된 해결책을 구조화한 설계 전략이다. 건축에서 ‘문을 여닫기 위한 구조’, ‘복층 구조를 위한 계단’ 등의 해법이 있는 것처럼, 소프트웨어에도 문제가 생겼을 때 ‘이럴 땐 이렇게 푼다’는 공통된 해결 틀이 필요하다.
이러한 해결 틀을 공식화한 것이 바로 디자인 패턴이다. 실무에서는 특정한 상황에서 “이걸 어떻게 풀지?”보다, “이 문제는 전략 패턴으로 풀 수 있어”라고 생각하는 것이 설계자 수준의 사고다. 이처럼 디자인 패턴은 단순 구현이 아니라 설계 관점의 레벨을 높이는 도구다.
1-2. 패턴은 왜 중요한가?
디자인 패턴의 핵심 가치는 공통 언어와 설계 구조의 표준화다. 팀원끼리 “이건 옵저버 패턴으로 구현해봤어”라고 말했을 때, 코드를 보지 않아도 “아, 이벤트 감지를 위한 구조겠구나”라는 이해가 바로 가능하다. 이러한 공통 언어는 협업에서 엄청난 시간과 사고 비용을 줄여준다. 또한 패턴은 유지보수 단계에서도 빛을 발한다. 구조가 명확하게 패턴 기반으로 설계돼 있다면, 후임 개발자가 설계 의도를 빠르게 파악할 수 있고, 변경이 발생하더라도 영향을 최소화할 수 있다. 설계의 일관성과 명확한 책임 분리, 의도 전달력 모두에서 디자인 패턴은 실무의 기반이 된다.
1-3. GOF의 23가지 패턴과 그 이상
디자인 패턴은 1994년, “Gang of Four”(Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides)가 집필한 『Design Patterns: Elements of Reusable Object-Oriented Software』에서 처음 체계화되었다. 이 책은 객체지향 설계의 고전으로, 총 23가지의 패턴을 제안했고, 이를 생성(Creational), 구조(Structural), 행위(Behavioral) 패턴으로 구분했다. 그러나 이후 마이크로서비스, 클린 아키텍처, DDD 등 새로운 설계 패러다임의 등장과 함께 더 많은 실무 패턴들이 추가로 제안되었다.
예컨대 CQRS, 이벤트 소싱, 리포지토리 패턴, DI 컨테이너 구조 등은 기존 GOF 패턴에는 포함되지 않지만 현대 아키텍처에서는 거의 필수처럼 사용된다. 따라서 패턴은 책에 있는 내용을 외우는 것이 아니라, 문제 상황과 기술 환경에 맞게 계속 진화하는 개념임을 인식해야 한다.
1-4. 패턴은 구조가 아니라 사고방식이다
디자인 패턴을 공부하다 보면, ‘이건 언제 써야 하나요?’, ‘어떤 상황에서 이걸 골라야 하나요?’라는 질문이 반복된다. 이런 질문은 대부분 패턴을 정답처럼 암기했기 때문에 생긴다. 하지만 패턴은 문제에 대한 사고 방식이다. 예를 들어 전략 패턴은 알고리즘을 분리하는 사고, 데코레이터는 책임을 계층적으로 덧붙이는 사고, 팩토리는 생성 책임을 위임하는 사고다.
즉, 코드보다 어떤 문제를 어떻게 바라보는 시각을 훈련하는 것이 패턴 학습의 핵심이다. 진짜 실력자는 패턴 이름 없이도 패턴의 사고방식을 설계에 녹여내고, 이름은 나중에 붙이는 사람이다.
2. 생성 패턴: 객체를 만드는 책임 분리
2-1. Factory Method – 객체 생성의 추상화
팩토리 메서드 패턴은 객체를 직접 생성하지 않고, 하위 클래스에서 어떤 객체를 생성할지를 결정하게 하는 구조다. 예를 들어 커피를 주문한다고 할 때, “아이스 아메리카노”와 “라떼”는 모두 커피지만, 각각의 제조 방식은 다르다. 클라이언트는 단순히 ‘커피 하나 줘’라고 말하면 되고, 구체적인 만드는 방식은 팩토리 구현체가 결정하는 구조가 되는 것이다.
abstract class CoffeeFactory {
abstract Coffee createCoffee();
}
class AmericanoFactory extends CoffeeFactory {
Coffee createCoffee() { return new Americano(); }
}
이 패턴은 특히 상속을 사용하는 프레임워크 기반 구조에서 자주 쓰인다.
예: 스프링의 BeanFactory 또는 템플릿 메서드 패턴과 함께 결합되어 사용되는 구조들이 여기에 해당한다.
2-2. Abstract Factory – 연관 객체들의 집합 생성
Abstract Factory 패턴은 관련된 여러 객체들을 통일된 방식으로 생성해야 할 때 사용된다. 예를 들어 다크 모드 테마를 지원하는 UI에서, 버튼, 체크박스, 입력창 등 모든 컴포넌트를 동일한 스타일로 바꿔야 한다면, 추상 팩토리를 통해 통합 관리할 수 있다.
interface ThemeFactory {
Button createButton();
CheckBox createCheckBox();
}
이 패턴의 핵심은 ‘객체 집합의 일관성’을 보장하는 것이다. 웹 프론트엔드에서도 컴포넌트 디자인 시스템을 테마별로 스위칭할 때 같은 개념이 적용된다. 또한 마이크로서비스에서 공통된 서비스 설정이나 인증 모듈을 추상화할 때도 유용하다.
2-3. Singleton – 인스턴스 하나를 보장
싱글턴 패턴은 애플리케이션 내에서 하나의 인스턴스만 존재하도록 제한하는 구조다. 대표적인 예는 로그 관리자, 설정 파일 로더, DB 커넥션 풀 등이다. 자바에서는 private static final 필드를 통해 객체를 하나만 만들고 getInstance()를 통해 접근하게 한다. 하지만 싱글턴은 의존성 감추기, 테스트 어려움, 전역 상태 남용 등의 부작용이 크기 때문에 현대 프레임워크에서는 대부분 DI(의존성 주입)을 통해 싱글턴 라이프사이클을 관리한다. 즉, 단순 구현보다 프레임워크 중심의 관리 구조 속에서 활용되는 것이 안전하다.
2-4. Builder – 복잡한 객체 생성 분리
빌더 패턴은 객체 생성 시 매우 많은 파라미터가 있거나, 필드의 조건에 따라 값이 달라지는 경우에 유용하다. 예를 들어 사용자의 프로필 객체가 필수 값(이름, 이메일)과 선택 값(프로필 이미지, 주소, 마케팅 수신 동의)으로 구성된다면, 빌더를 사용하면 깔끔하게 관리할 수 있다.
User user = User.builder()
.name("홍길동")
.email("gil@example.com")
.agreeMarketing(true)
.build();
특히 빌더 패턴은 불변 객체(Immutable Object)를 만들 때 자주 쓰이며, 체이닝 메서드를 통해 가독성도 높다. JPA 엔티티나 DTO 생성, JSON 응답 변환 객체 생성 시에도 매우 자주 쓰인다.
3. 구조 패턴: 구성 요소의 유연한 조합
3-1. Adapter – 기존 구조를 새 인터페이스에 맞추기
어댑터(Adapter) 패턴은 이름 그대로 ‘변환기’다. 기존 클래스의 인터페이스가 우리가 사용하고자 하는 인터페이스와 다를 때, 중간에 어댑터를 두어 호환시킨다. 실제 사례로는, 외부 API를 우리 내부 시스템의 DTO 형식으로 바꾸는 경우가 대표적이다.
예를 들어 외부 배송 시스템의 응답 구조가 우리 시스템의 배송 객체와 다를 경우, 어댑터 객체를 통해 변환해 사용할 수 있다. 이렇게 하면 외부 API가 바뀌어도 내부 시스템은 변경 없이 어댑터만 고치면 된다. 또한 오래된 레거시 코드를 현대화하는 과정에서도 어댑터 패턴은 유용하다. 새로운 인터페이스 구조를 만들고, 레거시 클래스에 어댑터를 연결하면 기존 코드를 수정하지 않고도 연동이 가능하다.
3-2. Decorator – 책임을 유연하게 덧붙이기
데코레이터는 객체의 기본 기능은 유지하면서도, 추가적인 기능을 동적으로 덧붙이는 방식이다. 상속과 달리, 객체를 래핑(wrapping)하여 계층적으로 확장할 수 있다. 실무 예로는 로그 메시지에 시간, 사용자 ID, 트랜잭션 ID 등을 점진적으로 덧붙이는 상황이 있다. 기본 로거는 메시지만 출력하고, 데코레이터를 통해 시간 로그, 트랜잭션 로그 등을 구성할 수 있다.
이렇게 하면 기존 코드에는 손대지 않고도 기능을 조합할 수 있다. 또한 데코레이터는 프론트엔드 스타일 컴포넌트에서도 자주 사용된다. 기본 버튼에 마우스 호버 효과, 그림자, 아이콘 등을 조건적으로 덧붙일 수 있는 방식이 데코레이터 사고와 유사하다.
3-3. Proxy – 접근을 제어하는 대리 객체
프록시(Proxy)는 실제 객체를 대신해서 접근 제어, 로깅, 캐싱, 보안 등을 처리하는 구조다. 가장 흔한 사례는 스프링의 AOP(Aspect-Oriented Programming)이다. 트랜잭션 처리, 로깅, 보안 필터링 등은 모두 내부적으로 프록시를 통해 구현된다. 예를 들어 사용자가 호출한 메서드 실행 전/후로 로그를 찍거나, DB 세션을 열고 닫는 등의 부가 처리를 하고 싶을 때, 프록시 객체가 진짜 객체를 감싸면서 해당 동작을 제어하는 것이다. 또한 Lazy Loading이나 원격 객체 접근(Remote Proxy)도 이 패턴을 기반으로 구현된다.
3-4. Composite – 계층 구조를 트리처럼 구성하기
Composite 패턴은 부분-전체 구조를 트리 구조로 표현하는 방법이다. 이 패턴에서는 각 구성 요소(leaf)와 구성체(composite)를 같은 인터페이스로 다룰 수 있도록 하여, 트리 전체를 재귀적으로 탐색하거나 표현할 수 있다.
예: 메뉴 구조, 카테고리 분류, 조직도, 파일 시스템 등.
interface Component {
void display();
}
Leaf는 단일 항목이고, Composite는 내부에 Component를 다수 포함한다. 이러한 구조는 복잡한 UI 구성, 백오피스 트리 메뉴, 복합 카테고리 구조에서 매우 유용하다.
4. 행위 패턴: 객체 간의 커뮤니케이션
4-1. Strategy – 알고리즘을 바꿀 수 있게 만들기
전략(Strategy) 패턴은 런타임에 알고리즘을 교체할 수 있도록 만든다. 예를 들어 할인 정책이 VIP, 일반, 신입 회원에 따라 달라질 때, DiscountStrategy 인터페이스를 만들고 각 정책별 클래스를 구현한다. 클라이언트는 어떤 전략을 사용할지 몰라도 인터페이스만으로 동작시킬 수 있다.
interface DiscountStrategy {
int calculate(int price);
}
이 패턴은 OCP를 만족시키면서도 SRP를 지키기 쉬운 구조로, 실제 서비스 확장에 매우 적합하다. 대표적 사용 예: 결제 로직, 요금 계산, 정책 엔진, AI 모델 스위칭 등.
4-2. Observer – 이벤트 구독 구조
Observer는 객체의 상태 변화가 있을 때 등록된 여러 리스너(구독자)에게 자동으로 알림을 보내는 구조다. 실무에서는 이벤트 버스, Pub/Sub 구조, 실시간 알림, 파일 변경 감지 등 다양한 상황에서 쓰인다. 프론트엔드의 이벤트 바인딩, 리액티브 프로그래밍(RxJS, LiveData), 백엔드의 Kafka, Redis Pub/Sub 시스템 등도 사실상 Observer 개념이 구현된 구조다. 이 패턴의 장점은 주체(Subject)와 구독자(Observer) 사이의 강한 결합 없이, 비동기적으로 동작을 연결할 수 있다는 점이다.
4-3. Command – 요청을 객체로 캡슐화
Command 패턴은 사용자의 요청을 메서드 호출이 아닌 객체로 만들어서 큐에 저장하거나 실행 순서를 조정하는 구조다. 각 명령은 execute() 메서드를 가지고 있고, 요청 내역을 히스토리 형태로 저장해두면 나중에 되돌리거나 재실행할 수 있다. 또한 멀티스레드 작업 분산에도 활용된다.
예: UI에서 실행 취소(Undo), 다시 실행(Redo), 매크로 기능, 큐 작업 처리 등.
interface Command {
void execute();
}
이 패턴은 특히 사용자 행동 이력 관리, 배치 작업, 예약 시스템 등에서 활용도가 높다.
4-4. State – 상태별 동작을 분리
State 패턴은 객체의 상태가 바뀜에 따라 행동이 달라지는 경우, 상태별로 객체를 분리하는 방식이다. if-else가 많은 상태 머신 구조를 클래스로 추상화해 깔끔하게 표현할 수 있다. 예를 들어 주문 상태가 CREATED, PAID, CANCELLED일 때, 각각 다른 전환 조건과 처리를 담당하는 클래스를 만들 수 있다. 이렇게 하면 기존 클래스가 상태에 따라 수십 개의 조건 분기를 갖는 일을 방지할 수 있다.
5. 디자인 패턴 선택 기준과 적용 전략
5-1. 디자인 패턴은 문제 중심으로 선택하라
디자인 패턴은 “무엇을 쓰는가”보다 “왜 쓰는가”가 중요하다. 패턴은 특정 문제 상황에서 유용한 구조를 제시할 뿐, 무조건적인 해답이 아니다. 예를 들어 전략 패턴은 알고리즘이 바뀔 가능성이 있을 때, 데코레이터는 기능이 확장될 가능성이 있을 때 선택하는 것이다. 따라서 패턴을 고를 때는 변화의 방향과 책임 분리의 필요성을 먼저 살펴야 한다.
5-2. 과한 추상화는 독이 된다
패턴을 적용하면서 자주 저지르는 실수가 ‘아직 필요하지 않은 추상화’를 미리 하는 것이다. 이런 구조는 코드가 복잡해지고, 팀원들이 쉽게 이해하지 못하게 만든다. YAGNI 원칙을 기억하자. 당장 필요하지 않은 건 만들지 말자. 대부분의 패턴은 반복되는 문제를 겪은 뒤 적용하는 게 가장 효과적이다.
5-3. 현실의 제약도 고려하라
패턴이 아무리 좋아도, 적용 대상이 너무 작거나 팀원들이 생소하다면 오히려 유지보수에 악영향을 미친다. 따라서 팀 규모, 코드 베이스의 복잡성, 사용 기술 스택 등을 종합적으로 고려해 선택해야 한다.
예: 혼자 개발하는 사이드 프로젝트에 추상 팩토리 + 전략 + 프록시를 전부 넣으면 과도한 구조가 된다.
5-4. 설계 의도 전달이 핵심이다
좋은 설계는 구조보다 의도를 잘 드러내는 구조다. 전략 패턴을 썼다면 “이건 나중에 바뀔 가능성이 있는 정책임”을 말하고 있는 것이고, 싱글턴을 썼다면 “이건 전역으로 단 하나여야 함”을 명시하고 있는 셈이다. 디자인 패턴은 그 자체보다 설계를 설명하는 언어라는 사실을 잊지 말자.
'컴퓨터공학' 카테고리의 다른 글
싱글턴과 DI 컨테이너 구조 (0) | 2025.05.23 |
---|---|
팩토리 vs 추상 팩토리 패턴 (0) | 2025.05.22 |
SRP vs OCP, 실제 적용 사례 (0) | 2025.05.22 |
SOLID 원칙 완전정복 (0) | 2025.05.21 |
의존성 역전 원칙의 진짜 의미 (0) | 2025.05.21 |