Design Pattern

전략 패턴 적용기 + 스프링에서 List로 빈 주입하기

Luti 2025. 8. 7. 17:03

 

1학기때는 한 주에 글 하나씩 작성하는 걸 목표로 했었는데 앞으로 그렇게까지는 못하지 않을까 싶습니다.

소소하게 시작한 프로젝트에서 새로 알게 된 내용들을 정리해 놓으면 좋겠다 싶어서 오랜만에 짧은 글 남기게 되었습니다.

 

 

# 00.  배경

 

동시성 문제를 해결하는 전략이 다양하게 있습니다. 

저는 스프링 애플리케이션 분산 네트워크 환경에서 동시성 문제를 유발하고 jvm락, db락, redis 락 등 다양한 방식으로 해결해 보고 여러 메트릭을 측정하거나 장단점을 비교해 보는 소소한 프로젝트를 시작했는데요.

처음에는 각 전략에 대해 요청을 받는 핸들러를 전략 별로 두어야겠다 생각을 했습니다.

그런데 스터디 같이 하시는 분이 본인 프로젝트에서 결제 시스템에 전략 패턴을 적용하겠다는 계획을 말씀하시길래 그 아이디어를 도용해서 제 프로젝트에 적용해보았습니다.

 

# 01.  전략 패턴

 

저는 전략패턴에 대해 이렇게 이해했습니다.

어떠한 문제를 해결하거나 요청을 처리하는 데에 다양한 방식(전략)이 있을 수 있습니다.

문제를 해결하는 알고리즘의 흐름(Context)을 추상화하고 전략들은 캡슐화합니다.

이후 런타임 때 어떤 전략을 활용할지 동적으로 선택하여 그 흐름에 주입해 주는 패턴을 전략패턴이라고 합니다. 

 

 

전략 패턴을 예로 보여줄 수 있는 가장 간단한 형태의 코드를 작성해 봤습니다.

interface CookingStrategy {
	void cook();
}

class Oven implements CookingStrategy {
	@Override
	public void cook() {
		System.out.println("오븐으로 재료를 쿠킹합니다.");
	}
}

class FryingPan implements CookingStrategy {
	@Override
	public void cook() {
		System.out.println("후라이펜으로 재료를 쿠킹합니다.");
	}
}

class Cooker {
	CookingStrategy strategy;

	void setCooking(CookingStrategy strategy) {
		this.strategy = strategy;
	}

	void cook() {
		strategy.cook();
	}
}

public class imsi {
	public static void main(String[] args) {
		Cooker cooker = new Cooker();

		cooker.setCooking(new Oven());
		cooker.cook();

		cooker.setCooking(new FryingPan());
		cooker.cook();
	}
}

 

CookingStrategy는 요리 방식(전략)을 결정짓는 인터페이스고 cook() 메서드를 통해 쿠킹을 하겠다는 흐름이 추상화되어 있습니다.

 

요리 방식을 구체화하는 구현체들이 Oven 클래스와 FryingPan 클래스입니다.

 

Cooker 클래스는 컨텍스트의 역할을 하며 추상화된 쿠킹 흐름에 실제 전략을 주입하는 역할을 합니다.

cook() 메서드를 호출하는 것으로 컨텍스트 내부에 저장된 전략을 실행합니다.

 

 

이렇게 전략 패턴을 사용함으로써

새로운 전략이 추가되더라도 알고리즘의 교체가 유연해지며,

데이터 은닉, 응집성, 재사용성 등의 캡슐화의 장점을 가져갈 수 있습니다.

 

 

# 02. 전략 패턴 적용

 

제가 구현한 락 전략을 다루는 클래스의 구조는 아래와 같습니다.

계좌에서 돈을 인출하는 상황을 가정한 실험 흐름입니다. 

 

 

 

LockStrategy 인터페이스

 

잔액 조회, 출금, 입금 등 계좌 트랜잭션 로직을 수행할 메서드 시그니처를 정의하는 전략의 공통 API역할을 합니다.

 

 

구현체 클래스 (NO_LOCK)

 

실제 전략의 로직이 구체화된 구현체 클래스입니다.

동시성 문제를 의도적으로 재현하고 성능 비교용으로 락 없는 처리를 테스트하기 위해 작성되었습니다.

 

 

컨텍스트 클래스

 

실제 트랜잭션 호출 시점에 어떤 LockStrategy를 사용할지 주입받고 그 전략의 메서드를 위임하는 역할을 합니다.

요청 측에서 직접 내부 전략 로직을 알 필요 없이, 일관된 API로 트랜잭션 흐름을 처리하도록 합니다.

 

 

핸들러 클래스

 

HTTP 요청을 받아 처리하는 부분에 해당하는 핸들러입니다.

요청에 담긴 Strategy 타입의 enum 값에 따라 팩토리를 통해 락 객체를 생성하고 해당 락 전략을 컨텍스트에 주입하는 흐름입니다.

관련해서 남겨둘 만한 포인트가 있는데 아래에서 다루도록 하겠습니다.

 

 

# 03. 팩토리와 함께 사용하기 + List로 빈 주입하기

 

전략을 캡슐화하고 인터페이스를 정의했는데, 실제 구현체를 런타임 때 어떤 방식으로 주입을 할지 고민이 있었습니다.

처음에는 스프링 프로필 기능을 이용하여 사전에 정한 하나의 구현체가 주입이 되도록 설계를 했습니다.

문제는 실험 환경이 도커 컴포즈를 이용하여 다수의 컨테이너를 띄우는 형태인데, 전략을 바꿔가며 요청을 보낼 때마다 매번 컴포즈를 내렸다 올렸다 하는 절차가 번거롭게 느껴졌습니다.

 

그래서 저는 코드나 설정에 개발자가 락 전략을 정해두는 것 대신, 애초에 요청받을 때 요청값으로 Strategy 타입의 Enum 값을 받기로 했고, 해당 값에 따라 알맞은 전략 객체를 컨텍스트에 자동으로 주입하는 방향으로 설계를 했습니다.

때문에 요청값에 매핑되는 락 전략을 반환해 주는 팩토리가 있어야겠다고 생각을 했고, 초기에는 아래와 같이 팩토리 코드를 작성했습니다.

나중에 또 다른 스터디원 분께서 말씀해 주셨는데 전략 패턴이랑 팩토리를 같이 사용하는 것이 알려진 좋은 방식이라고 하시더라고요.

 

 

스프링을 처음 공부할 때 배운 대로 빈 선언 해놓고 생성자 매개변수를 통해 필드에 주입하는 아주 정석적인(?) 형태의 방식이라 생각했습니다.

create 메서드에서는 switch 문을 통해 실제 락 전략 객체를 반환합니다.

 

작성해 놓고 보니까 뭔가 찜찜하더라고요.

당장은 프로젝트 초기 단계라 흐름을 잡아놓고 아직 구현체 코드를 작성하기 전인데, 앞으로 레디스 락 등 새로운 전략이 생길 때마다 팩토리 코드 내에서 필드 선언, 생성자 매개변수, 필드 주입, 스위치 케이스 문 추가 이 과정을 거쳐야 한다는 점이 객체지향스럽지 않다고 느껴졌습니다.

 

그래서 ApplicationContext에서 타입으로 빈을 검색하는 기능을 이용해야 하나 생각했는데 알아보니까 스프링에서 더욱 편리한 기능을 제공해 주더라고요

 

생성자 매개변수로 List, Set, Map과 같은 컬렉션들을 넘겨주면 알아서 스프링이 @Component로 등록된 알맞은 타입의 빈을 찾아 넘겨준다고 합니다.

해당 방식을 적용하여 수정한 코드가 아래와 같습니다.

 

 

저는 List로 LockStrategy 타입을 주입하는 방식으로 구현했습니다.

이렇게 함으로써 앞으로 락 전략이 10개, 100개가 추가되어도 팩토리 코드는 수정할 필요가 없어져 OCP를 준수할 수 있게 되었습니다.