최근 몇 년간 백엔드 생태계에서는 도메인 주도 설계(DDD)객체지향이 최고의 미덕처럼 여겨지고 있다. 

https://hsunnystory.tistory.com/255

 

[Skill] DDD(도메인 주도 설계)란

실무를 하다 보면, 수천 줄이 넘어가는 거대한 Service 클래스를 마주하게 되는 날이 온다. 비즈니스 로직은 모두 서비스 계층에 몰려있고, 정작 데이터의 주체여야 할 Entity 클래스들은 그저 getter

hsunnystory.tistory.com

 

트랜잭션 스크립트 패턴(Transaction Script Pattern)은 무조건 피해야 할 레거시(Legacy)이자 안티 패턴일까? 이번 포스팅에서는 트랜잭션 스크립트 패턴에 대해 알아본다.

 

1. 트랜잭션 스크립트 패턴(Transaction Script Pattern) 이란?

트랜잭션 스크립트는 사용자의 요청(Request) 하나당 하나의 비즈니스 트랜잭션(스크립트)을 매핑하여 처리하는 방식이다.

동작방식

컨트롤러에서 요청이 들어오면, Service 계층의 메서드 하나가 시작부터 끝까지 모든 흐름을 제어한다. DB에서 데이터를 읽어오고, 계산하고, 상태를 변경한 뒤 다시 DB에 저장하는 일련의 절차(Procedure)를 순서대로 작성한다.

특징

객체 지향이라기보다는 절차 지향(Procedural) 프로그래밍에 가깝다. 데이터베이스의 테이블과 1:1로 매핑되는 단순한 데이터 구조(DTO 또는 빈약한 Entity)를 다룬다.

 

2. 왜 실무에서 가장 많이 쓰일까?

트랜잭션 스크립트의 가장 큰 장점은 압도적인 직관성빠른 개발 속도다. 복잡한 도메인 지식이 없는 단순한 CRUD 중심의 애플리케이션에서는 이보다 좋은 패턴이 없다.

호텔 객실을 예약 하는 단순한 API

Good Code

@Service
@RequiredArgsConstructor
public class HotelBookingService {

    private final RoomRepository roomRepository;
    private final BookingRepository bookingRepository;

    // [Good Code] 단순한 요구사항에서는 가장 직관적이고 효율적이다.
    @Transactional
    public void bookRoom(Long roomId, Long userId) {
        // 1. 데이터 조회
        Room room = roomRepository.findById(roomId).orElseThrow();

        // 2. 비즈니스 로직 (검증 및 계산)
        if (!room.isAvailable()) {
            throw new IllegalStateException("이미 예약된 객실입니다.");
        }
        int price = room.getBasePrice();

        // 3. 데이터 저장 (상태 변경)
        room.setAvailable(false); // 엔티티는 그저 데이터를 담는 역할만 수행
        Booking booking = new Booking(roomId, userId, price);
        
        bookingRepository.save(booking);
        roomRepository.save(room);
    }
}

 

장점 : 코드를 위에서 아래로 읽어 내려가기만 하면 비즈니스 흐름이 완벽하게 이해된다. 객체 간의 복잡한 연관관계나 메시지 전달을 고민할 필요가 없어 학습 곡선이 매우 낮다.

 

3. 지옥이 되는 경우

이 패턴의 치명적인 단점은 요구사항이 복잡해질수로 서비스 계층이 통제 불능의 God Class로 비대해진다는 점이다.

서비스가 성공하여 예약 로직에 "VIP 회원 할인", "주말 할증", "연속 숙박 시 쿠폰 적용" 같은 복잡한 도메인 규칙이 추가된다고 가정해보자.

 

Bad Code

@Service
public class HotelBookingService {

    @Transactional
    public void bookRoom(Long roomId, Long userId, String couponCode, boolean isWeekend) {
        Room room = roomRepository.findById(roomId).orElseThrow();
        User user = userRepository.findById(userId).orElseThrow();

        if (!room.isAvailable()) { ... }

        // [Bad Code] 요구사항이 늘어날 때마다 if-else 분기가 끊임없이 추가된다.
        int price = room.getBasePrice();
        
        if (isWeekend) {
            price += 50000; // 주말 할증
        }
        
        if (user.getGrade() == Grade.VIP) {
            price = (int) (price * 0.8); // VIP 할인
        }
        
        if (couponCode != null) {
            Coupon coupon = couponRepository.findByCode(couponCode);
            if (coupon.isValid()) {
                price -= coupon.getDiscountAmount(); // 쿠폰 적용
                coupon.setUsed(true);
            }
        }
        
        // ... 코드가 수백 줄로 길어지며 유지보수가 불가능해진다.
        room.setAvailable(false);
        bookingRepository.save(new Booking(roomId, userId, price));
    }
}

 

문제점 1 (응집도 저하) : 가격을 계산하는 중요한 비즈니스 규칙이 Service 계층에 하드코딩 되어 있다. 만약 결제 서비스(PaymentService) 에서도 가격 계산 로직이 필요하다면 똑같은 코드를 복사 붙여넣기 해야 한다.

 

문제점 2 (테스트의 어려움) : 이 로직 하나를 테스트하기 위해 DB를 연결하거나 수많은 Repository를 Mocking 해야 한다.

 

결론

트랜잭션 스크립트를 선택하는 경우

 - 비즈니스 로직이 단순한 CRUD 수준일 때

 - 빠른 시장 진입(MVP 개발)이 최우선 목표일 때

 - 개발팀의 객체 지향 및 DDD 숙련도가 낮을 때

도메인 모델(DDD)을 선택하는 경우

 - 비즈니스 규칙이 복잡하고, 상태 변화가 다양한 도메인일 때

 - 동일한 로직이 여러 서비스에서 중복해서 사용되기 시작할 때

 

개발자라면 명목적으로 최신 트렌드를 쫓기보다는, 현재 우리 시스템의 복잡도가 어느 수준인지 파악하고 그에 맞는 적절한 패턴을 취사선택하는 안목을 길러야 한다.

 

 

 

 

+ Recent posts