Spring Boot 면접: 트랜잭션 전파 설명
Spring Boot 트랜잭션 전파 마스터하기: REQUIRED, REQUIRES_NEW, NESTED 등. 코드 예제와 일반적인 함정을 포함한 12가지 면접 질문.

트랜잭션 전파는 Spring Boot의 근본적인 개념으로, 기술 면접에서 정기적으로 평가됩니다. @Transactional 어노테이션이 적용된 메서드 간에 트랜잭션이 어떻게 상호작용하는지 이해하면 운영 환경의 미묘한 버그를 피하고 견고한 아키텍처를 설계할 수 있습니다.
면접관들은 비즈니스 컨텍스트에 따라 적절한 전파 수준을 선택하는 능력을 평가합니다. 특정 사례에서 REQUIRED 대신 REQUIRES_NEW를 사용하는 이유를 설명할 수 있는 것이 차이를 만듭니다.
트랜잭션 전파의 기초
질문 1: Spring에서 트랜잭션 전파란 무엇입니까?
전파는 기존 트랜잭션의 컨텍스트 내에서 호출될 때 트랜잭션 메서드의 동작을 정의합니다. 이는 다음 질문에 답합니다: "@Transactional 메서드가 어노테이션이 있는 다른 메서드를 호출할 때 어떤 일이 발생합니까?"
// 전파 개념의 시연
@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는 기본 전파입니다. 트랜잭션이 존재하면 메서드가 합류합니다. 그렇지 않으면 새 트랜잭션이 생성됩니다. 이는 가장 일반적이고 직관적인 동작입니다.
// 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()));
}
}다음 다이어그램은 트랜잭션 흐름을 보여줍니다:
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는 기존 트랜잭션을 일시 중단하고 독립적인 새 트랜잭션을 생성합니다. 부모 트랜잭션의 결과와 관계없이 작업을 커밋해야 할 때 유용합니다.
// 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를 사용한 트랜잭션 흐름:
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는 현재 트랜잭션 내에 세이브포인트를 만듭니다. 메서드가 실패하면 세이브포인트 이후의 변경 사항만 롤백되며, 부모 트랜잭션 전체가 롤백되지 않습니다.
// NESTED: 부모 트랜잭션 내의 세이브포인트
@Service
public class BatchProcessingService {
private final ItemRepository itemRepository;
private final ItemProcessor itemProcessor;
@Transactional
public BatchResult processBatch(List<Item> 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 비교:
NESTED:
├── 부모 TX 내에서 세이브포인트 사용
├── 실패 시 → 세이브포인트로 롤백
├── 부모 TX가 롤백되면 → NESTED도 롤백
└── 더 나은 성능 (새 연결 없음)
REQUIRES_NEW:
├── 완전히 독립적인 트랜잭션 생성
├── 실패 시 → 자식 TX만 롤백
├── 부모 TX가 롤백되면 → 자식 TX는 이미 커밋됨
└── 새 연결 필요NESTED 전파에는 JDBC 세이브포인트 지원이 필요합니다. 대부분의 최신 데이터베이스(PostgreSQL, MySQL, Oracle)는 이를 지원합니다. 사용 전 호환성을 확인하십시오.
고급 전파 유형
질문 5: SUPPORTS와 NOT_SUPPORTED는 언제 사용합니까?
SUPPORTS는 기존 트랜잭션이 있으면 그 안에서 실행하고, 없으면 트랜잭션 없이 실행합니다. NOT_SUPPORTED는 기존 트랜잭션을 일시 중단하고 트랜잭션 없이 실행합니다.
// 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 재개
}
}// 결합 사용 예
@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는 트랜잭션이 없어야 하며, 있으면 예외를 던집니다.
// 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);
}
}// 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);
}
}Spring Boot 면접 준비가 되셨나요?
인터랙티브 시뮬레이터, flashcards, 기술 테스트로 연습하세요.
일반적인 면접 함정
질문 7: 왜 @Transactional이 내부 호출에서 작동하지 않습니까?
가장 흔한 함정 중 하나입니다. 내부 메서드 호출(self-invocation)은 Spring 프록시를 우회하여 트랜잭션 관리를 비활성화합니다.
// 고전적 오류: self-invocation
@Service
public class BrokenService {
private final ItemRepository itemRepository;
public void processItems(List<Long> 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);
}
}이 함정을 피하기 위한 솔루션:
// 솔루션 1: 자기 주입
@Service
public class FixedServiceWithSelfInjection {
private final ItemRepository itemRepository;
@Lazy
@Autowired
private FixedServiceWithSelfInjection self; // 자기 주입
public void processItems(List<Long> 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<Long> 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 예외는 자동 롤백을 트리거하지 않습니다.
// 예외 유형에 따른 롤백 동작
@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"); // 예외에도 커밋
}
}비즈니스 사례를 위한 권장 구성:
// 일관된 트랜잭션 구성
@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: 트랜잭션 격리는 전파와 어떻게 작동합니까?
격리와 전파는 상호 보완적입니다. 격리는 동시 트랜잭션 간의 데이터 가시성을 결정합니다.
// 격리 + 전파 결합
@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);
}
}격리 수준 요약 표:
| 격리 | Dirty Read | Non-Repeatable | Phantom |
|------------------|------------|----------------|---------|
| READ_UNCOMMITTED | 가능 | 가능 | 가능 |
| READ_COMMITTED | 아니오 | 가능 | 가능 |
| REPEATABLE_READ | 아니오 | 아니오 | 가능 |
| SERIALIZABLE | 아니오 | 아니오 | 아니오 |격리가 엄격할수록 잠금으로 인해 성능에 영향이 더 큽니다. SERIALIZABLE은 트래픽이 많은 운영 환경에서 상당한 경쟁을 일으킬 수 있습니다.
고급 패턴
질문 10: "transactional outbox" 패턴을 어떻게 구현합니까?
outbox 패턴은 실패 시에도 데이터베이스 수정과 메시지/이벤트 전송 간의 일관성을 보장합니다.
// 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<OutboxEvent> 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: 다양한 전파 동작을 어떻게 테스트합니까?
전파 테스트는 예상되는 트랜잭션 동작을 검증하기 위해 특별한 주의가 필요합니다.
// 전파 동작 테스트
@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");
}
}// 실제 롤백 및 커밋과의 통합 테스트
@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: 트랜잭션 구성의 모범 사례는 무엇입니까?
프로젝트 수준의 일관된 트랜잭션 구성은 놀라움을 피하고 유지 관리를 용이하게 합니다.
// 중앙 집중식 트랜잭션 구성
@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) {
// 최대 격리 및 짧은 시간 초과 작업
}
}모범 사례 체크리스트:
트랜잭션 구성 - 체크리스트
✅ 기본적으로 readOnly = true, 쓰기에 대한 명시적 오버라이드
✅ checked 예외를 포함하기 위해 rollbackFor = Exception.class
✅ 작업 유형에 따른 적절한 시간 초과
✅ 내부 호출 피하기 (@Transactional 무시됨)
✅ 독립 커밋이 필요한 경우에만 REQUIRES_NEW
✅ 트랜잭션 컨텍스트를 보장하기 위한 MANDATORY
✅ 롤백 동작에 대한 명시적 테스트
✅ 장기 실행 트랜잭션 모니터링
✅ 비표준 전파 선택 문서화Spring Boot 면접 준비가 되셨나요?
인터랙티브 시뮬레이터, flashcards, 기술 테스트로 연습하세요.
결론
트랜잭션 전파는 Spring Boot 면접에서 평가되는 근본적인 개념입니다. 기억해야 할 핵심 포인트:
일반적인 전파 유형:
- ✅ REQUIRED (기본): 트랜잭션에 합류 또는 생성
- ✅ REQUIRES_NEW: 독립 트랜잭션, 별도 커밋
- ✅ NESTED: 부분 롤백을 위한 세이브포인트
- ✅ MANDATORY: 기존 트랜잭션 필요
피해야 할 함정:
- ✅ Self-invocation: 프록시 우회, @Transactional 무시
- ✅ Checked 예외: 기본적으로 롤백 없음
- ✅ 동일 데이터에 대한 REQUIRES_NEW: 교착 상태 위험
- ✅ 시간 초과 누락: 트랜잭션이 무기한 차단
모범 사례:
- ✅ 기본적으로 readOnly = true
- ✅ 체계적으로 rollbackFor = Exception.class
- ✅ Self-invocation을 피하기 위한 별도 서비스
- ✅ 롤백 동작을 명시적으로 테스트
트랜잭션 전파를 마스터하는 것은 Spring과 데이터 관리에 대한 깊은 이해를 보여줍니다. 이러한 개념은 견고한 애플리케이션을 설계하고 기술 면접을 성공적으로 통과하는 데 필수적입니다.
연습을 시작하세요!
면접 시뮬레이터와 기술 테스트로 지식을 테스트하세요.
태그
공유
관련 기사

Spring Boot 면접 질문 30선: 자바 개발자를 위한 완벽 가이드
오토 컨피그레이션, 스타터, Spring Data JPA, 보안, 테스트를 망라한 30문항으로 Spring Boot 면접을 준비하십시오.

Spring Modulith: 모듈러 모놀리스 아키텍처 해설
Spring Modulith로 자바 모듈러 모놀리스를 구축하는 방법을 배웁니다. 아키텍처, 모듈, 비동기 이벤트, Spring Boot 3 예제로 살펴보는 테스트.

Spring Batch 5 면접: 파티셔닝, 청크, 장애 허용
Spring Batch 5 면접을 정복하세요. 파티셔닝, 청크 처리, 장애 허용에 관한 15가지 핵심 질문과 Java 21 예제를 제공합니다.