개요
오늘 가져온 주제는 읽기 전용 트랜잭션인 @Transactional(readOnly = true)이다.
오늘 이전에 읽은 책 자바 ORM 표준 JPA 프로그래밍을 읽다가 Service 구현체에 @Transactional(readOnly = true)을 기본적으로 달고, 해당 클래스의 메서드 중 데이터베이스에 변경을 가하는 작업을 수행하는 메서드라면 @Transactional을 작성해 따로 처리하는 것이 성능에서 더 이득일 수 있다는 글을 보고 그 이유가 무엇인지 한번 더 공부해보고 싶어졌다.
@Transactional
@Transactional 은 데이터베이스의 작업에서 일관성과 무결성을 보장하기 위해 사용하는 어노테이션이다.
그렇기 때문에 @Transactional 어노테이션이 달린 메서드는 우리 눈에는 보이지 않지만 메서드 시작 부분에 Auto Commit이 false로 설정된다. 왜냐하면 내부의 작업 중 하나라도 정상적으로 종료되지 않는다면 모든 작업을 초기화(롤백) 해야 하기 때문이다.
또한 @Transactional은 커밋이나 롤백뿐만 아니라 JPA의 주요 기능 중 하나인 변경 감지(Dirty checking)를 사용할 수 있게 해 준다. 그렇기 때문에 조회한 엔티티를 수정하면 알아서 UPDATE 쿼리가 생성되는 것이다.
(참고) 변경 감지(Dirty checking)이란?
JPA의 영속성 컨텍스트가 관리하는 엔티티의 상태를 추적하여, 변경 사항이 있는 경우 자동으로 데이터베이스에 UPDATE 쿼리를 생성하고 실행하는 기능이다.
@Transactional(readOnly = true)와 Dirty checking의 관계
@Transactional(readOnly = true)은 읽기 전용 트랜잭션을 의미한다. 그럼 일반 트랜잭션와 읽기 전용 트랜잭션의 차이는 뭘까? 왜 읽기 전용 트랜잭션을 사용해야 이득인 걸까?
일반 트랜잭션 작업을 수행하는 메소드에선 엔티티를 조회함과 동시에 해당 엔티티의 스냅샷을 생성한다. 그리고 메소드의 작업을 모두 수행하고 해당 작업을 데이터베이스에 반영하는 flush을 진행하기 전 스냅샷과 영속성 컨텍스트에 있는 엔티티를 비교한다. 여기서 만약 변경된 데이터가 있다면 UPDATE 쿼리가 생성되는 것이다.
하지만 읽기 전용 트랜잭션의 경우엔 이 스냅샷이 생성되지 않는다. 그렇기 때문에 용량・시간 적으로 더 이득인 것이다. 하지만 당연히 변경 감지 기능은 사용할 수 없다. 조회할 당시 엔티티의 상태인 스냅샷도 없을뿐더러 읽기 전용 트랜잭션은 조회만을 목적으로 사용하는 기술이기 때문에 flush 작업도 수행하지 않기 때문이다.
(참고) flush 란?
영속성 컨텍스트에 쌓인 엔티티를 데이터베이스에 영구적으로 반영하는 작업이다.
아래는 내가 이전에 수행한 프로젝트의 Service 코드이다.
@Service
@RequiredArgsConstructor
public class BoardServiceImpl implements BoardService {
private final BoardRepository boardRepository;
private final MemberRepository memberRepository;
private final CommentRepository commentRepository;
@Override
@Transactional
public Long writeBoard(BoardWriteServiceRequest request) {
Member member = memberRepository.findMemberByEmail(request.getEmail())
.orElseThrow(() -> new RestApiException(USER_NOT_FOUND));
return boardRepository.save(request.toBoard(member)).getId();
}
@Override
@Transactional
public BoardDetailServiceResponse detailBoard(Long boardId) {
Board byId = boardRepository.findById(boardId)
.orElseThrow(() -> new RestApiException(DATA_NOT_FOUND));
byId.increaseViews();
return BoardDetailServiceResponse.builder()
.board(byId)
.comments(commentRepository.findCommentsByBoard(byId))
.build();
}
@Override
@Transactional(readOnly = true)
public List<BoardListServiceResponse> findAllBoard(Long boardId) {
Pageable pageable = PageRequest.of(0, 10);
List<Board> all = boardRepository.findByIdLessThanOrderByIdDesc(boardId, pageable);
return all.stream()
.map(board -> BoardListServiceResponse.builder()
.board(board)
.build())
.toList();
}
...
}
보다시피 findAllBoard() 메소드는 조회만을 목적으로 하고, 데이터베이스에 변경을 가하는 작업이 없기 때문에 @Transactional(readOnly = true) 어노테이션을 붙였고, writeBoard()나 detailBoard() 메소드 경우엔 게시글 데이터 추가, 게시글 조회수 증가와 같은 데이터베이스에 변경을 가하는 작업을 수행하기 때문에 @Transactional 어노테이션을 붙인 것이다.
'개발 일기' 카테고리의 다른 글
[개발 일기] 2025.01.06 - OSI 7계층 (물리 계층) (0) | 2025.01.06 |
---|---|
[개발 일기] 2025.01.05 - 자바 객체 락 (synchronized) (0) | 2025.01.05 |
[개발 일기] 2025.01.04 - Event, Publisher, Listener (0) | 2025.01.04 |
[개발 일기] 2025.01.03 - Redis의 만료기한 (0) | 2025.01.03 |
[개발 일기] 2025.01.01 - 도커 컨테이너 네트워크 (Feat : ports) (0) | 2025.01.01 |