2024. 9. 24. 09:01ㆍDataBase/JPA
1. 서론
프로젝트를 진행하고 코드를 작성할 때 DB를 사용하게 된다면 Spring Data JPA를 사용하게 된다. 그리고 이때 해당 JPA는 Java에서의 ORM의 표준으로 사용이 되며, Spring 프레임워크에서는 Spring Data JPA로 하여 사용을 하게 된다. 해당 JPA를 사용하면서 같이 등장하는 것이 바로 @Transactional 이다.
해당 어노테이션은 트랜잭션 관리를 간편, 용이하게 해주는 어노테이션이다. 이를 통해 해당 백엔드 서버와 데이터베이스 간의 작업이 하나의 단위로 실행하도록 보장하는 역할이다. 그렇기에 지금까지는 이러한 논리적 하나의 단위(원자성)을 위해 항상 해당 트랜잭션을 기입해주었다.
하지만 이번에 기능을 구현한 후 성능 개선을 위해 리팩토링 및 학습을 하던 도중 무분별한 트랜잭션의 사용은 성능을 저하시킨다는 것을 알았다. 그래서 해당 내용을 정리해보고자 한다.
2. 탐구
1.@Transactional의 기본 구조
먼저 해당 트랜잭션이 어떤 구조를 가지고 있기에 논리적 단위를 보장하는지 알고싶어져서 해당 구조부터 살펴보았다. 먼저 해당 프로젝트에서 사용하는 트랜잭션의 경우 Jakarta의 Transactions API를 사용하였다. 이때 트랜잭션의 경우 spring에서도 제공하는 트랜잭션 어노테이션이 존재하기에 공통적으로 어떤 요소를 다루는지 정리해봤다.
- 해당 어노테이션을 통해 다루는 것은 트랜잭션의 ACID 특성을 유지하는 것이다.
- A : Atomicity - 원자성
- 트랜잭션의 모든 작업은 하나의 단위로 처리
- 모든 작업이 성공해야 커밋되고, 하나라도 실패하면 롤백되어야 함
- 데이터베이스의 일관성을 유지하기 위한 중요한 역할
- C : Consistency - 일관성
- 트랜잭션이 완료되면 데이터베이스(DB)는 일관된 상태를 유지해야 함
- 트랜잭션이 시작되기 전, 후의 데이터는 정의된 모든 규칙을 만족해야 함
- I : Isolation - 격리성
- 트랜잭션이 실행되는 동안 다른 트랜잭션이 영향을 받지 않도록 격리되어야 함
- 트랜잭션의 완료 전까진 다른 트랜잭션이 해당 데이터에 접근하거나 변경하지 못하도록 해야함
- 종류
- READ UNCOMMITTED
- 다른 트랜잭션이 아직 커밋되지 않은 데이터 변경을 읽을 수 있음
- → 데이터를 읽은 후 다른 트랜잭션이 롤백하면 잘못된 데이터를 읽은 상태가 됨
- READ COMMITED
- 다른 트랜잭션이 커밋된 데이터만 읽을 수 있음
- 일반적으로 사용되는 격리 수준
- REPEATABLE READ
- 트랜잭션 동안 읽은 데이터가 다른 트랜잭션에 의해 변경되지 않도록 보장
- SERIALIZABLE
- 가장 높은 수준의 격리, 모든 트랜잭션을 순차적으로 실행하여 트랜잭션 간 간섭을 완전히 방지
READ UNCOMMITTED ❌ ❌ ❌ 낮음 READ COMMITTED ✅ ❌ ❌ 중간 REPEATABLE READ ✅ ✅ ❌ 높음 SERIALIZABLE ✅ ✅ ✅ 매우 높음 - READ UNCOMMITTED
- D : Durability - 지속성
- 트랜잭션이 커밋되고 난 후, 시스템에 장애가 발생해도 해당 결과가 영구적으로 저장되어야함
- 트랜잭션의 성공 후 DB에 반영된 변경 사항이 손실되지 않아야 함
- A : Atomicity - 원자성
트랜잭션에서 공통적으로 보장해야 하는 것은 ACID 특성이며, 이를 위한 기능을 제공한다.
- TxType Or propagation : 트랜잭션의 전파 방식을 정의
- REQUIRED
- 기존 트랜잭션 존재 시 해당 트랜잭션에 참여, 없으면 새로운 트랜잭션 시작
- REQUIRES_NEW
- 항상 새로운 트랜잭션 생성, 기존 트랜잭션이 있을 경우 이를 일시중단
- MANDATORY
- 기존 트랜잭션이 반드시 있어야함, 없는 경우 예외 발생
- SUPPORTS
- 트랜잭션이 있으면 트랜잭션 내에서, 없으면 트랜잭션 없이 실행
- NOT_SUPPORTED
- 트랜잭션이 없을 때 실행, 기존 트랜잭션이 있다면 일시 중단
- NEVER
- 트랜잭션이 없어야 실행, 트랜잭션 존재 시 예외 발생
- REQUIRED
- rollbackOn
- 해당 속성에 지정된 예외 발생 시 트랜잭션 롤백
- dontRollbackOn
- 해당 속성에 지정된 예외는 롤백 x
2. @Transactional을 사용해야하는 순간
- 여러 데이터베이스 조작이 하나의 단위로 이루어져야하는 경우
- 여러 테이블에 데이터를 삽입하거나 업데이트할 때 모든 작업이 성공해야 하는 경우
- @Transactional public void transferMoney(Account from, Account to, BigDecimal amount) { from.decreaseBalance(amount); to.increaseBalance(amount); accountRepository.save(from); accountRepository.save(to); }
- 복잡한 비즈니스 로직 처리하는 경우
- 데이터의 상태가 일관성을 유지해야 하며, 로직의 일부라도 실패하면 이전 상태 롤백을 위해 필요
- 쓰기 작업이 포함되는 경우
- insert, update, delete와 같은 쓰기 작업 진행 시 필요
- 해당 어노테이션을 통해 데이터의 무결성을 보장
- Lazy 로딩된 데이터가 필요한 경우
- Lazy 로딩된 엔티티를 조회할 때 해당 어노테이션이 있어야 엔티티와 연관된 데이터를 트랜잭션 범위 내에서 로드 가능
@Transactional(readOnly = true) public User getUserWithOrders(Long userId) { User user = userRepository.findById(userId).orElseThrow(() -> new UserNotFoundException()); user.getOrders().size(); // Lazy 로딩 return user; }
- 롤백이 필요한 경우
- 예외 상황 발생 시 롤백 필요한 경우 사용
3. @Transactional을 사용하지 말아야하는 순간
- 단순 읽기 전용 작업에 대해서 사용 최소화하는 경우
- 성능을 위해 트랜잭션의 오버헤드를 줄임
@Transactional(readOnly = true) public List<User> getAllUsers() { return userRepository.findAll(); }
- 단순 CRUD 작업의 경우
- 기본 CRUD의 경우 Repository 계층에서 자동으로 관리하므로 굳이 서비스 계층에서 해당 어노테이션을 사용할 필요는 없음
- 외부 API호출과 같은 비데이터베이스 작업하는 경우
- 트랜잭션의 경우 주로 데이터베이스 상태를 관리할 때 사용
- 외부 API를 사용하는 경우 불필요하게 트랜잭션 경계를 만들 필요가 없음
- 데이터 읽기 작업 중 예외 처리에 따라 롤백이 필요 없는 경우
- 조회만 수행 및 예외가 발생해도 롤백할 데이터가 필요없는 경우 해당 어노테이션은 불필요
- ex : 캐시에서 데이터를 가져오거나 단순 로깅하는 작업
- 배치 작업에서 부분적으로 커밋이 필요한 경우
- 큰 데이터 셋을 처리하는 배치 작업의 경우 하나의 트랜잭션 설정 시 메모리 사용량 증가
- → 트랜잭션을 여러 번 나눠서 사용하는 것이 좋음
3. 결론
기존에는 모든 비즈니스 로직을 처리하는 서비스 레이어의 모든 메서드에 트랜잭션을 삽입하였지만, 이에 따라 성능 저하가 발생할 수 있다는 것을 알 수 있었다. 그렇기에 알아본 결과를 팀원과 공유하였고 이에 따라 프로젝트 내에서는 다음과 해당 상황에서 트랜잭션을 적용하기로 결정하였다.
프로젝트 상황은 다음과 같으며 회의 결과는 다음과 같았다.
- 각 엔티티들은 서로가 서로의 연관관계로 이루어져있다.
- 엔티티 별로 서로를 호출하는 로직들이 다수 존재한다
- 레시피 - foodInformation, cookStep, Ingredient
- 리뷰 - (Recipe - RecipeReview) , (User - UserReview)
- User - Review, Recipe, Chatting
- 트랜잭션에 대해 알아본 결과에 맞춰서 다른 엔티티를 조회, Lazy로 가져오는 경우 Transactional 어노테이션 삽입
- 단일 엔티티에서 필요한 컬럼만 가져와서 읽는 경우 @Transactional(readOnly = true) 처리 진행
즉 읽기 작업이어도 여러 엔티티를 불러서 호출하는 경우에는 어노테이션 삽입을 진행하고 만약 단일 엔티티에서 처리할 수 있는 값으로 이루어진 읽기 작업이면 어노테이션을 제거하기로 하였다. 또한 기본적인 CRUD의 경우 Repository 계층에서 처리를 해준다고 하지만 단일 엔티티만 CRUD하는 경우가 없기에 해당 경우는 넘어가기로 했다.
'DataBase > JPA' 카테고리의 다른 글
[ETC] Fetch 옵션 (0) | 2023.08.28 |
---|---|
[JPA] 엔티티 매핑 (0) | 2022.12.21 |
[JPA] 영속성 관리 (0) | 2022.12.19 |