
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));
}
}컨트롤러에 비즈니스 로직을 전부 몰아 넣는다면 다음과 같은 문제가 발생한다.
- 수정이 어렵다
- 재사용이 어렵다
- 테스트가 어렵다
- 요구사항이 바뀔 때 영향 범위가 너무 넓다
결론적으로 코드가 길어지면 매우 불편하다. 이 불편함을 해결하기 위해 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에 있다면 다른 클래스에서도 주입을 받아 간단하게 호출할 수 있다.
'내일배움캠프' 카테고리의 다른 글
| [내일배움캠프] CI/CD와 AWS ECS (0) | 2026.05.05 |
|---|---|
| [내일배움캠프] Docker와 Docker Compose (0) | 2026.05.04 |
| [내일배움캠프] Spring MVC 기초 정리 (0) | 2026.04.17 |
| [내일배움캠프] 개발 프로세스 가이드 (0) | 2026.04.16 |
| [내일배움캠프] MSA 마무리 (0) | 2026.04.15 |