TIL (Today I Learned)

[Spring] @Transactional을 어디에 붙여야 할까🤔❔

기만나🐸 2025. 2. 12. 17:06

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을 적용