본문 바로가기

개발/면접

디자인 패턴 아는척하기 (1) - 전통적인 디자인 패턴

 

Design Patterns Series | Baeldung

 

크게 구분을 한다면 두갈래로 할수 있음

- 전통적인 디자인 패턴 -> 생성, 구조, 행위

- 현대적인(?) 디자인 패턴

 

위 baeldung에서도 전통적 디자인 패턴 3개 + Architectural Patterns으로 정리하고 있음

 

현실적으로 다 알고있기가 힘들지만 용어만이라도 알아두자

중요하다고 생각하는 것만 작성함

 

 

1. 싱글톤 패턴

 

📌 특징: 클래스의 인스턴스가 하나만 생성되도록 보장

✅ 장점
- 리소스 재사용
- 전역적 상태 관리
- 메모리 효율성

❌ 단점
- 단위 테스트 어려움
- 전역 상태로 인한 버그
- Thread-safe 구현 필요

💡 사용 시기
- 공유 리소스 관리
- 설정 정보 관리
- DB 연결 풀

🔧 사용 사례
- DB ConnectionPool
- Cache Manager
- Spring Bean의 기본 스코프

 

// Thread-safe Singleton
public class DatabaseConnection {
    private static volatile DatabaseConnection instance;
    private final Connection connection;
    
    private DatabaseConnection() {
        this.connection = createConnection();
    }
    
    public static DatabaseConnection getInstance() {
        if (instance == null) {
            synchronized (DatabaseConnection.class) {
                if (instance == null) {
                    instance = new DatabaseConnection();
                }
            }
        }
        return instance;
    }
    
    // 실제 사용 예시
    public void executeQuery(String sql) {
        try {
            PreparedStatement stmt = connection.prepareStatement(sql);
            // 쿼리 실행 로직
        } catch (SQLException e) {
            throw new DatabaseException("쿼리 실행 실패", e);
        }
    }
}

// Spring에서의 Singleton
@Component
public class CacheManager {
    private final Map<String, Object> cache = new ConcurrentHashMap<>();
    
    public void put(String key, Object value) {
        cache.put(key, value);
    }
    
    public Optional<Object> get(String key) {
        return Optional.ofNullable(cache.get(key));
    }
}

 

 

 

2. 전략 패턴

 

📌 특징: 알고리즘을 캡슐화하고 교체 가능하게 함

✅ 장점
- 알고리즘 교체 용이
- 비즈니스 로직 분리
- 확장성 좋음

❌ 단점
- 클래스 수 증가
- 전략 선택 로직 필요

💡 사용 시기
- 비즈니스 로직이 자주 변경될 때
- 조건문이 복잡할 때

🔧 사용 사례

- 결제 시스템의 다양한 결제 방식 처리
- 할인 정책 적용 (VIP, 시즌, 프로모션)
- 파일 저장 방식 (로컬, S3, FTP)
- 알림 발송 방식 (Email, SMS, Push)

 

// 결제 전략 인터페이스
public interface PaymentStrategy {
    PaymentResult process(Order order);
}

// 구체적인 전략들
@Component
public class CreditCardStrategy implements PaymentStrategy {
    @Override
    public PaymentResult process(Order order) {
        // 신용카드 결제 로직
        return new PaymentResult(/* ... */);
    }
}

@Component
public class KakaoPayStrategy implements PaymentStrategy {
    @Override
    public PaymentResult process(Order order) {
        // 카카오페이 결제 로직
        return new PaymentResult(/* ... */);
    }
}

// 전략 사용
@Service
public class PaymentService {
    private final Map<PaymentType, PaymentStrategy> strategies;
    
    public PaymentService(List<PaymentStrategy> strategyList) {
        strategies = strategyList.stream()
            .collect(Collectors.toMap(
                strategy -> getPaymentType(strategy.getClass()),
                strategy -> strategy
            ));
    }
    
    public PaymentResult processPayment(Order order) {
        PaymentStrategy strategy = strategies.get(order.getPaymentType());
        if (strategy == null) {
            throw new UnsupportedPaymentTypeException();
        }
        return strategy.process(order);
    }
}

 

 

 

3. 팩토리 메서드 패턴

 

📌 특징: 객체 생성을 서브클래스에 위임

✅ 장점
- 객체 생성 로직 캡슐화
- 확장에 유연
- 결합도 감소

❌ 단점
- 클래스 계층 구조 복잡
- 코드량 증가

💡 사용 시기
- 객체 생성 로직이 복잡할 때
- 객체 생성을 유연하게 처리해야 할 때
- 객체 생성 방식을 확장할 가능성이 있을 때

🔧 실무 사례
- 결제 시스템의 결제 수단 객체 생성
- 다양한 형식의 문서 생성기
- 데이터베이스 커넥션 생성
- API 클라이언트 생성

 

// 기본 인터페이스
public interface PaymentProcessor {
    void processPayment(Order order);
}

// 구체적인 구현체들
@Component
public class KakaoPayProcessor implements PaymentProcessor {
    @Override
    public void processPayment(Order order) {
        // 카카오페이 결제 처리
    }
}

@Component
public class CreditCardProcessor implements PaymentProcessor {
    @Override
    public void processPayment(Order order) {
        // 신용카드 결제 처리
    }
}

// Factory 클래스
@Component
public class PaymentProcessorFactory {
    private final Map<PaymentMethod, PaymentProcessor> processorMap;

    public PaymentProcessorFactory(List<PaymentProcessor> processors) {
        processorMap = processors.stream()
            .collect(Collectors.toMap(
                processor -> getPaymentMethod(processor.getClass()),
                processor -> processor
            ));
    }

    public PaymentProcessor getProcessor(PaymentMethod method) {
        PaymentProcessor processor = processorMap.get(method);
        if (processor == null) {
            throw new UnsupportedPaymentMethodException(method);
        }
        return processor;
    }

    // 실제 사용 예시
    @Service
    public class OrderService {
        private final PaymentProcessorFactory factory;

        public void processOrder(Order order) {
            PaymentProcessor processor = factory.getProcessor(order.getPaymentMethod());
            processor.processPayment(order);
        }
    }
}

// 다른 실무 예시: API 클라이언트 생성
public interface ApiClient {
    Response execute(Request request);
}

@Component
public class ApiClientFactory {
    private final RestTemplateBuilder restTemplateBuilder;
    private final WebClientBuilder webClientBuilder;

    public ApiClient createClient(ClientType type, String baseUrl) {
        switch (type) {
            case REST_TEMPLATE:
                return new RestTemplateApiClient(
                    restTemplateBuilder
                        .setConnectTimeout(Duration.ofSeconds(5))
                        .setReadTimeout(Duration.ofSeconds(5))
                        .rootUri(baseUrl)
                        .build()
                );
            case WEB_CLIENT:
                return new WebClientApiClient(
                    webClientBuilder
                        .baseUrl(baseUrl)
                        .filter(logRequest())
                        .build()
                );
            default:
                throw new IllegalArgumentException("Unsupported client type");
        }
    }
}