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 인터페이스
                 ↑
            AdapterAdaptee (호환되지 않는 기존 객체)
  • 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와 결합하면 실전에서 더 효과적으로 활용할 수 있습니다.

 

1. Template Method 패턴이란?

Template Method 패턴은 알고리즘의 뼈대를 상위 클래스에서 정의하고, 구체적인 구현은 하위 클래스에 위임하는 디자인 패턴입니다. 핵심 아이디어는 "공통된 처리 흐름은 고정하되, 세부 구현만 바꾼다"는 데 있습니다.

사용 목적

  • 중복되는 처리 로직을 공통화
  • 알고리즘 구조는 고정하되, 세부 내용만 유연하게 확장
  • 코드의 일관성과 유지보수성 향상

2. 구조와 동작 원리

기본 구조

AbstractClass
 ├─ templateMethod()     ← 고정된 처리 순서 정의
 ├─ stepOne()            ← 추상 메서드 → 하위 클래스 구현
 └─ stepTwo()            ← hook 메서드 (옵션)

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

Template Method 패턴은 다음과 같은 문제 상황에서 자주 사용됩니다:

1) 다수의 유사한 프로세스 흐름이 존재할 때

  • 예: 보고서 생성, 배치 작업, 외부 API 호출 등
  • 공통된 흐름이 있고, 일부 단계만 서로 다를 때 추상 클래스로 구조화

2) 타 시스템 연계/인터페이스 처리

  • 예: 외부 API 호출 시 RestTemplate, WebClient 등의 사용이 반복될 경우
  • 공통적인 요청 흐름(로그 남기기, 요청 생성, 응답 파싱 등)을 상위 클래스에서 고정하고, API별 세부 구현은 하위 클래스에서 처리
  • 공통 모듈화로 재사용성과 유지보수성 향상

3) 중복 제거 + 유지보수 단순화가 필요한 레거시 시스템 정리 시

  • 비슷한 로직이 퍼져 있는 코드를 통일된 상속 기반으로 재구성

핵심은 "공통된 처리 흐름을 고정하고, 변화가 필요한 지점만 유연하게 대체"하는 데 있습니다. 이 구조는 상위에 공통 추상 클래스를 만들고, 이를 상속받아 구체적인 동작을 구현하는 방식으로 이루어집니다.


4. 장단점

 

장점 단점
알고리즘 구조를 재사용 가능 상속 기반이라 유연성 제한 있음
로직 중복 최소화 하위 클래스 증가로 관리 부담 가능
일관된 흐름 보장 모든 단계가 추상화되면 복잡해질 수 있음

5. 실무 적용 팁

  • 같은 흐름의 처리 과정에서 다형성을 활용하고 싶을 때 고려
  • AbstractService, AbstractHandler 등으로 응용 가능

6. 마무리 요약

  • Template Method는 상위 클래스에서 전체 처리 흐름을 정의하고, 하위 클래스에서 각 단계의 로직을 세분화하는 패턴입니다.
  • 추상 클래스를 통해서 공통 흐름은 고정하고, 구체적인 처리만 커스터마이징하고 싶을 때 적합합니다.
  • 상속 구조가 강제되므로, 복잡한 상황에서는 전략 패턴과 조합하여 설계하는 것도 고려해야 합니다.
 

1. Proxy 패턴이란?

Proxy 패턴은 어떤 객체에 대한 접근을 제어하거나, 그 객체를 대신하여 추가 작업을 수행할 수 있도록 중간 대리 객체(Proxy)를 두는 디자인 패턴입니다.

사용 목적

  • 실제 객체의 접근 제어
  • 객체 생성 비용이 클 때 지연 로딩
  • 부가 기능 부여 (로그, 인증, 캐시 등)

적용 사례

  • 데이터베이스 연결 풀 관리
  • 보안 검사/권한 제어
  • Spring의 AOP 기반 기능들 (@Transactional, @Async, @Cacheable 등)

2. 구조와 동작 원리

기본 구조

클라이언트 → Proxy → 실제 객체(RealSubject)

예시 클래스 구조

public interface Service {
    void doSomething();
}

public class RealService implements Service {
    public void doSomething() {
        System.out.println("Real service logic");
    }
}

public class LoggingProxy implements Service {
    private final Service target;

    public LoggingProxy(Service target) {
        this.target = target;
    }

    public void doSomething() {
        System.out.println("[Log] Before");
        target.doSomething();
        System.out.println("[Log] After");
    }
}

3. Spring에서 Proxy 패턴 적용 예시

Spring에서는 Proxy 기반 AOP 구현이 핵심 메커니즘입니다. 실제 객체 앞에 프록시 객체를 두어 횡단 관심사를 처리합니다.

대표 예시: @Transactional

@Service
public class OrderService {
    @Transactional
    public void placeOrder() {
        // 트랜잭션 처리 전/후 프록시가 개입
    }
}

이때 Spring은 OrderService의 프록시를 생성하여, 메서드 실행 전후에 트랜잭션을 시작하고 커밋 또는 롤백합니다.


4. 장단점

장점단점

장점 단점
원래 객체 수정 없이 부가 기능 삽입 가능 클래스 수 증가, 구조 복잡도 증가
접근 제어, 보안, 로깅 등 분리 가능 성능 오버헤드 가능성 있음
AOP, 리모팅, 캐시 등 확장 기능에 유리 디버깅 난이도 상승

5. 실무 적용 팁

  • 횡단 관심사(Cross-cutting concern) 가 많다면 Proxy 또는 AOP 적용 고려
  • 무분별한 프록시 체인 생성은 성능 저하 원인이 될 수 있음
  • Spring에서는 @Aspect, @Around, @Before 등을 통해 선언형 AOP를 쉽게 구현 가능
@Aspect
@Component
public class LogAspect {
    @Before("execution(* com.example.service.*.*(..))")
    public void beforeLog() {
        System.out.println("[LOG] 메서드 실행 전");
    }
}

6. 실무에서 Proxy 패턴은 어디까지 쓰는가?

Spring에서 Proxy 패턴(AOP 포함)은 대부분 횡단 관심사(부가 기능) 를 분리해 처리하는 용도로 사용됩니다. 다음과 같은 기능들이 대표적입니다:

활용 예시 설명
@Transactional 트랜잭션 시작/커밋/롤백을 자동으로 처리
@Cacheable 메서드 결과 캐싱 및 캐시 무효화 관리
@Async 비동기 실행 처리
@PreAuthorize 메서드 실행 전 권한 검사
@Before, @After 메서드 실행 전후 로깅 또는 감사 처리

이처럼 대부분의 AOP 기반 기능은 실제 비즈니스 로직은 건드리지 않고, 그 앞뒤에서 감싸는 형태로 구현됩니다.

Proxy 패턴의 전형적인 실전 활용입니다.

💡 핵심 로직(예: 주문 생성, 결제 처리 등)은 여전히 Controller → Service → Repository 구조 내에서 구현되며, 프록시는 이를 감시하거나 감싸는 역할만 수행합니다.

Proxy를 통해 로직 자체를 변경하거나 처리하는 일은 매우 드물며, 오히려 설계를 복잡하게 만들고 유지보수를 어렵게 만들 수 있습니다. 따라서 프록시는 부가 기능 전용으로 활용하고, 핵심 비즈니스는 명확히 계층 구조 안에서 처리하는 것이 일반적인 설계 방향입니다.


7. 마무리 요약

  • Proxy 패턴은 실제 객체에 대한 접근을 제어하거나, 실행 전후로 로직을 삽입할 수 있는 구조입니다.
  • Spring AOP는 이러한 Proxy 패턴을 기반으로 작동하며, 트랜잭션, 보안, 로깅 등의 핵심 기능을 지원합니다.
  • 코드 분리와 유지보수성을 위해, 필요한 경우에만 명확하게 프록시를 도입해야 합니다.
 

1. Factory 패턴이란?

Factory 패턴은 객체 생성 로직을 별도의 클래스나 메서드로 위임하여 클라이언트 코드와 객체 생성 과정을 분리하는 디자인 패턴입니다. 즉, 어떤 객체를 생성해야 할지를 클라이언트가 직접 알지 않아도 되며, 이로 인해 코드의 유연성과 유지보수성이 높아집니다.

사용 목적

  • 객체 생성의 책임 분리
  • 객체 종류에 따라 생성 로직 분기 가능
  • 생성 과정을 캡슐화하여 클라이언트와 분리

적용 사례

  • 다양한 구현체를 조건에 따라 반환해야 할 때
  • 외부 설정 값이나 파라미터에 따라 객체를 동적으로 생성할 때
  • Spring의 FactoryBean, BeanFactory, ApplicationContext.getBean(...)

대부분의 간단한 예제나 작은 규모의 서비스에서는 Factory 패턴을 생략하고 직접 객체를 생성하는 방식으로 구현되는 경우가 많습니다. 하지만 구현체가 늘어나거나, 조건에 따라 다른 객체가 필요해지는 순간부터는 Factory 패턴을 도입하는 것이 유지보수성과 확장성 측면에서 매우 효과적입니다.

2. Spring에서의 Factory 패턴

Spring은 DI 컨테이너 자체가 거대한 Factory입니다. ApplicationContext.getBean()을 통해 Bean을 생성 및 주입하며, 개발자가 직접 Factory를 구현할 수도 있습니다.

예시 1: 간단한 팩토리 메서드 구현

public class AnimalFactory {
    public static Animal create(String type) {
        return switch (type.toLowerCase()) {
            case "dog" -> new Dog();
            case "cat" -> new Cat();
            default -> throw new IllegalArgumentException("Unknown type: " + type);
        };
    }
}

예시 2: 인터페이스 기반 전략과 함께 사용

public interface NotificationSender {
    void send(String message);
}

public class EmailSender implements NotificationSender {
    public void send(String message) {
        System.out.println("Email: " + message);
    }
}

public class SmsSender implements NotificationSender {
    public void send(String message) {
        System.out.println("SMS: " + message);
    }
}

public class NotificationFactory {
    public static NotificationSender getSender(String type) {
        return switch (type.toLowerCase()) {
            case "email" -> new EmailSender();
            case "sms" -> new SmsSender();
            default -> throw new IllegalArgumentException("Invalid type");
        };
    }
}

3. 장단점 정리

장점

  • 객체 생성 로직과 사용 로직을 분리하여 책임 명확화
  • 변경에 유연함 (Open-Closed Principle)
  • 코드 재사용성과 테스트 용이성 향상

단점

  • 클래스 수 증가
  • 복잡도 증가 가능
  • 간단한 케이스에서는 오히려 오버엔지니어링이 될 수 있음

4. 실무 적용 팁

1) 시스템 규모가 작을 때는 직접 객체를 생성하거나 고정된 방식으로 주입해도 큰 문제가 없지만, 규모가 커지고 모듈이 분리되거나 MSA 구조로 확장되면 Factory 패턴의 구조적 한계가 드러날 수 있습니다. 특히 ApplicationContext.getBean()을 사용하는 방식은 컴파일 타임 의존성이 명확하지 않아, 모듈 간 의존성이 꼬이거나 빌드 타이밍에 에러가 발생할 수 있습니다. MSA 환경에서는 객체 생성을 명확히 선언하고, 외부 시스템과의 연계를 고려해 전략 패턴, 인터페이스 기반의 DI, 또는 설정 기반의 유연한 Bean 등록 방식이 더 적합한 경우가 많습니다.

 2) ApplicationContext.getBean(...)은 Spring의 Factory 기능 중 하나지만, 남발할 경우 의존성이 명확하지 않아 프로젝트 구조를 복잡하게 만들 수 있습니다. 특히 A 모듈에서 특정 Bean을 getBean()으로 가져다 쓰고, B 모듈이 이를 몰랐을 경우 런타임 에러나 순환 참조, 빌드 실패 등 다양한 문제로 이어질 수 있습니다. 따라서 가능한 경우 명시적인 생성자 주입(@Autowired)을 활용하고, 동적 조건에 따른 Bean 선택이 필요한 상황에서만 제한적으로 사용하는 것이 좋습니다.

  • Service Layer에서 조건에 따라 여러 구현체를 리턴해야 할 때 매우 유용
  • ApplicationContext를 주입받아 직접 Bean을 생성하거나 동적으로 주입할 수도 있음
@Component
public class SenderRouter {
    private final Map<String, NotificationSender> senderMap;

    public SenderRouter(List<NotificationSender> senders) {
        this.senderMap = senders.stream().collect(Collectors.toMap(
            sender -> sender.getClass().getSimpleName().toLowerCase().replace("sender", ""),
            Function.identity()
        ));
    }

    public NotificationSender route(String type) {
        return senderMap.getOrDefault(type, new EmailSender());
    }
}

BeanFactory vs FactoryBean 간단 개념 비교

Spring에서 Factory 개념은 내부적으로도 다양하게 구현되어 있습니다. 그중 대표적인 것이 BeanFactoryFactoryBean입니다.

BeanFactory

  • Spring DI 컨테이너의 가장 기본적인 인터페이스
  • ApplicationContext의 상위 개념이며, getBean() 메서드를 통해 Bean을 생성하고 관리
  • 기본적으로 Lazy Loading 전략을 사용
  • 실제로는 ApplicationContext가 거의 모든 경우에 대체함
BeanFactory factory = new XmlBeanFactory(new ClassPathResource("beans.xml"));
MyService service = factory.getBean("myService", MyService.class);

FactoryBean

  • 개발자가 직접 구현할 수 있는 특별한 Bean 생성용 인터페이스
  • getObject() 메서드가 반환하는 객체가 실제로 Spring Container에 등록됨
  • 외부 API, Proxy, 복잡한 설정 기반 객체 생성 등에 활용
public class MyConnectionFactory implements FactoryBean<Connection> {
    @Override
    public Connection getObject() {
        return DriverManager.getConnection("jdbc:mysql://...");
    }

    @Override
    public Class<?> getObjectType() {
        return Connection.class;
    }
}
  • 이 경우 getBean("myConnectionFactory")를 호출하면 Connection 객체가 반환됨
  • 실제 FactoryBean 객체를 가져오고 싶다면 &myConnectionFactory를 사용함

요약 비교표

항목BeanFactoryFactoryBean

항목 BeanFactory FactoryBean
역할 Spring의 핵심 DI 컨테이너 커스텀 객체를 생성하는 Bean 인터페이스
사용 위치 Spring 내부/초기 구현 개발자 커스터마이징 시 주로 사용
특징 Lazy 로딩, 범용 컨테이너 역할 getObject()의 결과가 Bean으로 등록됨

항목 내용
패턴 목적 객체 생성 책임 분리 및 유연한 인스턴스 선택
주요 키워드 생성 캡슐화, 조건 분기, 구현체 선택
Spring 연관 BeanFactory, FactoryBean, 전략 패턴 결합

 

1. Spring Bean Scope란?

싱글톤 패턴이란?

싱글톤(Singleton) 패턴은 클래스의 인스턴스를 하나만 생성하여 전역적으로 공유하는 디자인 패턴입니다. 생성자를 private으로 막고 정적 메서드를 통해 유일한 인스턴스를 반환하는 방식으로 구현됩니다.

  • 사용 목적: 메모리 절약, 인스턴스 재사용, 설정이나 캐시 등의 전역 상태 공유
  • 적용 사례: 설정 클래스, 로깅 유틸, 스프링의 서비스 클래스

Spring은 이러한 싱글톤 패턴을 프레임워크 차원에서 기본 지원합니다. 대부분의 Bean은 특별한 설정이 없으면 singleton 스코프로 관리되며, 이는 내부적으로 싱글톤 패턴처럼 동작합니다.

하지만 모든 상황에 싱글톤이 적합한 것은 아닙니다. 요청(Request)마다 새로운 객체가 필요하거나, 사용자(Session)마다 상태를 분리해야 하는 경우에는 다른 스코프가 필요합니다.

따라서 Spring에서는 다양한 Bean Scope 설정을 통해 개발자가 목적에 따라 인스턴스 생명주기를 조절할 수 있도록 돕습니다.

 

싱글톤 Bean의 동작 방식과 주의점

Spring에서 싱글톤 스코프의 Bean은 하나의 인스턴스를 여러 요청이 공유하게 됩니다. 하지만 이는 요청이 순차적으로 처리된다는 뜻은 아닙니다. Spring Boot의 내장 웹 서버는 멀티스레드 기반으로 작동하기 때문에 동시에 들어온 요청들도 모두 같은 싱글톤 인스턴스를 동시에 참조하게 됩니다. 즉, 10명의 사용자가 같은 API를 동시에 호출하더라도 각각의 요청은 다른 스레드에서 처리되며, 같은 Bean 인스턴스를 공유합니다.

이로 인해 싱글톤 Bean 내부에서 상태를 가지는 필드가 있다면 동시성 문제가 발생할 수 있습니다. 따라서 싱글톤은 반드시 stateless하게 설계하는 것이 원칙입니다.

문제 사례

@Service
public class BadService {
    private String username; // 상태를 가지는 필드

    public void setUser(String name) {
        this.username = name;
    }

    public String getUser() {
        return this.username;
    }
}

위 코드는 여러 요청이 동시에 setUser()getUser()를 호출하면 값이 꼬일 수 있습니다.

이 문제는 public으로 선언되었기 때문이 아니라, Bean이 싱글톤이면서 상태(state)를 내부 필드로 보존하고 있기 때문에 생기는 문제입니다. 즉, 필드가 private여도 상태가 공유되면 동일하게 위험합니다.

하지만, 실제로 MVC 아키텍처에서는 DTO를 이용해 요청 값을 주고받는 구조가 일반적이며, 이 DTO들은 매 요청마다 생성되므로 상태 공유 문제가 발생하지 않습니다. 또한 서비스 계층은 대부분 stateless하게 구성되며, 필요한 데이터는 DB에서 조회하거나 파라미터로 전달받기 때문에 싱글톤의 상태 공유 문제와는 거리가 멉니다.


2. 주요 Scope 종류

Scope 이름 생성 범위 사용 예시 및 설명
singleton ApplicationContext 당 1개 생성 기본값. 서비스, DAO 등 재사용 가능한 컴포넌트
prototype 요청할 때마다 새로 생성됨 상태가 독립적인 객체 (ex: 동적 파라미터 기반 생성)
request HTTP 요청마다 1개 생성 요청 단위의 로깅/검증 객체, DTO 처리 (Web 환경)
session HTTP 세션마다 1개 생성 로그인 사용자 정보 저장, 세션 기반 상태 유지
application ServletContext 당 1개 전역 설정이나 공유 캐시

request, session, application 스코프는 Spring Web (Servlet) 환경이 구성된 경우에만 사용 가능합니다. 즉, Spring Boot에서도 spring-boot-starter-web 의존성을 포함한 웹 프로젝트에서는 정상적으로 동작하지만, CLI 또는 테스트 환경에서는 스코프 등록 오류가 발생할 수 있습니다.


3. 실전 예시별 적용 스코프 비교

실전 시나리오 권장 Scope 이유
API 서비스 로직 (UserService 등) singleton 상태 없음, 재사용성 중요
매 요청마다 검사하는 Validator request 요청 간 상태 분리 필요
로그인한 유저 정보 저장 session 세션 단위로 유지되어야 함
요청마다 DTO 새로 만들기 prototype 파라미터 기반 객체 필요

4. request + session 복합 사용 예시

로그인 사용자 정보를 저장하고, 요청마다 그 값을 검증하는 시나리오를 예로 들어봅니다.

SessionUser.java

@Component
@Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class SessionUser {
    private String username;
    private LocalDateTime loginTime = LocalDateTime.now();

    // getter/setter
}

RequestLogger.java

@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestLogger {
    public RequestLogger() {
        System.out.println("[RequestLogger] New instance created");
    }
}

Controller 예시

@RestController
public class UserController {

    @Autowired
    private SessionUser sessionUser;

    @Autowired
    private RequestLogger requestLogger;

    @GetMapping("/user")
    public String getUser() {
        System.out.println("RequestLogger: " + requestLogger.hashCode());
        return "Hello, " + sessionUser.getUsername();
    }
}

5. 테스트 시 주의사항

  • request, session 스코프는 Spring Boot Test 환경에서 WebApplicationContext로 테스트해야 정상 동작합니다.
  • @WebMvcTest 또는 @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) 등을 사용해야 함.
  • 일반 JUnit 테스트에서는 IllegalStateException: No Scope registered for scope 'request' 발생 가능.

6. 요약

Scope 언제 사용하나?
singleton 대부분의 경우 기본 선택
prototype 독립적인 상태를 가진 객체가 필요한 경우
request 요청 단위 분리 처리 시 (ex. API 로깅, 유효성 검증)
session 사용자별로 로그인 상태나 설정을 유지해야 할 때
 

1. 디자인 패턴이란?

디자인 패턴은 소프트웨어 설계 시 반복적으로 등장하는 문제를 해결하기 위한 일종의 '템플릿'이다. Java Spring에서는 프레임워크 특성상 구조적인 패턴(예: MVC)이나 생성/행위 패턴을 많이 활용한다.

실제로 Spring 기반 프로젝트를 개발하다 보면, 작은 규모의 서비스라도 다양한 디자인 패턴이 유기적으로 사용되는 것을 볼 수 있습니다. 각 컴포넌트가 자신의 책임에 집중하고, 변화에 유연하게 대응할 수 있도록 설계하는 과정에서 자연스럽게 전략(Strategy), 프록시(Proxy), 빌더(Builder) 등 다수의 패턴이 녹아들게 됩니다.

2. 시기별 디자인 패턴 사용 경향

시기 특징 주로 사용된 패턴
Spring Framework 2.x XML 기반 설정, DI/IoC 초창기 Singleton, Factory, Proxy, Template Method
Spring 3.x ~ 4.x Java Config 전환, AOP 정착 Strategy, Decorator, Observer, Command
Spring Boot Auto-configuration, Starter 기반 설정 최소화 Builder, Adapter, Chain of Responsibility, Specification

3. 주요 디자인 패턴 정리 및 실무 예제

 

Singleton 패턴

  • 정의: 인스턴스를 하나만 생성해 공유
  • Spring 적용: 기본 Bean scope가 Singleton
  • 대표 클래스: Spring의 모든 @Component, @Service, @Repository
  • UML (Text 기반):
+----------------+
|   Singleton    |
+----------------+
| - instance     |
+----------------+
| + getInstance()|
+----------------+
  • 실무 예제:
@Service
public class NotificationService {
    public void send(String message) {
        System.out.println("Sending message: " + message);
    }
}

Factory 패턴

  • 정의: 객체 생성 로직을 캡슐화
  • Spring 적용: BeanFactory, FactoryBean
  • 대표 클래스: LocalSessionFactoryBean, SqlSessionFactoryBean
  • UML (Text 기반):
      +-------------+
      |   Product   |
      +-------------+
         ^       ^
         |       |
+---------------------+
|ConcreteProductA/B   |
+---------------------+
         ^
         |
+---------------------+
|   ProductFactory    |
| +createProduct()    |
+---------------------+
  • 실무 예제:
public class AnimalFactory {
    public static Animal createAnimal(String type) {
        return switch (type) {
            case "dog" -> new Dog();
            case "cat" -> new Cat();
            default -> throw new IllegalArgumentException("Unknown type");
        };
    }
}

Proxy 패턴

  • 정의: 대리 객체를 통해 원래 객체 제어
  • Spring 적용: AOP (Aspect Oriented Programming)
  • 대표 클래스: ProxyFactoryBean, @Transactional
  • UML (Text 기반):
     +---------+
     | Subject |
     +---------+
        ^   ^
        |   |
+--------------+  +--------+
| RealSubject  |  | Proxy  |
+--------------+  +--------+
                     |
                     v
              +-------------+
              | RealSubject |
              +-------------+
  • 실무 예제:
@Aspect
@Component
public class LoggingAspect {
    @Before("execution(* com.example.service.*.*(..))")
    public void logBefore() {
        System.out.println("메서드 실행 전 로그 출력");
    }
}

Template Method 패턴

  • 정의: 알고리즘 골격을 정의하고, 일부 단계는 서브클래스에서 구현
  • Spring 적용: JdbcTemplate, RestTemplate
  • 대표 클래스: JdbcTemplate, AbstractController
  • UML (Text 기반):
+------------------+
|  AbstractClass   |
+------------------+
| +templateMethod()|
| +primitiveOp1()  |
| +primitiveOp2()  |
+------------------+
          ^
          |
+------------------+
|  ConcreteClass   |
+------------------+
| +primitiveOp1()  |
| +primitiveOp2()  |
+------------------+
  • 실무 예제:
public abstract class AbstractTask {
    public void execute() {
        start();
        process();
        end();
    }
    protected abstract void process();
    private void start() { System.out.println("Start"); }
    private void end() { System.out.println("End"); }
}

Strategy 패턴

  • 정의: 알고리즘을 클래스로 분리해 캡슐화
  • Spring 적용: AuthenticationProvider, ViewResolver
  • 대표 클래스: HandlerMethodArgumentResolver
  • UML (Text 기반):
     +------------+
     |  Strategy  |
     +------------+
        ^     ^
        |     |
+-------------------+
|ConcreteStrategies |
+-------------------+
        ^
        |
   +-----------+
   |  Context   |
   +-----------+
   | -strategy |
   | +execute()|
   +-----------+
  • 실무 예제:
public interface PaymentStrategy {
    void pay(int amount);
}

public class CreditCardPayment implements PaymentStrategy {
    public void pay(int amount) {
        System.out.println("Paid " + amount + " using credit card.");
    }
}


Builder 패턴

  • 정의: 복잡한 객체 생성을 단계별로 처리
  • Spring 적용: ResponseEntity.builder(), Lombok @Builder
  • 대표 클래스: ResponseEntity, User.builder()
  • UML (Text 기반):
+-----------+       +-----------+
|  Director | ----> |  Builder  |
+-----------+       +-----------+
                         ^
                         |
                +------------------+
                | ConcreteBuilder  |
                +------------------+
                         |
                         v
                    +---------+
                    | Product |
                    +---------+
  • 실무 예제:
@Builder
public class User {
    private String name;
    private int age;
}

Adapter 패턴

  • 정의: 인터페이스 호환을 위한 중간 계층
  • Spring 적용: HandlerAdapter, MessageConverter
  • 대표 클래스: HttpMessageConverter, HandlerAdapter
  • UML (Text 기반):
     +--------+
     | Target |
     +--------+
         ^
         |
     +--------+
     | Adapter|
     +--------+
         |
         v
     +--------+
     | Adaptee|
     +--------+
  • 실무 예제:
public interface MediaPlayer {
    void play(String fileName);
}

public class AudioPlayer implements MediaPlayer {
    public void play(String fileName) {
        System.out.println("Playing " + fileName);
    }
}

Specification 패턴

  • 정의: 조건을 조합해 쿼리 작성
  • Spring 적용: JpaSpecificationExecutor
  • 대표 클래스: Specification<T>
  • UML (Text 기반):
      +------------------+
      |  Specification   |
      +------------------+
      | +isSatisfiedBy() |
      | +and(), or(), not()|
      +------------------+
               ^
               |
   +----------------------------+
   |   ConcreteSpecification   |
   +----------------------------+
  • 실무 예제:
Specification<User> hasName(String name) = (root, query, cb) -> cb.equal(root.get("name"), name);

4. 목적별 디자인 패턴 추천 가이드

Spring 프로젝트에서 상황에 따라 어떤 디자인 패턴을 적용하면 좋을지 정리하면 다음과 같습니다:

목적/상황 추천 패턴 설명
JPA 동적 쿼리 처리 Specification 조건 조합 및 유연한 쿼리 생성을 위해 적합
다양한 외부 시스템 연계 Adapter, Template Method 연계 대상별로 추상화하여 유연하게 연동 가능
복잡한 객체 생성 Builder DTO나 Response 객체를 생성할 때 단계적 설정이 가능
비즈니스 로직 알고리즘 분리 Strategy 정책, 계산 방식, 인증처리 등 알고리즘 선택에 유리
횡단 관심사 처리 Proxy 트랜잭션, 로깅, 인증/인가 등 AOP 기반 처리
템플릿 제공 후 확장 유도 Template Method API 호출, DB 연동 등 공통 로직 제공 후 커스터마이징 허용
서로 다른 구현 통합 필요 Adapter 외부 API, 레거시 시스템 등을 일관된 인터페이스로 변환

이 표를 바탕으로 실무 상황에 맞는 패턴을 선택하면 아키텍처의 유연성과 유지보수성이 크게 향상됩니다

'Back-End > Java & Spring' 카테고리의 다른 글

Factory 패턴 개념  (0) 2025.05.11
싱글톤 패턴 개념& Spring Bean Scope  (0) 2025.05.11
Java GC 로그 해석과 튜닝 전략 정리  (0) 2025.05.10
Java GC 알고리즘 정리  (0) 2025.05.10
CORS 오류 해결하기(Spring)  (0) 2025.03.02

Java 애플리케이션에서 성능 문제의 많은 부분은 GC(Garbage Collection) 동작과 밀접한 관련이 있다. GC 로그를 통해 메모리 사용 상태를 파악하고, 튜닝 전략을 세워 시스템 안정성과 응답성을 높일 수 있다.


1. GC 로그 수집 설정

JVM 실행 시 다음 옵션을 통해 GC 로그를 수집할 수 있다:

-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:/var/log/gc.log

JDK 9 이상에서는 Unified Logging을 사용:

-Xlog:gc*:file=gc.log:tags,uptime,time,level

2. GC 로그 예시와 해석 (G1GC 기준)

2024-05-10T12:00:01.123+0000: 2.345: [GC pause (G1 Evacuation Pause) (young), 0.0456789 secs]
   [Parallel Time: 42.3 ms, GC Workers: 4]
   [Eden: 256M(256M)->0B(128M) Survivors: 32M->64M Heap: 1024M(2048M)->800M(2048M)]

주요 해석 포인트:

  • GC 종류: Young GC (G1 Evacuation Pause)
  • Pause Time: 0.045초 → GC로 인한 멈춤 시간
  • Heap Before/After: 1024MB → 800MB (224MB 회수)

경고 신호:

  • Full GC, to-space exhausted, Allocation Failure: 메모리 부족 가능성
  • Pause Time이 1초 이상이면 사용자 체감 가능 → 튜닝 필요

3. GC 튜닝 전략

튜닝은 애플리케이션 특성과 사용 환경에 따라 다르며, 아래는 일반적인 방향성이다.

A. Heap 사이즈 조정

-Xms2g -Xmx4g  # 초기값과 최대값 설정
  • 너무 작으면 GC 반복 발생
  • 너무 크면 Full GC 시 오히려 문제

B. G1GC 튜닝 옵션 예시

-XX:+UseG1GC
-XX:MaxGCPauseMillis=200         # 최대 멈춤 시간 설정 (ms)
-XX:InitiatingHeapOccupancyPercent=45  # Old GC 발생 기준

C. Throughput/Latency 목표에 따른 선택

목적 전략
최대 처리량 ParallelGC, 큰 Heap, 낮은 Pause 예외 허용
짧은 응답 시간 G1GC, ZGC, Heap 최소화, MaxGCPauseMillis 조절

4. 분석 도구 추천

  • GCViewer: 시각화 기반 GC 로그 해석
  • GCEasy.io: 웹 기반 분석 자동화 도구
  • JClarity Censum: 상용 도구, 히트맵 분석 제공

Java의 GC(Garbage Collection)는 JVM이 불필요한 객체를 자동으로 제거하여 메모리를 회수하는 기능이다. 이 작업을 수행하는 방식에는 여러 가지 GC 알고리즘이 존재하며, 각각의 특성과 목적에 따라 선택이 달라진다.


1. GC 기본 용어

  • Young Generation: 새롭게 생성된 객체들이 위치. 대부분의 객체는 이 영역에서 생성되고 빠르게 소멸함
  • Old (Tenured) Generation: Young 영역을 통과한 생명 주기가 긴 객체들이 위치
  • Metaspace (구 PermGen): 클래스 메타정보가 저장되는 영역
  • Stop-the-World: GC가 실행되기 위해 JVM의 모든 쓰레드를 멈추는 현상

2. 주요 GC 알고리즘 비교

알고리즘 동작 방식 장점 단점 적합한 환경
Serial GC 단일 스레드로 처리 단순함, 오버헤드 낮음 멀티코어 활용 불가, STW 길어짐 테스트/소규모 시스템
Parallel GC 여러 스레드로 Young GC 수행 Throughput 좋음 STW 길 수 있음 배치 처리, 대량 처리 중심
CMS Old GC 병행 처리 (Concurrent Mark-Sweep) 짧은 응답 시간 메모리 조각화, 종료 단계 STW 응답 지연 민감한 시스템
G1GC Heap을 Region으로 나눠 동시 관리 예측 가능한 GC 시간, 큰 힙에 강함 초기 튜닝 필요 대규모 서버, 최신 시스템
ZGC 매우 낮은 STW 시간, 병렬/동시 처리 중심 실시간 응답에 적합 최신 JVM 필요 대용량, 응답 지연 불허 환경
Shenandoah ZGC와 유사, OpenJDK 기반 낮은 지연 시간 실험적일 수 있음 지연이 매우 민감한 앱

3. GC 선택 가이드

GC 알고리즘 지원 JDK 버전 특징 요약
Serial GC 1.2 이상 가장 단순하고 모든 JVM에 존재
Parallel GC 1.4 이상 Throughput 위주, JDK 8까지 기본
CMS 1.5 ~ 14 STW 짧지만 JDK 15에서 제거됨
G1GC 7 이상, 9부터 기본 Region 기반, 예측 가능한 GC 시간
ZGC 11 이상, 15부터 안정화 초저지연, 실시간 환경에 적합
Shenandoah 12 이상 (OpenJDK 한정) OpenJDK용 저지연 GC
Epsilon 11 이상 GC를 하지 않음 (측정용)

JDK 버전별 기본 GC 요약

JDK 버전 기본 GC
8 이하 Parallel GC
9 ~ 13 G1GC
14 ~ 17 G1GC
18 이상 G1GC (향후 ZGC 기본 검토 중)
  • 개발/테스트 환경: Serial GC
  • 일반적인 웹 서비스: G1GC, 또는 Parallel GC
  • 실시간 응답형 시스템 (게임 등): ZGC, Shenandoah
  • 대량 데이터 배치/백엔드 서버: Parallel GC, G1GC

GC는 JVM 옵션으로 지정할 수 있다. 예시:

-XX:+UseG1GC
-XX:+UseParallelGC
-XX:+UseZGC

4. 참고 JVM 옵션

옵션 설명
-Xms<size> 초기 힙 사이즈
-Xmx<size> 최대 힙 사이즈
-XX:+PrintGCDetails GC 상세 로그 출력
-Xloggc:<file> GC 로그 파일 경로 지정
-XX:+UseCompressedOops 객체 포인터 압축 (기본값)

이 글은 각 GC 알고리즘의 특성과 목적을 파악해, 시스템 특성에 맞는 GC를 선택할 수 있도록 돕기 위한 기초 정리입니다.

+ Recent posts