[Java] 49. JPA에서 트랜잭션(transaction) 다루기와 공통 함수 만들기(옵서버 패턴)


Study/Java  2021. 6. 21. 18:52

안녕하세요. 명월입니다.


이 글은 JPA에서 트랜잭션(transaction) 다루기와 공통 함수 만들기(옵서버 패턴)에 대한 글입니다.


트랜잭션(transaction)이란 데이터베이스에서 논리적 상태 변화, 즉 Insert, Update, Delete로 데이터베이스의 데이터가 변화가 있는 것을 트랜잭션(transaction)이라고 합니다. 그런 의미로 데이터의 변화가 많은 테이블을 트랜잭션(Transaction)테이블이라고 하고 그렇지 않은 것은 마스터(Master)테이블이라고 합니다.

이런 트랜잭션은 범위를 설정할 수 있는데 범위 설정을 통해서 원자성(Atomicity), 일관성(Consistency), 독립성(Isolation), 영구성(Durability)를 유지할 수 있습니다.


좀 쉽게 이야기해서 웹을 통해서 회원 가입 설정을 만든다고 가정합니다.

회원 가입을 통해서 기본적인 id와 여러가지 테이블에 데이터를 동시에 insert를 한다고 생각합시다. 그런데 최초 id가 있는 기본 정보 테이블에 insert를 완료하고 여러가지 테이블에 insert하는 중에 에러가 발생했습니다.

즉, 테이블 하나 단위로 트랜잭션(transaction)을 걸어서 기본 정보에 데이터를 이미 insert했는데, 에러가 발생해서 다른 테이블에는 insert를 하지 못했습니다. 그렇게 되면 데이터의 무결성을 유지할 수 없게 됩니다.


그래서 모든 데이터가 제대로 insert된 후에 commit을 통해서 일괄적으로 동시에 데이터를 입력하고, 만약 에러가 발생을 하면 commit대신에 rollback을 통해서 중간에 insert된 데이터를 트랜잭션 범위가 시작하기 전 상태로 돌려야합니다.

그것을 트랜잭션(transaction) 범위 설정입니다.

import java.util.Date;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
import model.User;
import model.Info;
import model.Info2;

public class Main {
  // 실행 함수
  public static void main(String... args) {
    // FactoryManager를 생성합니다. "JpaExample"은 persistence.xml에 쓰여 있는 이름이다.
    EntityManagerFactory emf = Persistence.createEntityManagerFactory("JpaExample");
    // Manager를 생성한다.
    EntityManager em = emf.createEntityManager();
    try {
      // transaction을 가져온다.
      EntityTransaction transaction = em.getTransaction();
      try {
        // transaction 실행
        transaction.begin();
        // User 클래스 생성
        User user = new User();
        // 데이터 설정
        user.setId("nowonbun");
        user.setName("tester");
        user.setIsDeleted(false);
        // 데이터 입력
        em.persist(user);
        // Info 데이터를 생성
        Info info = new Info();
        // User 클래스에 info 데이터 연결
        user.addInfo(info);
        // 데이터 설정
        info.setAge(20);
        // 데이터 입력
        em.persist(info);
        // 에러 강제 발생
        if (true) {
          // 여기서 에러가 발생했다. 위 User 데이터와 Info에 넣은 데이터를 돌려야 한다.
          throw new RuntimeException();
        }
        // Info2 데이터를 생성
        Info2 info2 = new Info2();
        // Info 클래스에 info2 데이터 연결
        info.addInfo2(info2);
        // 데이터 설정
        info2.setBirth(new Date());
        // 데이터 입력
        em.persist(info);
        // transaction을 커밋한다.
        transaction.commit();
      } catch (Throwable e) {
        // 에러가 발생하면 rollback한다.
        if (transaction.isActive()) {
          transaction.rollback();
        }
        // 에러 출력
        e.printStackTrace();
      }
    } finally {
      // 각 FactoryManager와 Manager를 닫는다.
      em.close();
      emf.close();
    }
  }
}

위 소스는 제가 중간에 persist 함수를 통해서 데이터를 입력하는 명령을 했으나 중간에 RuntimeException로 강제로 에러를 발생시켰습니다. 그러나 위 트랜잭션(transaction) 범위에서 commit을 실행하지 않고 rollback를 실행하게 되면 persist함수나 merge함수, remove함수를 실행해도 데이터베이스의 데이터가 변하지 않고 그대로 트랜잭션(transaction)범위를 설정한 이전으로 돌아오게 됩니다.

사실 JPA는 이 transaction 범위를 설정하지 않으면 데이터가 입력이 되지 않습니다.

그러니깐 transaction 설정이 필수라고 할 수 있습니다.

그래서 이 transacion 부분을 공통 함수로 설정해서 따로 함수에서 트랜잭션(transacion)을 설정해서 완료되면 commit을 실행하고 에러가 나면 자동으로 rollback이 발생하는 공통 함수로 만들 필요가 있습니다.


이전 글에서도 제가 간단하게 공통 함수로 만들어서 트랜잭션을 설정해서 사용했습니다만, 이번에는 실제 프로젝트에서 사용할 만한 공통 함수를 작성해봅시다.

import java.io.Closeable;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;

// close 함수를 재정의하기 위한 상속
public class JPATransaction implements Closeable {
  // FactoryManager를 생성합니다. "JpaExample"은 persistence.xml에 쓰여 있는 이름이다.
  private EntityManagerFactory emf = Persistence.createEntityManagerFactory("JpaExample");

  // 람다식을 위한 interface
  interface EntityManagerCallable<V> {
    V run(EntityManager em);
  }
  // 람다식을 위한 interface
  interface EntityManagerRunable {
    void run(EntityManager em);
  }

  // 반환 값이 있는 트랜젝션 (일반 트랜젹션으로 데이터가 변화한다.)
  public <V> V transaction(EntityManagerCallable<V> callable) {
    return transaction(callable, false);
  }

  // 반환 값이 있는 트랜젝션 (readonly를 true 설정하면 함수를 호출하는 가운데서는 commit이 발생하지 않는다.)
  public <V> V transaction(EntityManagerCallable<V> callable, boolean readonly) {
    // Manager를 생성한다.
    EntityManager em = emf.createEntityManager();
    // transaction을 가져온다.
    EntityTransaction transaction = em.getTransaction();
    // 트랜젝션을 시작한다.
    transaction.begin();
    try {
      // 람다식을 실행한다.
      V ret = callable.run(em);
      // readonly가 true면 rollback한다.
      if (readonly) {
        transaction.rollback();
      } else {
        // 트랜젝션을 데이터베이스에 입력한다.
        transaction.commit();
      }
      // 결과를 리턴한다.
      return ret;
      // 에러가 발생할 경우
    } catch (Throwable e) {
      // transaction이 활성 중이라면
      if (transaction.isActive()) {
        // rollback
        transaction.rollback();
      }
      // RuntimeException로 변환
      throw new RuntimeException(e);
    } finally {
      // Manager를 닫는다.
      em.close();
    }
  }

  // 반환 값이 없는 transaction (일반 트랜젹션으로 데이터가 변화한다.)
  public void transaction(EntityManagerRunable runnable) {
    transaction(runnable, false);
  }

  // 반환 값이 없는 트랜젝션 (readonly를 true 설정하면 함수를 호출하는 가운데서는 commit이 발생하지 않는다.)
  public void transaction(EntityManagerRunable runnable, boolean readonly) {
    // Manager를 생성한다.
    EntityManager em = emf.createEntityManager();
    // transaction을 가져온다.
    EntityTransaction transaction = em.getTransaction();
    // 트랜젝션을 시작한다.
    transaction.begin();
    try {
      // 람다식을 실행한다.
      runnable.run(em);
      // readonly가 true면 rollback한다.
      if (readonly) {
        transaction.rollback();
      } else {
        // 트랜젝션을 데이터베이스에 입력한다.
        transaction.commit();
      }
      // 에러가 발생할 경우
    } catch (Throwable e) {
      // transaction이 활성 중이라면
      if (transaction.isActive()) {
        // rollback
        transaction.rollback();
      }
      // RuntimeException로 변환
      throw new RuntimeException(e);
    } finally {
      // Manager를 닫는다.
      em.close();
    }
  }

  // close 함수를 재정의 (Closeable 인터페이스)
  @Override
  public void close() {
    // 닫기
    emf.close();
  }
}

저는 JPATransaction의 클래스 이름으로 트랜잭션 클래스를 만들었습니다.

기본적으로 사용되는 함수는 transaction로 총 4가지 형태로 오버로딩(Overloading)했습니다.

크게 transacion에서 반환 값이 없는 형태(void)와 결과가 있는 Object 형태입니다. 여기서 Object는 람다식에서 제네릭 형태로 반환 데이터 타입을 설정합니다.

그리고 그 아래에 boolean readonly를 설정해서 만약 파라미터가 boolean 값이 없으면 기본 false가 되는 총 4가지 형태입니다.


여기서 transacion의 void형태는 보통의 insert, update, delete를 할 때 사용하는 transacion이고 제네릭 타입으로 반환 데이터 타입을 설정하는 경우는 보통 select를 사용할 때 사용하는 transaction입니다.

import java.util.ArrayList;
import java.util.Date;
import model.User;
import model.Info;
import model.Info2;

public class Main {
  // 실행 함수
  public static void main(String... args) {
    try (JPATransaction transaction = new JPATransaction()) {
      // 람다식 안에 return이 없기 때문에 반환 값이 없는 transaction 함수를 호출합니다.
      // 그리고 boolean 값도 없기 때문에 public void transaction(EntityManagerRunable runnable)를 호출합니다.
      transaction.transaction((em) -> {
        // User 클래스 생성
        User user = new User();
        // 데이터 설정
        user.setId("nowonbun");
        user.setName("tester");
        user.setIsDeleted(false);
        user.setInfos(new ArrayList<>());
        // Info 데이터를 생성
        Info info = new Info();
        // User 클래스에 info 데이터 연결
        user.addInfo(info);
        // 데이터 설정
        info.setAge(20);
        info.setInfo2s(new ArrayList<>());
        // Info2 데이터를 생성
        Info2 info2 = new Info2();
        // Info 클래스에 info2 데이터 연결
        info.addInfo2(info2);
        // 데이터 설정
        info2.setBirth(new Date());
        // 데이터 입력
        em.persist(user);
      });
      // 람다식 안에 return이 있기 때문에 반환 값이 있는 transaction 함수를 호출합니다.
      // 그리고 boolean 값도 없기 때문에 public <V> V transaction(EntityManagerCallable<V> callable)를 호출합니다.
      // 여기서 transaction에서 받는 데이터 타입이 User이기 때문에 제네릭은 자동으로 User로 설정됩니다.
      User user = transaction.transaction((em) -> {
        // 데이터 하나 취득
        return (User) em.createNamedQuery("User.findAll").getSingleResult();
      });
      // 콘솔 출력
      System.out.println(user.getId());
    }
  }
}

JPATransaction 클래스는 Closeable 인터페이스를 상속 받았기 때문에 try() 키워드를 이용해서 자동으로 close 함수를 호출하게 만듭니다.

그리고 transaction의 함수를 두번 호출했는데 첫번째에서는 반환 값이 없는 transacion 함수로 boolean 파라미터도 없기 때문에 기본적으로 readonly는 false로 설정되어 호출이 됩니다.

그렇게 첫번째 transacion 람다식을 종료하게 되면 데이터가 insert하게 되고 두번째 transaction은 데이터를 변환하는 것이 아닌 단순히 EntityManager의 인스턴스를 받아서 select해서 데이터를 취득하는 함수힙니다.

제네릭은 따로 설정을 하지 않았지만 람다식에서 제네릭은 반환 데이터 타입으로 자동으로 설정하기 때문에 return 타입이 User 타입이므로 변수는 User 타입으로 받아야 합니다.

실제 데이터를 콘솔에 출력을 하니 첫번째 트랜잭션에서 입력한 데이터가 출력되었습니다.


JPA의 기본 함수에 위처럼 트랜잭션 공통 함수가 있으면 좋은데 그런 함수는 없기 때문에 프로젝트를 만들 때마다 작성해야합니다. 하지만 한 번에 만들면 재사용으로 쉽게 사용할 수 있으므로 JPA 프로젝트 만들 때마다 기본적으로 작성하면 좋을 듯 싶네요.


여기까지 JPA에서 트랜잭션(transaction) 다루기와 공통 함수 만들기(옵서버 패턴)에 대한 글이었습니다.


궁금한 점이나 잘못된 점이 있으면 댓글 부탁드립니다.