자바 디자인 패턴 정리

이경헌·2024년 11월 24일

소프트웨어 개발에서 디자인 패턴(Design Pattern)은 자주 발생하는 문제에 대해 재사용 가능한 설계의 해법을 제시합니다. 자바는 객체 지향 언어의 특성을 바탕으로 다양한 디자인 패턴을 효과적으로 구현할 수 있습니다. 디자인 패턴은 일반적으로 생성 패턴(Creational), 구조 패턴(Structural), 행위 패턴(Behavioral)의 세 가지 범주로 나뉘며, 아래에서 각각의 대표적인 패턴들을 소개합니다.


✅ 디자인 패턴 전체 목록

1. 생성(Creational) 패턴

객체 생성과 관련된 패턴으로, 객체 생성의 로직을 분리하여 유연하게 객체를 생성할 수 있도록 함.

패턴 이름설명
Singleton애플리케이션 전체에서 인스턴스를 하나만 생성하여 공유
Factory Method객체 생성 코드를 서브클래스에서 구현하여, 객체 생성을 캡슐화
Abstract Factory관련 객체들을 그룹으로 생성할 수 있는 팩토리 제공
Builder복잡한 객체를 단계적으로 생성. 동일한 생성 절차에 다양한 표현 가능
Prototype기존 객체를 복사하여 새 객체 생성 (clone() 활용)

2. 구조(Structural) 패턴

클래스나 객체를 조합해 더 큰 구조를 만들 때 사용하는 패턴

패턴 이름설명
Adapter호환되지 않는 인터페이스를 맞춰주는 패턴 (예: 레거시 시스템 연결)
Bridge구현과 추상화를 분리하여 독립적으로 확장 가능하게 함
Composite객체들을 트리 구조로 구성해 전체-부분 구조 표현
Decorator기존 객체에 기능을 동적으로 추가
Facade복잡한 서브시스템에 단순한 인터페이스 제공
Flyweight공유 가능한 객체를 사용하여 메모리 사용 절약
Proxy실제 객체 대신 대리 객체를 사용하여 접근 제어

3. 행위(Behavioral) 패턴

객체 간의 책임 분배 및 통신 패턴

패턴 이름설명
Observer한 객체의 상태 변화 시, 의존 객체들에게 자동으로 알림
Strategy알고리즘을 캡슐화하고, 실행 시 변경 가능하게 함
Command요청을 객체로 캡슐화. 작업 실행을 큐에 저장하거나 되돌리기 가능
Template Method알고리즘 구조는 유지하고, 세부 구현은 서브클래스에서 정의
State상태에 따라 객체의 행동을 변경할 수 있도록 설계
Chain of Responsibility요청을 처리할 수 있는 객체를 체인 형태로 연결
Mediator객체 간의 복잡한 상호작용을 중재자에게 위임
Iterator컬렉션 요소를 순차적으로 접근할 수 있도록 함
Visitor객체 구조는 수정하지 않고, 새로운 기능 추가
Interpreter언어나 문법의 해석을 위한 패턴 (간단한 언어 구현에 사용)
Memento객체 상태를 저장하고 복원할 수 있게 함 (undo 기능 등)

🎯 상황별 사용 예시

상황추천 패턴
인스턴스를 한 번만 생성해야 함Singleton
다양한 방식으로 객체 생성이 필요함Factory Method, Abstract Factory
데이터 구조를 트리처럼 구성Composite
기존 객체에 기능을 유연하게 추가Decorator
상태 변경 시, 관련 객체에게 알림 필요Observer
실행할 기능을 캡슐화하여 요청하고 싶을 때Command
알고리즘을 런타임에 교체Strategy
복잡한 시스템에 단순한 접근 제공Facade

📌 정리 요약

  • 생성 패턴: 객체 "어떻게 만들지"에 대한 패턴
  • 구조 패턴: 객체/클래스 "어떻게 구성할지"에 대한 패턴
  • 행위 패턴: 객체 "어떻게 상호작용할지"에 대한 패턴

✅ 1. 생성 패턴 (Creational Patterns)

객체 생성 과정을 추상화하여 유연하고 재사용 가능한 객체 생성을 돕는 패턴입니다.

📌 1-1. Singleton 패턴

  • 의도: 클래스의 인스턴스를 오직 하나만 생성하도록 보장
  • 특징: 전역 접근 지점 제공
  • 예시: Runtime.getRuntime(), Spring의 Bean 객체
public class Singleton {
    private static final Singleton instance = new Singleton();
    private Singleton() {}
    public static Singleton getInstance() {
        return instance;
    }
}

📌 1-2. Factory Method 패턴

  • 의도: 객체 생성 코드를 서브 클래스에 위임
  • 특징: 인터페이스 기반으로 다양한 객체 생성 가능
public interface Product {}
public class AProduct implements Product {}
public class BProduct implements Product {}

public class ProductFactory {
    public static Product createProduct(String type) {
        if (type.equals("A")) return new AProduct();
        else return new BProduct();
    }
}

📌 1-3. Builder 패턴

  • 의도: 복잡한 객체 생성을 단계별로 처리
  • 특징: 가독성 높은 코드, 불변 객체 생성에 유리
public class User {
    private final String name;
    private final int age;
    
    private User(Builder builder) {
        this.name = builder.name;
        this.age = builder.age;
    }

    public static class Builder {
        private String name;
        private int age;

        public Builder name(String name) { this.name = name; return this; }
        public Builder age(int age) { this.age = age; return this; }
        public User build() { return new User(this); }
    }
}

✅ 2. 구조 패턴 (Structural Patterns)

클래스나 객체를 조합하여 더 큰 구조를 만들 때 사용하는 패턴입니다.

📌 2-1. Adapter 패턴

  • 의도: 서로 다른 인터페이스를 가진 클래스들을 호환시키기 위해 중간 어댑터 클래스 도입
public interface Target {
    void request();
}

public class Adaptee {
    public void specificRequest() {
        System.out.println("Adaptee 호출");
    }
}

public class Adapter implements Target {
    private Adaptee adaptee = new Adaptee();
    public void request() {
        adaptee.specificRequest();
    }
}

📌 2-2. Decorator 패턴

  • 의도: 객체에 기능을 동적으로 추가
  • 특징: 상속 대신 조합을 통해 유연한 기능 확장
public interface Coffee {
    String getDescription();
    int cost();
}

public class BasicCoffee implements Coffee {
    public String getDescription() { return "Basic Coffee"; }
    public int cost() { return 1000; }
}

public class MilkDecorator implements Coffee {
    private Coffee coffee;
    public MilkDecorator(Coffee coffee) { this.coffee = coffee; }
    public String getDescription() { return coffee.getDescription() + ", Milk"; }
    public int cost() { return coffee.cost() + 500; }
}

✅ 3. 행위 패턴 (Behavioral Patterns)

객체 간의 책임 분산, 통신 방법 등을 정의합니다.

📌 3-1. Strategy 패턴

  • 의도: 알고리즘을 객체로 캡슐화하여 동적으로 교체 가능
  • 특징: if-else 없이 전략을 주입
public interface PaymentStrategy {
    void pay(int amount);
}

public class CardStrategy implements PaymentStrategy {
    public void pay(int amount) {
        System.out.println("카드로 결제: " + amount);
    }
}

public class User {
    private PaymentStrategy strategy;
    public User(PaymentStrategy strategy) {
        this.strategy = strategy;
    }
    public void pay(int amount) {
        strategy.pay(amount);
    }
}

📌 3-2. Observer 패턴

  • 의도: 한 객체의 상태 변화에 따라 의존 객체들에 자동으로 알림
  • 특징: 이벤트 기반 시스템에 활용
public interface Observer {
    void update(String msg);
}

public class User implements Observer {
    private String name;
    public User(String name) { this.name = name; }
    public void update(String msg) {
        System.out.println(name + "에게 알림: " + msg);
    }
}

✨ 마무리

디자인 패턴은 무조건 따라야 할 규칙이 아닌, 더 나은 구조와 유지보수성을 위한 도구입니다. 팀과 프로젝트 상황에 맞춰 적절히 선택하고 활용한다면 코드의 품질을 크게 향상시킬 수 있습니다.


참고

Singleton 패턴은 애플리케이션 내에서 특정 클래스의 인스턴스가 하나만 존재하도록 보장하는 디자인 패턴입니다. 그러나 이 패턴을 사용할 때 몇 가지 문제점이 발생할 수 있습니다:

  • 글로벌 상태(Global State): Singleton은 전역적으로 접근할 수 있는 단일 인스턴스를 제공하므로, 전역 상태를 관리하게 됩니다. 이로 인해 애플리케이션이 커지면서 코드 간에 의존성이 증가하고, 특정 클래스에 대한 변경이 다른 부분에 영향을 미칠 수 있습니다. 이는 코드의 유지 보수를 어렵게 만들고, 테스트를 방해할 수 있습니다.

  • 테스트 어려움 (Testing Difficulty): Singleton은 상태를 유지하는 전역 객체를 제공하므로, 테스트 환경에서 해당 인스턴스를 독립적으로 초기화하거나 격리하는 것이 어려울 수 있습니다. 예를 들어, 같은 테스트에서 같은 인스턴스를 공유하기 때문에 테스트 간에 상태가 공유되어 결과가 의도하지 않게 영향을 받을 수 있습니다.

  • 멀티스레드 환경에서의 문제: 멀티스레드 환경에서 Singleton 인스턴스가 동시에 여러 스레드에서 접근하려 할 때, 인스턴스가 두 번 생성되는 문제나, 초기화가 제대로 이루어지지 않는 문제가 발생할 수 있습니다. 이를 해결하려면 추가적인 동기화 작업이 필요하지만, 동기화는 성능 저하를 일으킬 수 있습니다.

  • 유연성 부족 (Reduced Flexibility): Singleton은 클래스 인스턴스를 한 번만 생성하므로, 애플리케이션이 동작하는 동안 인스턴스를 변경하거나 대체하는 것이 불가능합니다. 이는 시스템의 확장성이나 유연성을 제한할 수 있습니다.

  • 의존성 주입(DI) 방해: Singleton 패턴은 객체의 생명주기를 클래스 내에서 직접 관리하므로 의존성 주입(DI)을 활용한 유연한 객체 관리를 방해할 수 있습니다. DI를 통해 객체의 생성 및 의존성을 외부에서 관리하는 방식이 일반적으로 선호되지만, Singleton은 이를 어렵게 만듭니다.

0개의 댓글