Spring 입문주차/2주차

11. SpringBoot의 JPA

note994 2024. 8. 28. 02:28

Memo 프로젝트를 연다.

 

build.gradle의 dependency에 아래의 코드를 추가한다. 그리고 코끼리 새로고침

// JPA 설정
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

 

application.properties로 이동 후 아래의 코드 추가

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

 

Memo.java의 기존 코드를 전부 지우고 아래의 코드를 복붙

package com.sparta.memo.entity;

import com.sparta.memo.dto.MemoRequestDto;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Entity // JPA가 관리할 수 있는 Entity 클래스 지정
@Getter
@Setter
@Table(name = "memo") // 매핑할 테이블의 이름을 지정
@NoArgsConstructor
public class Memo {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(name = "username", nullable = false)
    private String username;
    @Column(name = "contents", nullable = false, length = 500)
    private String contents;

    public Memo(MemoRequestDto requestDto) {
        this.username = requestDto.getUsername();
        this.contents = requestDto.getContents();
    }

    public void update(MemoRequestDto requestDto) {
        this.username = requestDto.getUsername();
        this.contents = requestDto.getContents();
    }
}

 

SpringBoot 환경에서는 EntityManagerFactory와 EntityManager를 자동으로 생성해준다.

 

application.properties에 DB 정보를 전달해 주면 이를 토대로 EntityManagerFactory가 생성된다.

 

테스트 폴더에 TransactionTest 클래스 파일을 만든다.

package com.sparta.memo;

import com.sparta.memo.entity.Memo;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.Rollback;
import org.springframework.transaction.annotation.Transactional;

@SpringBootTest
public class TransactionTest {
    @PersistenceContext
    EntityManager em;

    @Test
    @Transactional
    @Rollback(value = false) // 테스트 코드에서 @Transactional 를 사용하면 테스트가 완료된 후 롤백하기 때문에 false 옵션 추가
    @DisplayName("메모 생성 성공")
    void test1() {
        Memo memo = new Memo();
        memo.setUsername("Robbert");
        memo.setContents("@Transactional 테스트 중!");

        em.persist(memo);  // 영속성 컨텍스트에 메모 Entity 객체를 저장합니다.
    }
}
@PersistenceContext
EntityManager em;

이 코드가 있으면 스프링부트에서 자동 생성된 EntityManager를 주입받아 사용할 수 있다.


트랜잭션 테스트 : 메모 생성 실패

@Test
@DisplayName("메모 생성 실패")
void test2() {
    Memo memo = new Memo();
    memo.setUsername("Robbie");
    memo.setContents("@Transactional 테스트 중!");

    em.persist(memo);  // 영속성 컨텍스트에 메모 Entity 객체를 저장합니다.
}

이 코드는 트랜잭션이 적용되어있지 않기 때문에 메모 생성 실패한다.

 

@Disabled 애노테이션을 적용하면 테스트 함수로 취급하지 않기 때문에 오류가 발생해도 그냥 넘어간다.


영속성 컨텍스트와 트랜잭션의 생명주기

스프링 컨테이너 환경에서는 영속성 컨텍스트와 트랜잭션의 생명주기가 일치한다.

쉽게 설명하면 트랜잭션이 유지되는 동안은 영속성 컨텍스트도 계속 유지가 되기 때문에 영속성 컨텍스트의 기능을 사용할 수 있다.

따라서 앞에서 작성한 테스트 코드 메서드에 트랜잭션이 적용되지 않았기 때문에 영속성 컨텍스트가 유지되지 못해 오류가 발생했다.

Spring은 어떻게 Service부터 Repository까지 Transaction을 유지할 수 있는 걸까?

Service의 트랜잭션이 적용된 메서드에서 Repository의 메서드를 호출할 때 무언가 처리되고 있는걸까?

Spring에서는 이러한 상황에서 트랜잭션을 제어할 수 있도록 트랜잭션 전파 기능을 제공하고 있다.


트랜잭션 전파

MemoRepository 클래스로 이동

 

가장 하단에 아래의 코드 복붙

@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;
}

 

TransactionTest 테스트 클래스로 이동 후 전부 지우고 아래의 코드 삽입

package com.sparta.memo;

import com.sparta.memo.entity.Memo;
import com.sparta.memo.repository.MemoRepository;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.Rollback;
import org.springframework.transaction.annotation.Transactional;

@SpringBootTest
public class TransactionTest {

    @PersistenceContext
    EntityManager em;

    @Autowired
    MemoRepository memoRepository;

    @Test
    @Transactional
    @Rollback(value = false) // 테스트 코드에서 @Transactional 를 사용하면 테스트가 완료된 후 롤백하기 때문에 false 옵션 추가
    @DisplayName("메모 생성 성공")
    void test1() {
        Memo memo = new Memo();
        memo.setUsername("Robbert");
        memo.setContents("@Transactional 테스트 중!");

        em.persist(memo);  // 영속성 컨텍스트에 메모 Entity 객체를 저장합니다.
    }

    @Test
    @Disabled
    @DisplayName("메모 생성 실패")
    void test2() {
        Memo memo = new Memo();
        memo.setUsername("Robbie");
        memo.setContents("@Transactional 테스트 중!");

        em.persist(memo);  // 영속성 컨텍스트에 메모 Entity 객체를 저장합니다.
    }

    @Test
    @Transactional
    @Rollback(value = false)
    @DisplayName("트랜잭션 전파 테스트")
    void test3() {
        memoRepository.createMemo(em);
        System.out.println("테스트 test3 메서드 종료");
    }
}

TransactionTest

지금 @Autowired를 통해 MemoRepository를 주입받았다.

TransactionTest

test3 메서드에서 memoRepository에 있는 createMemo 메서드를 호출하고 있다. 즉 test3는 부모 메서드라고 한다.

 

createMemo는 자식 메서드다.

MemoRepository 클래스

기본기(ID) 값이 1인 데이터를 찾고 변경감지가 일어나면서 업데이트 쿼리가 발생한다.

 

원래라면 자식 메서드의 트랜잭션이 종료되면 다 종료되어야 하지만 지금은 부모 트랜잭션과 합쳐져서 부모 메서드가 끝나야 트랜잭션이 종료된다.

 

MemoRepository 클래스

@Transactional에는 propagation 이라는 옵션이 있는데 디폴트 값은 Propagation.REQUIRED 이다.

 

부모 메서드에 @Transactional이 존재한다면 자식 메서드의 @Transactional은 부모 메서드와 합류한다. 는 뜻이다.

 

부모 메서드의 @Transactional을 제거한다면, 자식 메서드에서 트랜잭션 처리가 모두 이루어진다. 즉, 부모 메서드는 트랜잭션에 아무 관련이 없어진다.