@Transactional
와 같이 propagation 옵션 없이 실행 시 아래의 옵션이 디폴트로 적용된다.
@Transactional(propagation = Propagation.REQUIRED)
또한 readOnly 옵션 없이 실행 시, 아래의 옵션이 디폴트로 적용된다.
@Transactional(ReadOnly = false)
[readOnly 속성]
import javax.transaction.Transactional
import org.springframework.transaction.annotation.Transactional;
@Transactional(readOnly=true)
**주의 사항
1. org.springframework.transaction.annotation.Transactional 패키지는 readOnly옵션을 사용 가능하다.
2. javax.transaction.Transactional 패키지는 readOnly 옵션이 없다.
언듯보면 @Transactional을 사용하지 않는 것과 유사하지만, 다음과 같은 장점이 있다.
1) 조회한 데이터를 return 한다고 해도 의도치 않게 데이터가 변경되는 일을 사전에 방지한다.
2) 해당 옵션인 경우 CUD 작업이 동작하지 않고, 스냅샷 저장, 변경 감지(dirty check)의 작업을 수행하지 않아 성능이 향상된다.
변경감지란?
JPA에서는 트랜잭션이 끝나는 시점에 변화가 있는 모든 엔티티 객체를 데이터베이스에 자동으로 반영한다. JPA에서는 엔티티를 조회하면 해당 엔티티의 처음 조회 상태 그대로 스냅샷을 만든다. 그리고 트랜잭션이 끝나는 시점에는 이 스냅샷과 비교해서 다른 점이 있다면 Update Query를 데이터베이스로 전달한다.
3) @Transactional(readOnly=true)는 데이터베이스 락을 덜 얻게 되므로(read 락은 동시에 가능) 읽기 작업이 빠르게 수행된다.
또한, 트랜잭션을 유지할 필요가 없으므로, 더 많은 읽기 작업을 수행할 수 있다
4) MySQL을 사용할 때 데이터가 날아가는 것을 방지하기 위해서 이중화 구성(master - Slave)을 하는 경우가 있는데 DB가 master와 slave로 나누어져 있다면 readOnly = true로 있는 경우에는 읽기 전용으로 master가 아닌 slave를 호출하게 됩니다. 즉, 상황에 따라 DB 서버의 부하를 줄이고 약간의 최적화를 할 수 있다.
5) @Transactional(readOnly=true) 어노테이션이 있다면 코드를 접하는 사람들이 직관적으로 보기에 해당 메서드는 READ에 대한 동작만 수행할 것이라고 예상가능하여 가독성이 좋아진다.
Transaction Options
- isolation
- 트랜잭션에서 일관성없는 데이터 허용 수준을 설정 (격리 수준)
- propagation
- 동작 도중 다른 트랜잭션을 호출할 때, 어떻게 할 것인지 지정하는 옵션 (전파 옵션)
- noRollbackFor
- 특정 예외 발생 시 rollback이 동작하지 않도록 설정
- rollbackFor
- 특정 예외 발생 시 rollback이 동작하도록 설정
- timeout
- 지정한 시간 내에 메소드 수행이 완료되지 않으면 rollback이 동작하도록 설정
- readOnly
- 트랜잭션을 읽기 전용으로 설정
이 중 Propagation 옵션에 대해 살펴보자.
일반적으로 데이터베이스에 대한 read-only 작업은 트랜잭션을 유지할 필요가 없기 때문에, @Transactional(readOnly=true) 어노테이션을 사용하는 것이 좋다.
하지만, 불필요한 곳에 적용하는 것은 성능에 오히려 악영향을 끼칠 수 있습니다. 예를 들어, 일부 메소드에서는 조회한 데이터를 수정해야 할 수도 있다. 이 경우에는 어차피 자신이 조회한 findby에 대해, modify를 수행해야하므로, 오히려 성능에 악영향을 미친다.
[Propagtaion 속성]
아래는 전파에 관련한 Propagation 관련 설정이다.
- REQUIRED (default)
- 활성 트랜잭션이 있는지 확인하고, 아무것도 없으면 새 트랜잭션을 생성
- SUPPORTS
- 활성 트랜잭션이 있는지 확인하고, 있으면 기존 트랜잭션 사용. 없으면 트랜잭션 없이 실행
- MANDATORY
- 활성 트랜잭션이 있으면 사용하고, 없으면 예외 발생
- 독립적으로 트랜잭션을 진행하면 안 되는 경우 사용
- NEVER
- 활성 트랜잭션이 있으면 예외 발생
- 트랜잭션을 사용하지 않도록 제어할 경우
- NOT_SUPPORTED
- 현재 트랜잭션이 존재하면 트랜잭션을 일시 중단한 다음 트랜잭션 없이 비즈니스 로직 실행
- REQUIRES_NEW
- 현재 트랜잭션이 존재하는 경우, 현재 트랜잭션을 일시 중단하고 새 트랜잭션을 생성
- NESTED
- 트랜잭션이 존재하는지 확인하고 존재하는 경우 저장점을 표시
- 비즈니스 로직 실행에서 예외가 발생하면 트랜잭션이 이 저장 지점으로 롤백
- 활성 트랜잭션이 없으면 REQUIRED 처럼 작동
이제 2가지 예시를 통해 배운 내용에 대해 학습해보자.
Q1) ChatRoom과 MemberChatRoom 는 OneToMany 관계이다.
ChatRoom에서 연관된 MemberChatRoom의 개수를 memberCount로 관리하기 위해, 새로운 memberChatRoom이 추가될 때마다, 연관된 ChatRoom의 memberCount를 1씩 증가 시키려한다.
@Override
@Transactional
@CacheEvict(value = "chatRoom", key = "#chatRoomId") //채팅방에 유저 추가. 기존의 채팅방 멤버에 대한 캐시는 불필요해졌기에, 제거한다 (업데이트보다 이편이 전체적으로 나은 성능 예상)
public void inviteMemberToChatRoom(Long memberId,Long chatRoomId){
ClubMember findMember = clubMemberService.findEntityById(memberId);
ChatRoom findChatRoom = chatRoomService.findEntityById(chatRoomId);
addMemberToChatRoom(findMember, findChatRoom);
}
@Transactional
public void addMemberToChatRoom(ClubMember findMember, ChatRoom findChatRoom) {
//findChatRoom.increaseMemberCount();
MemberChatRoom memberChatRoom = MemberChatRoom.builder()
.clubMember(findMember)
.chatRoom(findChatRoom)
.build();
this.save(memberChatRoom);//새로운 채팅방 멤버 추가.
}
아래는 MemberChatRoom에서 @PrePersist, @PreRemove 엔티티 리스너를 통해 생성, 삭제 시에 Count 수를 조정하는 메소드를 정의한 것이다.
@PrePersist
public void increaseChatRoomMemberCount() {
this.getChatRoom().increaseMemberCount();
}
@PreRemove
public void decreaseChatRoomMemberCount() {
this.getChatRoom().decreaseMemberCount();
}
위와 같이, 모든 내부 메소드 ( 위에는 나와있지 않지만, findEntituById() 내부에도 @Transactional 을 추가해야한다.)에
@Transactional 을 선언하여, 디폴트인 REQUIRED 로 설정 해준다.
만약 트랜잭션 설정을 해주지 않는다면, MemberChatRoom을 저장하는데 성공한 이후 @PrePersist
이렇게해야만 각 메소드들은 자신을 호출한 메소드에 트랜잭션을 따라가게 되고, 전체가 하나의 트랜잭션으로 처리되기에,
문제가 생기지 않는다.
ex) 만약 MemberChatRoom을 저장하는데 성공한 이후, 예외가 발생하여 rollback()을 하는데, memberCount를 증가시키는
increaseChatRoomCount()와 MemberChatRoom을 저장하는 트랜잭션이 갈리게 되어, memberChatRoom의 저장은 rollback 되었지만, memberCount는 증가하지 않아 일관성 문제가 생길 수 있다.
Q2)
@Transactioanl
void change(){
Member member= memberService.findById(1L);
member.changeName("shyswy");
}
......
@Transactional(readOnly=true)
void findById(Long id){
...
}
위와 같은 메소드에서,
memberService.findById() 메소드에 @Transactional(readOnly=true) 어노테이션이 걸려있으면,
전체 트랜잭션은 readOnly=true일까 false일까?
우선 propagation옵션이 따로 없기에, 디폴트인 REQUIRED로 가게된다.
따라서, 만약 트랜잭션이 있다면, 이미 존재하는 트랜잭션을 따라가고, 트랜잭션이 없다면 새롭게 생성하는 것.
따라서, 위의 상황에서는 change()의 옵션인 readOnly=false( 디폴트 ) 가 findById를 덮어쓰고,
만약 Change() 에 Transaction이 걸려있지 않다면, findByid에서 올바르게 readOnly 옵션으로 트랜잭션이 생성될 것이다.
**주의
@Transactional은 스프링 트랜잭션 AOP를 통해 트래잭션을 시작한 뒤 본래 메소드를 호출하고, 종료되면 트랜잭션을 종료하는
"프록시" 를 통해 트랜잭션을 적용하는 것인데, 같은 클래스 내에있는 함수를 호출하게되면, 프록시를 가져오지않고(DI 시 프록시를 가져온다) 실제 객체를 가져오기에, 트랜잭션이 적용되지않는다. (
ex)
public class Member{
@Transactionl(xxx)
memberupdate(){
....
memberSave();
}
@Transactionl(yyy)
memberSave(){....};
}
위에서 memberUpdate가 호출한 memberSave() -> this.memberSave()이다. 즉 프록시가 아닌, 실제 메소드 호출 -> 트랜잭션 프록시가 트랜잭션 기능을 수행하지 못한다!
'Spring boot' 카테고리의 다른 글
[spring 데이터 접근 핵심 원리 2] jdbc를 통한 crud (0) | 2023.05.25 |
---|---|
[spring 데이터 접근 핵심 원리 1] OverView (0) | 2023.05.25 |
[query Optimization] 캐싱 (Caching) (0) | 2023.04.26 |
[query Optimization] rdb와 객체의 차이에 대해 (0) | 2023.04.26 |
[query Optimization] LAZY 로딩과 N+1 문제 (0) | 2023.04.26 |