Backend

비즈니스 로직이란? 구분하는 법과 분리하는 이유


백엔드 코드를 이야기하다 보면 “비즈니스 로직은 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 또는 엔티티)에 모아 둔다.
  • 분리해 두면 규칙 변경이 한 곳에서 끝나고, 서버 없이 규칙만 테스트할 수 있다.

참고