디자인 패턴 (Design Pattern)
디자인 패턴이란 아키텍처 설계 수준보다 낮은 수준의 설계 문제를 효과적으로 해결하기 위한 “재사용 가능한 솔루션”이다. 디자인 패턴이 적용되는 절차는 다음과 같다.
- 요구 사항을 고려하여 아키텍처를 확정한다.
- 아키텍처의 컴포넌트들을 구현하는 클래스의 역할과 동작을 결정한다.
- 발생하는 설계 이슈에 대해 솔루션으로 적합한 디자인 패턴을 적용하고 개선시킨다.
디자인 패턴의 혜택
디자인 패턴을 사용하면 누리게 되는 혜택은 다음과 같다.
- 쉽게 재사용이 가능하다.
- 설계 작업이 쉬워진다.
- 설계 관련 지식이 정리된다.
- 설계를 논의하기 위한 의사소통이 쉬워진다.
- 객체지향 설계 원리를 잘 따르게 된다.
디자인 패턴의 형식
소프트웨어 디자인 패턴을 설명하는 일관되는 형식이 존재한다. 디자인 패턴을 서명할 때 사용되는 형식은 다음과 같다.
- `패턴 이름`: 짧은 설명과 이름은 의사소통을 촉진하고 디자인 아이디어를 논의하기 위한 어휘를 제공한다.
- `소개`: 각 디자인 패턴에 대한 설명은 배경을 정의하고 패턴을 학습하는 동기를 제공하는 소개로 시작한다.
- `해결하는 문제`: 해당 디자인 패턴으로 해결되는 설계 이슈에 대해 설명한다.
- `솔루션`: 솔루션은 디자인 패턴의 본질로, 디자인 문제를 해결하기 위한 해법이기에 디자인 패턴은 실행 코드보다 추상적인 설계 조각이다.
- `예제`: 디자인 패턴은 추상적 해법이기에, 특정 예제에 적용된 것을 보며 이해를 높인다.
- `관련 패턴`: 한 패턴이 다른 패턴을 완전히 포함하거나, 비슷한 문제를 해결할 수 있다. 따라서 유사한 패턴이 있는 경우 구분하기 위한 설명이 추가된다.
싱글톤 패턴 (Singleton Pattern)
해결하려는 문제
싱글톤 패턴은 다음과 같은 상황에서 발생하는 문제를 해결하고자 한다:
- 클래스의 인스턴스가 단 하나만 존재해야 하는 경우
- 여러 곳에서 해당 인스턴스에 대한 일관된 접근이 필요한 경우
- 전역 상태를 관리해야 하지만, 전역 변수를 사용하면 안 되는 경우
솔루션
싱글톤 패턴은 다음과 같은 방식으로 문제를 해결한다:
- 클래스의 생성자를 private으로 선언하여 외부에서 인스턴스 생성을 막는다
- 클래스 내부에 자신의 인스턴스를 private static으로 보관한다
- public static 메서드를 통해 단일 인스턴스에 대한 접근점을 제공한다
- 처음 호출될 때만 인스턴스를 생성하고, 이후에는 이미 생성된 인스턴스를 반환한다
구현 사례
public class Singleton {
// private static 인스턴스 변수
private static Singleton instance;
// private 생성자
private Singleton() {}
// public static 접근 메서드
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
실제 활용 사례
- 데이터베이스 연결 관리자: 애플리케이션에서 단일 데이터베이스 연결을 유지하고 관리
- 설정 관리자: 애플리케이션의 환경 설정을 중앙에서 관리
- 로깅 시스템: 로그 기록을 중앙에서 일관되게 관리
- 프린터 스풀러: 프린터 작업을 중앙에서 관리하고 조정
반복자 패턴 (Iterator Pattern)
해결하려는 문제
반복자 패턴은 다음과 같은 상황에서 발생하는 문제를 해결하고자 한다:
- 컬렉션의 내부 구조를 노출하지 않고 모든 요소에 순차적으로 접근해야 하는 경우
- 서로 다른 자료구조(배열, 리스트, 트리 등)에 대해 동일한 방식으로 순회해야 하는 경우
- 컬렉션의 순회 방식을 변경하거나 확장할 필요가 있는 경우
솔루션
반복자 패턴은 다음과 같은 방식으로 문제를 해결한다:
- 순회 동작을 별도의 반복자 객체로 캡슐화한다
- 컬렉션의 내부 구현과 상관없이 동일한 인터페이스로 접근할 수 있게 한다
- hasNext()와 next() 같은 표준화된 메서드를 제공한다
- 여러 개의 반복자가 동시에 컬렉션을 순회할 수 있게 한다
구현 사례
public interface Iterator<T> {
boolean hasNext();
T next();
}
public class ArrayIterator<T> implements Iterator<T> {
private T[] array;
private int position = 0;
public ArrayIterator(T[] array) {
this.array = array;
}
public boolean hasNext() {
return position < array.length;
}
public T next() {
return array[position++];
}
}
실제 활용 사례
- Java의 Collection Framework: List, Set 등의 컬렉션에서 표준화된 반복자 제공
- 데이터베이스 결과셋 순회: JDBC ResultSet을 통한 쿼리 결과 순회
- 파일 시스템 탐색: 디렉토리와 파일을 순회하는 파일 탐색기
- 복합 객체 구조 순회: 트리나 그래프 같은 복잡한 자료구조의 순회
어댑터 패턴 (Adapter Pattern)
해결하려는 문제
어댑터 패턴은 다음과 같은 상황에서 발생하는 문제를 해결하고자 한다:
- 서로 다른 인터페이스를 가진 클래스들이 함께 동작해야 하는 경우
- 기존 코드를 수정하지 않고 호환되지 않는 인터페이스를 사용해야 하는 경우
- 레거시 시스템과 새로운 시스템을 통합해야 하는 경우
솔루션
어댑터 패턴은 다음과 같은 방식으로 문제를 해결한다:
- 기존 클래스를 감싸는 어댑터 클래스를 생성한다
- 클라이언트가 기대하는 인터페이스를 어댑터가 구현한다
- 어댑터 내부에서 기존 클래스의 메서드를 호출하여 변환 작업을 수행한다
- 클라이언트는 어댑터를 통해 기존 클래스의 기능을 이용한다
구현 사례
// 기존 인터페이스
interface MediaPlayer {
void play(String filename);
}
// 변환이 필요한 클래스
class AdvancedMediaPlayer {
void playMp4(String filename) {
System.out.println("Playing mp4 file: " + filename);
}
}
// 어댑터 클래스
class MediaAdapter implements MediaPlayer {
private AdvancedMediaPlayer advancedPlayer;
public MediaAdapter() {
this.advancedPlayer = new AdvancedMediaPlayer();
}
public void play(String filename) {
advancedPlayer.playMp4(filename);
}
}
실제 활용 사례
- 외부 라이브러리 통합: 서로 다른 API를 연결하여 사용
- 레거시 시스템 통합: 오래된 시스템과 새로운 시스템 간의 인터페이스 변환
- 다양한 데이터 형식 처리: JSON, XML 등 다른 형식의 데이터를 변환하여 처리
- 하드웨어 인터페이스: 다양한 하드웨어 장치와의 통신을 위한 인터페이스 제공
데코레이터 패턴 (Decorator Pattern)
해결하려는 문제
데코레이터 패턴은 다음과 같은 상황에서 발생하는 문제를 해결하고자 한다:
- 객체의 기능을 동적으로 확장하고 싶은 경우
- 상속을 사용하지 않고 유연하게 기능을 추가하고 싶은 경우
- 클래스 수의 증가 없이 다양한 조합의 기능을 제공하고 싶은 경우
솔루션
데코레이터 패턴은 다음과 같은 방식으로 문제를 해결한다:
- 기본 기능을 정의한 인터페이스나 추상 클래스를 만든다
- 데코레이터는 해당 인터페이스를 구현하거나 상속한다
- 데코레이터는 원래 객체를 감싸고 있으며, 추가 기능을 제공한다
- 클라이언트는 데코레이터를 통해 객체에 확장된 기능을 적용한다
구현 사례
interface Printer {
void print();
}
class Text implements Printer {
public void print() {
System.out.print("Hello");
}
}
class Exclamation implements Printer {
private Printer p;
Exclamation(Printer p) { this.p = p; }
public void print() {
p.print();
System.out.print("!");
}
}
// 사용
public class Main {
public static void main(String[] args) {
Printer p = new Exclamation(new Text());
p.print(); // 출력: Hello!
}
}
실제 활용 사례
- GUI 컴포넌트 꾸미기: 버튼, 스크롤바 등의 기능을 동적으로 추가
- 입출력 스트림: Java의 BufferedInputStream, DataInputStream 등
- 로깅 기능 추가: 객체 동작 전후에 로그 출력
- 기능 모듈화: 인증, 캐싱, 암호화 등을 유연하게 조합
팩토리 메서드 패턴 (Factory Method Pattern)
해결하려는 문제
팩토리 메서드 패턴은 다음과 같은 상황에서 발생하는 문제를 해결하고자 한다:
- 객체 생성 코드를 클라이언트 코드에서 분리하고 싶을 때
- 객체 생성 과정이 복잡하거나, 하위 클래스에 따라 다른 객체를 생성해야 할 때
- 코드 변경 없이 새로운 타입의 객체를 유연하게 추가하고 싶을 때
솔루션
팩토리 메서드 패턴은 다음과 같은 방식으로 문제를 해결한다:
- 객체 생성을 캡슐화한 팩토리 메서드를 정의한다
- 서브클래스에서 이 메서드를 오버라이드하여 원하는 객체를 생성하도록 한다
- 클라이언트는 구체적인 클래스 대신 팩토리 메서드를 통해 객체를 얻는다
- 이를 통해 객체 생성 로직을 변경하거나 확장하기 쉬워진다
구현 사례
// 제품 인터페이스
interface Product {
void use();
}
// 구체적인 제품
class ConcreteProduct implements Product {
public void use() {
System.out.println("Using product");
}
}
// 팩토리 클래스
abstract class Creator {
abstract Product createProduct();
public void operation() {
Product p = createProduct();
p.use();
}
}
// 구체적인 팩토리
class ConcreteCreator extends Creator {
public Product createProduct() {
return new ConcreteProduct();
}
}
public class Main {
public static void main(String[] args) {
Creator creator = new ConcreteCreator();
creator.operation(); // 출력: Using product
}
}
실제 활용 사례
- Java의 Calendar.getInstance(), Connection.getConnection() 등
- UI 컴포넌트 생성 시 테마나 플랫폼에 따라 다른 객체 반환
- 로그 시스템, 알림 시스템 등에서 다양한 구현체를 유연하게 교체
추상 팩토리 패턴 (Abstract Factory Pattern)
해결하려는 문제
추상 팩토리 패턴은 다음과 같은 문제를 해결하고자 한다:
- 관련된 객체들을 일관되게 생성해야 하는 경우 (ex. 버튼 + 입력창 세트 등)
- 구상 클래스에 의존하지 않고, 제품군을 생성하고 싶을 때
- 제품들의 호환성을 보장하면서 다양한 제품군을 지원하고 싶을 때
솔루션
추상 팩토리 패턴은 다음과 같은 방식으로 문제를 해결한다:
- 관련 객체들을 생성하는 팩토리 인터페이스를 정의한다
- 각 제품군에 대해 구체 팩토리 클래스를 만든다
- 클라이언트는 구체 클래스가 아닌 팩토리 인터페이스를 통해 제품을 생성한다
- 이를 통해 제품군 간의 호환성과 일관성을 유지할 수 있다
구현 사례
// 제품 인터페이스
interface Button { void click(); }
// 구체 제품
class MacButton implements Button {
public void click() { System.out.println("Mac Button"); }
}
class WinButton implements Button {
public void click() { System.out.println("Windows Button"); }
}
// 추상 팩토리
interface GUIFactory {
Button createButton();
}
// 구체 팩토리
class MacFactory implements GUIFactory {
public Button createButton() { return new MacButton(); }
}
class WinFactory implements GUIFactory {
public Button createButton() { return new WinButton(); }
}
// 사용
public class Main {
public static void main(String[] args) {
GUIFactory factory = new MacFactory(); // 또는 new WinFactory()
Button btn = factory.createButton();
btn.click(); // 출력: Mac Button
}
}
실제 활용 사례
- UI 테마 변경: 다크 모드 / 라이트 모드 컴포넌트 일괄 생성
- 운영체제별 컴포넌트: Windows / macOS / Linux 용 UI 위젯
- 게임 아이템 생성: 종족 또는 클래스에 따라 무기, 방어구 등을 일괄 제공
- 테스트 환경 분리: 실제 서비스 객체 vs Mock 객체 팩토리로 전환
상태 패턴 (State Pattern)
해결하려는 문제
상태 패턴은 다음과 같은 문제를 해결하고자 한다:
- 객체의 내부 상태에 따라 행동이 달라지는 경우
- 조건문(if, switch)으로 상태별 동작을 처리하는 코드가 복잡하고 유지보수가 어려운 경우
- 상태 전환 로직과 동작을 분리하고 싶을 때
솔루션
상태 패턴은 다음과 같이 문제를 해결한다:
- 상태를 클래스로 캡슐화하여, 각 상태에 따른 동작을 클래스로 나눈다
- 컨텍스트 객체는 상태 객체에 위임하여 동작을 수행한다
- 상태 전환은 상태 객체 내부나 컨텍스트에서 명확하게 처리한다
- 조건문 없이도 상태에 따라 동작이 유연하게 바뀐다
구현 사례
// 상태 인터페이스
interface State {
void handle(Context ctx);
}
// 상태 A
class OnState implements State {
public void handle(Context ctx) {
System.out.println("전원 ON");
ctx.setState(new OffState());
}
}
// 상태 B
class OffState implements State {
public void handle(Context ctx) {
System.out.println("전원 OFF");
ctx.setState(new OnState());
}
}
// 컨텍스트
class Context {
private State state;
public Context(State state) { this.state = state; }
public void setState(State state) { this.state = state; }
public void pressButton() { state.handle(this); }
}
// 사용 예
public class Main {
public static void main(String[] args) {
Context c = new Context(new OffState());
c.pressButton(); // 전원 ON
c.pressButton(); // 전원 OFF
}
}
실제 활용 사례
- UI 버튼 상태 관리: 활성/비활성/누름 상태에 따라 동작 변경
- 문서 편집기: 편집 모드 / 읽기 전용 모드 / 선택 모드 전환
- 게임 캐릭터: 걷기 / 점프 / 공격 / 죽음 상태에 따라 다른 행동
- 네트워크 연결: 연결됨 / 연결 중 / 끊김 상태에서 전송 방식 달라짐
옵서버 패턴 (Observer Pattern)
해결하려는 문제
옵서버 패턴은 다음과 같은 문제를 해결하고자 한다:
- 어떤 객체의 상태 변화에 따라 다른 객체들을 자동으로 갱신해야 하는 경우
- 객체 간의 느슨한 결합이 필요할 때
- 이벤트 기반 시스템에서 변경을 알리고 반응하는 구조가 필요할 때
솔루션
옵서버 패턴은 다음과 같이 문제를 해결한다:
- 주체(Subject)가 옵서버(Observer)들을 관리한다
- 상태가 변경되면 주체는 등록된 옵서버들에게 자동으로 알림을 보낸다
- 옵서버들은 주체와 직접 연결되지 않고, 인터페이스를 통해 느슨하게 연결된다
구현 사례
// 옵서버 인터페이스
interface Observer {
void update(String msg);
}
// 주체 인터페이스
interface Subject {
void addObserver(Observer o);
void notifyObservers(String msg);
}
// 구체 주체
class NewsAgency implements Subject {
private List<Observer> observers = new ArrayList<>();
public void addObserver(Observer o) { observers.add(o); }
public void notifyObservers(String msg) {
for (Observer o : observers) o.update(msg);
}
}
// 구체 옵서버
class NewsReader implements Observer {
private String name;
public NewsReader(String name) { this.name = name; }
public void update(String msg) {
System.out.println(name + " received: " + msg);
}
}
// 사용 예
public class Main {
public static void main(String[] args) {
NewsAgency agency = new NewsAgency();
agency.addObserver(new NewsReader("Alice"));
agency.addObserver(new NewsReader("Bob"));
agency.notifyObservers("Breaking News!");
}
}
실제 활용 사례
- GUI 이벤트 시스템: 버튼 클릭 시 여러 리스너에게 알림 전달
- 채팅 앱: 새로운 메시지가 오면 구독한 사용자에게 알림
- 모델-뷰(Model-View) 구조: 모델이 변경되면 뷰가 자동으로 갱신
- 파일 감시 시스템: 파일 변경 시 등록된 작업 수행
반응형