소프트웨어 개발에서 디자인 패턴(Design Pattern)은 자주 발생하는 문제에 대해 재사용 가능한 설계의 해법을 제시합니다. 자바는 객체 지향 언어의 특성을 바탕으로 다양한 디자인 패턴을 효과적으로 구현할 수 있습니다. 디자인 패턴은 일반적으로 생성 패턴(Creational), 구조 패턴(Structural), 행위 패턴(Behavioral)의 세 가지 범주로 나뉘며, 아래에서 각각의 대표적인 패턴들을 소개합니다.
객체 생성과 관련된 패턴으로, 객체 생성의 로직을 분리하여 유연하게 객체를 생성할 수 있도록 함.
| 패턴 이름 | 설명 |
|---|---|
| Singleton | 애플리케이션 전체에서 인스턴스를 하나만 생성하여 공유 |
| Factory Method | 객체 생성 코드를 서브클래스에서 구현하여, 객체 생성을 캡슐화 |
| Abstract Factory | 관련 객체들을 그룹으로 생성할 수 있는 팩토리 제공 |
| Builder | 복잡한 객체를 단계적으로 생성. 동일한 생성 절차에 다양한 표현 가능 |
| Prototype | 기존 객체를 복사하여 새 객체 생성 (clone() 활용) |
클래스나 객체를 조합해 더 큰 구조를 만들 때 사용하는 패턴
| 패턴 이름 | 설명 |
|---|---|
| Adapter | 호환되지 않는 인터페이스를 맞춰주는 패턴 (예: 레거시 시스템 연결) |
| Bridge | 구현과 추상화를 분리하여 독립적으로 확장 가능하게 함 |
| Composite | 객체들을 트리 구조로 구성해 전체-부분 구조 표현 |
| Decorator | 기존 객체에 기능을 동적으로 추가 |
| Facade | 복잡한 서브시스템에 단순한 인터페이스 제공 |
| Flyweight | 공유 가능한 객체를 사용하여 메모리 사용 절약 |
| Proxy | 실제 객체 대신 대리 객체를 사용하여 접근 제어 |
객체 간의 책임 분배 및 통신 패턴
| 패턴 이름 | 설명 |
|---|---|
| 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 |
객체 생성 과정을 추상화하여 유연하고 재사용 가능한 객체 생성을 돕는 패턴입니다.
Runtime.getRuntime(), Spring의 Bean 객체public class Singleton {
private static final Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
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();
}
}
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); }
}
}
클래스나 객체를 조합하여 더 큰 구조를 만들 때 사용하는 패턴입니다.
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();
}
}
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; }
}
객체 간의 책임 분산, 통신 방법 등을 정의합니다.
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);
}
}
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은 이를 어렵게 만듭니다.