@Transactional(readOnly = true)
는 트랜잭션을 사용할 때 읽기 전용으로 사용하겠다는 의미이다.
readOnly로 사용 시 아래와 같은 장점이 있다고 한다.
장점
- JPA를 사용할 경우 변경감지를 수행하지 않아 성능상 이점을 가져올 수 있다.
- mysql 이중화 구성(master/slave)를 사용할 경우 readOnly를 사용할 경우 slave를 자동으로 호출해줘서 DB 부하를 줄여줄 수 있다.
- 읽기 가독성 증가 (의도적으로 읽기 전용으로 사용하겠다고 명시)
- transactionId를 부여되지 않아 오버헤드를 줄일 수 있다.
여기서 readOnly 사용을 할 때 몇 가지 궁금점이 생겼다.
1) @Transactional(readOnly = true)일 때 수정작업(update)을 하면 어떻게 될까?
2) @Transactional(readOnly=true/false) 이 중첩되어 사용하는 경우는 어떻게 적용될까?
1. @Transactional(readOnly = true)일 때 수정작업(update)을 하면 어떻게 될까?
테스트를 위해 샘플 소스를 작성해보았다.
@Transactional(readOnly = true)
public void changeNameReadOnly(int id, String name) {
UserJpo userJpo = userRepository.findById(id).orElseThrow(IllegalArgumentException::new);
userJpo.setName(name); // 수정
}
아래는 테스트 코드이다.
@BeforeEach
void setUp() {
userService.register(new User(1, "홍길동", "서울"));
}
@Test
void changeNameReadOnly() {
userService.changeNameReadOnly(1, "홍길동2");
User user = userService.find(1);
log.info(user.getName()); // 홍길동
}
@Transactional(readOnly = true)일 때 홍길동
=> 홍길동2
로 변경하는 코드이다.
실행결과
실행해보면 오류가 발생하지 않고 잘 실행된다.
하지만 결과는 홍길동
으로 출력이 된다.
좀 더 상세한 로그확인을 위해 로직을 좀 더 추가해보았다.
@Transactional(readOnly = true)
public void changeNameReadOnly(int id, String name) {
UserJpo userJpo = userRepository.findUserJpoById(id);
FlushModeType flushMode = em.getFlushMode();
log.info("flushMode: {}", flushMode);
log.info("readOnly: {}", TransactionSynchronizationManager.isCurrentTransactionReadOnly());
printEntityState(userJpo);
userJpo.setName(name);
}
실행결과
main] com.example.user.service.UserService : flushMode: COMMIT
main] com.example.user.service.UserService : readOnly: true
위에서 flushMode: COMMIT
으로 출력된다.
여기서 flushMode는 무엇일까?
flush는 영속성 컨텍스트와 데이터베이스를 동기화하는 것을 의미한다. 영속성 컨텍스트의 객체 상태가 변경이 되고 해당 객체의 변경이 발생했을 때 DB에 변경내용을 반영하기 위해서 DML(insert/update/delete)이 실행된다.
flushMode는 자동으로 반영되는 내용을 조절하기 위해서 사용되는 모드이다.
- FlushModeType.AUTO
- 커밋이나 쿼리를 실행할 때 플러시한다는 의미 (기본값)
- FlushModeType.COMMIT
- 커밋할 때만 플러시한다는 의미
즉, @Transactional(readOnly = true)로 설정하면 FlushMode
가 COMMIT
으로 되어 자동으로 플러시되지 않는다는 의미이다. 그래서 readOnly=true로 되면 commit을 따로 해주지 않는 한 DB에 변경사항이 반영되지 않는다.
2. @Transactional(readOnly=true/false) 이 중첩되어 사용하는 경우는 어떻게 적용될까?
service -> repository를 사용하는 구조에서 아래와 같은 형태일 때 readOnly가 적용될까?
1번 케이스: UserService(readOnly=true), UserRepository(readOnly=false)
2번 케이스: UserService(readOnly=false), UserRepository(readOnly=true)
1번 케이스 : UserService(readOnly=true), UserRepository(readOnly=false)
@Transactional(readOnly = true) // service는 readOnly=true
public void changeNameTrueFalse(int id, String name) {
UserJpo userJpo = userRepository.findById(id) // repository는 readOnly=false
.orElseThrow(IllegalArgumentException::new);
FlushModeType flushMode = em.getFlushMode();
log.info("flushMode: {}", flushMode);
log.info("readOnly: {}", TransactionSynchronizationManager.isCurrentTransactionReadOnly());
printEntityState(userJpo);
userJpo.setName(name);
}
UserService는 readOnly=true이고, UserRepository는 readOnly=false인 경우는 readOnly
가 true
로 설정이 된다.
그래서 name은 홍길동
으로 변경이 되지 않는다.
2번 케이스 : UserService(readOnly=false), UserRepository(readOnly=true)
@Transactional(readOnly = false)
public void changeNameFalseTrue(int id, String name) {
UserJpo userJpo = userRepository.findUserJpoById(id);
FlushModeType flushMode = em.getFlushMode();
log.info("flushMode: {}", flushMode);
log.info("readOnly: {}", TransactionSynchronizationManager.isCurrentTransactionReadOnly());
printEntityState(userJpo);
userJpo.setName(name);
}
@Repository
public interface UserRepository extends JpaRepository<UserJpo, Integer> {
@Transactional(readOnly = true)
UserJpo findUserJpoById(int id);
}
UserService는 readOnly=false이고, UserRepository는 readOnly=true인 경우는 readOnly
가 false
로 설정이 된다.
그래서 name은 홍길동2
로 변경이 된다.
결론: readOnly가 중첩으로 사용될 때는 @Transactional이 시작된 트랜잭션의 readOnly의 값으로 하위 트랜잭션에 모두 적용이 된다.