서비스 트랜잭션과 잠금 충돌
Repository를 직접 여러 번 호출하면 각 호출이 성공해도 전체 업무는 반쯤만 반영될 수 있습니다. 세션 완료는 행을 읽고, 상태 규칙을 확인하고, 변경을 DB에 확정하는 하나의 유스케이스입니다. @Transactional은 이 업무 경계에 놓습니다.
트랜잭션 경계는 유스케이스 경계와 맞춘다
Spring의 transaction interceptor는 public service method 호출 앞뒤에 transaction을 열고 닫습니다. 그 안에서 조회한 entity는 managed 상태가 되므로 setter 후 save()를 반드시 다시 호출할 필요는 없습니다. 다만 SQL은 보통 commit 직전까지 지연되므로, DB 제약과 낙관적 잠금 실패를 서비스 안에서 다루려면 flush() 시점을 의도적으로 당깁니다.
상태 변경과 flush를 한 서비스에 모은다
complete()는 ID로 entity를 찾지 못했을 때와, 이미 완료된 상태일 때를 다른 실패로 남깁니다. 이 차이는 5장에서 404와 409 ProblemDetail로 옮겨갑니다. 지금은 Controller를 만들지 않고 서비스가 어떤 원인을 보존하는지에 집중합니다.
package com.andongmin.studylog.session;
import java.util.UUID;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class StudySessionCommandService {
private final StudySessionJpaRepository repository;
public StudySessionCommandService(StudySessionJpaRepository repository) {
this.repository = repository;
}
@Transactional
public StudySessionEntity complete(UUID id) {
StudySessionEntity entity = repository.findById(id)
.orElseThrow(() -> new SessionNotFoundException(id));
if (entity.getStatus() != SessionStatus.PLANNED) {
throw new IllegalStateException("only PLANNED session can complete");
}
entity.complete();
repository.flush();
return entity;
}
}package com.andongmin.studylog.session;
import java.util.UUID;
public class SessionNotFoundException extends RuntimeException {
public SessionNotFoundException(UUID id) {
super("study session not found: " + id);
}
}flush가 실패를 드러내는 위치를 고정한다
아직 HTTP 수정 endpoint가 없으므로 이 절에서 “두 요청”을 억지로 연출하지 않습니다. 컴파일로 transaction 경계와 flush() 위치를 고정하고, 실제 412·40409 분기는 ETag endpoint가 완성되는 ch5-4에서 두 터미널로 재현합니다.
./mvnw -q -DskipTests compile`complete` 메서드의 transaction 안에서 조회·상태 전이·flush가 하나의 call stack으로 이어지면 준비가 된 것입니다.외부 호출과 데이터 변경을 같은 트랜잭션에 묶지 않는다
느린 네트워크 호출을 transaction 안에 넣으면 DB connection과 lock을 잡은 채 원격 서비스를 기다리게 됩니다. 더 큰 문제는 DB rollback이 이미 보낸 메시지나 HTTP 요청을 되돌릴 수 없다는 점입니다. 7장에서 Outbox로 커밋과 발행 사이를 분리할 때 이 제약으로 돌아옵니다.
서비스가 도달할 실제 테이블이 없으면 transaction 설계는 검증할 수 없습니다. 이제 schema 생성 책임을 Hibernate에서 Flyway로 옮깁니다.