[Spring] @Transactional을 어디에 붙여야 할까🤔❔
Spring JPA로 일정 관리 앱을 만들어보면서, @Transactionla을 붙이는 기준을 공부해봤다.
@Transactional
`@Transactional`은 데이터베이스의 일관성을 보장하기 위해 사용된다.
쓰기(INSERT, UPDATE, DELETE) 연산에는 필수적이지만, 읽기(SELECT) 연산에서도 성능 최적화를 위해 고려할 수 있다.
1. @Transactional(readOnly = true)는 GET 메서드에 왜 필요할까?
JPA의 영속성 컨텍스트는 기본적으로 변경 감지(Dirty Checking)를 수행한다.
단순 조회(SELECT) 메서드는 변경이 필요 없으므로 readOnly = true를 사용하면 성능이 향상된다.
➡️ Spring이 "읽기 전용 트랜잭션"을 적용하면, 영속성 컨텍스트가 Dirty Checking을 하지 않아 성능이 최적화된다.
➡️ @Transactional(readOnly = true)를 GET 메서드에 추가하는 것이 좋다.
예시:
@Transactional(readOnly = true)
public CommentDetailResponseDto getCommentById(Long id) {
Comment findComment = commentRepository.findByIdOrElseThrow(id);
return CommentDetailResponseDto.toDto(findComment);
}
2. DELETE 메서드에서 @Transactional이 꼭 필요할까?
❗ `JpaRepository.delete()` 자체는 트랜잭션 없이 실행 가능
`JpaRepository.delete(entity)`는 내부적으로 JPA가 트랜잭션을 시작해서 실행한다.
단순한 delete 요청이면 @Transactional 없이도 정상 동작한다.
예시: 단순한 삭제 요청
public void deleteComment(Long id) {
commentRepository.deleteById(id); // ✅ 트랜잭션 없이 실행 가능
}
✅ @Transactional이 필요한 경우와 이유
public void deleteComment(Long id) {
Comment findComment = commentRepository.findByIdOrElseThrow(id); // ❗SELECT 실행
commentRepository.delete(findComment); // ❗DELETE 실행
}
@Transactional이 없으면 두 개의 SQL이 서로 다른 트랜잭션에서 실행된다.
findByIdOrElseThrow(id)는 조회(SELECT) 쿼리를 실행하는데, 이 과정에서 엔티티는 영속성 컨텍스트에 등록되지 않는다.
delete(findComment)를 호출할 때, 이미 조회된 엔티티가 영속성 컨텍스트에 없을 수도 있다.
➡️ 삭제가 제대로 되지 않거나, Lazy Loading 관련 예외가 발생할 가능성이 있음
➡️ 트랜잭션을 명확하게 지정하여, 하나의 트랜잭션 내에서 SELECT → DELETE가 수행되도록 보장!
3. 메서드별 @Transactional 적용 기준
예시: 일정 관리 앱의 댓글 관리 API 서비스단
@Service
@RequiredArgsConstructor
public class CommentService {
private final CommentRepository commentRepository;
private final MemberRepository memberRepository;
private final ScheduleRepository scheduleRepository;
@Transactional // ✅ INSERT 작업이므로 트랜잭션 필요
public CommentResponseDto createComment(String contents, Long memberId, Long scheduleId) {
Member findMember = memberRepository.findByIdOrElseThrow(memberId);
Schedule findSchedule = scheduleRepository.findByIdOrElseThrow(scheduleId);
Comment comment = new Comment(contents);
comment.setMember(findMember);
comment.setSchedule(findSchedule);
Comment savedComment = commentRepository.save(comment);
return CommentResponseDto.toDto(savedComment);
}
@Transactional(readOnly = true) // ✅ 성능 최적화를 위해 readOnly = true 설정
public List<CommentDetailResponseDto> getComments() {
return commentRepository.findAll()
.stream()
.map(CommentDetailResponseDto::toDto)
.toList();
}
@Transactional(readOnly = true) // ✅ 단순 조회이므로 readOnly = true 설정
public CommentDetailResponseDto getCommentById(Long id) {
Comment findComment = commentRepository.findByIdOrElseThrow(id);
return CommentDetailResponseDto.toDto(findComment);
}
@Transactional // ✅ UPDATE 작업이므로 트랜잭션 필요
public CommentResponseDto updateComment(Long id, String contents) {
Comment findComment = commentRepository.findByIdOrElseThrow(id);
findComment.updateComment(contents);
return CommentResponseDto.toDto(findComment);
}
@Transactional // ✅ DELETE 작업이므로 트랜잭션 필요
public void deleteComment(Long id) {
Comment findComment = commentRepository.findByIdOrElseThrow(id);
commentRepository.delete(findComment);
}
}
4. 결론: 언제 @Transactional을 붙여야 할까?
💡 GET 요청 메서드에는 @Transactional(readOnly = true)를 적용
💡 INSERT/UPDATE/DELETE 메서드에는 일반 @Transactional을 적용