내일배움캠프

[내일배움캠프] Controller와 Service를 분리해야 하는 이유

munsik22 2026. 4. 20. 17:58

Controller와 Service를 분리해야 하는 이유

아래와 같은 컨트롤러 코드가 있다고 가정해 보자.

@RestController
@RequestMapping("/api/orders")
@RequiredArgsConstructor
public class OrderController {

    private final OrderRepository orderRepository;
    private final ProductRepository productRepository;

    @PostMapping
    @Transactional
    public ResponseEntity<OrderResponse> createOrder(@Valid @RequestBody OrderRequest request) {
        Product product = productRepository.findById(request.getProductId())
                .orElseThrow(() -> new IllegalArgumentException(
                        "해당 상품이 존재하지 않습니다. id=" + request.getProductId()));

        Order order = new Order(product);
        Order saved = orderRepository.save(order);
        OrderResponse response = new OrderResponse(saved);
        return ResponseEntity.created(URI.create("/api/orders/" + response.getOrderId())).body(response);
    }

    @GetMapping("/{id}")
    @Transactional(readOnly = true)
    public ResponseEntity<OrderResponse> getOrder(@PathVariable Long id) {
        Order order = orderRepository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException(
                        "해당 주문이 존재하지 않습니다. id=" + id));
        return ResponseEntity.ok(new OrderResponse(order));
    }
}

컨트롤러에 비즈니스 로직을 전부 몰아 넣는다면 다음과 같은 문제가 발생한다.

  1. 수정이 어렵다
  2. 재사용이 어렵다
  3. 테스트가 어렵다
  4. 요구사항이 바뀔 때 영향 범위가 너무 넓다

결론적으로 코드가 길어지면 매우 불편하다. 이 불편함을 해결하기 위해 Controller와 Service의 역할을 분리해야 한다.

  • Controller는 요청을 받는 창구 역할만 해야 한다.
    • 요청을 받는다
    • 필요한 값을 꺼낸다
    • Service에게 일을 맡긴다
    • 결과를 응답으로 돌려준다
  • Service는 무엇을 해야 할지 결정하는 곳으로, 비즈니스 로직을 다뤄야 한다.

위 코드는 아래와 같이 분리할 수 있다.

  • OrderController: @RequestBody로 요청을 받고 결과가 성공인지 실패인지 응답하는 창구 역할만 수행
@RestController
@RequestMapping("/api/orders")
public class OrderController {

    private final OrderService orderService;

    public OrderController(OrderService orderService) {
        this.orderService = orderService;
    }

    @PostMapping
    public ResponseEntity<OrderResponse> createOrder(@Valid @RequestBody OrderRequest request) {
        OrderResponse response = orderService.createOrder(request);
        return ResponseEntity.created(URI.create("/api/orders/" + response.getOrderId())).body(response);
    }

    @GetMapping("/{id}")
    public ResponseEntity<OrderResponse> getOrder(@PathVariable Long id) {
        OrderResponse response = orderService.getOrder(id);
        return ResponseEntity.ok(response);
    }
}
  • OrderService: 실제 비즈니스 로직을 모두 가져감
@Service
@Transactional
public class OrderService {

    private final OrderRepository orderRepository;
    private final ProductRepository productRepository;

    public OrderService(OrderRepository orderRepository, ProductRepository productRepository) {
        this.orderRepository = orderRepository;
        this.productRepository = productRepository;
    }

    public OrderResponse createOrder(OrderRequest request) {
        Product product = productRepository.findById(request.getProductId())
                .orElseThrow(() -> new IllegalArgumentException("해당 상품이 존재하지 않습니다. id=" + request.getProductId()));

        Order order = new Order(product);
        Order saved = orderRepository.save(order);

        return new OrderResponse(saved);
    }

    @Transactional(readOnly = true)
    public OrderResponse getOrder(Long id) {
        Order order = orderRepository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("해당 상품이 존재하지 않습니다. id=" + id));

        return new OrderResponse(order);
    }

}
  • 트랜잭션의 단위
    트랜잭션이란 여러 개의 작업을 하나로 묶어서 전부 성공하면 확정, 하나라도 실패하면 롤백하는 단위로, "어디까지를 한 번에 성공하거나 실패해야 하는 작업 묶음으로 볼 것인가"를 다룬다. DB의 여러 테이블을 건드리는 작업은 하나의 단위로 묶여야 하고, 이 작업 단위를 관리하기 가장 적합한 위치가 Service 레이어이기 때문에 @Transactional 어노테이션을 Controller가 아니라 Service 레이어에 붙인다.
  • 코드의 재사용성
    만약 웹 API가 아니라 예약된 배치 작업이나 다른 내부 로직에서 주문 기능을 쓰고 싶다면, 로직이 Controller에 있을 때는 외부 호출 없이 재사용하기 힘들지만, Service에 있다면 다른 클래스에서도 주입을 받아 간단하게 호출할 수 있다.