0. 들어가며
소프트웨어 개발은 언제나 최소한의 비용으로 최대의 가치를 만들어내는 것을 목표로 해 왔습니다.
그래서 개발자라면 한 번쯤은 들어봤을 법한 단어들이 있죠.
[재사용성], [유지보수], [효율적], [생산적]
우리는 단순히 지금 당장의 문제만 해결하는 게 아닙니다.
좋은 개발자는 항상 이런 질문을 떠올립니다:
- 나중에 어떤 문제가 생길지도 대비했는가?
- 새로운 요구사항이 들어와도 쉽게 바꿀 수 있을까?
- 비슷한 기능을 또 만들 땐 이걸 재사용할 수 있을까?
- 다른 개발자가 내 코드를 봐도 바로 이해할 수 있을까?
이런 고민은 결국 하나의 방향으로 수렴됩니다.
바로 "더 나은 설계"를 향한 끊임없는 탐색입니다.
그리고 그 과정 속에서 등장한 것이 바로 디자인 패턴(Design Pattern)입니다.
1. 디자인 패턴(Design Pattern)이란?
디자인 패턴은 소프트웨어 설계과정에서 자주 마주치는 문제들에 대한 전형적인 해결책입니다.
쉽게 말해, 반복되는 문제에 대한 “경험에서 나온 모범 답안”이라 할 수 있죠.
이러한 패턴들은 수많은 개발자들이 다양한 프로젝트에서 직접 부딪히며 고민하고 다듬은 결과물입니다.
그래서 흔히 모범 사례(Best Practice)라고도 불립니다.
“디자인 패턴 = 코드 템플릿?”
아니요, 아닙니다.
우리가 표준 라이브러리의 함수나 API는 복사해서 붙여 쓸 수 있지만,
디자인 패턴은 그렇게 쓰는 게 아닙니다.
디자인 패턴은 재사용 가능한 코드 조각이 아니라,
특정 상황에서 효과적인 설계를 이끄는 개념적 가이드입니다.
즉, 하나의 패턴을 적용해도
누가, 어디에, 어떻게 적용했느냐에 따라 완전히 다른 코드가 나올 수 있어요.
그래서 디자인 패턴은 종종 "복붙이 아닌 사고 방식”이라고 불립니다.
객체지향과 디자인 패턴의 관계
디자인 패턴은 객체지향 프로그래밍(OOP)의 4대 특성:
디자인 패턴은 대부분 객체지향 프로그래밍(OOP)의 핵심 특성들을 기반으로 합니다:
- 캡슐화: 객체 내부 상태 보호
- 상속: 기능의 재사용과 계층 구조
- 추상화: 공통 개념을 묶어내는 설계
- 다형성: 같은 인터페이스, 다양한 구현
그리고 더 나아가 SOLID 원칙에 맞춰진 패턴들은 보다
확장 가능하고, 유지보수하기 쉬운 코드 구조를 만드는 데 큰 도움을 줍니다.
SOLID원칙이란?
SOLID 원칙은 객체지향 설계에서 유지보수성과 확장성이 뛰어난 구조를 만들기 위해 제안된 5가지 설계 원칙의 약자입니다.
1. S - 단일 책임 원칙 (SRP: Single Responsibility Principle)
“클래스는 하나의 책임만 가져야 한다.”
- 하나의 클래스는 오직 하나의 기능(책임)만 담당해야 합니다.
- 기능이 많아질수록 클래스는 커지고, 변경에 취약해지며, 테스트도 어려워집니다.
예시)
`ReportPrinter` 클래스가 출력도 하고 PDF 저장도 하고 DB 저장도 하면? → ❌
→ 출력은 `Printer`, 저장은 `Saver`로 나누는 것이 바람직합니다. ✅
2. O - 개방/폐쇄 원칙 (OCP: Open/Closed Principle)
“확장에는 열려 있어야 하고, 변경에는 닫혀 있어야 한다.”
- 기능을 추가할 때는 기존 코드를 수정하지 않고 확장으로 처리해야 합니다.
- 즉, 코드 기본 구조는 그대로 두고 새로운 클래스로 확장합니다.
예시)
결제 시스템에 신용카드 외에 카카오페이를 추가하려면?
→ 기존 `PaymentProcessor` 코드를 변경하지 않고 `KakaoPay` 클래스를 새로 만들어 확장합니다. ✅
3. L - 리스코프 치환 원칙 (LSP: Liskov Substitution Principle)
“자식 클래스는 언제나 부모 클래스를 대체할 수 있어야 한다.”
- 부모 클래스의 객체를 사용하는 곳에 자식 클래스 객체를 넣어도 문제가 없어야 합니다.
- 기능을 "확장"하기 위한 수단이지, "깨뜨리는" 도구가 되어선 안 됩니다.
예시)
`Bird` 클래스에는 `fly()`가 있지만, `Penguin`이 상속받는다면?
→ `Penguin.fly()`는 동작하지 않으므로 원칙 위배 ❌
→ 이 경우 `FlyingBird`와 `NonFlyingBird`로 따로 분리해야 합니다. ✅
4. I - 인터페이스 분리 원칙 (ISP: Interface Segregation Principle)
“클라이언트는 자신이 사용하지 않는 메서드에 의존하면 안 된다.”
- 하나의 큰 인터페이스보다는 여러 개의 구체적인 인터페이스로 나누는 게 좋습니다.
- 인터페이스가 너무 많은 기능을 담고 있으면, 필요 없는 기능도 구현해야 하므로 불필요한 코드가 생깁니다.
예시)
`Machine` 인터페이스에 `print()`, `scan()`, `fax()`가 있는데, `OldPrinter`는 `print()`만 필요하다면?
→ 기능별로 `Printable`, `Scannable`, `Faxable`로 나눠야 합니다. ✅
5. D - 의존 역전 원칙 (DIP: Dependency Inversion Principle)
“고수준 모듈은 저수준 모듈에 의존하면 안 된다. 둘 다 추상화에 의존해야 한다.”
“결정권을 구체적인 것(MySQL 등)이 아니라, 추상적인 것(인터페이스)에 두자!”
- 클래스는 구체적인 구현이 아니라, 인터페이스나 추상 클래스에 의존해야 합니다.
- 이는 의존성 주입(Dependency Injection)의 기반 개념이기도 합니다.
예시)
`OrderService`가 직접 `MySQLRepository`를 생성하면 ❌
→ 인터페이스 `Repository`를 주입받아 `MySQLRepository`, `InMemoryRepository`로 구현하게 해야 합니다. ✅
디자인 패턴의 5가지 장점
- 재사용성 (Reusability)
- 비슷한 문제에 매번 다른 해결책을 고민할 필요 없이, 이미 검증된 방식을 재사용할 수 있습니다.
- 가독성 (Readability)
- 디자인 패턴은 일정한 구조를 따르기 때문에, 다른 개발자도 코드를 이해하기 쉬워집니다.
팀 내 커뮤니케이션도 원활해집니다.
- 디자인 패턴은 일정한 구조를 따르기 때문에, 다른 개발자도 코드를 이해하기 쉬워집니다.
- 유지보수성 (Maintainability)
- 패턴 기반 설계는 보통 기능이 명확하게 분리(모듈화)되어 있습니다.
따라서 코드 일부만 수정해도 전체 시스템에 영향이 가지 않도록 할 수 있습니다.
- 패턴 기반 설계는 보통 기능이 명확하게 분리(모듈화)되어 있습니다.
- 확장성 (Extensibility)
- 새로운 기능을 추가하더라도, 기존 코드를 거의 수정하지 않고도 기능을 확장할 수 있습니다.
→ 이는 OCP(개방-폐쇄 원칙)의 구현에도 도움이 되며, 유지보수 비용 절감과 버그 발생 가능성 최소화로 이어집니다.
- 새로운 기능을 추가하더라도, 기존 코드를 거의 수정하지 않고도 기능을 확장할 수 있습니다.
- 안정성과 신뢰성 (Stability & Reliability)
- 수많은 개발자들이 현장에서 검증한 솔루션입니다.
→ 결과적으로 예상치 못한 버그 발생률을 줄이고, 품질을 높이는 데 기여합니다.
- 수많은 개발자들이 현장에서 검증한 솔루션입니다.
요리로 이해하는 디자인 패턴
예를 들어, 우리가 떡볶이를 만들 때 된장을 넣는다면?
뭐... 법적으로 문제는 없겠지만,
대부분의 사람들은 이렇게 말할 겁니다:
“그게 떡볶이냐?”
왜일까요?
이미 많은 사람들이 공감하고 합의한 ‘떡볶이 레시피’가 있기 때문입니다.
프로그래밍도 마찬가지예요.
문제를 해결하는 방법은 무궁무진하지만,
그중에서도 많은 사람들이 겪어본 문제에 대해
검증된 해법(디자인 패턴)을 쓰는 것이 더 낫습니다.
- 이미 검증된 설계 방식이기 때문에 안정적이고
- 다른 개발자도 쉽게 이해할 수 있어 협업이 쉬우며
- 구조가 명확하니 유지보수도 편해집니다
2. 디자인 패턴의 구성 요소
디자인 패턴은 단순한 "아이디어"가 아닙니다.
누구나 이해하고 적용할 수 있도록 템플릿 형식으로 정리되어 있어요.
보통 하나의 패턴은 다음과 같은 구성으로 설명됩니다:
1. 의도(Intent)
- 이 패턴이 어떤 문제를 해결하고자 하는가?
- 핵심 아이디어와 목표를 요약합니다.
2. 동기(Motivation)
- 실제로 어떤 상황에서 이 패턴이 등장했고,
- 왜 이 접근법이 효과적인지를 설명합니다.
3. 구조(Structure)
- UML 다이어그램 등으로 각 구성요소와 그 관계를 시각화합니다.
→ 패턴의 전반적인 설계 흐름을 한눈에 파악할 수 있게 도와줍니다.
4. 코드 예시(Code Example)
- 자바, 파이썬, C++ 등 다양한 언어로 구현된 예제를 통해 개념을 명확히 합니다.
5. 기타 정보
- 적용 방법, 장단점, 구현 시 주의점, 그리고 다른 패턴들과의 관계 등도 함께 다루는 경우가 많습니다.
3. GoF의 23가지 디자인 패턴
가장 유명하고 널리 사용되는 디자인 패턴은 GoF(Gang of Four)가 정리한 23가지 패턴입니다.
이들은 목적에 따라 세 가지 카테고리로 분류됩니다:
생성(Creational) 패턴
객체를 어떻게 생성할지에 대한 패턴입니다.
복잡한 객체 생성 과정을 추상화하고, 객체 간의 결합도를 줄여줍니다.
- Singleton – 오직 하나의 인스턴스만 존재하게 함
- Factory Method – 객체 생성을 서브클래스에 위임
- Abstract Factory – 관련 객체를 묶어서 생성
- Builder – 복잡한 객체를 단계적으로 생성
- Prototype – 기존 객체를 복사해 새 객체 생성
구조(Structural) 패턴
객체와 클래스를 어떻게 조합할 것인지에 대한 패턴입니다.
클래스나 객체들을 더 유연하게 연결해 확장성과 재사용성을 높여줍니다.
- Adapter – 인터페이스가 맞지 않는 클래스 연결
- Bridge – 구현과 추상을 분리하여 독립적으로 확장
- Composite – 트리 구조로 객체를 구성 (부분-전체 관계)
- Decorator – 기능을 동적으로 추가
- Facade – 복잡한 시스템을 단순한 인터페이스로 제공
- Flyweight – 메모리를 절약하기 위한 공유 객체
- Proxy – 접근 제어나 지연 로딩 등을 위한 대리 객체
행위(Behavioral) 패턴
객체들 간의 커뮤니케이션과 책임 분배에 관한 패턴입니다.
행동의 유연성과 객체 간 협력 구조를 설계하는 데 중점을 둡니다.
- Observer – 변화가 생기면 관련 객체에 알림
- Strategy – 알고리즘을 런타임에 교체 가능
- Command – 요청을 객체로 캡슐화
- State – 상태에 따라 객체 행동 변경
- Chain of Responsibility – 요청 처리 책임을 체인으로 분리
- Visitor – 객체 구조를 변경하지 않고 새로운 연산 수행
- Interpreter – 언어나 문법 해석
- Memento – 객체의 이전 상태를 저장 및 복원
- Mediator – 객체 간 상호작용을 중재자로 캡슐화
- Template Method – 알고리즘의 구조는 고정하고 일부 단계만 재정의
- Iterator – 컬렉션 내부 구조를 노출하지 않고 순회
4. 디자인 패턴의 문제점
디자인 패턴은 분명히 강력하고 유용한 도구입니다.
하지만 그렇다고 해서 모든 상황에서 무조건 좋은 선택은 아닙니다.
실제로 몇몇 개발자들은 디자인 패턴에 대해 비판적인 시각을 가지고 있기도 하죠.
왜 그럴까요?
1. 약한 프로그래밍 언어를 위한 "임시방편"?
일부 비판자들은 이렇게 말합니다:
“디자인 패턴은 언어의 한계를 보완하기 위한 ‘꼼수’일 뿐이다.”
들어보면 어느 정도 일리가 있습니다.
일부 패턴은 모던 언어가 가진 강력한 기능으로 더 간단하게 해결할 수 있기 때문이죠.
예를 들어, Strategy 패턴은 객체의 동작(전략)을 런타임에 바꾸기 위해 자주 사용됩니다.
그런데 최근 언어(JavaScript, Python, Kotlin 등)에서는 그냥 람다 함수나 고차 함수로 처리하는 것이 더 직관적이고 효율적일 수 있습니다.
즉,
- 언어에 추상화 기능이 부족할수록 → 디자인 패턴이 필요해지고
- 언어 자체가 유연하고 표현력이 좋을수록 → 디자인 패턴이 ‘과한 설계’처럼 느껴질 수도 있습니다.
어떤 경우엔 디자인 패턴이 오히려 불필요한 **보일러플레이트(중복 코드)**를 양산하기도 해요.
2. '패턴이 먼저'인 도그마(Dogma)의 위험
디자인 패턴은 공통 문제에 대한 해법이지만,
이게 어느 순간부터 "무조건 따라야 할 법칙"처럼 여겨지는 경우가 생깁니다.
예를 들어 이런 상황이죠:
- 단순한 기능인데 굳이 패턴을 써서 구조가 복잡해짐
- 불필요한 추상화, 클래스, 인터페이스가 우후죽순 생김
- 팀원들은 "왜 이걸 썼는지"보다 "그냥 패턴이니까"를 이해함
- 패턴 그 자체가 문제보다 더 중요해져버림
이런 ‘패턴 중심 사고’는 오히려 개발을 느리고 어렵게 만들 수 있어요.
좋은 코드는 문제를 잘 푸는 코드이지, 디자인 패턴을 많이 쓴 코드가 아닙니다.
5. 마치며: 디자인 패턴, 도구인가 정답인가?
디자인 패턴은 분명 강력한 무기입니다.
하지만 그 무기를 언제 꺼내야 할지, 왜 꺼내야 할지에 대한 판단이 더 중요합니다.
- 최신 언어나 프레임워크에서는 오히려 더 간결하고 직관적인 대안이 존재할 수 있습니다.
- 코드보다 중요한 건, 해결하고자 하는 '문제 자체'를 중심으로 사고하는 것입니다.
- 결국 중요한 것은, 패턴이라는 이름보다 설계의 본질을 꿰뚫는 사고력입니다.
디자인 패턴은 정답이 아니라,
개발자들이 함께 일할 수 있도록 도와주는 ‘공통 언어’이자 ‘설계 도구’입니다.
무작정 적용하기보다는,
“이 패턴이 지금 이 문제에 정말 필요한가?”
“더 나은 방식은 없을까?”
이런 질문을 던질 수 있어야, 패턴을 제대로 ‘도구처럼’ 사용할 수 있습니다.