Design Patterns Skill
Java 최신 예제를 통한 실용적인 디자인 패턴 참조입니다.
사용 시기
- 사용자가 특정 패턴 구현을 요청할 때
- 확장 가능하고 유연한 컴포넌트 설계 시
- 경직된 코드 구조 리팩토링 시
- 코드 리뷰에서 패턴 사용이 제안될 때
빠른 참조: 무엇을 언제 사용할까
| 문제 | 패턴 |
|---|---|
| 복잡한 객체 생성 | Builder |
| 클래스 지정 없이 객체 생성 | Factory |
| 여러 알고리즘, 런타임에 교체 | Strategy |
| 클래스 변경 없이 동작 추가 | Decorator |
| 여러 객체에 변경 알림 | Observer |
| 단일 인스턴스 보장 | Singleton |
| 호환되지 않는 인터페이스 변환 | Adapter |
| 알고리즘 골격 정의 | Template Method |
생성 패턴 (Creational Patterns)
Builder
사용 시기: 객체 매개변수가 많고 일부는 선택적일 때.
// ❌ 생성자 매개변수가 늘어나는 안티패턴
public class User {
public User(String name) { }
public User(String name, String email) { }
public User(String name, String email, int age) { }
public User(String name, String email, int age, String phone) { }
// ... 생성자 폭발
}
// ✅ Builder 패턴
public class User {
private final String name; // 필수
private final String email; // 필수
private final int age; // 선택
private final String phone; // 선택
private final String address; // 선택
private User(Builder builder) {
this.name = builder.name;
this.email = builder.email;
this.age = builder.age;
this.phone = builder.phone;
this.address = builder.address;
}
public static Builder builder(String name, String email) {
return new Builder(name, email);
}
public static class Builder {
// 필수
private final String name;
private final String email;
// 선택 (기본값)
private int age = 0;
private String phone = "";
private String address = "";
private Builder(String name, String email) {
this.name = name;
this.email = email;
}
public Builder age(int age) {
this.age = age;
return this;
}
public Builder phone(String phone) {
this.phone = phone;
return this;
}
public Builder address(String address) {
this.address = address;
return this;
}
public User build() {
return new User(this);
}
}
}
// 사용법
User user = User.builder("John", "[email protected]")
.age(30)
.phone("+1234567890")
.build();
Lombok 사용 시:
@Builder
@Getter
public class User {
private final String name;
private final String email;
@Builder.Default private int age = 0;
private String phone;
}
Factory Method
사용 시기: 정확한 클래스를 지정하지 않고 객체를 생성해야 할 때.
// ✅ Factory Method 패턴
public interface Notification {
void send(String message);
}
public class EmailNotification implements Notification {
@Override
public void send(String message) {
System.out.println("Email: " + message);
}
}
public class SmsNotification implements Notification {
@Override
public void send(String message) {
System.out.println("SMS: " + message);
}
}
public class PushNotification implements Notification {
@Override
public void send(String message) {
System.out.println("Push: " + message);
}
}
// Factory
public class NotificationFactory {
public static Notification create(String type) {
return switch (type.toUpperCase()) {
case "EMAIL" -> new EmailNotification();
case "SMS" -> new SmsNotification();
case "PUSH" -> new PushNotification();
default -> throw new IllegalArgumentException("Unknown type: " + type);
};
}
}
// 사용법
Notification notification = NotificationFactory.create("EMAIL");
notification.send("Hello!");
Spring 사용 시 (권장):
public interface NotificationSender {
void send(String message);
String getType();
}
@Component
public class EmailSender implements NotificationSender {
@Override public void send(String message) { /* ... */ }
@Override public String getType() { return "EMAIL"; }
}
@Component
public class SmsSender implements NotificationSender {
@Override public void send(String message) { /* ... */ }
@Override public String getType() { return "SMS"; }
}
@Component
public class NotificationFactory {
private final Map<String, NotificationSender> senders;
public NotificationFactory(List<NotificationSender> senderList) {
this.senders = senderList.stream()
.collect(Collectors.toMap(
NotificationSender::getType,
Function.identity()
));
}
public NotificationSender getSender(String type) {
return Optional.ofNullable(senders.get(type))
.orElseThrow(() -> new IllegalArgumentException("Unknown: " + type));
}
}
Singleton
사용 시기: 정확히 하나의 인스턴스만 필요할 때 (신중히 사용!).
// ✅ 최신 Singleton (열거형 기반, 스레드 안전)
public enum DatabaseConnection {
INSTANCE;
private Connection connection;
DatabaseConnection() {
// 연결 초기화
}
public Connection getConnection() {
return connection;
}
}
// 사용법
Connection conn = DatabaseConnection.INSTANCE.getConnection();
Spring 사용 시 (권장):
@Component // 기본 스코프는 singleton
public class DatabaseConnection {
// Spring이 단일 인스턴스 관리
}
주의: Singleton은 문제가 될 수 있습니다:
- 테스트가 어려움 (전역 상태)
- 숨겨진 의존성
- 의존성 주입을 대신 고려하세요
행동 패턴 (Behavioral Patterns)
Strategy
사용 시기: 동일한 작업에 대해 여러 알고리즘이 있고, 런타임에 교체해야 할 때.
// ✅ Strategy 패턴
public interface PaymentStrategy {
void pay(BigDecimal amount);
}
public class CreditCardPayment implements PaymentStrategy {
private final String cardNumber;
public CreditCardPayment(String cardNumber) {
this.cardNumber = cardNumber;
}
@Override
public void pay(BigDecimal amount) {
System.out.println("Paid " + amount + " with card " + cardNumber);
}
}
public class PayPalPayment implements PaymentStrategy {
private final String email;
public PayPalPayment(String email) {
this.email = email;
}
@Override
public void pay(BigDecimal amount) {
System.out.println("Paid " + amount + " via PayPal: " + email);
}
}
public class CryptoPayment implements PaymentStrategy {
private final String walletAddress;
public CryptoPayment(String walletAddress) {
this.walletAddress = walletAddress;
}
@Override
public void pay(BigDecimal amount) {
System.out.println("Paid " + amount + " to wallet: " + walletAddress);
}
}
// Context
public class ShoppingCart {
private PaymentStrategy paymentStrategy;
public void setPaymentStrategy(PaymentStrategy strategy) {
this.paymentStrategy = strategy;
}
public void checkout(BigDecimal total) {
paymentStrategy.pay(total);
}
}
// 사용법
ShoppingCart cart = new ShoppingCart();
cart.setPaymentStrategy(new CreditCardPayment("4111-1111-1111-1111"));
cart.checkout(new BigDecimal("99.99"));
// 런타임에 전략 변경
cart.setPaymentStrategy(new PayPalPayment("[email protected]"));
cart.checkout(new BigDecimal("49.99"));
Java 8+ 함수형 사용 시:
// 함수형 인터페이스로서의 Strategy
@FunctionalInterface
public interface PaymentStrategy {
void pay(BigDecimal amount);
}
// 람다를 사용한 사용법
PaymentStrategy creditCard = amount ->
System.out.println("Card payment: " + amount);
PaymentStrategy paypal = amount ->
System.out.println("PayPal payment: " + amount);
cart.setPaymentStrategy(creditCard);
Observer
사용 시기: 객체들이 다른 객체의 변경 사항을 통지받아야 할 때.
// ✅ Observer 패턴 (최신 Java)
public interface OrderObserver {
void onOrderPlaced(Order order);
}
public class OrderService {
private final List<OrderObserver> observers = new ArrayList<>();
public void addObserver(OrderObserver observer) {
observers.add(observer);
}
public void removeObserver(OrderObserver observer) {
observers.remove(observer);
}
public void placeOrder(Order order) {
// 주문 처리
saveOrder(order);
// 모든 옵저버에게 알림
observers.forEach(observer -> observer.onOrderPlaced(order));
}
}
// 옵저버들
public class InventoryService implements OrderObserver {
@Override
public void onOrderPlaced(Order order) {
// 재고 감소
order.getItems().forEach(item ->
reduceStock(item.getProductId(), item.getQuantity())
);
}
}
public class EmailNotificationService implements OrderObserver {
@Override
public void onOrderPlaced(Order order) {
sendConfirmationEmail(order.getCustomerEmail(), order);
}
}
public class AnalyticsService implements OrderObserver {
@Override
public void onOrderPlaced(Order order) {
trackOrderEvent(order);
}
}
// 설정
OrderService orderService = new OrderService();
orderService.addObserver(new InventoryService());
orderService.addObserver(new EmailNotificationService());
orderService.addObserver(new AnalyticsService());
Spring Events 사용 시 (권장):
// Event
public record OrderPlacedEvent(Order order) {}
// Publisher
@Service
public class OrderService {
private final ApplicationEventPublisher eventPublisher;
public void placeOrder(Order order) {
saveOrder(order);
eventPublisher.publishEvent(new OrderPlacedEvent(order));
}
}
// 리스너 (옵저버)
@Component
public class InventoryListener {
@EventListener
public void handleOrderPlaced(OrderPlacedEvent event) {
// 재고 감소
}
}
@Component
public class EmailListener {
@EventListener
public void handleOrderPlaced(OrderPlacedEvent event) {
// 이메일 발송
}
@EventListener
@Async // 비동기 처리
public void handleOrderPlacedAsync(OrderPlacedEvent event) {
// 비동기로 이메일 발송
}
}
Template Method
사용 시기: 알고리즘의 골격을 정의하고, 하위 클래스가 단계를 채우도록 할 때.
// ✅ Template Method 패턴
public abstract class DataProcessor {
// Template method - 알고리즘 정의
public final void process() {
readData();
processData();
writeData();
if (shouldNotify()) {
notifyCompletion();
}
}
// 하위 클래스가 구현할 단계
protected abstract void readData();
protected abstract void processData();
protected abstract void writeData();
// Hook - 선택적 재정의
protected boolean shouldNotify() {
return true;
}
protected void notifyCompletion() {
System.out.println("Processing completed!");
}
}
public class CsvDataProcessor extends DataProcessor {
@Override
protected void readData() {
System.out.println("Reading CSV file...");
}
@Override
protected void processData() {
System.out.println("Processing CSV data...");
}
@Override
protected void writeData() {
System.out.println("Writing to database...");
}
}
public class ApiDataProcessor extends DataProcessor {
@Override
protected void readData() {
System.out.println("Fetching from API...");
}
@Override
protected void processData() {
System.out.println("Transforming API response...");
}
@Override
protected void writeData() {
System.out.println("Writing to cache...");
}
@Override
protected boolean shouldNotify() {
return false; // Hook 재정의
}
}
// 사용법
DataProcessor csvProcessor = new CsvDataProcessor();
csvProcessor.process();
DataProcessor apiProcessor = new ApiDataProcessor();
apiProcessor.process();
구조 패턴 (Structural Patterns)
Decorator
사용 시기: 기존 클래스를 수정하지 않고 동적으로 동작을 추가할 때.
// ✅ Decorator 패턴
public interface Coffee {
String getDescription();
BigDecimal getCost();
}
public class SimpleCoffee implements Coffee {
@Override
public String getDescription() {
return "Coffee";
}
@Override
public BigDecimal getCost() {
return new BigDecimal("2.00");
}
}
// 기본 데코레이터
public abstract class CoffeeDecorator implements Coffee {
protected final Coffee coffee;
public CoffeeDecorator(Coffee coffee) {
this.coffee = coffee;
}
@Override
public String getDescription() {
return coffee.getDescription();
}
@Override
public BigDecimal getCost() {
return coffee.getCost();
}
}
// 구체적인 데코레이터
public class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return coffee.getDescription() + ", Milk";
}
@Override
public BigDecimal getCost() {
return coffee.getCost().add(new BigDecimal("0.50"));
}
}
public class SugarDecorator extends CoffeeDecorator {
public SugarDecorator(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return coffee.getDescription() + ", Sugar";
}
@Override
public BigDecimal getCost() {
return coffee.getCost().add(new BigDecimal("0.20"));
}
}
public class WhippedCreamDecorator extends CoffeeDecorator {
public WhippedCreamDecorator(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return coffee.getDescription() + ", Whipped Cream";
}
@Override
public BigDecimal getCost() {
return coffee.getCost().add(new BigDecimal("0.70"));
}
}
// 사용법
Coffee coffee = new SimpleCoffee();
System.out.println(coffee.getDescription() + " $" + coffee.getCost());
// 데코레이터로 감싸기
coffee = new MilkDecorator(coffee);
coffee = new SugarDecorator(coffee);
coffee = new WhippedCreamDecorator(coffee);
System.out.println(coffee.getDescription() + " $" + coffee.getCost());
// 출력: Coffee, Milk, Sugar, Whipped Cream $3.40
모범 사례 요약
- 필요할 때만 패턴 사용 – 모든 것에 패턴을 억지로 적용하지 마세요.
- 합성(Composition)을 상속(Inheritance)보다 선호 – is-a보다 has-a 관계가 더 유연합니다.
- 인터페이스에 맞춰 프로그래밍 – 구체적인 구현보다 추상화에 의존하세요.
- Spring의 도움을 활용 – Spring은 싱글톤, 팩토리, 프록시, 이벤트 등의 많은 패턴을 기본 지원합니다.
- Strategy에는 Java 8+ 함수형 인터페이스 고려 – 많은 경우에 람다가 더 간결합니다.
- 깊은 데코레이터 체인 주의 – 과도한 래핑은
StackOverflowError를 유발할 수 있습니다. - Singleton과의 전역 상태 싸움보다 의존성 주입 사용 – 테스트와 유지보수가 훨씬 쉬워집니다.