Community코딩 & 개발github.com

viccom/design-patterns

Java 예제를 통한 일반적인 디자인 패턴 (Factory, Builder, Strategy, Observer, Decorator 등). 사용자가 '패턴 구현', '팩토리 사용', '전략 패턴'을 요청하거나 확장 가능한 컴포넌트를 설계할 때 사용합니다.

design-patterns란 무엇인가요?

이 스킬은 Java 예제를 통해 가장 일반적인 디자인 패턴에 대한 실용적인 참조를 제공합니다. Factory, Builder, Strategy, Observer, Decorator, Singleton 등과 같은 패턴을 다루며, 각 패턴을 언제 사용해야 하는지, 어떻게 구현하는지, 그리고 최신 Java 및 Spring 생태계에서의 모범 사례를 설명합니다. 사용자가 특정 디자인 패턴을 요청하거나(예: '팩토리 패턴 사용'), 확장 가능하고 유연한 컴포넌트를 설계해야 하거나, 결합도가 높은 코드를 리팩토링할 때 유용합니다. 이 스킬은 일반 Java 구현과 Lombok, Spring, Java 8+ 함수형 프로그래밍을 활용한 더 간결하고 현대적인 접근 방식을 모두 제공하여, 개발자가 현재 프로젝트와 기술 스택에 가장 적합한 방식을 선택할 수 있도록 돕습니다.

지원 대상~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/viccom/viccom-agent-skills/tree/HEAD/skills/design-patterns

Installed? Explore more 코딩 & 개발 skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

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

모범 사례 요약

  1. 필요할 때만 패턴 사용 – 모든 것에 패턴을 억지로 적용하지 마세요.
  2. 합성(Composition)을 상속(Inheritance)보다 선호 – is-a보다 has-a 관계가 더 유연합니다.
  3. 인터페이스에 맞춰 프로그래밍 – 구체적인 구현보다 추상화에 의존하세요.
  4. Spring의 도움을 활용 – Spring은 싱글톤, 팩토리, 프록시, 이벤트 등의 많은 패턴을 기본 지원합니다.
  5. Strategy에는 Java 8+ 함수형 인터페이스 고려 – 많은 경우에 람다가 더 간결합니다.
  6. 깊은 데코레이터 체인 주의 – 과도한 래핑은 StackOverflowError를 유발할 수 있습니다.
  7. Singleton과의 전역 상태 싸움보다 의존성 주입 사용 – 테스트와 유지보수가 훨씬 쉬워집니다.

Individual skills in this repo

This repo contains 2 individual skills — each has its own dedicated page.

관련 스킬