백엔드 코드를 이야기하다 보면 “비즈니스 로직은 Service에 둬야 한다”, “Controller에 비즈니스 로직이 섞여 있다” 같은 말을 자주 듣는다. 비즈니스 로직이 정확히 무엇이고, 왜 따로 다루는지 정리한다.
비즈니스 로직이란
비즈니스 로직(Business Logic)은 서비스가 어떤 규칙으로 동작해야 하는지를 코드로 옮긴 부분이다. 도메인 로직(Domain Logic)이라고도 부른다.
여기서 “비즈니스”는 돈을 버는 일만 뜻하지 않는다. 그 소프트웨어가 해결하려는 업무 영역의 규칙 전체를 말한다. 쇼핑몰이라면 주문·결제·배송 규칙, 병원 예약 시스템이라면 진료 시간과 예약 취소 규칙이 비즈니스 로직이다.
예시로 구분하기
쇼핑몰에서 “주문하기” 버튼을 눌렀을 때 실행되는 코드를 나눠 보면 다음과 같다.
| 코드 | 비즈니스 로직 | 이유 |
|---|---|---|
| 재고가 없으면 주문할 수 없다 | O | 쇼핑몰의 규칙 |
| VIP 회원은 10% 할인 | O | 회사가 정한 정책 |
| 5만 원 이상 구매 시 배송비 무료 | O | 회사가 정한 정책 |
| 주문 후 30분이 지나면 취소할 수 없다 | O | 서비스 운영 규칙 |
| HTTP 요청에서 상품 ID 꺼내기 | X | 웹 통신 처리 |
| 주문 결과를 HTML이나 JSON으로 만들기 | X | 화면·응답 처리 |
| DB에 INSERT 쿼리 실행하기 | X | 저장 방법 |
| 로그 남기기, 트랜잭션 처리 | X | 기술적인 공통 처리 |
구분하는 질문
헷갈릴 때는 다음 질문을 해 보면 된다.
이 규칙을 바꾸자고 요청하는 사람이 기획자·운영자인가, 개발자인가?
- “VIP 할인율을 15%로 올려 주세요” → 기획자가 요청한다 → 비즈니스 로직
- “응답 형식을 JSON에서 XML로 바꿔 주세요” → 기술적인 결정이다 → 비즈니스 로직 아님
다르게 말하면, 개발자가 아닌 사람에게 말로 설명할 수 있는 규칙이 비즈니스 로직이다. “재고가 없으면 주문이 안 돼요”는 누구나 이해하지만, “트랜잭션을 커밋하기 전에 flush해요”는 개발자만 이해한다.
왜 따로 분리하나
비즈니스 로직은 화면이나 기술이 바뀌어도 그대로여야 하는 코드이기 때문이다.
- 웹에서 주문하든, 모바일 앱 API로 주문하든, 관리자가 대신 주문하든 “재고가 없으면 주문 불가” 규칙은 같아야 한다.
- DB를 MySQL에서 PostgreSQL로 바꿔도 VIP 할인율은 변하지 않는다.
반대로 비즈니스 로직이 여러 곳에 흩어져 있으면 문제가 생긴다.
// 웹 주문 Controller
if (member.isVip()) price = (int) (price * 0.9);
// 앱 주문 API Controller
if (member.isVip()) price = (int) (price * 0.9);
// 관리자 대리 주문 Controller
if (member.getGrade() == Grade.VIP) price = price * 9 / 10; // 조금 다르게 작성됨
할인율이 15%로 바뀌면 세 곳을 모두 찾아 고쳐야 한다. 하나라도 빠뜨리면 경로에 따라 가격이 달라지는 버그가 생기고, 세 번째처럼 미묘하게 다르게 작성된 코드는 검색으로 찾기도 어렵다.
어디에 두나
비즈니스 로직은 웹·DB 같은 기술 코드와 섞이지 않는 곳에 모아 둔다. Spring 프로젝트에서는 보통 Service 계층이다.
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
@Transactional
public Order placeOrder(Member member, Product product, int quantity) {
// 재고 확인
if (product.getStock() < quantity) {
throw new IllegalStateException("재고가 부족합니다.");
}
// VIP 할인
int price = product.getPrice() * quantity;
if (member.isVip()) {
price = (int) (price * 0.9);
}
// 5만 원 이상 무료 배송
int shippingFee = price >= 50_000 ? 0 : 3_000;
product.decreaseStock(quantity);
return orderRepository.save(new Order(member, product, quantity, price + shippingFee));
}
}
이 메서드에는 HTTP 요청, HTML, SQL이 전혀 나오지 않는다. 규칙만 있다. Controller는 요청에서 값을 꺼내 이 메서드를 호출하기만 한다.
@PostMapping("/orders")
public OrderResponse order(@RequestBody OrderRequest request) {
Member member = memberService.get(request.getMemberId());
Product product = productService.get(request.getProductId());
Order order = orderService.placeOrder(member, product, request.getQuantity());
return OrderResponse.from(order);
}
MVC 패턴에서 비즈니스 로직은 Model 계층에 속한다. MVC 구조 전체는 MVC 패턴 개요 글에 정리했다.
엔티티에 두는 방법도 있다
규칙이 특정 데이터 하나에만 관련된다면 엔티티 안에 두기도 한다.
@Entity
public class Product {
private int stock;
public void decreaseStock(int quantity) {
if (stock < quantity) {
throw new IllegalStateException("재고가 부족합니다.");
}
this.stock -= quantity;
}
}
이렇게 하면 재고를 줄이는 모든 코드가 자동으로 재고 확인 규칙을 거친다. Service는 여러 엔티티를 조합하는 흐름(주문 생성, 결제 연동 등)을 맡고, 데이터 하나에 대한 규칙은 엔티티가 맡는 식으로 나눈다.
| 방식 | 비즈니스 로직 위치 | 특징 |
|---|---|---|
| 트랜잭션 스크립트 | Service에 절차대로 작성 | 단순하고 읽기 쉬움. 규칙이 많아지면 Service가 비대해짐 |
| 도메인 모델 | 엔티티(도메인 객체)에 분산 | 규칙이 데이터 옆에 있어 재사용·일관성이 좋음. 설계가 더 필요함 |
처음에는 Service에 두고, 같은 규칙이 여러 Service에서 반복되기 시작하면 엔티티로 옮기는 방식으로 시작해도 충분하다.
분리했을 때 얻는 것: 테스트
비즈니스 로직이 웹·DB 코드와 분리되어 있으면, 서버를 띄우지 않고 규칙만 따로 테스트할 수 있다.
@Test
void VIP_회원은_10퍼센트_할인된다() {
Member vip = new Member("kim", true);
Product product = new Product("키보드", 30_000, 10);
Order order = orderService.placeOrder(vip, product, 2);
// 60,000원 × 0.9 = 54,000원, 5만 원 이상이라 배송비 0원
assertThat(order.getTotalPrice()).isEqualTo(54_000);
}
@Test
void 재고가_부족하면_주문할_수_없다() {
Member member = new Member("lee", false);
Product product = new Product("마우스", 20_000, 1);
assertThatThrownBy(() -> orderService.placeOrder(member, product, 2))
.isInstanceOf(IllegalStateException.class);
}
테스트 이름이 곧 비즈니스 규칙 목록이 된다. 규칙이 바뀌면 어떤 테스트가 깨지는지로 영향 범위를 바로 알 수 있다.
orderService는OrderRepository를 가짜 객체(Mock)로 바꿔 생성하면 DB 없이 테스트할 수 있다. 이때save()가 받은 주문을 그대로 돌려주도록 설정해 둔다.OrderRepository orderRepository = mock(OrderRepository.class); given(orderRepository.save(any())).willAnswer(inv -> inv.getArgument(0)); OrderService orderService = new OrderService(orderRepository);
정리
- 비즈니스 로직은 서비스가 따라야 하는 규칙을 코드로 옮긴 것이다.
- “기획자·운영자가 바꾸자고 할 규칙인가?“로 구분할 수 있다.
- 화면이나 기술이 바뀌어도 그대로여야 하므로 한 곳(Service 또는 엔티티)에 모아 둔다.
- 분리해 두면 규칙 변경이 한 곳에서 끝나고, 서버 없이 규칙만 테스트할 수 있다.