두 손끝의 창조자

JPA Test를 위한 @BeforeEach 와 트랜잭션 분리 본문

JPA

JPA Test를 위한 @BeforeEach 와 트랜잭션 분리

codinglog 2022. 3. 25. 17:04

구조

Car - Body < Part

구조가 있을 때, 즉 CarBody 와 1:1,BodyPart 는 1:n 관계가 있다.

JPA에서는 모두 양방향 참조로 Car에서 Part 까지 접근 가능하고, 연관관계 주인은 Car -Body 에서는Body 가,Body < Part 에서는Part 가 가지고 있다.

엔티티 저장을 편리하게 하기위해서 모든 OneToXXX 는 cascade 모드를 ALL 로 하였다.

각 엔티티는 Audit 처리를 위해 Audit MappedSuperclass를 상속하고 이 클래스는 int 타입의 version 필드를 가지고 있다.

Audit 엔티티에 @EntityListeners 를 등록해서 Create, Update 시 버전을 올릴려고 AuditListener 을 구현하고 참조한다.

Car

package hellojpa.car;

import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.OneToOne;

import static javax.persistence.CascadeType.ALL;

@Entity
public class Car extends Audit {
    @Id
    private Long id;
    @OneToOne(mappedBy = "car", cascade = ALL)
    private Body body;

    public Car() {
    }

    public Car(Long id) {
        this.id = id;
    }

    public Body getBody() {
        return body;
    }

    public void setBody(Body body) {
        this.body = body;
        body.setCar(this);
    }
}

Body

package hellojpa.car;

import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;

import static javax.persistence.CascadeType.ALL;

@Entity
public class Body extends Audit {
    @Id
    @GeneratedValue
    private Long id;
    @OneToMany(cascade = ALL, mappedBy = "body")
    private List<Part> parts = new ArrayList<>();

    @OneToOne
    private Car car;

    public void setCar(Car car) {
        this.car = car;
    }

    public void addPart(Part part) {
        this.parts.add(part);
        part.setBody(this);
    }

    public List<Part> getParts() {
        return parts;
    }
}

Part

package hellojpa.car;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.ManyToOne;

@Entity
public class Part extends Audit {
    @Id
    @GeneratedValue
    private Long id;
    private String name;
    @ManyToOne
    private Body body;

    public void setName(String name) {
        this.name = name;
    }

    public void setBody(Body body) {
        this.body = body;
    }
}

AuditListener

package hellojpa.car;

import javax.persistence.PrePersist;
import javax.persistence.PreUpdate;

public class AuditListener {
    @PrePersist
    public void initVersion(Audit audit) {
        audit.setVersion(0);
    }

    @PreUpdate
    public void increaseVersion(Audit audit) {
        audit.setVersion(audit.getVersion() + 1);
    }
}

Audit

package hellojpa.car;

import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;

@MappedSuperclass
@EntityListeners(AuditListener.class)
public class Audit {
    private Integer version;

    public void setVersion(Integer version) {
        this.version = version;
    }

    public Integer getVersion() {
        return version;
    }
}

CarRepository

package hellojpa.car;

import org.springframework.data.jpa.repository.JpaRepository;

public interface CarRepository extends JpaRepository<Car, Long> {
}

문제

오딧 테스트를 위해 @BeforeEach 애노테이션으로 데이터를 넣고 @Test 애노테이션으로 테스트를 수행했다.
Car 를 리포지토리에서 가져와서 Body 를 통해 Part 를 가져왔고 Part 의 이름을 업데이트 했다.
기대하는 값은 Part 테이블의 version 필드가 1 이 되는 것이다.

그런데 Part 테이블은 정상적으로 1 로 업데이트 됐는데, Car 테이블의 버전도 1 로 바뀌었다. 기대하지 않은 업데이트다.

테스트 코드

package hellojpa;

import hellojpa.car.Body;
import hellojpa.car.Car;
import hellojpa.car.CarRepository;
import hellojpa.car.Part;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.test.annotation.Rollback;
import org.springframework.transaction.annotation.Transactional;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

@SpringBootTest
@Rollback(value = false)
@EnableJpaRepositories(basePackages = {"hellojpa"})
@Transactional
public class CarTest {
    @Autowired
    CarRepository carRepository;

    @BeforeEach
    void addProc() {
        Part part = new Part();

        Body body = new Body();
        body.addPart(part);

        Car car = new Car(1L);
        car.setBody(body);
        carRepository.save(car);
    }

    @Test
    void doThing() {
        Car car = carRepository.findById(1L).get();
        car.getBody().getParts().get(0).setName("break");
    }
}

실행결과 로그

실행 환경은 스프링부트와 Spring-data-jpa를 사용했는데 혹시 Spring-data-jpa 문제인가 해서 플래인 jpa 프로젝트를 만들어서 똑같은 엔티티를 만들고 main 함수에 엔티티 매니저를 이용해서 기초 데이터를 넣은 후 업데이트 했을때 기대한 대로 Part 테이블만 1 로 업데이트 되었다.

플래인 JPA 테스트 코드

package car;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;

public class JpaMain {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        new JpaTemplate() {
            @Override
            void run(EntityManager em) {
                Part part = new Part();

                Body body = new Body();
                body.addPart(part);

                Car car = new Car(1L);
                car.setBody(body);

                em.persist(car);
            }
        }.execute(emf);

        new JpaTemplate() {

            @Override
            void run(EntityManager em) {
                Car car = em.find(Car.class, 1L);
                car.getBody().getParts().get(0).setName("break");

            }
        }.execute(emf);


        emf.close();
    }

    static abstract class JpaTemplate {
        public void execute(EntityManagerFactory emf) {
            EntityManager em = emf.createEntityManager();
            EntityTransaction transaction = em.getTransaction();
            transaction.begin();
            run(em);
            transaction.commit();
            em.close();
        }

        abstract void run(EntityManager em);
    }
}

실행결과

더 이해하기 힘든것은 Car 엔티티의 ID를 기존에는 생성자에서 받아서 객체를 만들도록 했는데 @GeneratedValue 로 자동으로 발번하게 하면 또 spring-data-jpa에서도 정상적으로 Part 테이블만 버전이 업데이트 되었다.

실행결과

그래서 트랜잭션의 문제인가 해서 다시 @GeneratedValue 를 지우고 id를 외부에서 입력하게 한 뒤에 @BeforeEach 에 있는 메소드에 @Transactional 을 붙이고 @Test 메소드에 @Transactional 따로 붙여서 해봤는데 여전히 테이블 둘다 업데이트 되었다.

무언가 두 메소드가 구분이 안되는것 같아 데이터를 따로 테이블어 넣어놓고, @BeforeEach 를 삭제한 뒤에 @Test 메소드만 실행하면 또 원하는데로 업데이트가 잘 되었다. 무엇이 문제인가?

@BeforeEach 메소드만 EntityManager 를 직접 사용하게 변경해봤다.

package hellojpa;

import hellojpa.car.Body;
import hellojpa.car.Car;
import hellojpa.car.CarRepository;
import hellojpa.car.Part;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.test.annotation.Rollback;
import org.springframework.transaction.annotation.Transactional;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

@SpringBootTest
@Rollback(value = false)
@EnableJpaRepositories(basePackages = {"hellojpa"})
@Transactional
public class CarTest {
    @Autowired
    CarRepository carRepository;
    @PersistenceContext
    EntityManager entityManager;

    @BeforeEach
    void addProc() {
        Part part = new Part();

        Body body = new Body();
        body.addPart(part);

        Car car = new Car(1L);
        car.setBody(body);
//        carRepository.save(car);
        entityManager.persist(car);
    }

    @Test
    void doThing() {
        Car car = carRepository.findById(1L).get();
        car.getBody().getParts().get(0).setName("break");
    }
}

이건 또 원하는데로 잘 되었다.

무엇이 문제인가?

 

쿠쥬플리즈 헬프미?

springDataJpaWithBoot.zip
0.01MB
plain_jpa.zip
0.00MB

반응형
Comments