Service 코드를 짜다가 관례적으로 @Transactional 이 붙는 것을 보고, 명확한 의미를 파악해야겠다고 생각했다.
간단하게, 트랜잭션의 개념
- commit or rollback
- 데이터베이스를 사용할 때 트랜잭션을 적용하면 데이터 추가, 갱신, 삭제 등 작업을 처리하던 중 오류가 발생했을 때 모든 작업을 원상태로 되돌릴 수 있다.
- 모든 작업들이 성공해야만 최종적으로 데이터베이스에 반영한다.
ACID 성질; 트랜잭션의 특징으로는 안전성을 보장하기 위해 필요한 4가지 성질이 있다.
1. 원자성(Atomicity) -
2. 일관성(Consistency)
3. 독립성(Isolation)
4. 지속성(Durability)
@Transactional
- 스프링에서는 클래스, 메서드에 @Transactional 애노테이션을 붙일 수 있다.
- 둘 다 붙어있다면 메서드 레벨의 @Transactional이 우선 적용된다.
JPA dirty checking
일반적으로 update를 한다고 하면, setXXX로 값을 설정한 뒤, save를 해야지 적용된다고 생각할 수 있다.
하지만 JPA에서는 다음과 같이 save를 하지 않아도 update 기능을 구현할 수 있다.
@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {
private final UserRepository userRepository;
@Override
public UserDTO.UserSimpleInfoResponse updateUser(Long userId, UserDTO.UpdateUserRequest request) {
Users user = userRepository.findById(userId).orElseThrow(() -> new EntityNotFoundException("존재하지 않는 사용자 입니다."));
user.setNickname(request.getNickname());
user.setProfileImage(request.getProfileImage());
// userRepository.save(user);
return new UserDTO.UserSimpleInfoResponse(user);
}
}
이는 dirty checking 때문이다.
JPA에서는 트랜잭션이 끝나는 시점에 변화가 생긴 모든 엔티티들을 데이터베이스에 자동으로 반영해준다.
JPA에서 영속성 컨텍스트가 관리하고 있는 엔티티를 조회하면 해당 엔티티의 조회로 snapshot을 만들어놓고, 트랜잭션이 끝나는 시점에 snapshot과 비교하여 변화가 생긴다면 update를 해서 데이터베이스로 전달하게 된다.
[*** spring-data-jpa를 사용한다면]
새로운 엔터티를 추가할 때는 repository.save() 메서드 사용을 호출해야 한다.
하지만 기존의 엔터티를 수정하는 작업에서는 repository.save() 메서드를 사용하지 않는 것이 더 깔끔하다.
위의 굵은 문장들을 읽고, 이상함을 느꼈다면 똑똑이...
위에서 dirty checking을 왜 설명했는가?!
'트랜잭션이 끝나느 시점'에 변화가 생긴 엔티티들을 DB에 자동 반영해준다고 했으므로,
@Transactional이 붙어있어야, save를 하지 않아도 자동으로 update해주는 것이다.
dirty checking은 트랜잭션이 commit될 때 작동하기 때문에 이런 일이 발생하는 것이다.
즉, 다음과 같이 코드를 수정하면 dirty checking을 하게 되고, DB에 커밋해서 수정된 사항을 save없이 반영할 수 있는 것이다.
@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {
private final UserRepository userRepository;
@Override
@Transactional
public UserDTO.UserSimpleInfoResponse updateUser(Long userId, UserDTO.UpdateUserRequest request) {
Users user = userRepository.findById(userId).orElseThrow(() -> new EntityNotFoundException("존재하지 않는 사용자 입니다."));
user.setNickname(request.getNickname());
user.setProfileImage(request.getProfileImage());
return new UserDTO.UserSimpleInfoResponse(user);
}
}
@Transactional 주의사항
@Transactional 어노테이션을 사용한 트랜잭션 안에서 예외가 발생했을 경우, 해당 예외가 런타임 예외일 경우에는 자동적으로 롤백이 발생하지만 아닐 경우 롤백이 되지 않는다.
이러한 경우에는 @Transactional 어노테이션에서의 rollbackFor 옵션을 사용해서 해당 체크 예외를 적어 줘야 한다.
@Transactional(rollbackFor=CustomException.class)
public void updateuser(UserDTO dto) throws CustomException {
// 로직 구현
}
@Transactional 활용법 및 사용 위치
- 일반적으로 비즈니스 로직이 담겨있는 서비스 레이어에서 트랜잭션 처리를 많이 한다.
@Transactional(readOnly=true)
- C, U, D가 아닌 R(읽어 오는) 메서드에 이를 적용하는 경우가 있다.
- 이의 특징을 알아봤다.
- 조회한 데이터를 리턴한다고 해도 의도치 않게 데이터가 변경되는 일을 사전에 방지해준다.
- 해당 옵션인 경우 CUD 작업이 동작하지 않고, snapshot 저장, dirty check 작업을 수행하지 않아 성능이 향상된다.
- MySQL을 사용할 때 이중화 구성(master-slave)을 하는 경우가 있는데, DB가 master-slave로 나뉘어 있다면 readOnly=true로 있는 경우, 읽기 전용으로 master가 아닌 slave를 호출하게 된다. 즉, 상황에 따라 DB 서버의 부하를 줄이고 최적화할 수 있다.
- 코드를 접하는 사람들이 직관적으로 해당 메서드는 READ에 대한 동작만 수행할 것이라고 예상한다. 그리고 결과적으로도 읽기 동작만 수행된다.
- 트랜잭션 애노테이션이 없을 경우 @OneToMany, @ManyToMany 등 lazy loading을 디폴트로 사용하는 엔티티들을 정상적으로 조회할 수 없다.
- lazy loading은 프록시 패턴을 통해 객체를 조회할 때 연관된 객체를 바로 조회하지 않고 실제 사용할 때 조회한다.
- 즉, 프록시를 사용해 조회할 경우, 해당 객체에 접근할 때 조회하겠다고 요청해야 한다.
- @Transactional이 붙어있지 않을 경우, 준영속 상태에 있는 엔티티들은 지연로딩을 할 수 없다.
- lazy loading을 사용해서 프록시 객체로 존재할 때는 해당 객체에서 실제로 값을 뽑으려고 하는 행위가 불가능하다는 것이다.
- 따라서 트랜잭션이 있어야 lazy loading이 필요한 엔티티들을 정상 조회할 수 있다.
참고
https://resilient-923.tistory.com/415#recentComments
'개발 > Spring' 카테고리의 다른 글
Entity와 DTO (0) | 2023.08.02 |
---|---|
@Transactional 동작원리와 public method calls private method에서의 트랜잭션 적용 여부 (0) | 2023.08.02 |
연관관계 매핑된 Entity 생성하기 (0) | 2023.08.01 |
JPA OSIV(Open Session In View)와 성능 최적화 (0) | 2023.07.12 |
ConnectionPool과 JPA HikariCP 개념 (0) | 2023.07.09 |
댓글