전략 패턴을 사용할 때 Context를 싱글톤으로 설계하면 동시성 문제가 발생할 수 있다. 특히 Context
내부에 Strategy
를 필드로 가지고 있고, setStrategy()
로 전략을 변경하는 구조일 경우 문제의 소지가 크다.
💥 문제 예시
@Slf4j
public class Context {
private Strategy strategy;
public void setStrategy(Strategy strategy) {
this.strategy = strategy;
}
public void execute() {
log.info("Context.execute() 호출");
strategy.call(); // 문제 발생 가능 지점
}
}
이 Context
를 싱글톤으로 생성했다고 가정하면 아래와 같은 동시성 문제가 발생할 수 있다:
public void 동시성문제테스트() {
Context context = new Context(); // 싱글톤처럼 공유
Runnable userA = () -> {
context.setStrategy(() -> log.info("유저A 전략 실행"));
context.execute();
};
Runnable userB = () -> {
context.setStrategy(() -> log.info("유저B 전략 실행"));
context.execute();
};
Thread threadA = new Thread(userA);
Thread threadB = new Thread(userB);
threadA.start();
threadB.start();
}
🧨 예상 출력
유저A 전략 실행
유저B 전략 실행
이론적으로는 위처럼 나와야 하지만, 실행 타이밍에 따라 다음처럼 유저B 전략이 두 번 실행되는 상황이 발생할 수 있다:
유저B 전략 실행
유저B 전략 실행
이유는 A가 실행하기 전에 B가 전략을 바꿔버리면, A도 B의 전략을 사용하게 되기 때문이다.
✅ 해결 방법 1: 상태 없는(stateless) 방식
전략을 Context 내부 상태로 두지 않고, execute()
호출 시에 매번 넘겨주는 방식으로 바꾸면 안전하다. 즉, 템플릿 콜백 패턴처럼 작성하면 된다.
@Slf4j
public class Context {
public void execute(Strategy strategy) {
log.info("Context.execute() 호출");
strategy.call();
}
}
이 구조는 공유된 싱글톤 Context
를 여러 스레드가 동시에 사용해도 문제가 없다.
구조 | 동시성 안전 여부 | 설명 |
---|---|---|
전략을 필드에 저장 (setStrategy() ) |
❌ 위험 | 전략이 바뀌는 동안 다른 스레드가 엉뚱한 전략 실행 가능 |
전략을 파라미터로 전달 (execute(strategy) ) |
✅ 안전 | 전략을 상태로 저장하지 않기 때문에 스레드 간 영향 없음 |
✅ 해결 방법 2: 불변(Immutable)한 의존성 주입
Context가 전략을 생성자 주입으로 받고 이후 절대 변경하지 않도록 설계하면, 동시성 문제를 원천 차단할 수 있다.
@Slf4j
public class Context {
private final Strategy strategy;
public Context(Strategy strategy) {
this.strategy = strategy; // 한 번만 주입
}
public void execute() {
log.info("Context.execute() 호출");
strategy.call();
}
}
strategy
는final
이며,- setter가 존재하지 않고,
- 한 번 박힌 전략은 바뀌지 않는다.
따라서 싱글톤으로 설계하더라도 동시성 문제가 전혀 발생하지 않는다. 이 방식은 Spring 프레임워크에서 생성자 주입이 권장되는 이유이기도 하다.
🧠 정리
한 번 의존성이 박히면 다신 다른 의존성을 받을 수 없게 설계하면 동시성 문제가 해결된다.
바로 이것이 불변 설계(Immutable Design)의 핵심이며, 전략 패턴을 안전하게 적용하는 가장 효과적인 방법이다.
'spring' 카테고리의 다른 글
ThreadLocal, 안 쓰면 큰일 나는 이유 (멀티스레드 동시성 문제 해결법) (0) | 2025.03.20 |
---|---|
스프링 MVC 학습목록 (2) (0) | 2025.01.08 |
HandlerMapping & RequestMapping (0) | 2024.12.02 |
SpringMVC ArgumentResolver (0) | 2024.12.02 |
Entity 클래스와 DTO를 분리하는 이유 (0) | 2024.11.30 |
댓글