#10 - Service에서 하지 말아야 할 것들
중년개발자
@loxo
4일 전
Service 실무 가이드: Service에서 하지 말아야 할 것들
Spring Boot에서 Service 레이어를 순수하게 유지하기 위한 "금지 목록"
1. Web 기술을 Service로 가져오지 않는다
Service는 어떤 환경(Web, Console, Batch)에서도 실행 가능해야 한다.
Controller가 처리해야 할 HTTP 관련 객체가 Service 메서드 파라미터나 반환값에 등장하면 안 된다.
❌ 절대 금지: Web 객체 의존
// BAD
public void createOrder(HttpServletRequest req) { ... }
public ResponseEntity<Order> getOrder(Long id) { ... }✅ 올바른 방법: POJO(Plain Old Java Object) 사용
// GOOD
public void createOrder(Long userId, CreateOrderCommand command) { ... }
public OrderDto getOrder(Long id) { ... }2. 인증(Authentication)을 Service에서 처리하지 않는다
"누가 요청했는가"를 확인하고 의심하는 것은 **입구(Controller/Security)**의 역할이다.
Service가 실행될 때는 이미 신뢰할 수 있는 사용자임이 확정되어야 한다.
❌ 절대 금지: Security Context 직접 조회 또는 토큰 파싱
// BAD
public void updateProfile(ProfileRequest req) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication(); // ❌
// ...
}✅ 올바른 방법: 확정된 자격(Identity)을 파라미터로 받기
// GOOD (Controller에서 @AuthenticationPrincipal 등으로 꺼내서 전달)
public void updateProfile(Long userId, ProfileRequest req) {
// Service는 userId가 검증된 것임을 믿고 로직만 수행
}3. 입력 형식 검증(Shape Validation)을 하지 않는다
데이터가 비어있는지, 양수인지, 이메일 형식인지는 들어오기 전에(Controller/DTO) 막아야 한다.
Service는 비즈니스 규칙(상태, 권한, 재고 등) 검증에만 집중해야 한다.
❌ 절대 금지: null 체크, 단순 포맷팅 검증 반복
// BAD
public void create(CreateOrderRequest req) {
if (req.getName() == null) { // ❌ Controller에서 @NotNull로 막았어야 함
throw new IllegalArgumentException();
}
}✅ 올바른 방법: 비즈니스 판단
// GOOD
public void create(CreateOrderRequest req) {
// 상품이 실제로 존재하는지, 판매 가능 상태인지 등 "DB/로직" 기반 검증
Product product = findProduct(req.getProductId());
if (!product.isSaleable()) {
throw new BusinessException("판매 중지된 상품");
}
}4. Entity를 단순 데이터 덩어리(Getter/Setter)로 취급하지 않는다
Entity는 단순히 데이터를 담아두는 그릇(Data Holder)이 아닙니다.
Service가 Entity의 값을 하나하나 꺼내서(get) 수정하고 다시 넣는다면(set), 객체지향이 아니라 C언어 구조체를 다루는 것과 같습니다.
Entity가 스스로의 상태를 변경하도록 의미 있는 메서드를 제공해야 합니다.
❌ 절대 금지: Setter 남발로 상태 변경
// BAD
Order order = orderRepository.findById(id);
order.setStatus("CANCEL"); // ❌
order.setCancelReason("단순 변심");✅ 올바른 방법: Entity에게 행동을 위임
// GOOD
Order order = orderRepository.findById(id);
order.cancel("단순 변심"); // ⭕ Entity 내부에서 상태와 규칙(이미 배송중이면 취소 불가 등)을 체크5. 트랜잭션 경계를 흐리지 않는다 (1) - Controller 조립 금지
하나의 유스케이스는 하나의 트랜잭션으로 완결되어야 한다.
Controller에서 여러 Service를 호출해서 로직을 조립하면, 부분 실패 시 데이터 정합성이 깨진다.
❌ 절대 금지: Controller에서의 로직 조립
// In Controller
userService.updatePoint(); // 트랜잭션 A (성공)
orderService.create(); // 트랜잭션 B (실패) -> 이미 차감된 포인트는?? 😱✅ 올바른 방법: Service가 유스케이스의 단위가 됨
// In Controller
orderFacade.orderWithPoint(); // 하나의 트랜잭션으로 묶인 메서드 호출6. 트랜잭션 경계를 흐리지 않는다 (2) - 외부 API 호출 주의
반대로, 트랜잭션은 가능한 짧게 유지해야 한다.
특히 **외부 API 호출(결제, 알림 발송 등)**을 트랜잭션 안에서 수행하면 DB Connection이 불필요하게 오래 점유된다.
❌ 절대 금지: 트랜잭션 안에서 긴 외부 호출
@Transactional
public void order() {
paymentApi.pay(); // ⏰ 3초 소요 (DB Connection을 잡고 있음)
orderRepository.save(order);
}✅ 올바른 방법: 트랜잭션 범위 최소화
public void order() {
paymentApi.pay(); // 트랜잭션 없이 실행
saveOrder(); // 내부에서 @Transactional 수행
}
@Transactional
public void saveOrder() { ... }7. 결과 반환 시 null을 리턴하지 않는다
Service에서 null을 반환하면 호출하는 쪽(Controller)에서 NullCheck 지옥이 시작된다.
데이터가 없을 때는 Optional을 사용하거나, 명시적인 Exception을 던져야 한다.
❌ 절대 금지: null 리턴
// BAD
public Order getOrder(Long id) {
if (not found) return null; // ❌ 이것은 폭탄 돌리기다.
}✅ 올바른 방법: Optional 또는 Exception
// GOOD 1
public Order getOrder(Long id) {
return repository.findById(id)
.orElseThrow(() -> new NotFoundException("주문 없음"));
}
// GOOD 2 (Service)
public Optional<Order> findOrder(Long id) {
return repository.findById(id);
}
// (Controller 사용 예시)
@GetMapping("/{id}")
public ResponseEntity<OrderResponse> get(@PathVariable Long id) {
return orderService.findOrder(id)
.map(OrderResponse::from)
.map(ResponseEntity::ok)
.orElseThrow(() -> new NotFoundException("주문 없음"));
}8. 자주 묻는 질문 (FAQ)
Q1. Service는 배치나 다른 곳에서도 쓰이는데, @Valid가 없어도 되나요? (Spring 표준 관점)
A. 네, 여전히 Service에는 @Valid를 두지 않는 것이 좋습니다.
"Controller"라고 표현했지만, 정확한 의미는 **"시스템의 입구(Entry Point)"**입니다.
- Web 요청: Controller가 입구 → Controller가 검증
- Batch 작업: ItemReader/JobParameter가 입구 → Batch 단계에서 검증
- Event 수신: Event Listener가 입구 → Listener에서 검증
기술적 관점:
Spring은 @Validated와 JSR-303(Bean Validation)을 통해 Service 계층에서의 파라미터 유효성 검사를 공식 지원합니다. 기술적으로 불가능하거나 금지된 패턴은 아닙니다.
아키텍처/Best Practice 관점:
하지만 **Layered Architecture(계층형 아키텍처)**의 일반적인 Best Practice는 **"관심사의 분리(Separation of Concerns)"**입니다.
- Controller: 올바른 형태(Shape)의 데이터인가? (입력 검증)
- Service: 올바른 상태(State)의 요청인가? (비즈니스 검증)
결론: Spring 기능으로 불가능한 것은 아니지만, **"입구에서 막고 내부는 신뢰한다"**는 아키텍처 표준을 따르는 것이 코드 품질 유지 유리합니다.
Q2. Controller의 Request DTO를 Service 파라미터로 그대로 써도 되나요?
A. Spring Boot 실무(Pragmatic) 관점에서는 '허용'합니다.
원칙적으로는 Controller의 Request DTO와 Service의 Command DTO를 분리하는 것이 이상적입니다. (의존성 분리)
하지만 Spring Boot 4 기반의 빠른 개발 환경에서는 과도한 DTO 변환이 되려 복잡도를 높일 수 있습니다.
✅ 허용 기준
- 단순한 CRUD 로직일 경우
- Request DTO의 필드가 Service 로직과 1:1로 매핑 될 경우
❌ 분리해야 하는 경우 (Command Pattern 사용)
- Controller 요청과 Service 로직의 파라미터가 구조적으로 다를 때
- Web 전용 필드(CAPTCHA, View 전용 플래그 등)가 포함되어 있을 때
- Service가 여러 Controller(Web, App, Admin)에서 재사용 될 때
결론: 복잡해지기 전까지는 편하게 사용하세요. 복잡해지는 시점에 분리해도 늦지 않습니다.