Spring Boot 面接: トランザクション伝播の解説

Spring Boot のトランザクション伝播をマスターします。REQUIRED、REQUIRES_NEW、NESTED など。コード例と典型的な落とし穴を含む 12 の面接質問。

Spring Boot トランザクション伝播: 面接質問と実践例

トランザクション伝播は Spring Boot の基本概念であり、技術面接で定期的に評価されます。@Transactional アノテーションが付いたメソッド間でトランザクションがどのように相互作用するかを理解することは、本番環境での微妙なバグを回避し、堅牢なアーキテクチャを設計するのに役立ちます。

準備のアドバイス

面接官は、ビジネスコンテキストに応じて適切な伝播レベルを選択する能力をテストします。特定のケースで REQUIRED の代わりに REQUIRES_NEW を使用する理由を説明できることが差をつけます。

トランザクション伝播の基礎

質問 1: Spring におけるトランザクション伝播とは何ですか?

伝播は、既存のトランザクションのコンテキスト内で呼び出されたときのトランザクショナルメソッドの動作を定義します。「@Transactional メソッドが、同じくアノテーションが付いた別のメソッドを呼び出すと何が起こるのか?」という問いに答えます。

OrderService.javajava
// 伝播の概念のデモンストレーション
@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 はデフォルトの伝播です。トランザクションが存在する場合、メソッドはそれに参加します。それ以外の場合は新しいトランザクションが作成されます。これは最も一般的で直感的な動作です。

UserService.javajava
// 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);

        // Audit は同じトランザクションに参加
        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 は既存のトランザクションを一時停止し、独立した新しいトランザクションを作成します。親トランザクションの結果に関係なくコミットしなければならない操作に役立ちます。

PaymentService.javajava
// 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 は現在のトランザクション内にセーブポイントを作成します。メソッドが失敗した場合、セーブポイント以降の変更のみがロールバックされ、親トランザクション全体ではありません。

BatchProcessingService.javajava
// 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 の比較:

text
NESTED:
├── 親 TX 内でセーブポイントを使用
├── 失敗時 → セーブポイントへロールバック
├── 親 TX がロールバックすると → NESTED もロールバック
└── パフォーマンスがよい (新しい接続なし)

REQUIRES_NEW:
├── 完全に独立したトランザクションを作成
├── 失敗時 → 子 TX のみロールバック
├── 親 TX がロールバックすると → 子 TX はすでにコミット済み
└── 新しい接続を必要とする
NESTED のサポート

NESTED 伝播には JDBC セーブポイントのサポートが必要です。ほとんどの最新のデータベース (PostgreSQL、MySQL、Oracle) はこれをサポートしています。使用前に互換性を確認してください。

高度な伝播タイプ

質問 5: SUPPORTS と NOT_SUPPORTED はいつ使用しますか?

SUPPORTS は既存のトランザクションがあればその中で実行し、なければトランザクションなしで実行します。NOT_SUPPORTED は既存のトランザクションを一時停止し、トランザクションなしで実行します。

ReportingService.javajava
// 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 が再開
    }
}
OrderService.javajava
// 組み合わせ使用の例
@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 はトランザクションが存在しないことを必要とし、存在する場合は例外をスローします。

AuditService.javajava
// 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);
    }
}
SecurityService.javajava
// 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 は内部呼び出しで動作しないのですか?

最も一般的な落とし穴の 1 つです。内部メソッド呼び出し (self-invocation) は Spring プロキシをバイパスし、トランザクション管理を無効にします。

BrokenService.javajava
// 古典的なエラー: 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);
    }
}

この落とし穴を回避するための解決策:

java
// 解決策 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: 2 つのサービスに分割 (推奨)
@Service
public class ItemOrchestrator {

    private final ItemProcessor processor;

    public void processItems(List<Long> itemIds) {
        for (Long id : itemIds) {
            // 別の Bean への呼び出し → プロキシが動作
            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 でのみロールバックします。チェック例外は自動ロールバックをトリガーしません。

TransactionRollbackDemo.javajava
// 例外タイプに基づくロールバック動作
@Service
public class TransactionRollbackDemo {

    private final OrderRepository orderRepository;

    // RuntimeException → 自動ロールバック
    @Transactional
    public void methodWithRuntimeException() {
        orderRepository.save(new Order());
        throw new RuntimeException("Error"); // ROLLBACK
    }

    // チェック例外 → デフォルトでロールバックなし
    @Transactional
    public void methodWithCheckedException() throws IOException {
        orderRepository.save(new Order());
        throw new IOException("File error"); // それでもコミット!
    }

    // チェック例外でロールバックを強制
    @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"); // 例外にもかかわらずコミット
    }
}

ビジネスケースに推奨される設定:

BaseTransactionalService.javajava
// 一貫したトランザクション設定
@Service
public abstract class BaseTransactionalService {

    // すべての例外でロールバック (チェック済みおよびチェック外)
    @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: トランザクション分離は伝播とどのように連携しますか?

分離と伝播は補完的です。分離は同時実行トランザクション間のデータの可視性を決定します。

IsolationDemo.javajava
// 分離 + 伝播の組み合わせ
@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 パターンは、障害が発生した場合でも、データベースの変更とメッセージ/イベントの送信間の一貫性を保証します。

OutboxService.javajava
// 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: 異なる伝播動作をどのようにテストしますか?

伝播テストは、期待されるトランザクション動作を検証するために特別な注意が必要です。

TransactionPropagationTest.javajava
// 伝播動作のテスト
@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");
    }
}
PropagationIntegrationTest.javajava
// 実際のロールバックとコミットを伴う統合テスト
@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: トランザクション設定のベストプラクティスは何ですか?

プロジェクトレベルでの一貫したトランザクション設定により、サプライズを回避しメンテナンスが容易になります。

TransactionConfig.javajava
// 集中化されたトランザクション設定
@Configuration
@EnableTransactionManagement
public class TransactionConfig {

    // すべてのトランザクションのデフォルトタイムアウト
    @Bean
    public PlatformTransactionManager transactionManager(DataSource dataSource) {
        DataSourceTransactionManager tm = new DataSourceTransactionManager(dataSource);
        tm.setDefaultTimeout(30); // 1 トランザクションあたり最大 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 をデフォルトに、書き込みには明示的オーバーライド
✅ チェック例外を含めるため rollbackFor = Exception.class
✅ 操作タイプに応じた適切なタイムアウト
✅ 内部呼び出しを避ける (@Transactional は無視される)
✅ 独立したコミットが必要な場合のみ REQUIRES_NEW
✅ トランザクションコンテキストを保証するための MANDATORY
✅ ロールバック動作の明示的なテスト
✅ 長時間実行されるトランザクションを監視
✅ 非標準の伝播選択を文書化

Spring Bootの面接対策はできていますか?

インタラクティブなシミュレーター、flashcards、技術テストで練習しましょう。

結論

トランザクション伝播は Spring Boot 面接で評価される基本概念です。覚えておくべきキーポイント:

一般的な伝播タイプ:

  • ✅ REQUIRED (デフォルト): トランザクションに参加または作成
  • ✅ REQUIRES_NEW: 独立したトランザクション、別個のコミット
  • ✅ NESTED: 部分的なロールバック用のセーブポイント
  • ✅ MANDATORY: 既存のトランザクションが必要

回避すべき落とし穴:

  • ✅ Self-invocation: プロキシをバイパス、@Transactional は無視
  • ✅ チェック例外: デフォルトでロールバックなし
  • ✅ 同じデータでの REQUIRES_NEW: デッドロックのリスク
  • ✅ タイムアウトなし: トランザクションが無期限にブロック

ベストプラクティス:

  • ✅ デフォルトで readOnly = true
  • ✅ 体系的に rollbackFor = Exception.class
  • ✅ Self-invocation を避けるためにサービスを分離
  • ✅ ロールバック動作を明示的にテスト

トランザクション伝播をマスターすることは、Spring とデータ管理の深い理解を示します。これらの概念は、堅牢なアプリケーションを設計し、技術面接で成功するために不可欠です。

今すぐ練習を始めましょう!

面接シミュレーターと技術テストで知識をテストしましょう。

タグ

#spring boot
#transactions
#propagation
#java
#interview

共有

関連記事