앞선 "[JPA] 영속성 컨텍스트란 무엇일까❓"포스팅에서 알아보았듯이 EntityManagerFactory를 만들기 위해서는 DB에 대한 정보를 전달해야 했습니다.
Spring boot 의 JPA 환경 설정
1. build.gradle
// JPA 설정
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
build.gradle에 위와 같이 코드를 추가합니다.
dependencies {
// JPA
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
// Thymeleaf
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
// web
implementation 'org.springframework.boot:spring-boot-starter-web'
// lombok
compileOnly 'org.projectlombok:lombok'
// devTools
developmentOnly 'org.springframework.boot:spring-boot-devtools'
//MySql
runtimeOnly 'mysql:mysql-connector-java'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
2. DB정보 추가
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.use_sql_comments=true
application.properties에 DB 정보를 전달해 주면 이를 토대로 EntityManagerFactory와 EntityManager를 자동으로 생성됩니다.
Spring boot에서 자동으로 생성해주는 EntityManager를 주입(DI)받아오고 싶을 땐 @PersistenceContext 어노테이션을 사용하면됩니다.
@Transaction 어노테이션
@Transactional 어노테이션을 클래스나 메서드에 추가하면 쉽게 트랜잭션 개념을 적용할 수 있고, 트랜잭션의 범위와 속성을 정의합니다.
JPA를 사용하여 DB에 데이터를 저장, 수정, 삭제하려면 트랜잭션 적용이 반드시 필요합니다. 조회 작업은 단순하게 데이터를 읽기만 하기 때문에 트랜잭션이 적용이 필수는 아닙니다.
Spring은 이 어노테이션을 사용하여 메서드가 호출될 때 트랜잭션을 시작하고, 메서드 실행이 완료되면 트랜잭션을 커밋하거나 예외 발생 시 롤백합니다.
또한 @Transaction 어노테이션을 추가한 메서드를 호출하면 해당 메서드 내에서 수행되는 모든 DB 연산 내용은 하나의 트랜잭션으로 묶입니다.
@Transaction 주요 옵션
1. readOnly
@Transactional(readOnly = true)
public List<User> getAllUsers() {
// 데이터 읽기
}
readOnly 옵션은 트랜잭션이 읽기 전용인지 여부를 설정합니다. 기본값은 flase으로, readOnly옵션을 true로 설정하면 읽기 작업에 대한 최적화를 수행할 수 있습니다.
2. propagation
propagaion 옵션은 트랜잭션의 전파 방식을 설정합니다. 기본값은 REQUIRED으로, 이미 존재하면 기존 트랙잭션을 사용하고, 존재하지 않는다면 새로운 트랜잭션을 시작합니다.
- 선택 가능한 값
- REQUIRED
- 기본값, 기존 트랜잭션을 사용하거나 새 트랜잭션을 시작합니다.
- REQUIRES_NEW
- 항상 새로운 트랜잭션을 시작합니다.
- SUPPORTS
- 기존 트랜잭션이 있으면 이를 지원하고, 없으면 트랜잭션 없이 실행합니다.
- NOT_SUPPORTED
- 트랜잭션 없이 실행하며, 트랜잭션이 존재하면 이를 일시 중단합니다.
- NEVER
- 트랜잭션 없이 실행하며, 트랜잭션이 존재하면 예외를 발생시킵니다.
- MANDATORY
- 기존 트랜잭션이 반드시 있어야 하며, 없으면 예외를 발생시킵니다.
- REQUIRED
3. isolation
isolation 옵션은 트랜잭션의 격리 수준을 설정합니다. 데이터베이스의 격리 수준을 정의하여 동시성 문제를 조절할 수 있습니다.
- 선택 가능한 값
- DEFAULT
- 데이터베이스의 기본 격리 수준을 사용합니다.
- READ_UNCOMMITTED
- 읽기 미확정, 낮은 격리 수준.
- READ_COMMITTED
- 읽기 확정, 일반적으로 사용되는 수준.
- REPEATABLE_READ
- 반복 가능한 읽기, 높은 격리 수준.
- SERIALIZABLE
- 직렬화 가능, 가장 높은 격리 수준.
- DEFAULT
4. timout
@Transactional(timeout = 10)
public void longRunningTransaction() {
// 트랜잭션이 10초 이상 걸리면 예외 발생
}
timout 옵션은 트랜잭션의 최대 실행 시간을 설정합니다. 트랜잭션이 이 시간 내에 완료되지 않으면 예외가 발생합니다.
5. rollbackFor
rollbackFor 옵션은 지정한 예외가 발생하면 트랜잭션을 롤백합니다.
영속성 컨텍스트와 트랜잭션 생명주기
스프링 컨테이너에서는 영속성 컨텍스트와 트랜잭션 생명주기가 일치합니다. 이는 트랜잭션이 시작될 때 영속성 컨텍스트가 생성되고, 트랜잭션이 커밋되거나 롤백될 때 영속성 컨텍스트도 함께 종료되기 때문입니다.
@Test
@DisplayName("메모 생성 실패")
void test2() {
Memo memo = new Memo();
memo.setUsername("ZINU");
memo.setContents("@Transactional 테스트 중!");
em.persist(memo); // 영속성 컨텍스트에 메모 Entity 객체를 저장합니다.
}
위 테스트 코드를 실행한다면 어떠한 결과가 나올까요❓
결과는 에러가 발생합니다. 왜냐하면 앞서 말했듯이 영속성 컨텍스트와 트랜잭션 생명주기는 일치하기 때문에 트랜잭션이 적용되지 않았고, persist() 메서드로 엔티티를 영속성 컨텍스트에 추가하지 못해 오류가 발생하기 때문입니다.
영속성 컨텍스트와 트랜잭션의 생명주기를 그림으로 표현한다면 위와 같이 표현할 수 있습니다.
따라서 JPA를 사용하여 DB에 데이터를 저장, 수정, 삭제하려면트랜잭션 적용이 반드시 필요합니다.
트랜잭션 전파(propagation : REQUIRED)
앞선 "영속성 컨텍스트와 트랜잭션 생명주기" 그림에서 보면 Service부터 Repository까지 Transaction을 유지할 수 있는 걸까요❓
이는 @Transaction 어노테이션의 propagation REQUIRED옵션 때문입니다.
propagation REQUIRED옵션을 세부적으로 살펴본다면 다음과 같습니다.
- 기존 트랜잭션이 있을 때
- 만약 현재 메서드가 호출될 때 이미 트랜잭션이 존재하면, REQUIRED 옵션은
새로운 트랜잭션을 시작하지 않고, 기존의 트랜잭션 콘텍스트를 사용하여 메서드를 실행합니다.- 즉, 부모 메서드에 트랜잭션이 존재하면 자식 메서드의 트랜잭션은 부모의 트랜잭션에 합류하게 됩니다. (자식 ➡️ 부모)
- 만약 현재 메서드가 호출될 때 이미 트랜잭션이 존재하면, REQUIRED 옵션은
- 기존 트랜잭션이 없을 때
- 만약 현재 메서드가 호출될 때 트랜잭션이 존재하지 않으면, REQUIRED 옵션은 새로운 트랜잭션을 시작합니다. 이 새로운 트랜잭션은 메서드의 실행 동안 유지되며, 메서드가 완료되면 커밋되거나 롤백됩니다.
- 즉,
트랜잭션이 전혀 없는 상태에서 REQUIRED 옵션이 있는 메서드가 호출되면, 그 메서드는 새 트랜잭션을 생성하고, 이 트랜잭션 컨텍스트 내에서 실행됩니다.
- 즉,
- 만약 현재 메서드가 호출될 때 트랜잭션이 존재하지 않으면, REQUIRED 옵션은 새로운 트랜잭션을 시작합니다. 이 새로운 트랜잭션은 메서드의 실행 동안 유지되며, 메서드가 완료되면 커밋되거나 롤백됩니다.
트랜잭션 전파(propagation : REQUIRED) 동작 방식
@Transactional
public Memo createMemo(EntityManager em) {
Memo memo = em.find(Memo.class, 1);
memo.setUsername("Robbie");
memo.setContents("@Transactional 전파 테스트 중!");
System.out.println("createMemo 메서드 종료");
return memo;
}
////////////////////////////////////////////////////////////////////////////////
@Test
@Transactional
@Rollback(value = false)
@DisplayName("트랜잭션 전파 테스트")
void test3() {
memoRepository.createMemo(em);
System.out.println("테스트 test3 메서드 종료");
}
보기 용이하게 두 코드를 합쳐놨습니다. 위 코드를 보면 test3 메서드에서 MemoRepository에 있는 createMemo메서드를 호출했습니다.
이때 부모 메서드와 자식 메서드는 다음과 같습니다.
- 부모 메서드 ➡️ test3 메서드
- 자식 메서드 ➡️ createMemo 메서드
현재 find() 메서드를 통해 새롭게 Entity를 데이터 베이스에 저장을 합니다. 그러면 현재 데이터 베이스에는 (username: Robbert, contests : @Transactional 테스트 중!)이라는 데이터가 존재합니다.
자식 메서드인 createMemo메서드가 실행되면 기존에 "Robbert"데이터가 존재하기 때문에 변경 감지가 일어나서 최종 결과는 createMemo가 종료되면 출력문을 찍고, commit 되면서 update문이 실행될 것이라고 예상해 볼 수 있습니다.
JPA에서 영속성 컨텍스트로 관리하고 있는 변경이 발생한 객체들의 정보를 쓰기 지연 저장소에 전부 가지고 있다가 마지막에 SQL을 한 번에 DB에 요청해 변경을 반영.
그러나 위 실행결과를 보면 자식 메서드 createMemo가 종료될 때 update가 실행되는 것이 아니라 부모 메서드에 트랜잭션이 합류되면서 부모 메서드의 출력문 "테스트 test3 메서드 종료"가 실행되고 부모 메서드가 종료된 후 그제야 트랜잭션이 커밋될 때 update가 실행된 것을 확인할 수 있습니다.
@Transactional @Rollback(value = false)
부모 메서드 test3의 위 코드를 주석 처리하고 다시 한번 update를 시도해 보면 합류할 부모 트랜잭션이 없기 때문에 처음 예상대로 자식 메서드가 종료된 후 트랜잭션이 커밋되면서 update가 실행된 것을 확인할 수 있습니다.
'Framework > JPA' 카테고리의 다른 글
[JPA] JPA Auditing란 무엇일까❓(@EnableJpaAuditing, @MappedSuperclass, @EntityListeners) (0) | 2024.07.27 |
---|---|
[JPA] JPA의 복잡함을 줄이는 Spring Data JPA (0) | 2024.07.27 |
[JPA] JPA Entity 상태 (비영속, 영속, 준영속, 삭제 상태의 이해) (0) | 2024.07.27 |
[JPA] 영속성 컨텍스트란 무엇일까❓ #2(1차 캐시, 변경 감지, 쓰기지연 감소) (0) | 2024.07.27 |
[JPA] 영속성 컨텍스트란 무엇일까❓ (0) | 2024.07.26 |