TransactionManager ArchUnit 가이드
목적: TransactionManager의 구조 규칙을 ArchUnit으로 자동 검증 (Zero-Tolerance)
📌 테스트 위치: application/src/test/java/.../architecture/manager/TransactionManagerArchTest.java
1) 핵심 원칙
TransactionManager = 트랜잭션 경계 제공자
| 원칙 |
설명 |
| Class 필수 |
TransactionManager는 클래스로 선언 (구현체) |
| persist() 메서드만 |
순수 위임만, 비즈니스 로직 금지 |
| @Component 필수 |
Spring Bean으로 등록 |
| @Transactional 필수 |
트랜잭션 경계 정의 |
| 단일 Out Port |
PersistencePort, QueryPort, LockQueryPort 중 하나만 의존 |
| Lombok 금지 |
Plain Java로 작성 |
2) 테스트 구조
TransactionManagerArchTest.java (17개 테스트, 6개 그룹)
├── @Nested BasicStructureRules (4개) - 기본 구조 규칙
├── @Nested AnnotationRules (2개) - 어노테이션 규칙
├── @Nested DependencyRules (2개) - 의존성 규칙
├── @Nested MethodRules (2개) - 메서드 규칙
├── @Nested ProhibitionRules (6개) - 금지 규칙
└── @Nested ConstructorRules (1개) - 생성자 규칙
3) 검증 규칙 (17개)
기본 구조 규칙 (4개) - 필수
| 규칙 |
설명 |
위반 시 |
*TransactionManager 접미사 |
TransactionManager 접미사 필수 |
빌드 실패 |
..manager.. 패키지 |
manager 패키지에 위치 |
빌드 실패 |
| Class 필수 |
Interface가 아닌 Class여야 함 |
빌드 실패 |
| Public 필수 |
외부에서 접근 가능해야 함 |
빌드 실패 |
어노테이션 규칙 (2개)
| 규칙 |
설명 |
위반 시 |
| @Component 필수 |
Spring Bean 등록 |
빌드 실패 |
| @Transactional 필수 |
트랜잭션 경계 정의 |
빌드 실패 |
의존성 규칙 (2개)
| 규칙 |
설명 |
위반 시 |
| Out Port만 의존 |
PersistencePort, QueryPort, LockQueryPort만 허용 |
빌드 실패 |
| 단일 Out Port |
하나의 Port만 의존 (여러 개 금지) |
빌드 실패 |
메서드 규칙 (2개)
| 규칙 |
설명 |
위반 시 |
| persist() 메서드만 |
persist() 외 다른 public 메서드 금지 |
빌드 실패 |
| persist() 필수 |
최소 하나의 persist() 메서드 필수 |
빌드 실패 |
금지 규칙 (6개)
| 규칙 |
설명 |
위반 시 |
| Lombok 금지 |
Lombok 어노테이션 사용 금지 |
빌드 실패 |
| 필드 주입 금지 |
@Autowired 필드 주입 금지 |
빌드 실패 |
| TransactionManager 의존 금지 |
다른 TransactionManager 의존 금지 |
빌드 실패 |
| UseCase 의존 금지 |
UseCase 의존 금지 |
빌드 실패 |
| Service 의존 금지 |
Service 의존 금지 |
빌드 실패 |
| Facade 의존 금지 |
Facade 의존 금지 |
빌드 실패 |
생성자 규칙 (1개)
| 규칙 |
설명 |
위반 시 |
| 생성자 주입 필수 |
Out Port를 생성자로 주입 |
빌드 실패 |
4) 클래스 존재 여부 체크
테스트는 해당 클래스가 존재하지 않으면 자동 스킵(SKIPPED)됩니다:
assumeTrue() 패턴: 클래스가 없으면 테스트 리포트에서 “스킵됨”으로 명확히 표시
@BeforeAll
static void setUp() {
transactionManagerClasses = classes.stream()
.filter(javaClass -> javaClass.getSimpleName().endsWith("TransactionManager"))
.filter(javaClass -> javaClass.getPackageName().contains(".manager"))
.filter(javaClass -> !javaClass.isInterface())
.collect(Collectors.toList());
hasTransactionManagerClasses = !transactionManagerClasses.isEmpty();
}
@Test
void transactionManager_MustHavePersistMethod() {
assumeTrue(hasTransactionManagerClasses, "TransactionManager 클래스가 없어 테스트를 스킵합니다");
// ... 실제 테스트 로직
}
5) persist() 메서드만 허용하는 이유
왜 persist()만?
| 관점 |
설명 |
| 순수 위임 |
TransactionManager는 Out Port 호출만 담당 |
| 비즈니스 로직 금지 |
비즈니스 로직은 UseCase에서 처리 |
| 네이밍 일관성 |
Out Port의 persist()와 동일한 네이밍 |
| Soft Delete 지원 |
delete 대신 상태 변경 후 persist() |
비즈니스 로직은 어디에?
// ❌ Bad - TransactionManager에서 비즈니스 로직
@Component
@Transactional
public class OrderTransactionManager {
public Order markAsSent(Order order, DeliveryEvent event) {
order.startDelivery(event); // 비즈니스 로직 금지!
return orderPersistencePort.persist(order);
}
}
// ✅ Good - UseCase에서 비즈니스 로직, TransactionManager는 persist만
@Service
public class SendOrderService implements SendOrderUseCase {
public OrderResponse execute(SendOrderCommand command) {
Order order = orderQueryPort.findById(command.orderId());
DeliveryEvent event = createDeliveryEvent(command);
order.startDelivery(event); // 비즈니스 로직은 UseCase에서
Order savedOrder = orderTransactionManager.persist(order); // 순수 위임
externalDeliveryApi.send(event); // 외부 API (트랜잭션 외부)
return assembler.toResponse(savedOrder);
}
}
6) 테스트 실행
# TransactionManager ArchUnit 테스트 실행
./gradlew :application:test --tests "*TransactionManagerArchTest"
# 특정 규칙 그룹만 실행
./gradlew :application:test --tests "*TransactionManagerArchTest\$BasicStructureRules"
./gradlew :application:test --tests "*TransactionManagerArchTest\$AnnotationRules"
./gradlew :application:test --tests "*TransactionManagerArchTest\$DependencyRules"
./gradlew :application:test --tests "*TransactionManagerArchTest\$MethodRules"
./gradlew :application:test --tests "*TransactionManagerArchTest\$ProhibitionRules"
# 전체 ArchUnit 테스트
./gradlew :application:test --tests "*ArchTest"
7) 위반 예시 및 해결
위반 1: persist() 외 다른 메서드
// ❌ Bad - save(), markAsSent() 등 다른 메서드
@Component
@Transactional
public class OrderTransactionManager {
public Order save(Order order) { ... } // 금지!
public Order markAsSent(Order order) { ... } // 금지!
}
// ✅ Good - persist()만
@Component
@Transactional
public class OrderTransactionManager {
private final OrderPersistencePort orderPersistencePort;
public OrderTransactionManager(OrderPersistencePort orderPersistencePort) {
this.orderPersistencePort = orderPersistencePort;
}
public Order persist(Order order) {
return orderPersistencePort.persist(order);
}
}
위반 2: 여러 Out Port 의존
// ❌ Bad - 여러 Port 의존
@Component
@Transactional
public class OrderTransactionManager {
private final OrderPersistencePort orderPersistencePort;
private final InventoryPersistencePort inventoryPort; // 금지!
}
// ✅ Good - 단일 Port, 여러 Port는 Facade로
@Component
public class OrderFacade {
private final OrderTransactionManager orderTxManager;
private final InventoryTransactionManager inventoryTxManager;
// Facade에서 조합
}
위반 3: @Transactional 누락
// ❌ Bad - @Transactional 누락
@Component
public class OrderTransactionManager {
public Order persist(Order order) { ... }
}
// ✅ Good - @Transactional 필수
@Component
@Transactional
public class OrderTransactionManager {
public Order persist(Order order) { ... }
}
위반 4: Lombok 사용
// ❌ Bad - Lombok 사용
@Component
@Transactional
@RequiredArgsConstructor
public class OrderTransactionManager {
private final OrderPersistencePort orderPersistencePort;
}
// ✅ Good - Plain Java
@Component
@Transactional
public class OrderTransactionManager {
private final OrderPersistencePort orderPersistencePort;
public OrderTransactionManager(OrderPersistencePort orderPersistencePort) {
this.orderPersistencePort = orderPersistencePort;
}
}
위반 5: 다른 컴포넌트 의존
// ❌ Bad - Service, UseCase, Facade 의존
@Component
@Transactional
public class OrderTransactionManager {
private final OrderService orderService; // 금지!
private final PlaceOrderUseCase useCase; // 금지!
private final OrderFacade facade; // 금지!
}
// ✅ Good - Out Port만 의존
@Component
@Transactional
public class OrderTransactionManager {
private final OrderPersistencePort orderPersistencePort;
}
8) 허용 Out Port 목록
TransactionManager가 의존할 수 있는 Out Port:
| Port 타입 |
설명 |
예시 |
| PersistencePort |
CUD 작업 (영속화) |
OrderPersistencePort |
| QueryPort |
Read 작업 (조회) |
OrderQueryPort |
| LockQueryPort |
비관적 락 조회 |
OrderLockQueryPort |
// PersistencePort - 영속화 담당
@Component
@Transactional
public class OrderTransactionManager {
private final OrderPersistencePort persistencePort;
public Order persist(Order order) {
return persistencePort.persist(order);
}
}
// QueryPort - 조회 담당 (읽기 전용 트랜잭션)
@Component
@Transactional(readOnly = true)
public class OrderQueryTransactionManager {
private final OrderQueryPort queryPort;
public Order persist(Long orderId) {
return queryPort.findById(orderId);
}
}
// LockQueryPort - 비관적 락 조회
@Component
@Transactional
public class OrderLockTransactionManager {
private final OrderLockQueryPort lockQueryPort;
public Order persist(Long orderId) {
return lockQueryPort.findByIdWithLock(orderId);
}
}
9) 관련 문서
작성자: Development Team
최종 수정일: 2025-12-04
버전: 1.0.0 (persist() Only 패턴)