진행 중인 사이드 프로젝트의 상품의 재고 관리 기능 로직에 디자인 패턴을 적용해보고 싶었다. 단순한 비즈니스 로직을 넘어 유연하고 확장 가능한 코드 구조를 기대해보며.. 전략 패턴과 팩토리 패턴을 적용해보기로 했다. 이번 글에서는 재고 증감 로직을 리팩토링하면서 전략 패턴을 어떻게 적용했고, 이후 팩토리 패턴으로 확장하며 얻은 경험을 공유해보려고 한다.
전략 패턴 도입
전략 패턴은 서로 다른 구현체를 개별 클래스로 캡슐화하고, 실행 시점에 필요에 따라 전략을 선택할 수 있도록 한다. 이를 통해 재고 증감과 같은 구체적인 로직을 각각의 UseCase로 분리하고, 공통된 실행 흐름을 UpdateStockUseCase라는 추상 클래스에서 정의하도록 했다.
@RequiredArgsConstructor
public abstract class UpdateStockUseCase extends UseCase<UpdateStockUseCase.Input, UpdateStockUseCase.Output> {
private final ItemReader itemReader;
private final ItemWriter itemWriter;
private final ClockManager clockManager;
private final StockTransactionType stockTransactionType;
@Override
public Output execute(Input input) {
Item foundItem = findItem(input.getItemId());
Item updatedItem = updateStock(foundItem, input.getQuantity());
return toOutput(updatedItem);
}
public StockTransactionType getTransactionType() {
return stockTransactionType;
}
protected abstract void updateStockQuantity(Item item, long quantity);
}
그리고 이를 상속하는 IncreaseStockUseCase와 DecreaseStockUseCase는 각각 재고 증가와 감소에 대한 구체적인 로직(updateStockQuantity())만 구현했다.
@Transactional
@Service
public class IncreaseStockUseCase extends UpdateStockUseCase {
public IncreaseStockUseCase(ItemReader itemReader, ItemWriter itemWriter, ClockManager clockManager) {
super(itemReader, itemWriter, clockManager, StockTransactionType.INCREASE);
}
@Override
protected void updateStockQuantity(Item item, long quantity) {
item.increaseStock(quantity);
}
}
@Transactional
@Service
public class DecreaseStockUseCase extends UpdateStockUseCase {
public DecreaseStockUseCase(ItemReader itemReader, ItemWriter itemWriter, ClockManager clockManager) {
super(itemReader, itemWriter, clockManager, StockTransactionType.DECREASE);
}
@Override
protected void updateStockQuantity(Item item, long quantity) {
item.decreaseStock(quantity);
}
}
이제 전략패턴으로 작성된 UpdateStockUseCase를 사용할 일만 남았다. 구현체를 상황에따라 하드코딩으로 직접 주입하는 방식에서 팩토리패턴을 적용해 자동으로 주입해주도록 해보았다.
기존 로직
처음에 재고 증감 로직은 매우 직관적으로 작성했다. ItemController에서 재고를 증가시키는 IncreaseStockUseCase와 감소시키는 DecreaseStockUseCase를 사용해 재고를 관리했다. 컨트롤러에서는 StockTransactionType에 따라 두 개의 UseCase 중 하나를 선택했다. 이 방식은 간단했지만, 하드코딩으로 유즈케이스를 주입하는 식이었고, 새로운 재고 관리 로직이 추가될 때마다 컨트롤러의 코드가 점점 복잡해질 수 있고 확장에 유연하게 대처하지 못할 것이 예상되었다.
@RequiredArgsConstructor
@RequestMapping("/api/items")
@RestController
public class ItemController {
private final IncreaseStockUseCase increaseStockUseCase;
private final DecreaseStockUseCase decreaseStockUseCase;
@PostMapping("{id}/stock-update")
public RestApiResponse<ItemResponse> updateItemStock(
@PathVariable long id,
@Valid @RequestBody UpdateItemStockRequest request) {
StockTransactionType transactionType = request.convertTransactionTypeToEnum();
UpdateStockUseCase updateStockUseCase = setUseCaseImplementBy(transactionType);
UpdateStockUseCase.Input input = UpdateStockUseCase.Input.of(id, request.quantity());
UpdateStockUseCase.Output output = updateStockUseCase.execute(input);
return RestApiResponse.created(ItemResponse.from(output));
}
private UpdateStockUseCase setUseCaseImplementBy(StockTransactionType transactionType) {
return transactionType == StockTransactionType.INCREASE ? increaseStockUseCase : decreaseStockUseCase;
}
}
이 코드에서 컨트롤러가 재고 증가/감소 로직을 직접 선택하고 있다. 재고 변경 유형이 추가될 때마다 조건을 추가해야 했고, 이는 코드의 가독성을 떨어뜨리며 확장성도 제한적이었다.
팩토리 패턴으로 확장
기존 로직에서 컨트롤러가 전략을 선택하는 책임을 가지고 있는 부분에서 개선이 필요했다. 이는 컨트롤러의 역할을 단순화하고, 책임을 분리하는 방향으로 리팩토링할 필요가 있었다.
그래서 전략 패턴과 함께 팩토리 패턴을 도입하여, 재고 증감 로직의 선택을 팩토리 클래스에서 담당하게 했다. UpdateStockUseCaseFactory라는 팩토리 클래스를 만들어, StockTransactionType에 따라 적절한 UseCase를 반환하는 방식으로 변경했다.
@Component
public class UpdateStockUseCaseFactory {
private final Map<StockTransactionType, UpdateStockUseCase> strategies = new HashMap<>();
public UpdateStockUseCaseFactory(List<UpdateStockUseCase> useCases) {
useCases.forEach(useCase -> {
StockTransactionType type = useCase.getTransactionType();
strategies.put(type, useCase);
});
}
public UpdateStockUseCase getUseCaseBy(StockTransactionType transactionType) {
return strategies.get(transactionType);
}
}
이렇게 팩토리 클래스를 도입함으로써, 컨트롤러에서는 더 이상 전략을 직접 선택하지 않고 팩토리에게 해당 책임을 위임했다. 이를 통해 컨트롤러는 단순히 요청을 처리하고, 비즈니스 로직의 실행은 UpdateStockUseCaseFactory에서 선택된 전략이 담당하게 되었다.
@RequiredArgsConstructor
@RequestMapping("/api/items")
@RestController
public class ItemController {
private final UpdateStockUseCaseFactory updateStockUseCaseFactory;
@PostMapping("{id}/stock-update")
public RestApiResponse<ItemResponse> updateItemStock(@PathVariable long id, @Valid @RequestBody UpdateItemStockRequest request) {
UpdateStockUseCase updateStockUseCase = updateStockUseCaseFactory.getUseCaseBy(request.convertStockTransactionTypeToEnum());
UpdateStockUseCase.Input input = UpdateStockUseCase.Input.of(id, request.quantity());
UpdateStockUseCase.Output output = updateStockUseCase.execute(input);
return RestApiResponse.ok(ItemResponse.from(output));
}
}
리팩토링 결과
이번 리팩토링을 통해 코드가 한층 더 확장 가능하고 유지보수성이 높아졌다. 새로운 StockTransactionType이 추가되더라도 팩토리에 쉽게 등록하여 UseCase를 확장할 수 있게 되었고(물론 증가와 감소 외에 추가될게 없을 것으로 예상되지만… 확장 가능성을 가정했다…), 컨트롤러는 본연의 역할인 HTTP 요청 처리에만 집중할 수 있게 되었다.
1. 전략 패턴 적용으로 인해 얻은 유연성
- 서로 다른 재고 관리 로직을 UpdateStockUseCase의 하위 클래스에서 구현함으로써, 재고 관리 로직을 쉽게 확장할 수 있었다.
2. 팩토리 패턴 도입으로 얻은 책임 분리
- 컨트롤러가 UseCase를 직접 선택하는 책임을 팩토리로 이동시켜, 컨트롤러의 역할을 단순화하고 SRP(Single Responsibility Principle)를 준수할 수 있었다.
결론
디자인 패턴을 활용한 이번 리팩토링을 통해 코드가 보다 유연해졌고, 유지보수에 용이한 구조를 갖추게 되었다. 앞으로 새로운 기능을 추가하거나 비즈니스 로직이 복잡해지더라도, 이러한 패턴을 통해 확장 가능하고 안정적인 코드를 유지할 수 있을 것 같다. 끝.