velog에서 이전한 글 입니다.

 

2023.07.13 - [spring] - 스프링) JPA persistence/트랜잭션/Entity상태 (23-07-08)

앞의 포스팅에서 jpa를 다루던 것은 hibernate-core이다.

implementation 'org.hibernate:hibernate-core:6.1.7.Final'

이번엔 spring boot에서 jpa를 어떻게 다루는지 살펴보자.

Spring에서 JPA

implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

data-jpa 를 사용한다. 그냥 java에선 xml파일로 설정하고 EntityManager, EntityManagerFactory 를 직접 만들었지만 Springboot 환경에선 application.properties에 DB 정보를 전달해 주면 이를 토대로 ManagerFactory가 자동 생성되고

@PersistenceContext
EntityManager em;

@PersistenceConext 애너테이션을 사용하면 자동으로 생성된 EntityManager를 주입받아 사용할 수 있다.

@PersistenceContext
EntityManager em;

@Test
@Transactional 
@Rollback(value = false) // @SpringBootTest에서 @Transactional를 사용하면 기본값은 테스트가 완료된 후 롤백이다.
@DisplayName("메모 생성 성공")
void test1() {
    Memo memo = new Memo();
    memo.setUsername("Robbert");
    memo.setContents("@Transactional 테스트 중!");

    em.persist(memo);
}

core에선 직접 트랜잭션을 생성/시작/종료 했지만 Spring에선 @Transactional 애노테이션을 사용해 쉽게 트랜잭션을 사용한다. 트랜잭션을 import할 때 jakarta의 @Transactional도 자동 완성에 보이는데 일단 스프링 환경에선 jakarta 트랜잭션도 같은 결과를 보인다.

@Transactional

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

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

        em.persist(memo);
    }

위의 코드는 에러가 발생한다. 스프링 컨테이너 환경에서는 영속성 컨텍스트와 트랜잭션의 생명주기가 일치하기 때문이다. 영속성 컨텍스트를 사용하려면 @Transactional을 사용해야 한다.

트랜잭션 전파

@Transactional(propagation = Propagation.REQUIRED)

전파의 기본 옵션은 REQUIRED로 부모 메서드에 트랜잭션이 존재하면 자식 메서드의 트랜잭션은 부모의 트랜잭션에 합류한다는 옵션이다.

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

(createMemo)
@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;
}

위 코드를 실행하면 부모 메서드가 종료된 후 커밋될 때 update가 실행됨을 확인할 수 있다. @Transactional 어노테이션이 없다면 자식메서드가 종료된 후 커밋되면서 update가 동작하고 부모 메서드가 종료되는 것을 볼 수 있다.

Spring Data JPA

Spring Data JPA는 JPA를 추상화시킨 Repository 인터페이스를 제공한다.
Repository 인터페이스는 Hibernate와 같은 JPA구현체를 사용해서 구현한 클래스를 통해 사용된다. (위로 갈수록 추상화고 아래일수록 구현체이다.)

SimpleJpaRepository

Spring Data JPA에서는 JpaRepository 인터페이스를 구현하는 클래스를 자동으로 생성해준다.
Spring 서버가 시작될 때 JpaRepository 인터페이스를 상속받은 인터페이스가 자동으로 스캔이 되면, 해당 인터페이스의 정보를 토대로 자동으로 SimpleJpaRepository 클래스를 생성해 주고, 이 클래스를 Spring Bean 으로 등록한다.
따라서 인터페이스의 구현 클래스를 직접 작성하지 않아도 JpaRepository 인터페이스를 통해 JPA의 기능을 사용할 수 있다.

public interface PostRepository extends JpaRepository<Post, Long> {
}

다음과 같이 JpaRepository<Entity, Entity 식별자 데이터 타입> 인터페이스를 상속받기만 하면 자동으로 기본기능들을 구현한 클래스를 생성하고 Bean 으로 관리한다.

(SimpleJpaRepository.java)
    @Transactional
    @Override
    public <S extends T> S save(S entity) {

        Assert.notNull(entity, "Entity must not be null");

        if (entityInformation.isNew(entity)) {
            em.persist(entity);
            return entity;
        } else {
            return em.merge(entity);
        }
    }

구현 클래스를 들어가보면 앞선 포스팅에서 hibernate-core 를 사용해 db에 저장했던 코드와 비슷한 것을 볼 수 있다.

Post savePost = postRepository.save(post);

이제는 직접 EntityManager를 사용해 작성하던 코드를 한줄로 사용할 수 있다.

@Transactional
public Long updateMemo(Long id, MemoRequestDto requestDto) {
    Memo memo = findMemo(id);
    // memo 내용 수정
    memo.update(requestDto);
    return id;
}

SimpleJpaRepository에 update라는 메서드는 존재하지 않고 영속성 컨텍스트의 변경감지를 통해 update를 진행한다. 영속성 컨텍스트와 트랜잭션의 생명주기는 일치하므로 update를 수행하기 위해선 @Transactional을 추가해야 한다.

    @Override
    @Transactional
    @SuppressWarnings("unchecked")
    public void delete(T entity) {

        Assert.notNull(entity, "Entity must not be null");

        if (entityInformation.isNew(entity)) {
            return;
        }

        Class<?> type = ProxyUtils.getUserClass(entity);

        T existing = (T) em.find(type, entityInformation.getId(entity));

        // if the entity to be deleted doesn't exist, delete is a NOOP
        if (existing == null) {
            return;
        }

        em.remove(em.contains(entity) ? entity : em.merge(entity));
    }

SimpleJpaRepository에서 구현하는 delete를 보면 @Transactional을 볼 수 있다. 즉 구현 클래스도 영속성 컨텍스트의 변경 감지로 수정/삭제한다.
다만 update는 구현된 메서드가 아니므로 service에서 직접 작성할 때 @Transactional을 넣어줘야 하는 것을 잊지말자.

+ case

@Transactional
public Long saveMemos(Long id, MemoRequestDto requestDto) {
    Memo memo1 = new Memo();
    Memo memo2 = new Memo();
    Memo memo3 = new Memo();
    ...
    memo.save(memo1);
    ...
    memo.save(memo2);
    ...
    memo.save(memo3);
    return id;
}

save에는 @Transactional이 적용되어 있지만 위와 같은 경우에 @Transactional이 필요하다.
3개의 save 중간에 문제가 발생했을 때 전부Rollback 시킬 필요가 있다면 트랜잭션 전파를 이용해 부모의 트랜잭션에 합류시키고 하나의 트랜잭션으로 처리해야한다.