트랜잭션은 데이터베이스 작업에서 데이터의 일관성을 유지하고 오류 발생 시 롤백을 보장하는 중요한 개념입니다. Spring Framework는 이러한 트랜잭션 관리를 단순화하기 위해 @Transactional 어노테이션을 제공합니다.
그중에서도 @Transactional(readOnly = true)는 읽기 전용 작업에서 성능을 최적화할 수 있는 옵션으로, 데이터 조회를 최적화하고 불필요한 쓰기 락을 방지하는 데 유용합니다.
@Transactional(readOnly = true)란 무엇인가?
- 기본 정의:
- readOnly = true는 트랜잭션을 읽기 전용 모드로 설정하는 옵션입니다. 이는 데이터 변경 없이 조회 작업만 수행할 때 사용됩니다.
- 기본 동작:
- 데이터 조회 성능 최적화: Hibernate와 같은 ORM에서는 읽기 전용 트랜잭션일 경우 더 간단한 SQL을 생성할 수 있습니다.
- 쓰기 락 방지: 데이터베이스 수준에서 읽기 전용 트랜잭션으로 처리되므로 쓰기 작업을 시도하면 오류를 유발할 수 있습니다.
@Service
public class UserService {
// 이렇게 일반 트렌젝션과 구분
@Transactional(readOnly = true)
public List<User> getAllUsers() {
return userRepository.findAll();
}
@Transactional
public User updateUser(Long id, UserUpdateRequest request) {
User user = userRepository.findById(id).orElseThrow(() -> new UserNotFoundException());
user.update(request);
return userRepository.save(user);
}
}
왜 사용해야 할까? (장점)
1) 데이터 무결성 보장
- 읽기 전용 트랜잭션은 데이터 수정이 발생하지 않도록 보장합니다.
- 예를 들어, 잘못된 코드로 인해 데이터가 변경되는 것을 예방할 수 있습니다.
2) 성능 최적화
- Hibernate의 1차 캐시 관리가 간소화됩니다.
- 쓰기 락이 제거되어 데이터베이스 작업이 더 빠르게 처리됩니다.
3) 코드 가독성 향상
- 개발자에게 해당 메서드가 데이터 변경이 없는 안전한 조회 작업임을 명확히 알려줍니다.
주의사항
- 데이터 수정 불가
- readOnly = true가 설정된 트랜잭션에서는 데이터베이스 변경 작업이 제한됩니다. 예를 들어, EntityManager.persist()를 호출하면 오류가 발생할 수 있습니다.
- 적용 범위 확인
- @Transactional(readOnly = true)는 선언된 범위에서만 적용됩니다. 즉, 같은 클래스 내 다른 메서드가 호출되면 새로운 트랜잭션 설정이 필요할 수 있습니다.
- 데이터베이스 지원 여부
- 일부 데이터베이스에서는 readOnly 트랜잭션을 명시적으로 지원하지 않을 수 있으므로 사용 전에 확인이 필요합니다.
@Transactional(readOnly = true)는 데이터 조회 성능을 최적화하고, 데이터 변경을 방지하여 코드 안정성을 높이는 데 효과적인 도구입니다. 읽기 전용 작업이 많은 서비스라면 적극적으로 활용해보세요. 그러나 상황에 따라 적절히 사용해야 하며, 데이터베이스와 프레임워크의 동작 방식을 잘 이해하는 것이 중요합니다
솔직히 얼마나 성능이 개선되는지궁금해서 성능을 비교해 놓은 그래프나 자료들을 검색했는데 없어서 직접 테스트를 해봤습니다.
작성한 테스트 코드의 한계 (감안해서 이해해주세요 ㅎ):
- 데이터베이스 작업 대신 메모리에서 데이터를 생성하므로, 실제 데이터베이스와 Hibernate의 동작을 반영하지 못합니다.
- 단순 문자열 리스트를 사용하므로, 복잡한 연관 관계가 있는 엔티티 구조에서 발생하는 성능 차이를 반영하기 어렵습니다.
- 데이터 크기가 상대적으로 작아 성능 차이가 미미할 수 있습니다.
예상 결과는 의외로 @Transactional(readOnly = true)가 조금 느리게 나왔습니다.
이번 테스트 결과를 바탕으로 다음과 같은 결론을 추측할 수 있었습니다.
위의 결과와 이론적인 부분을 참고하면 @Transactional(readOnly = true)는 복잡한 GET 요청이나 대규모 데이터 조회 작업에서 더 큰 성능 차이를 보일 가능성이 높습니다. 이 어노테이션은 Hibernate의 변경 감지(Dirt Checking)를 비활성화하여 CPU 부하를 줄이고, 데이터베이스 쓰기 락을 방지하여 병렬 처리 성능을 향상시킬 수 있습니다. 특히, 여러 테이블 간 조인이나 연관 관계가 많은 데이터 조회 작업에서 이러한 최적화 효과는 더욱 두드러질 것으로 예상됩니다.
반면, 단순한 읽기 작업이나 작은 규모의 프로젝트에서는 성능 차이가 거의 없거나 미미할 수 있습니다. 이러한 경우, @Transactional(readOnly = true)의 주요 이점은 성능보다는 코드의 가독성과 명확성에 있습니다. 이 어노테이션은 메서드가 데이터를 변경하지 않는다는 의도를 명확히 전달하여 코드 품질을 높이고, 유지보수를 용이하게 만듭니다.
결론적으로, 프로젝트의 규모와 작업의 복잡성에 따라 @Transactional(readOnly = true)를 사용하는 것이 적합합니다. 복잡한 데이터 조회 작업에서는 성능 최적화의 이점이 두드러질 수 있으며, 단순한 작업에서도 코드의 의도를 명확히 드러내는 데 유용합니다.
사용한 테스트 코드:
//서비스
@PersistenceContext
private EntityManager entityManager;
@Transactional(readOnly = true)
public List<String> readOnlyOperation() {
List<String> data = generateMockData();
entityManager.clear(); // Hibernate 1차 캐시 초기화
return data;
}
@Transactional
public List<String> regularOperation() {
List<String> data = generateMockData();
entityManager.clear(); // Hibernate 1차 캐시 초기화
return data;
}
private List<String> generateMockData() {
List<String> data = new ArrayList<>();
for (int i = 0; i < 100000; i++) {
data.add("Mock Data " + i);
}
return data;
}
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class TranctionalTest {
@Autowired
private NoticeService noticeService;
@Test
public void compareTransactionalPerformance() {
int iterations = 10; // 반복 실행 횟수
long readOnlyTotal = 0;
long regularTotal = 0;
for (int i = 0; i < iterations; i++) {
// Read-only 트랜잭션 테스트
long startReadOnly = System.nanoTime();
noticeService.readOnlyOperation();
long endReadOnly = System.nanoTime();
readOnlyTotal += (endReadOnly - startReadOnly);
// 일반 트랜잭션 테스트
long startRegular = System.nanoTime();
noticeService.regularOperation();
long endRegular = System.nanoTime();
regularTotal += (endRegular - startRegular);
}
// 결과 출력
System.out.println("Read-only Transaction Average: " + (readOnlyTotal / iterations) + " ns");
System.out.println("Regular Transaction Average: " + (regularTotal / iterations) + " ns");
}
}
'Spring' 카테고리의 다른 글
[JPA] JPA로 페이지네이션 구현하기 (0) | 2025.01.04 |
---|---|
JPA (Java Persistence API)란? (0) | 2024.12.10 |
Spring의 7가지 요청 데이터 처리 어노테이션 (0) | 2024.12.07 |
Spring Boot에서 H2 Console 'localhost에서 연결을 거부했습니다' 에러 해결 (1) | 2024.12.06 |
[Spring] Optional의 활용: Repository, Service, 그리고 ResponseEntity의 역할 (0) | 2024.12.06 |