1. Specification 패턴이란?

Specification 패턴은 비즈니스 규칙(조건)을 명확하고 선언적으로 표현하고, 다양한 조건을 조합할 수 있도록 설계하는 패턴입니다. 주로 객체 필터링, 조건 분리, 동적 쿼리 조합 등에 사용됩니다.


2. 사용 목적

  • 복잡한 조건 로직을 캡슐화하여 재사용 가능하게 만듦
  • 다양한 조건을 동적으로 조합 (AND / OR / NOT)
  • 코드 중복을 줄이고 가독성 및 테스트 용이성 향상

3. Java 예시

public interface Specification<T> {
    boolean isSatisfiedBy(T t);

    default Specification<T> and(Specification<T> other) {
        return t -> this.isSatisfiedBy(t) && other.isSatisfiedBy(t);
    }

    default Specification<T> or(Specification<T> other) {
        return t -> this.isSatisfiedBy(t) || other.isSatisfiedBy(t);
    }

    default Specification<T> not() {
        return t -> !this.isSatisfiedBy(t);
    }
}
public class AgeSpecification implements Specification<User> {
    public boolean isSatisfiedBy(User user) {
        return user.getAge() >= 18;
    }
}

public class NameSpecification implements Specification<User> {
    public boolean isSatisfiedBy(User user) {
        return user.getName().startsWith("K");
    }
}

// 조합 사용
Specification<User> adultNamedK = new AgeSpecification().and(new NameSpecification());

4. JPA & Spring Data 에서의 실무 활용

Spring Data JPA의 JpaSpecificationExecutor는 이 패턴을 기반으로 조건을 쿼리 객체로 변환해 동적 where 절을 생성합니다.

Specification<User> spec = (root, query, cb) -> cb.greaterThan(root.get("age"), 18);
userRepository.findAll(spec);

복잡한 쿼리를 분리된 조건 단위로 구성하고, 필요 시 조합하여 동적으로 처리할 수 있어 유지보수성이 높아집니다.


5. 실무 사례와 비판적 관점

실제로 마이크로서비스 아키텍처(MSA) 프로젝트에서는 하나의 서비스가 하나의 스키마만 접근하는 원칙 하에, 여러 서비스에서 데이터를 가져와 조합하는 방식이 사용됩니다. 이때 서비스 계층에서 각 조건을 재사용 가능한 컴포넌트로 만들어 조합한 경우, Specification 패턴의 개념을 어느 정도 따랐다고 볼 수 있습니다.

일반적인 JPA 기반 프로젝트에서 Specification 패턴의 사용 빈도는 팀이나 프로젝트의 성격에 따라 달라집니다. 흔하게 사용되지는 않지만, 다음과 같은 장점 때문에 채택하는 사례도 존재합니다:

  • 비즈니스 규칙을 명확히 분리하고 재사용할 수 있는 구조가 필요할 때
  • 도메인 중심 설계(DDD)를 채택한 프로젝트에서 복잡한 조건을 도메인 객체와 가까운 위치에서 선언적으로 처리하고자 할 때
  • Querydsl의 도입이 어렵거나, 의존성을 최소화하고 싶은 경우

다만 다음과 같은 현실적인 이유로 Querydsl이 더 널리 채택되는 경향도 분명 존재합니다:

  • JPA의 Criteria API는 문법이 복잡하고 직관성이 떨어짐
  • Querydsl은 컴파일 타임에 타입 검사를 지원하며, 쿼리 작성이 간결하고 명확함
  • Specification 패턴은 설계 난이도와 러닝 커브가 높고, 클래스 수가 늘어나 유지보수가 어려워질 수 있음

결론적으로, Specification 패턴은 "JPA 환경에서 잘 쓰이지 않는다"기보다는 특정한 설계 철학이나 프로젝트 구조에 따라 신중하게 도입되는 경향이 있는 패턴입니다. 복잡한 비즈니스 조건을 자주 재사용하고 조합해야 하는 환경에서는 오히려 매우 유용한 선택이 될 수 있습니다.

1. Adapter 패턴이란?

Adapter 패턴은 서로 호환되지 않는 두 객체를 연결해주는 중간 매개체 역할을 하는 구조 패턴입니다. 즉, 기존 인터페이스를 클라이언트가 기대하는 인터페이스로 변환하여, 기존 코드의 변경 없이도 새로운 기능을 통합할 수 있게 해줍니다.

사용 목적

  • 기존 코드를 수정하지 않고 외부 시스템 또는 레거시 코드를 통합
  • 인터페이스가 맞지 않아 함께 사용하기 어려운 두 객체를 연결
  • 시스템 확장성 및 유지보수성 향상

2. 구조와 동작 원리

기본 구조

클라이언트 → Target 인터페이스
                 ↑
            Adapter ← Adaptee (호환되지 않는 기존 객체)
  • Target: 클라이언트가 기대하는 인터페이스
  • Adaptee: 기존 또는 외부 시스템의 실제 기능을 가진 객체
  • Adapter: Adaptee의 기능을 감싸서 Target처럼 보이게 해주는 클래스

3. 예시 코드 (Java 스타일)

// 클라이언트가 기대하는 인터페이스
public interface MediaPlayer {
    void play(String filename);
}

// 기존 시스템 또는 외부 API
public class LegacyAudioPlayer {
    public void playFile(String path) {
        System.out.println("재생 중: " + path);
    }
}

// Adapter
public class AudioPlayerAdapter implements MediaPlayer {
    private final LegacyAudioPlayer legacyPlayer = new LegacyAudioPlayer();

    public void play(String filename) {
        legacyPlayer.playFile(filename);
    }
}

4. 어떤 문제를 해결하기 위해 자주 쓰일까?

  • 외부 라이브러리의 인터페이스가 우리가 쓰는 방식과 다를 때
  • 기존 시스템(레거시 코드)을 리팩터링 없이 유지하면서 새 코드와 연결할 때
  • 다양한 데이터 포맷(JSON, XML 등)을 표준 인터페이스로 맞춰야 할 때

5. 실무 적용 예시

1) 외부 결제 API 연동

  • 카드사마다 결제 API 구조가 다를 경우, 내부에서 사용하는 공통 인터페이스(PaymentGateway)를 만들고, 각 카드사마다 Adapter를 구현하여 연결

2) Spring에서 HandlerAdapter

  • 다양한 타입의 Controller를 일관된 방식으로 처리하기 위해, Spring MVC는 HandlerAdapter를 통해 DispatcherServlet과 실제 Controller를 연결

3) 파일 포맷 변환기

  • 예: CSV, Excel, JSON 데이터를 내부 표준 객체로 변환하는 Adapter 구성

5.5. Adapter 패턴과 API Gateway의 관계

API Gateway는 여러 마이크로서비스의 진입점을 통합하여 클라이언트와 서버 간의 요청을 중재하는 역할을 합니다.

이 구조는 Adapter 패턴과 매우 유사한 목적을 가지고 있습니다.

  Adapter 패턴 API Gateway
역할 코드 단위의 인터페이스 변환 네트워크 레벨의 요청 라우팅 및 변환
목적 서로 다른 시스템 간 호환성 확보 여러 API를 통합된 진입점으로 제공
범위 클래스/객체 설계 패턴 시스템/아키텍처 계층 구성 요소

예를 들어, 클라이언트가 /pay로 요청을 보냈을 때 API Gateway가 내부적으로 /kakao, /naverpay 등의 실제 서비스로 요청을 라우팅해주는 방식은, 내부의 다양한 서비스 구조를 클라이언트에 맞춰 추상화해준다는 점에서 Adapter와 동일한 설계 철학을 따릅니다. 따라서 API Gateway는 실질적으로 Adapter 패턴을 아키텍처 레벨에서 구현한 사례라고 볼 수 있습니다.


장점 단점
기존 코드 변경 없이 시스템 통합 가능 Adapter 클래스가 많아질 수 있음
레거시 코드 재사용 가능 설계가 복잡해질 수 있음
인터페이스 호환성 확보 성능 이슈 발생 가능성 (간접 호출 등)
 

1. Strategy 패턴이란?

Strategy 패턴은 행위를 캡슐화한 알고리즘을 런타임에 바꿀 수 있도록 하는 디자인 패턴입니다. 즉, 특정 행위(전략)를 사용하는 주체가 그 구현을 직접 알지 않아도 되고, 실행 시점에 전략을 선택하거나 변경할 수 있도록 하여 유연성과 확장성을 극대화합니다.

사용 목적

  • 조건에 따라 다양한 행위를 적용할 수 있도록 분리
  • 코드 변경 없이 알고리즘 교체 가능
  • if/else 또는 switch 문을 객체 구조로 치환

2. 구조와 동작 원리

기본 구조

Context
 ├─ Strategy 인터페이스 타입을 필드로 보유
 └─ setStrategy() 로 전략 주입 가능

Strategy (인터페이스)
 ├─ ConcreteStrategyA
 └─ ConcreteStrategyB

예시 코드

public interface PaymentStrategy {
    void pay(int amount);
}

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

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

public class PaymentContext {
    private PaymentStrategy strategy;

    public void setStrategy(PaymentStrategy strategy) {
        this.strategy = strategy;
    }

    public void pay(int amount) {
        strategy.pay(amount);
    }
}

3. 어떤 문제를 해결하기 위해 자주 쓰일까?

전략 적용의 일반적 배경

기존에는 하나의 클래스 내에서 다양한 조건(if-else 또는 switch)으로 로직을 분기하는 경우가 많았습니다. 예를 들어 결제 서비스 하나에 카드, 카카오페이, 포인트 등 여러 결제 방식이 혼재돼 있는 경우, 코드가 복잡해지고 유지보수가 어려워지며 테스트 또한 힘들어집니다.

Strategy 패턴을 적용하면 각 결제 수단별로 클래스를 분리하여 관리할 수 있습니다. 이렇게 하면 클래스 수는 증가하지만 각 전략 클래스가 단일 책임 원칙(SRP)을 만족하게 되어 가독성과 확장성이 훨씬 높아집니다.

런타임에 다른 알고리즘을 사용하는 이유

Strategy 패턴의 핵심은 행위를 캡슐화해서 실행 중에 전략을 동적으로 교체할 수 있다는 점입니다. 사용자 입력이나 환경 설정, 혹은 요청 컨텍스트에 따라 적절한 전략을 런타임에 선택해 적용하는 것이 가능합니다.

예를 들어 결제 시스템에서 사용자가 '카드', '카카오페이', '포인트' 중 어떤 결제 방식을 사용할지 모른다면, 실행 시점에 맞는 전략 객체를 주입해 결제를 수행할 수 있습니다.

주요 활용 케이스

  • 조건 분기 제거: 전략 객체로 교체
  • 런타임에 알고리즘이 바뀌는 경우 (결제 방식, 정렬 방식 등)
  • OCP(Open-Closed Principle) 준수: 기존 코드를 수정하지 않고 전략 추가 가능

4. Spring에서 실전 활용 예시

1) Bean 주입으로 전략 교체

@Component
public class PaymentService {
    private final Map<String, PaymentStrategy> strategyMap;

    public PaymentService(List<PaymentStrategy> strategies) {
        this.strategyMap = strategies.stream()
            .collect(Collectors.toMap(
                s -> s.getClass().getSimpleName().replace("Strategy", "").toLowerCase(),
                s -> s
            ));
    }

    public void pay(String type, int amount) {
        PaymentStrategy strategy = strategyMap.get(type);
        strategy.pay(amount);
    }
}

2) @Qualifier로 특정 전략 명시 주입

  • 복수 전략 중 하나만 사용할 경우 명시적으로 주입

3) enum 기반 전략 매핑

전략 선택 시 문자열 대신 enum 타입을 사용하는 방식은 타입 안정성과 명시성이 뛰어납니다.

public enum PaymentType {
    CARD, KAKAO
}
@Component
public class PaymentService {
    private final Map<PaymentType, PaymentStrategy> strategyMap;

    public PaymentService(List<PaymentStrategy> strategies) {
        this.strategyMap = strategies.stream()
            .collect(Collectors.toMap(
                s -> PaymentType.valueOf(s.getClass().getSimpleName().replace("Strategy", "").toUpperCase()),
                s -> s
            ));
    }

    public void pay(PaymentType type, int amount) {
        PaymentStrategy strategy = strategyMap.get(type);
        strategy.pay(amount);
    }
}

5. 장단점

장점단점

런타임 전략 교체 가능 클래스 수 증가 가능
조건문 제거로 가독성 향상 전략 선택을 위한 관리 코드 필요
OCP 원칙 충족 전략 간 공통 인터페이스 설계가 중요함

6. 실무 적용 팁

  • 전략이 많다면 Map 주입 방식 추천 (Spring에서는 자동 주입 가능)
  • 전략 간 입력값이 다르면 공통 인터페이스 설계 또는 래퍼 객체 사용 고려
  • enum + 함수형 인터페이스 조합도 최근 자주 사용됨

7. 마무리 요약

Strategy 패턴은 조건 분기 대신 전략 객체를 통해 행위를 유연하게 교체할 수 있게 해주는 구조입니다. 실행 중 전략을 교체할 수 있고, 새로운 전략 추가 시 기존 코드를 수정하지 않아도 되므로 유지보수와 확장성 면에서 매우 유리합니다. Spring의 DI와 결합하면 실전에서 더 효과적으로 활용할 수 있습니다.

 

+ Recent posts