-
실무에서 Service → Manager → Processor 구조로 설계하는 이유시스템설계 2026. 2. 5. 21:58728x90반응형
들어가며
실무에서 코드를 작성하다 보면 이런 고민을 하게 된다.
- Service가 점점 커진다
- 로직이 한 메서드에 몰린다
- 테스트하기가 점점 힘들어진다
- “이 로직 어디까지가 한 작업이지?”가 헷갈린다
나도 비슷한 문제를 겪었고, 그 과정에서
Service → Manager → Processor 구조로 점진적으로 정리하게 되었다.이 글에서는 다음을 아주 쉽게 설명해보려고 한다.
- 유스케이스란 무엇인가
- 트랜잭션 경계는 왜 필요한가
- Service / Manager / Processor는 각각 무슨 역할인가
- 실제 코드 예제
1. 유스케이스란 무엇인가?
한 줄 정의
유스케이스 = 사용자가 의도를 가지고 한 번 행동하는 것
예시 (우리 서비스 기준)
- 백테스트를 실행한다
- 백테스트를 공유한다
- 공유된 백테스트를 구매한다
- 구매한 백테스트를 조회한다
👉 이 중 “공유된 백테스트를 구매한다”
이게 하나의 유스케이스다.
2. 트랜잭션 경계란?
말이 어려워서 헷갈리는 개념
트랜잭션 경계라는 말은 어려워 보이지만,
사실 뜻은 굉장히 단순하다.“이 작업은 전부 성공하거나, 전부 실패해야 한다”
비유로 이해하기: 편의점 결제
편의점에서 결제할 때:
- 돈이 빠져나간다
- 영수증이 출력된다
- 물건이 나온다
이 중 하나라도 실패하면?
- 돈만 빠져나가면 ❌
- 물건만 나오면 ❌
👉 전부 성공하거나 전부 취소되어야 한다
이 “묶음”이 바로 트랜잭션
백테스트 구매 유스케이스에 적용하면
공유된 백테스트를 구매할 때:
- 구매 가능한지 확인
- 구매자 크레딧 차감
- 구매 기록 저장
- 판매자에게 크레딧 지급
- 레벨 포인트 적립
👉 이 다섯 단계는 하나의 작업
그래서 이 전체를 하나의 트랜잭션으로 묶어야 한다
3. 전체 구조 한 번에 보기
우리가 만들고 싶은 구조는 이렇다.
Controller ↓ Service (유스케이스 + 트랜잭션) ↓ Manager (흐름/순서 조합) ↓ Processor (단일 작업)이제 하나씩 아주 쉽게 보자.
4. Controller – HTTP만 처리
역할
- 요청 받기
- DTO 바인딩
- 인증 정보 꺼내기
- Service 호출
- 응답 반환
👉 비즈니스 로직 절대 금지
예제
@RestController @RequiredArgsConstructor @RequestMapping("/api/backtest-shares") public class BacktestShareController { private final BacktestShareService service; @PostMapping("/{shareId}/purchase") public PurchaseResponse purchase( @PathVariable Long shareId, @AuthenticationPrincipal Member member ) { return service.purchase(member.getId(), shareId); } }Controller는 “구매”가 뭔지 모른다
그냥 “구매해달래서 Service에 넘긴다”
5. Service – 유스케이스의 시작점
Service의 역할
- 유스케이스 단위 메서드 제공
- 트랜잭션 경계 설정
- Manager 호출
👉 “이 작업은 하나의 책임이다”를 표현하는 곳
예제
@Service @RequiredArgsConstructor public class BacktestShareService { private final BacktestSharePurchaseManager purchaseManager; @Transactional public PurchaseResponse purchase(Long buyerId, Long shareId) { return purchaseManager.execute(buyerId, shareId); } }여기서 중요한 포인트:
- purchase() = 유스케이스
- @Transactional = 이 유스케이스 전체가 하나의 묶음
6. Manager – 오케스트레이션(순서 담당)
오케스트레이션이란?
누가 먼저 하고, 다음에 누가 하고, 언제 멈출지 정하는 일
비유
- 요리사가 직접 재료가 되지는 않음
- 하지만 순서와 타이밍은 요리사가 정함
Manager의 역할
- 여러 Processor를 조합
- 순서와 분기 처리
- “조립”만 담당
👉 규칙 판단은 Processor에게 맡긴다
예제
@Component @RequiredArgsConstructor public class BacktestSharePurchaseManager { private final LoadShareProcessor loadShare; private final ValidateNotOwnerProcessor validateNotOwner; private final ValidateNotPurchasedProcessor validateNotPurchased; private final DeductCreditProcessor deductCredit; private final SavePurchaseProcessor savePurchase; private final AwardCreditToSellerProcessor awardCredit; public PurchaseResponse execute(Long buyerId, Long shareId) { BacktestShareEntity share = loadShare.byId(shareId); validateNotOwner.check(share, buyerId); validateNotPurchased.check(buyerId, shareId); int price = share.getPriceCredit(); var wallet = deductCredit.execute(buyerId, price); savePurchase.execute(buyerId, shareId, price); awardCredit.execute(share.getMemberId(), price); return new PurchaseResponse( shareId, price, wallet.getTotalPoints() ); } }Manager는:
- “무엇을 먼저 할지”만 결정
- 비즈니스 규칙은 직접 판단하지 않음
7. Processor – 하나만 잘하는 작은 부품
Processor의 역할
- 딱 한 가지 책임
- 가능한 한 Stateless
- 테스트가 쉬워야 함
예제 1: 소유자 구매 방지
@Component public class ValidateNotOwnerProcessor { public void check(BacktestShareEntity share, Long buyerId) { if (share.getMemberId().equals(buyerId)) { throw new IllegalArgumentException("owner cannot purchase"); } } }
예제 2: 크레딧 차감
@Component @RequiredArgsConstructor public class DeductCreditProcessor { private final CreditService creditService; public Wallet execute(Long buyerId, int price) { return creditService.deductCredit( buyerId, price, "BACKTEST_SHARED" ); } }
예제 3: 구매 기록 저장
@Component @RequiredArgsConstructor public class SavePurchaseProcessor { private final BacktestSharePurchaseRepository repo; public void execute(Long buyerId, Long shareId, int price) { repo.save( BacktestSharePurchaseEntity.builder() .buyerMemberId(buyerId) .shareId(shareId) .paidCredit(price) .build() ); } }각 Processor는:
- 이해하기 쉽고
- 테스트하기 쉽고
- 재사용 가능하다
8. 이 구조의 장점 정리
1️⃣ 읽기 쉽다
- Service: “아, 이게 구매 유스케이스구나”
- Manager: “구매 흐름은 여기구나”
- Processor: “이 규칙은 여기구나”
2️⃣ 테스트하기 쉽다
- Processor → 순수 단위 테스트
- Manager → 흐름 테스트
- Service → 트랜잭션 테스트
3️⃣ 변경에 강하다
- 레벨 포인트 정책 변경 → Processor 하나 수정
- 구매 순서 변경 → Manager만 수정
마무리
처음엔 이런 용어들이 굉장히 어렵게 느껴진다.
- 유스케이스
- 트랜잭션 경계
- 오케스트레이션
하지만 사실 다 풀어보면:
“한 번의 사용자 행동을, 안전하게, 읽기 좋게, 테스트 가능하게 나누는 방법”
이라는 아주 현실적인 이야기다.
이 구조는 정답은 아니지만,
복잡해지는 서비스에서 버티기 좋은 구조임은 확실하다고 느꼈다.728x90반응형