업데이트:

저는 처음으로 SpringMVC를 배우면서 “Service 계층에서 비즈니스 로직을 처리해야 한다.”라고 배웠습니다.

오늘은 과연 위 문장이 맞는지? 틀리다면 어떤 것이 맞는 것인지 알아보도록 하겠습니다.

Spring 웹 계층


먼저 Service 계층을 알아보기 전에 Spring의 일반적인 웹 계층 구조를 보도록 하겠습니다.

servicelayer

Web Layer

보통 ControllerView 페이지 영역을 말합니다.

외부와의 요청, 응답을 담당하는 전반적인 영역 입니다.

Service Layer

@Service  어노테이션이 적용되는 계층이며, 일반적으로 ControllerRepository 중간에 위치하여 Controller에서 데이터를 받아 로직을 처리하고 Repository 영역에 전달하는 역할을 맡고 있습니다.

Repository Layer

Database와 같은 데이터 저장소에 접근하는 계층입니다.

왜 Service가 비즈니스 로직을 담당하게 되었을까?

이건 제 추측입니다만 위와 같은 일반적인 Spring 웹 계층 구조에서 Web LayerRepository Layer는 담당 역할이 확실하기 때문에 남은 Service 계층이 비즈니스 로직을 맡을 수 밖에 없었을 것이라 생각합니다.

그리고 Spring 웹 계층Presentation(=Web), Business(=Service), Persistence(=Repository)로도 불리기 때문에 더더욱 Service 계층이 비즈니스 로직을 맡게 되지 않았을까 싶습니다.

비즈니스 로직의 위치


Service

먼저 Service에서 비즈니스로직을 처리한 예시를 보여드리도록 하겠습니다.

다음은 주문을 처리하는 서비스 클래스의 메소드 order()입니다.

@RequiredArgsConstructor
public class OrderService {

    private final StockRepository stockRepository;
    private final PaymentRepository paymentRepository;
    private final OrderRepository orderRepository;

    public void order(OrderRequest orderRequest) {
        
        // 재고 확인
        Stock stock = stockRepository.findById(orderRequest.getOrderItem().getId())
            .orElseThrow(() -> new NotFoundException());

        if (stock.getCount() < orderRequest.getOrderItem().getCount() ||
            stock.getCount() <= 0) {
            throw new StockException("재고가 부족합니다.");
        }

        // 재고 차감
        stock.setCount(stock.getCount() - orderRequest.get);
        stockRepository.save(stock);

        // 결제 정보 저장
        Payment payment = new Payment(orderRequest);
        paymentRepository.save(payment);

				// 주문 정보 저장
        orderRepository.save(orderRequest.toEntity());
    }
}

보통 Service에서 하나의 메소드는 한 가지 일만 하지 않습니다. 위의 예시처럼 “주문하기”이라는 메소드에는 재고, 결제 등 다양한 비즈니스가 엮여 있는 경우가 대부분입니다.

위의 예시는 아주 간단한 예시이지만 점점 더 요구사항이 많아지고 메소드가 커진다면 어떻게 될까요?

로직은 점점 더 복잡해지고 수정이 어려워지며, 점점 유지보수 및 확장이 어렵게 됩니다.

(위의 예시만 봐도 이미 메소드가 하나의 기능만을 담당하지 못하고 있습니다.)

💡또한, 수정이 계속 발생하는 것은 SOLID에서 OCP를 위반하는 것입니다.

위와 같이 메소드 작성 방식을 트랜잭션 스크립트 패턴이라고 합니다.

트랜잭션 스크립트 패턴?

우선 트랜잭션에 대해 생각해보면 원자성이란 특성이 있습니다.

All or Nothing이란 말처럼 모든 것이 반영되던지 모든 것이 반영 안되던지 하는 특성인데요.

이렇게 하나의 트랜잭션으로 구성된 로직을 하나의 메소드에 작성하는 패턴을 트랜잭션 스크립트 패턴이라 말합니다.

Domain

다음은 도메인 영역에서 비즈니스 로직을 처리하는 코드를 보여드리도록 하겠습니다.

💡도메인이란? 비즈니스 적으로 유사한 업무를 하는 집합체라고 생각하시면 됩니다.

Service

@RequiredArgsConstructor
public class OrderService {

    public void order(OrderRequest orderRequest) {
		
        // 재고 확인 및 차감
        Stock stock = Stock.getInstance(orderRequest.getOrderItem());
        stock.checkAndDecreaseStock(orderRequest.getOrderItem());
		
        // 결제
        Payment payment = Payment.getInstance(orderRequest.getOrder());
        payment.paymentOrder();
		
        // 주문 정보 저장
        Order order = Order.getInstance(orderRequest.getOrderId());
        order.update();
    }
}

Stock

@RequiredArgsConstructor
public class Stock {

    private final StockRepository stockRepository;
    private static Stock stock = null;

    public static Stock getInstance(OrderItem orderItem) {
        if (stock == null) {
            stock = stockRepository.findById(orderItem.getId())
                .orElseThrow(() -> new NotFoundException());
        }
        return stock;
    }

    public void checkAndDecreaseStock(OrderItem orderItem) {
        if (stock.getCount() < orderItem.getCount() ||
            stock.getCount() <= 0) {
            throw new StockException("재고가 부족합니다.");
        }
    }
}

Payment

@RequiredArgsConstructor
public class Payment {

    private final PaymentRepository paymentRepository;
    private static Payment payment = null;

    public static Payment getInstance(Order order) {
        if (payment == null) {
            payment = paymentRepository.findById(order.getId())
                .orElseThrow(() -> new NotFoundException());
        }
        return payment;
    }

    public void paymentOrder() {
				paymentRepository.save(payment.toEntity());
    }
}

Order

@RequiredArgsConstructor
public class Order {

    private final OrderRepository orderRepository;
    private static Order order = null;

    public static Stock getInstance(OrderRequest request) {
        if (order == null) {
            order = orderRepository.findById(request.getId())
                .orElseThrow(() -> new NotFoundException());
        }
        return order;
    }

    public void update() {
        orderRepository.save(order.toEntity());
    }
}

각각의 비즈니스를 담당하는 Stock, Payment, Order 도메인 별로 메소드를 호출하여 처리하도록 하였습니다.

이렇게 되면 책임도 확실해지고, Service에서 Repository에 대한 의존성도 없어지게 됩니다.

또한, 각 도메인의 메소드가 수정된다고 해도 Service는 수정할 일이 없어집니다.

즉, 유연하고 확장성이 높아지며 테스트도 용이해진 코드가 된 것입니다.

여기서 Service의 역할은 트랜잭션과 프로세스의 순서만 보장해주는 역할만 수행합니다.

마무리


이렇게 도메인에 비즈니스 로직을 작성하는 것은 책임의 분리, 메소드의 재사용성, 확장성, 유지보수성 등 다양한 장점이 있습니다.

기존에 Service 영역에 모든 비즈니스 로직을 작성하는 것보다 당연히 객체 지향 스럽게 코드를 작성할 수 있게되죠.

하지만 도메인 모델은 도메인의 관계를 정립하고 설계 및 구축하는데 많은 노력을 기울여야 하기 때문에 당연하게도 러닝커브가 존재합니다.

쉽고 빠르게 코드를 작성하는데는 트랜잭션 스크립트 패턴이 유용하겠지만 그럼에도 우리는 개발자이기 때문에(?) 조금이라도 나은 모델이 존재한다면 그 모델을 다룰 수 있도록 공부하고 성장해야 하지 않을까요? (물론 저부터 실천하겠습니다.)

오늘도 제 글을 봐주셔서 감사합니다. 😄

📌참고


[Spring] 비즈니스 로직은 Service에서 처리해야할까? (feat. Spring 웹 계층)

트랜잭션 스크립트 패턴 vs 도메인 모델 패턴

댓글남기기