[Java] 46. JPA의 Entity 클래스의 기본 설정(@GeneratedValue, @ManyToMany)


Study/Java  2021. 6. 14. 19:37

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


이 글은 JPA의 Entity 클래스의 기본 설정(@GeneratedValue, @ManyToMany)에 대한 글입니다.


이전 글에서 JPA를 기본적으로 설정하는 방법과 사용하는 방법에 대해서 소개했었습니다.

링크 - [Java] 45. JPA 설정하는 방법


JPA ORM의 구성은 일단 데이터베이스에 테이블을 생성하면 IDE(eclipse)를 통해서 테이블 구조를 가져와서 자동으로 Entity 클래스를 생성합니다. 그런데 이게 eclipse의 버그인지 아니면 JPA 사양상 생성할 때 기본적인 설정만 하고 나머지는 유저가 설정해야 하는 건지는 모르겠으나 class 생성이 완벽하지는 않습니다.

예를 들면 기본키를 설정하고 auto_increment 값에 대해서도 설정을 따로 해야하고 각 reference에 관해서도 어떤 형식으로 설정되는 지 설정해야 합니다.

그리고 데이터 타입만 하더라고 bit 값이나 datetime을 설정하면 정확히 class에서 어떤 데이터 타입을 설정해야 할지 정확하게 설정해야 합니다.

이런 부분이 조금씩 불편한 점이 있네요.


먼저 제가 예제 쿼리 값을 만들고 class를 설정하는 방법에 대해서 소개하겠습니다.

-- 유저 테이블
create table user(
  id varchar(255) not null,
  name nvarchar(255) not null,
  isDeleted bit not null,
  
  primary key(id)
);
 
-- 유저 테이블과 1:n 관계의 info 테이블
create table info(
  idx int not null auto_increment,
  id varchar(255) not null,
  age int not null,
  
  primary key(idx),
  foreign key(id) references user(id) -- user 테이블과 reference
);
-- info 테이블과 1:n 관계의 info2 테이블
create table info2(
  idx int not null auto_increment,
  info_idx int not null,
  birth date,
  
  primary key(idx),
  foreign key(info_idx) references info(idx) -- info 테이블과 reference
);
-- user 테이블과 m:n 관계의 permission 테이블
create table permission(
  code char(4) not null,
  name varchar(255) not null,
  
  primary key(code)
);
-- 데이터 베이스에서 m:n관계를 나타내기 위한 map 테이블
-- 즉 user 테이블과 m:1, permission 테이블과 1:n 관계 테이블
create table permission_map(
  id varchar(255) not null,
  code char(4) not null,
  
  foreign key(id) references user(id),
  foreign key(code) references permission(code)
);

user 테이블의 키는 id 컬럼입니다. 그리고 info 테이블은 user 테이블의 id로 reference를 연결하였고, info2 테이블은 info 테이블의 key(idx)로 reference를 연결하였습니다.

그리고 permission 테이블은 user 테이블과 m:n 관계입니다만, 데이터베이스는 m:n을 표현할 수 없기 때문에 permission_map 테이블을 가운데 두어서 m:n 관계를 만들었습니다.


위 테이블을 JPA로 클래스를 생성(Generate)하겠습니다.

테이블은 5개입니만 클래스는 4개가 생성이 되었습니다. 왜냐하면 데이터베이스에서는 m:n 관계를 표현할 수 없지만 프로그램 클래스에서는 m:n 관계를 표현할 수 있기 때문에 permission_map에 관한 테이블이 생성되지 않았습니다.


그럼 예제를 통해서 데이터 추가해 보도록 하겠습니다.

import java.util.Date;
import java.util.LinkedList;
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 {
  // 람다식 인터페이스
  interface Expression {
    void run(EntityManager em);
  }
  // Persistence로 EntityManager를 가져와서 실행하고 종료하는 함수
  private static void excute(Expression lambda) {
    // FactoryManager를 생성합니다. "JpaExample"은 persistence.xml에 쓰여 있는 이름이다.
    EntityManagerFactory emf = Persistence.createEntityManagerFactory("JpaExample");
    // Manager를 생성한다.
    EntityManager em = emf.createEntityManager();
    try {
      // 람다식을 실행한다.
      lambda.run(em);
    } finally {
      // 각 FactoryManager와 Manager를 닫는다.
      em.close();
      emf.close();
    }
  }
  // 실행 함수
  public static void main(String... args) {
    excute((em) -> {
      // transaction을 가져온다.
      EntityTransaction transaction = em.getTransaction();
      // transaction 실행
      transaction.begin();
      try {

        // User 데이터를 생성
        User user = new User();
        // User 테이블의 id와 name을 입력한다.
        user.setId("nowonbun");
        user.setName("tester");
        user.setIsDeleted(false);
        // user 테이블에 info 테이블의 리스트를 생성한다.
        user.setInfos(new LinkedList<>());

        // Info 데이터를 생성
        Info info = new Info();
        // user 테이블에 info 데이터를 추가한다. 
        user.addInfo(info);
        // info 데이터를 설정한다.
        info.setAge(20);
        // info 테이블에 info2 테이블의 리스트를 생성한다.
        info.setInfo2s(new LinkedList<>());

        // Info2 데이터를 생성
        Info2 info2 = new Info2();
        // info 테이블에 info2 테이블을 추가한다.
        info.addInfo2(info2);
        // birth 데이터에 오늘 날짜를 설정한다.
        info2.setBirth(new Date());

        // 에상하는 결과는 user테이블과 info테이블, info2테이블에 insert
        em.persist(user);
        // transaction을 커밋한다.
        transaction.commit();
      } catch (Throwable e) {
        // 에러가 발생하면 rollback한다.
        if (transaction.isActive()) {
          transaction.rollback();
        }
        e.printStackTrace();
      }
    });
  }
}

먼저 User 테이블에서 isDeleted의 컬럼에서 데이터 타입이 맞지 않다고 에러가 발생하세요.

쿼리를 보면 제가 확실히 bit 타입으로 설정했습니다만 eneity 클래스 파일을 보면 Object 타입으로 설정되어 있습니다.

우리는 bit 타입으로 물리적으로는 0과 1, 논리적으로는 true와 false의 데이터를 사용할 것입니다.

그러므로 프로그램에서는 boolean 타입으로 설정해야 합니다.

그러면 저 Object 타입을 boolean 바꾸고 getter, setter 데이터 타입도 변경합니다.

boolean로 설정했으면 다시 실행해 보겠습니다.

이번에는 cascade PERSIST 에러가 발생했습니다.

cascade PERSIST 에러란 user 테이블에서 데이터를 추가하고 info 테이블과 info2를 동시에 추가할 수 없다는 뜻입니다.

이런 제약이 왜 있는 것인가 하면 우리가 데이터 베이스를 설계할 때, Master 테이블과 Transaction 테이블을 구분해서 설계를 합니다.

Transaction 테이블이란 유저의 조작으로 데이터가 추가되고 삭제, 수정이 되는 테이블이고 Master 테이블이란 프로그램에서 사용하는 데이터로 마음대로 추가, 삭제, 수정이 이루어 지면 안되는 데이터입니다.

즉, 우리가 Entity 설정을 할때 cascade PERSIST 설정을 해서 데이터를 추가할 때 연결된 데이터가 수정이 가능한지 아닌지 설정이 필요한 것입니다.

cascade에 관해서는 다음 글에서 자세히 설명하고 일단은 user 클래스와 info 클래스에 cascade 설정을 추가합니다.

user 테이블에 데이터가 추가할 때 info 테이블도 수정이 가능하다는 뜻이고 info 테이블에 데이터가 추가될 때 info2 테이블도 수정이 가능하다는 뜻입니다.

cascade 설정을 했으면 다시 실행합니다.

이번에는 info 테이블과 info2 테이블에서 idx 값을 자동으로 증가해야 하는데 설정이 되지 않아서 에러가 발생했습니다.

info 클래스와 info2 클래스에 identity 설정을 해야 한다는 뜻입니다.

자동 증가 설정을 해야 하는 변수에 @GeneratedValue(strategy = GenerationType.IDENTITY)를 설정하면 됩니다.

사실 이 부분은 JPA로 class를 생성할 때 설정도 가능한 부분입니다만, 테이블이 많아 질때 설정이 복잡해 질 수 있기 때문에 기본 생성에서 저 설정을 추가하는게 가장 편합니다.


설정을 했으면 다시 실행합니다.

이번에는 실행이 되었습니다.

데이터베이스 결과를 보니 데이터가 제대로 insert 된 것을 확인할 수 있습니다.

이번에는 데이터를 검색해서 가져와서 Permission 테이블과 연결해 보겠습니다.

먼저 Permission 테이블은 구조가 자동 증가식의 키가 아니고 특정 char 값을 키로 사용하는 것을 보니 Master 테이블입니다.

즉, Master 테이블은 프로그램 상에서 다루는 데이터가 아니고 프로그램 관리자(SA)가 직접 관리하는 데이터입니다.

insert into permission (code, name) values ('BASC', 'Basic Permission');

이제 permission_map 테이블에 nowonbun의 id를 가진 user에 BASC의 권한을 주고 싶습니다.

import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
import model.User;
import model.Permission;

public class Main {
  // 람다식 인터페이스
  interface Expression {
    void run(EntityManager em);
  }

  // Persistence로 EntityManager를 가져와서 실행하고 종료하는 함수
  private static void excute(Expression lambda) {
    // FactoryManager를 생성합니다. "JpaExample"은 persistence.xml에 쓰여 있는 이름이다.
    EntityManagerFactory emf = Persistence.createEntityManagerFactory("JpaExample");
    // Manager를 생성한다.
    EntityManager em = emf.createEntityManager();
    try {
      // 람다식을 실행한다.
      lambda.run(em);
    } finally {
      // 각 FactoryManager와 Manager를 닫는다.
      em.close();
      emf.close();
    }
  }

  // 실행 함수
  @SuppressWarnings("unchecked")
  public static void main(String... args) {
    excute((em) -> {
      // transaction을 가져온다.
      EntityTransaction transaction = em.getTransaction();
      // transaction 실행
      transaction.begin();
      try {
        // user 테이블로 부터 데이터 취득
        List<User> users = em.createNamedQuery("User.findAll").getResultList();
        // permission 테이블로 부터 데이터 취득
        List<Permission> permissions = em.createNamedQuery("Permission.findAll").getResultList();
        // nowonbun 데이터 취득
        User user = users.stream().filter(x -> "nowonbun".equals(x.getId())).findFirst().get();
        // BASC 데이터 취득
        Permission permission = permissions.stream().filter(x -> "BASC".equals(x.getCode())).findFirst().get();
        // user의 permission에 BASC 데이터를 추가
        user.getPermissions().add(permission);
        // update
        em.merge(user);
        // transaction을 커밋한다.
        transaction.commit();
      } catch (Throwable e) {
        // 에러가 발생하면 rollback한다.
        if (transaction.isActive()) {
          transaction.rollback();
        }
        e.printStackTrace();
      }
    });
  }
}

에러가 발생하지 않고 실행되었습니다. 그리나 데이터베이스를 확인하면 데이터가 없습니다.

이런 현상은 permision_map의 데이터 추가하는 부분이 permission 클래스에 설정되어 있습니다.

permission 클래스는 마스터 테이블이기 때문에 우리는 user 테이블에서 permission을 설정해야 합니다.

@ManyToMany 설정이 되어 있는 양쪽 클래스를 서로 바꿉니다.

즉, user 클래스에 JoinTable를 넣고 user에서 permision_map를 insert하도록 설정해야 합니다.

다시 main 함수를 실행합니다.

데이터베이스에 추가된 것을 확인할 수 있습니다.


JPA가 개인적으로 성능은 꽤 나쁘지는 않는데, 이런 설정이 자동으로 되지 않고, 기본적으로 자동 증가 설정과 Cascade 설정, @ManyToMany는 수동으로 설정해야 한다는 것이 매우 불편합니다.


여기까지 JPA의 Entity 클래스의 기본 설정(@GeneratedValue, @ManyToMany)에 대한 글이었습니다.


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