Service 계층 설계 정리
1. Service의 본질
Service는 Controller의 코드가 너무 많아지니까 영역을 나눈 것이지,
공통 기능을 재사용하려는 목적이 아니다.
공통 기술 기능(파일 처리, 날짜 포맷 등) →
Util클래스
비즈니스 로직 →Service
2. 코드가 같아도 메서드를 분리할지 기준
“이 코드가 나중에 한쪽만 바뀔 가능성이 있는가?”
| 상황 | 결정 |
|---|---|
| 정책이 바뀌면 두 곳 모두 똑같이 바뀌어야 함 | 공통 Service로 묶기 |
| 겉보기엔 같지만 맥락(Context)이 다름 | 별도 메서드/서비스로 분리 |
예시
- 사용자용 상세조회 (민감정보 마스킹) vs 관리자용 상세조회 (전체 노출) → 분리
- 결제 취소 시 환불 정책 → 어디서 취소하든 동일 → 공통 Service
3. Service가 커지는 문제 (Fat Service)
AService가 a, b, c, d DAO를 전부 가지면:
- 하나가 바뀌면 전체에 영향
- 재사용 불가
- 트랜잭션 경계가 불명확
해결책 → 계층 분리
4. 계층 구조
Controller
↓ (1:1 호출)
Facade (상위 Service) ← 시나리오 설계, @Transactional 담당
↓
Domain Service (하위 Service) ← 도메인 전문 로직, 공통 부품
↓
Repository (DAO)
각 계층의 역할
| 계층 | 역할 | 공통화 | 비유 |
|---|---|---|---|
| Controller | 요청/응답만 | X | 창구 직원 |
| Facade | 시나리오 조립, 트랜잭션 | 보통 X (Controller 전용) | 프로젝트 매니저 |
| Domain Service | 도메인 규칙 처리 | O (적극 재사용) | 전문 기술자 |
| Repository | DB 접근 | - | - |
5. Facade 패턴
Controller에서 직접 여러 Service 호출하면 안 되는 이유
// ❌ 위험 - 트랜잭션이 각각 따로 동작
public void order() {
orderService.createOrder(); // 성공
stockService.decrease(); // 성공
financeService.update(); // 실패 → 위 둘은 롤백 안 됨!
}
각 Service 메서드마다 트랜잭션이 따로 동작하므로,
중간에 실패해도 이전 작업이 롤백되지 않는다.
Facade로 해결
// ✅ Facade에서 @Transactional → 전체가 하나로 묶임
@Service
@RequiredArgsConstructor
public class OrderFacade {
private final OrderService orderService;
private final StockService stockService;
private final FinanceService financeService;
@Transactional
public void placeOrder() {
orderService.createOrder(); // 주문
stockService.decreaseStock(); // 재고
financeService.recordTransaction(); // 재무
// 셋 중 하나라도 실패 → 전체 롤백
}
}
6. 같은 Domain Service를 다르게 조합하는 예시
시나리오: 사용자 주문 vs 관리자 강제 주문
// 사용자용 Facade
@Service
public class UserOrderFacade {
private final OrderService orderService;
private final StockService stockService;
private final NotificationService notificationService; // 알림 O
@Transactional
public void placeOrder() {
orderService.createOrder();
stockService.decreaseStock();
notificationService.sendSms(); // 사용자에게 알림
}
}
// 관리자용 Facade
@Service
public class AdminOrderFacade {
private final OrderService orderService;
private final StockService stockService;
private final FinanceService financeService; // 재무 기록 O
@Transactional
public void createOrderForce() {
orderService.createOrder(); // 같은 OrderService 재사용
stockService.decreaseStock(); // 같은 StockService 재사용
financeService.recordTransaction(); // 알림 없이 재무만
}
}
OrderService, StockService는 두 Facade 모두에서 공통으로 재사용
NotificationService, FinanceService는 필요한 곳에서만 선택적 사용
7. Domain Service 기준
“이 Service는 어디까지 담당하는가?”
- 하나의 도메인 엔티티 생명주기만 책임진다
- 자기 영역의 Repository만 주입받는다
// OrderService → 주문 테이블만
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final OrderHistoryRepository historyRepository; // 주문 히스토리도 주문 영역
public void createOrder(OrderInfo info) {
orderRepository.save(info);
historyRepository.save(new History(info)); // 주문 관련만
}
}
8. 언제 Facade가 필요한가?
| 상황 | 결정 |
|---|---|
| 단순 CRUD, 도메인 하나만 건드림 | Facade 불필요, Domain Service 직접 사용 |
| 여러 도메인이 엮이고 트랜잭션이 중요 | Facade 도입 |
| Facade 자체가 너무 커짐 | 행위 단위로 쪼개기 (PlaceOrderFacade, CancelOrderFacade) |
9. 최종 정리
"상위 서비스(Facade)는 컨트롤러의 영역 분리이자 시나리오 설계도,
하위 서비스(Domain Service)는 비즈니스 규칙을 지키는 공통 부품"
- Controller ↔ Facade : 1:1 (컨트롤러 전용)
- Facade ↔ Domain Service : N:M (여러 Facade에서 공통 재사용)
-
Domain Service ↔ Repository : 1:1 (자기 도메인만)
- 물론 간단한건 Controller에서 바로 DomainService 호출가능