ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 실무에서 Service → Manager → Processor 구조로 설계하는 이유
    시스템설계 2026. 2. 5. 21:58
    728x90
    반응형

    들어가며

    실무에서 코드를 작성하다 보면 이런 고민을 하게 된다.

    • Service가 점점 커진다
    • 로직이 한 메서드에 몰린다
    • 테스트하기가 점점 힘들어진다
    • “이 로직 어디까지가 한 작업이지?”가 헷갈린다

    나도 비슷한 문제를 겪었고, 그 과정에서
    Service → Manager → Processor 구조로 점진적으로 정리하게 되었다.

    이 글에서는 다음을 아주 쉽게 설명해보려고 한다.

    • 유스케이스란 무엇인가
    • 트랜잭션 경계는 왜 필요한가
    • Service / Manager / Processor는 각각 무슨 역할인가
    • 실제 코드 예제

    1. 유스케이스란 무엇인가?

    한 줄 정의

    유스케이스 = 사용자가 의도를 가지고 한 번 행동하는 것

    예시 (우리 서비스 기준)

    • 백테스트를 실행한다
    • 백테스트를 공유한다
    • 공유된 백테스트를 구매한다
    • 구매한 백테스트를 조회한다

    👉 이 중 “공유된 백테스트를 구매한다”
    이게 하나의 유스케이스다.


    2. 트랜잭션 경계란?

    말이 어려워서 헷갈리는 개념

    트랜잭션 경계라는 말은 어려워 보이지만,
    사실 뜻은 굉장히 단순하다.

    “이 작업은 전부 성공하거나, 전부 실패해야 한다”


    비유로 이해하기: 편의점 결제

    편의점에서 결제할 때:

    1. 돈이 빠져나간다
    2. 영수증이 출력된다
    3. 물건이 나온다

    이 중 하나라도 실패하면?

    • 돈만 빠져나가면 ❌
    • 물건만 나오면 ❌

    👉 전부 성공하거나 전부 취소되어야 한다

    이 “묶음”이 바로 트랜잭션


    백테스트 구매 유스케이스에 적용하면

    공유된 백테스트를 구매할 때:

    1. 구매 가능한지 확인
    2. 구매자 크레딧 차감
    3. 구매 기록 저장
    4. 판매자에게 크레딧 지급
    5. 레벨 포인트 적립

    👉 이 다섯 단계는 하나의 작업

    그래서 이 전체를 하나의 트랜잭션으로 묶어야 한다


    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
    반응형
Designed by Tistory.