# Spring Boot 면접: 트랜잭션 전파 설명 > Spring Boot 트랜잭션 전파 마스터하기: REQUIRED, REQUIRES_NEW, NESTED 등. 코드 예제와 일반적인 함정을 포함한 12가지 면접 질문. - Published: 2026-03-18 - Updated: 2026-05-01 - Author: SharpSkill - Tags: spring boot, transactions, propagation, java, interview - Reading time: 14 min --- 트랜잭션 전파는 Spring Boot의 근본적인 개념으로, 기술 면접에서 정기적으로 평가됩니다. `@Transactional` 어노테이션이 적용된 메서드 간에 트랜잭션이 어떻게 상호작용하는지 이해하면 운영 환경의 미묘한 버그를 피하고 견고한 아키텍처를 설계할 수 있습니다. > **준비 조언** > > 면접관들은 비즈니스 컨텍스트에 따라 적절한 전파 수준을 선택하는 능력을 평가합니다. 특정 사례에서 REQUIRED 대신 REQUIRES_NEW를 사용하는 이유를 설명할 수 있는 것이 차이를 만듭니다. ## 트랜잭션 전파의 기초 ### 질문 1: Spring에서 트랜잭션 전파란 무엇입니까? 전파는 기존 트랜잭션의 컨텍스트 내에서 호출될 때 트랜잭션 메서드의 동작을 정의합니다. 이는 다음 질문에 답합니다: "`@Transactional` 메서드가 어노테이션이 있는 다른 메서드를 호출할 때 어떤 일이 발생합니까?" ```java // OrderService.java // 전파 개념의 시연 @Service public class OrderService { private final PaymentService paymentService; private final OrderRepository orderRepository; // 부모 트랜잭션 - 새 트랜잭션 시작 @Transactional public void createOrder(OrderRequest request) { // 현재 트랜잭션에 주문 저장 Order order = orderRepository.save(new Order(request)); // 다른 @Transactional 메서드 호출 // 전파가 결정: 같은 트랜잭션 또는 새 트랜잭션? paymentService.processPayment(order.getId(), request.getAmount()); } } @Service public class PaymentService { // 기본 전파: REQUIRED // createOrder()의 기존 트랜잭션에 합류 @Transactional public void processPayment(Long orderId, BigDecimal amount) { // createOrder()와 같은 트랜잭션에서 실행 // 이 메서드가 실패하면 주문도 롤백 } } ``` Spring은 7가지 전파 수준을 제공하며, 각각 특정 비즈니스 요구사항에 적합합니다. 선택은 데이터 일관성과 성능에 직접적인 영향을 미칩니다. ### 질문 2: REQUIRED(기본 전파)의 동작을 설명하십시오 REQUIRED는 기본 전파입니다. 트랜잭션이 존재하면 메서드가 합류합니다. 그렇지 않으면 새 트랜잭션이 생성됩니다. 이는 가장 일반적이고 직관적인 동작입니다. ```java // UserService.java // REQUIRED: 기본 동작 @Service public class UserService { private final UserRepository userRepository; private final AuditService auditService; @Transactional(propagation = Propagation.REQUIRED) public void updateUser(Long userId, UserUpdateRequest request) { // 존재하지 않으면 트랜잭션 시작 User user = userRepository.findById(userId).orElseThrow(); user.setEmail(request.getEmail()); userRepository.save(user); // 감사가 같은 트랜잭션에 합류 auditService.logUpdate(userId, "EMAIL_CHANGED"); } } @Service public class AuditService { private final AuditLogRepository auditLogRepository; @Transactional(propagation = Propagation.REQUIRED) public void logUpdate(Long userId, String action) { // updateUser()의 트랜잭션에 합류 // 함께 커밋 또는 롤백 auditLogRepository.save(new AuditLog(userId, action, Instant.now())); } } ``` 다음 다이어그램은 트랜잭션 흐름을 보여줍니다: ```text updateUser()가 TX-1 시작 ├── save(user) → TX-1 └── logUpdate() → TX-1에 합류 (REQUIRED) └── save(audit) → TX-1 logUpdate() 실패 시 → TX-1 롤백 → user와 audit 모두 취소 ``` ### 질문 3: REQUIRED 대신 REQUIRES_NEW를 언제 사용합니까? REQUIRES_NEW는 기존 트랜잭션을 일시 중단하고 독립적인 새 트랜잭션을 생성합니다. 부모 트랜잭션의 결과와 관계없이 작업을 커밋해야 할 때 유용합니다. ```java // PaymentService.java // REQUIRES_NEW: 독립적인 트랜잭션 @Service public class PaymentService { private final PaymentRepository paymentRepository; private final PaymentAuditService auditService; @Transactional public void processPayment(Long orderId, BigDecimal amount) { Payment payment = new Payment(orderId, amount); paymentRepository.save(payment); // 결제가 나중에 실패해도 감사는 반드시 저장되어야 함 auditService.logPaymentAttempt(orderId, amount); // 감사 후 오류 시뮬레이션 if (amount.compareTo(BigDecimal.ZERO) < 0) { throw new InvalidAmountException("Negative amount"); } } } @Service public class PaymentAuditService { private final PaymentAuditRepository auditRepository; // REQUIRES_NEW: 부모 트랜잭션과 독립적으로 커밋 @Transactional(propagation = Propagation.REQUIRES_NEW) public void logPaymentAttempt(Long orderId, BigDecimal amount) { // 새 트랜잭션 TX-2 생성 // TX-1 (processPayment)는 일시 중단 auditRepository.save(new PaymentAudit(orderId, amount, "ATTEMPTED")); // TX-2는 여기서 커밋, TX-1과 독립적 } } ``` REQUIRES_NEW를 사용한 트랜잭션 흐름: ```text processPayment()가 TX-1 시작 ├── save(payment) → TX-1 ├── logPaymentAttempt() → TX-1 일시 중단 │ └── TX-2 시작 → 새 트랜잭션 │ └── save(audit) → TX-2 │ └── COMMIT TX-2 → audit 저장됨 │ └── TX-1 재개 └── throw InvalidAmountException └── ROLLBACK TX-1 → 결제 취소, 그러나 audit는 보존 ``` > **교착 상태 주의** > > 새 트랜잭션이 일시 중단된 트랜잭션에 의해 잠긴 동일한 리소스에 액세스하면 REQUIRES_NEW가 교착 상태를 일으킬 수 있습니다. 부모 트랜잭션과 동일한 테이블을 수정하기 위해 REQUIRES_NEW를 사용하지 마십시오. ### 질문 4: NESTED 전파를 설명하고 REQUIRES_NEW와 어떻게 다른지 설명하십시오 NESTED는 현재 트랜잭션 내에 세이브포인트를 만듭니다. 메서드가 실패하면 세이브포인트 이후의 변경 사항만 롤백되며, 부모 트랜잭션 전체가 롤백되지 않습니다. ```java // BatchProcessingService.java // NESTED: 부모 트랜잭션 내의 세이브포인트 @Service public class BatchProcessingService { private final ItemRepository itemRepository; private final ItemProcessor itemProcessor; @Transactional public BatchResult processBatch(List items) { BatchResult result = new BatchResult(); for (Item item : items) { try { // 각 항목은 세이브포인트로 처리 itemProcessor.processItem(item); result.addSuccess(item.getId()); } catch (ProcessingException e) { // 전체 배치가 아닌 이 항목만 롤백 result.addFailure(item.getId(), e.getMessage()); } } return result; // 성공한 항목 커밋 } } @Service public class ItemProcessor { private final ItemRepository itemRepository; // NESTED: 세이브포인트 생성, 부분 롤백 가능 @Transactional(propagation = Propagation.NESTED) public void processItem(Item item) { item.setStatus("PROCESSING"); itemRepository.save(item); // 비즈니스 검증 if (!isValid(item)) { throw new ProcessingException("Invalid item"); // 세이브포인트로 롤백 → 이 항목만 } item.setStatus("COMPLETED"); itemRepository.save(item); } } ``` NESTED와 REQUIRES_NEW 비교: ```text NESTED: ├── 부모 TX 내에서 세이브포인트 사용 ├── 실패 시 → 세이브포인트로 롤백 ├── 부모 TX가 롤백되면 → NESTED도 롤백 └── 더 나은 성능 (새 연결 없음) REQUIRES_NEW: ├── 완전히 독립적인 트랜잭션 생성 ├── 실패 시 → 자식 TX만 롤백 ├── 부모 TX가 롤백되면 → 자식 TX는 이미 커밋됨 └── 새 연결 필요 ``` > **NESTED 지원** > > NESTED 전파에는 JDBC 세이브포인트 지원이 필요합니다. 대부분의 최신 데이터베이스(PostgreSQL, MySQL, Oracle)는 이를 지원합니다. 사용 전 호환성을 확인하십시오. ## 고급 전파 유형 ### 질문 5: SUPPORTS와 NOT_SUPPORTED는 언제 사용합니까? SUPPORTS는 기존 트랜잭션이 있으면 그 안에서 실행하고, 없으면 트랜잭션 없이 실행합니다. NOT_SUPPORTED는 기존 트랜잭션을 일시 중단하고 트랜잭션 없이 실행합니다. ```java // ReportingService.java // SUPPORTS: 선택적 트랜잭션 @Service public class ReportingService { private final ReportRepository reportRepository; // SUPPORTS: 트랜잭션이 있거나 없거나 작동 // 트랜잭션 보장이 필요 없는 읽기에 유용 @Transactional(propagation = Propagation.SUPPORTS) public Report generateReport(Long reportId) { // @Transactional 메서드에서 호출 시 → 해당 TX 사용 // 직접 호출 시 → 트랜잭션 없음 (읽기 OK) return reportRepository.generateComplexReport(reportId); } } @Service public class ExternalNotificationService { private final ExternalApiClient apiClient; // NOT_SUPPORTED: 절대 트랜잭션 내부에 없음 // 느린 외부 호출 동안 TX 차단을 방지 @Transactional(propagation = Propagation.NOT_SUPPORTED) public void sendExternalNotification(String message) { // 호출 동안 부모 TX 일시 중단 apiClient.send(message); // 잠재적으로 느린 HTTP 호출 // 이후 부모 TX 재개 } } ``` ```java // OrderService.java // 결합 사용 예 @Service public class OrderService { private final OrderRepository orderRepository; private final ReportingService reportingService; private final ExternalNotificationService notificationService; @Transactional public void completeOrder(Long orderId) { Order order = orderRepository.findById(orderId).orElseThrow(); order.setStatus("COMPLETED"); orderRepository.save(order); // 같은 TX에서 보고서 생성 (SUPPORTS) Report report = reportingService.generateReport(orderId); // 트랜잭션 외부에서 외부 알림 (NOT_SUPPORTED) // 외부 API가 느린 경우 TX 시간 초과 방지 notificationService.sendExternalNotification( "Order " + orderId + " completed" ); } } ``` ### 질문 6: MANDATORY와 NEVER를 설명하십시오 MANDATORY는 기존 트랜잭션을 요구하고, 그렇지 않으면 예외를 던집니다. NEVER는 트랜잭션이 없어야 하며, 있으면 예외를 던집니다. ```java // AuditService.java // MANDATORY: 트랜잭션 내부에서 호출되어야 함 @Service public class AuditService { private final AuditRepository auditRepository; // MANDATORY: 기존 트랜잭션 없이는 실행 거부 // 감사가 감사된 작업과 항상 원자적임을 보장 @Transactional(propagation = Propagation.MANDATORY) public void logCriticalAction(String action, Long entityId) { // 트랜잭션 없이 호출 시 → IllegalTransactionStateException auditRepository.save(new AuditLog(action, entityId, Instant.now())); } } // CacheService.java // NEVER: 절대 트랜잭션 내부에 있어서는 안 됨 @Service public class CacheService { private final CacheManager cacheManager; // NEVER: 캐시는 트랜잭션에 참여하지 않아야 함 // 롤백 후 캐시와 DB 간의 불일치 방지 @Transactional(propagation = Propagation.NEVER) public void invalidateCache(String cacheKey) { // @Transactional에서 호출 시 → IllegalTransactionStateException cacheManager.getCache("entities").evict(cacheKey); } } ``` ```java // SecurityService.java // MANDATORY의 올바른 사용 @Service public class SecurityService { private final AuditService auditService; @Transactional public void changeUserPassword(Long userId, String newPassword) { // 민감한 작업... updatePassword(userId, newPassword); // 감사는 같은 트랜잭션에 있어야 함 // MANDATORY가 이 아키텍처 제약을 강제 auditService.logCriticalAction("PASSWORD_CHANGE", userId); } // 오류: 트랜잭션 없이 직접 호출 public void badUsage() { // TX가 없어 IllegalTransactionStateException 발생 auditService.logCriticalAction("TEST", 1L); } } ``` ## 일반적인 면접 함정 ### 질문 7: 왜 @Transactional이 내부 호출에서 작동하지 않습니까? 가장 흔한 함정 중 하나입니다. 내부 메서드 호출(self-invocation)은 Spring 프록시를 우회하여 트랜잭션 관리를 비활성화합니다. ```java // BrokenService.java // 고전적 오류: self-invocation @Service public class BrokenService { private final ItemRepository itemRepository; public void processItems(List itemIds) { for (Long id : itemIds) { // 함정: 내부 호출 → 프록시 우회 // processItem()의 @Transactional 무시됨 processItem(id); } } @Transactional public void processItem(Long itemId) { // 이 트랜잭션은 내부 호출에서 절대 생성되지 않음 Item item = itemRepository.findById(itemId).orElseThrow(); item.setStatus("PROCESSED"); itemRepository.save(item); } } ``` 이 함정을 피하기 위한 솔루션: ```java // 솔루션 1: 자기 주입 @Service public class FixedServiceWithSelfInjection { private final ItemRepository itemRepository; @Lazy @Autowired private FixedServiceWithSelfInjection self; // 자기 주입 public void processItems(List itemIds) { for (Long id : itemIds) { // 프록시를 통한 호출 → @Transactional 작동 self.processItem(id); } } @Transactional public void processItem(Long itemId) { // 트랜잭션이 올바르게 생성됨 Item item = itemRepository.findById(itemId).orElseThrow(); item.setStatus("PROCESSED"); itemRepository.save(item); } } // 솔루션 2: 두 서비스로 분리 (권장) @Service public class ItemOrchestrator { private final ItemProcessor processor; public void processItems(List itemIds) { for (Long id : itemIds) { // 다른 빈에 대한 호출 → 프록시 작동 processor.processItem(id); } } } @Service public class ItemProcessor { private final ItemRepository itemRepository; @Transactional public void processItem(Long itemId) { // 트랜잭션이 올바르게 관리됨 Item item = itemRepository.findById(itemId).orElseThrow(); item.setStatus("PROCESSED"); itemRepository.save(item); } } ``` ### 질문 8: 예외와 롤백을 어떻게 처리합니까? 기본적으로 Spring은 RuntimeException 및 Error에서만 롤백합니다. Checked 예외는 자동 롤백을 트리거하지 않습니다. ```java // TransactionRollbackDemo.java // 예외 유형에 따른 롤백 동작 @Service public class TransactionRollbackDemo { private final OrderRepository orderRepository; // RuntimeException → 자동 롤백 @Transactional public void methodWithRuntimeException() { orderRepository.save(new Order()); throw new RuntimeException("Error"); // ROLLBACK } // Checked Exception → 기본적으로 롤백 없음 @Transactional public void methodWithCheckedException() throws IOException { orderRepository.save(new Order()); throw new IOException("File error"); // 그래도 커밋! } // Checked 예외에서 롤백 강제 @Transactional(rollbackFor = IOException.class) public void methodWithRollbackFor() throws IOException { orderRepository.save(new Order()); throw new IOException("Error"); // rollbackFor 덕분에 ROLLBACK } // 롤백에서 RuntimeException 제외 @Transactional(noRollbackFor = BusinessException.class) public void methodWithNoRollbackFor() { orderRepository.save(new Order()); throw new BusinessException("Warning"); // 예외에도 커밋 } } ``` 비즈니스 사례를 위한 권장 구성: ```java // BaseTransactionalService.java // 일관된 트랜잭션 구성 @Service public abstract class BaseTransactionalService { // 모든 예외에서 롤백 (checked 및 unchecked) @Transactional(rollbackFor = Exception.class) protected void executeInTransaction(Runnable operation) { operation.run(); } } // PaymentService.java @Service public class PaymentService extends BaseTransactionalService { private final PaymentRepository paymentRepository; @Transactional( rollbackFor = Exception.class, noRollbackFor = InsufficientFundsException.class ) public PaymentResult processPayment(PaymentRequest request) { Payment payment = new Payment(request); paymentRepository.save(payment); if (request.getAmount().compareTo(getBalance()) > 0) { // 롤백하지 않음 - 시도 기록을 보존하고 싶음 throw new InsufficientFundsException("Insufficient balance"); } return new PaymentResult(payment.getId(), "SUCCESS"); } } ``` ### 질문 9: 트랜잭션 격리는 전파와 어떻게 작동합니까? 격리와 전파는 상호 보완적입니다. 격리는 동시 트랜잭션 간의 데이터 가시성을 결정합니다. ```java // IsolationDemo.java // 격리 + 전파 결합 @Service public class IsolationDemo { private final AccountRepository accountRepository; // READ_COMMITTED: 다른 트랜잭션이 커밋한 데이터 확인 @Transactional( isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED ) public BigDecimal getAccountBalance(Long accountId) { // 트랜잭션 중 다시 읽으면 다른 값을 볼 수 있음 return accountRepository.findById(accountId) .map(Account::getBalance) .orElse(BigDecimal.ZERO); } // REPEATABLE_READ: 트랜잭션 동안 같은 읽기 보장 @Transactional( isolation = Isolation.REPEATABLE_READ, propagation = Propagation.REQUIRED ) public void transferWithConsistentRead(Long fromId, Long toId, BigDecimal amount) { Account from = accountRepository.findById(fromId).orElseThrow(); Account to = accountRepository.findById(toId).orElseThrow(); // 다른 트랜잭션이 'from'을 수정해도, // 항상 초기 값 (스냅샷)을 봄 from.debit(amount); to.credit(amount); accountRepository.save(from); accountRepository.save(to); } // SERIALIZABLE: 최대 격리, 동시성 없음 @Transactional( isolation = Isolation.SERIALIZABLE, propagation = Propagation.REQUIRES_NEW ) public void criticalOperation(Long accountId) { // 이 데이터에 대한 다른 모든 트랜잭션 차단 // 절약하여 사용 - 성능 영향 Account account = accountRepository.findById(accountId).orElseThrow(); account.performCriticalUpdate(); accountRepository.save(account); } } ``` 격리 수준 요약 표: ```text | 격리 | Dirty Read | Non-Repeatable | Phantom | |------------------|------------|----------------|---------| | READ_UNCOMMITTED | 가능 | 가능 | 가능 | | READ_COMMITTED | 아니오 | 가능 | 가능 | | REPEATABLE_READ | 아니오 | 아니오 | 가능 | | SERIALIZABLE | 아니오 | 아니오 | 아니오 | ``` > **성능 영향** > > 격리가 엄격할수록 잠금으로 인해 성능에 영향이 더 큽니다. SERIALIZABLE은 트래픽이 많은 운영 환경에서 상당한 경쟁을 일으킬 수 있습니다. ## 고급 패턴 ### 질문 10: "transactional outbox" 패턴을 어떻게 구현합니까? outbox 패턴은 실패 시에도 데이터베이스 수정과 메시지/이벤트 전송 간의 일관성을 보장합니다. ```java // OutboxService.java // Transactional Outbox 패턴 @Service public class OutboxService { private final OutboxRepository outboxRepository; // 비즈니스 엔티티와 같은 트랜잭션에 이벤트 저장 @Transactional(propagation = Propagation.MANDATORY) public void saveEvent(String aggregateType, Long aggregateId, String eventType, String payload) { OutboxEvent event = OutboxEvent.builder() .aggregateType(aggregateType) .aggregateId(aggregateId) .eventType(eventType) .payload(payload) .status("PENDING") .createdAt(Instant.now()) .build(); outboxRepository.save(event); } } @Service public class OrderService { private final OrderRepository orderRepository; private final OutboxService outboxService; @Transactional public Order createOrder(OrderRequest request) { // 주문 생성 Order order = new Order(request); orderRepository.save(order); // 같은 트랜잭션의 outbox 이벤트 (MANDATORY) // 커밋 성공 → 둘 다 저장 // 롤백 → 둘 다 저장 안 됨 outboxService.saveEvent( "ORDER", order.getId(), "ORDER_CREATED", toJson(new OrderCreatedEvent(order)) ); return order; } } // OutboxPublisher.java // 이벤트를 게시하는 별도 프로세스 @Service public class OutboxPublisher { private final OutboxRepository outboxRepository; private final MessageBroker messageBroker; // 각 게시에 대한 독립 트랜잭션 @Scheduled(fixedDelay = 1000) @Transactional(propagation = Propagation.REQUIRES_NEW) public void publishPendingEvents() { List events = outboxRepository .findByStatusOrderByCreatedAt("PENDING"); for (OutboxEvent event : events) { try { messageBroker.publish(event.getEventType(), event.getPayload()); event.setStatus("PUBLISHED"); event.setPublishedAt(Instant.now()); } catch (Exception e) { event.setStatus("FAILED"); event.setError(e.getMessage()); } outboxRepository.save(event); } } } ``` ### 질문 11: 다양한 전파 동작을 어떻게 테스트합니까? 전파 테스트는 예상되는 트랜잭션 동작을 검증하기 위해 특별한 주의가 필요합니다. ```java // TransactionPropagationTest.java // 전파 동작 테스트 @SpringBootTest @Transactional class TransactionPropagationTest { @Autowired private OrderService orderService; @Autowired private PaymentService paymentService; @Autowired private TestEntityManager entityManager; @Test void required_shouldShareTransaction() { // Given Order order = orderService.createOrder(new OrderRequest()); // When - 같은 트랜잭션의 결제 (REQUIRED) paymentService.processPayment(order.getId(), BigDecimal.TEN); // Then - 커밋 전 둘 다 표시 entityManager.flush(); assertThat(entityManager.find(Order.class, order.getId())).isNotNull(); assertThat(entityManager.find(Payment.class, order.getId())).isNotNull(); } @Test void requiresNew_shouldCommitIndependently() { // Given Long orderId = null; try { // When - REQUIRES_NEW의 감사 orderId = orderService.createOrderWithAudit(new OrderRequest()); throw new RuntimeException("Simulated failure after audit"); } catch (RuntimeException e) { // 메인 트랜잭션 롤백 } // Then - 감사 (REQUIRES_NEW)는 여전히 커밋됨 assertThat(findAuditLog(orderId)).isNotNull(); // 그러나 주문은 롤백 assertThat(findOrder(orderId)).isNull(); } @Test void mandatory_shouldThrowWithoutTransaction() { // Given - 활성 트랜잭션 없음 // When/Then - 예외를 던져야 함 assertThatThrownBy(() -> auditService.logCriticalAction("TEST", 1L)) .isInstanceOf(IllegalTransactionStateException.class) .hasMessageContaining("No existing transaction"); } } ``` ```java // PropagationIntegrationTest.java // 실제 롤백 및 커밋과의 통합 테스트 @SpringBootTest class PropagationIntegrationTest { @Autowired private OrderService orderService; @Autowired private OrderRepository orderRepository; @Autowired private AuditLogRepository auditLogRepository; @Test void nested_shouldRollbackOnlyNestedOnFailure() { // Given int initialCount = orderRepository.findAll().size(); // When - NESTED를 사용한 배치 처리 BatchResult result = orderService.processBatchWithNested( List.of(validItem(), invalidItem(), validItem()) ); // Then - 유효한 항목만 저장 assertThat(result.getSuccessCount()).isEqualTo(2); assertThat(result.getFailureCount()).isEqualTo(1); assertThat(orderRepository.findAll().size()).isEqualTo(initialCount + 2); } } ``` ### 질문 12: 트랜잭션 구성의 모범 사례는 무엇입니까? 프로젝트 수준의 일관된 트랜잭션 구성은 놀라움을 피하고 유지 관리를 용이하게 합니다. ```java // TransactionConfig.java // 중앙 집중식 트랜잭션 구성 @Configuration @EnableTransactionManagement public class TransactionConfig { // 모든 트랜잭션의 기본 시간 초과 @Bean public PlatformTransactionManager transactionManager(DataSource dataSource) { DataSourceTransactionManager tm = new DataSourceTransactionManager(dataSource); tm.setDefaultTimeout(30); // 트랜잭션당 최대 30초 return tm; } } // BaseService.java // 서비스의 기본 어노테이션 @Service @Transactional( readOnly = true, // 기본적으로 읽기 전용 rollbackFor = Exception.class // 모든 예외에 대한 롤백 ) public abstract class BaseService { // 읽기 메서드는 readOnly = true 상속 } // OrderService.java // 일관된 구성을 가진 서비스 @Service public class OrderService extends BaseService { private final OrderRepository orderRepository; // readOnly = true 상속 public Order findById(Long id) { return orderRepository.findById(id).orElseThrow(); } // 쓰기에 대한 오버라이드 @Transactional(readOnly = false) public Order createOrder(OrderRequest request) { return orderRepository.save(new Order(request)); } // 중요한 경우에 대한 명시적 구성 @Transactional( readOnly = false, propagation = Propagation.REQUIRES_NEW, isolation = Isolation.SERIALIZABLE, timeout = 10 ) public void criticalOperation(Long orderId) { // 최대 격리 및 짧은 시간 초과 작업 } } ``` 모범 사례 체크리스트: ```text 트랜잭션 구성 - 체크리스트 ✅ 기본적으로 readOnly = true, 쓰기에 대한 명시적 오버라이드 ✅ checked 예외를 포함하기 위해 rollbackFor = Exception.class ✅ 작업 유형에 따른 적절한 시간 초과 ✅ 내부 호출 피하기 (@Transactional 무시됨) ✅ 독립 커밋이 필요한 경우에만 REQUIRES_NEW ✅ 트랜잭션 컨텍스트를 보장하기 위한 MANDATORY ✅ 롤백 동작에 대한 명시적 테스트 ✅ 장기 실행 트랜잭션 모니터링 ✅ 비표준 전파 선택 문서화 ``` ## 결론 트랜잭션 전파는 Spring Boot 면접에서 평가되는 근본적인 개념입니다. 기억해야 할 핵심 포인트: **일반적인 전파 유형:** - ✅ REQUIRED (기본): 트랜잭션에 합류 또는 생성 - ✅ REQUIRES_NEW: 독립 트랜잭션, 별도 커밋 - ✅ NESTED: 부분 롤백을 위한 세이브포인트 - ✅ MANDATORY: 기존 트랜잭션 필요 **피해야 할 함정:** - ✅ Self-invocation: 프록시 우회, @Transactional 무시 - ✅ Checked 예외: 기본적으로 롤백 없음 - ✅ 동일 데이터에 대한 REQUIRES_NEW: 교착 상태 위험 - ✅ 시간 초과 누락: 트랜잭션이 무기한 차단 **모범 사례:** - ✅ 기본적으로 readOnly = true - ✅ 체계적으로 rollbackFor = Exception.class - ✅ Self-invocation을 피하기 위한 별도 서비스 - ✅ 롤백 동작을 명시적으로 테스트 트랜잭션 전파를 마스터하는 것은 Spring과 데이터 관리에 대한 깊은 이해를 보여줍니다. 이러한 개념은 견고한 애플리케이션을 설계하고 기술 면접을 성공적으로 통과하는 데 필수적입니다. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/ko/blog/spring-boot/spring-boot-transaction-propagation-interview