Home 3. 엔티티
Post
Cancel

3. 엔티티

01. 엔티티 클래스

JPA에서 엔티티는 영속성을 가진 객체로서 가장 중요한 타입이다. JPA의 엔티티는 DB 테이블에 보관할 대상이 된다. EntityManager를 사용해서 엔티티 단위로 저장하고 조회하고 삭제한다.

JPA는 두 가지 방법으로 엔티티를 설정하는데 1)@Entity 애노테이션을 사용하는 방법 2)XML 매핑 설정을 사용하는 방법이 있다. 많은 프로젝트에서 주로 @Entity를 이용한 설정을 주로 사용한다.

1.1 @Entity 애노테이션과 @Table 애노테이션

EntityManager는 @Entity 애노테이션을 적용한 클래스를 이용해서 SQL 쿼리를 생성할 때 클래스 이름을 테이블 이름으로 사용한다. DBMS가 테이블 이름의 대소문자를 구분하거나 클래스 이름과 테이블 이름이 다른 경우 @Table 애노테이션을 사용해서 테이블 이름을 직접 지정할 수 있다.

1.2 @Id 애노테이션

엔티티의 가장 큰 특징은 식별자를 갖는다는 것이다. DB에서 주요키를 사용하는 것처럼 엔티티를 구분할 때 식별자를 사용한다.

1
2
3
4
5
6
7
8
9
@Entity
public class User{

  @Id
  private String email;
  private String name;

  //...
}

@Id 애노테이션을 필드에 적용했는데 이 경우 모든 필드가 매핑 대상이 된다. 즉 email 뿐만 아니라 name 필드도 DB 테이블과의 매핑 대상이 된다. 필드뿐만 아니라 getter 메서드에도 @Id를 적용할 수도 있다.

@Id 애노테이션을 적용한 필드 값은 EntityManager.find() 메서드에서 엔티티 객체를 찾을 때 식별자로 사용된다.

1.3 @Basic 애노테이션과 지원 타입

@Id 애노테이션을 적용한 대상을 제외한 나머지 영속 대상은 @Basic 에노테이션을 이용해서 설정한다. 설정하지 않은 경우 생략한 것으로 생각하면 된다.

@Basic 애노테이션을 사용할 수 있는 타입으로는

  • 자바의 기본 데이터타입
  • 기본 데이터 타입에 대응하는 래퍼타입
  • BigInteger, BigDecimal
  • String
  • Date, Calendar
  • Date, Time, TimeStamp
  • enum
  • byte[], Byte[], char[], Character[]

날짜와 시간 타입은 Temporal 애노테이션과 함께 사용한다.

또 열거 타입에 대한 매핑은 Enumerated 애노테이션을 사용한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public enum Grade{
  STAR1, STAR2, STAR3, STAR4, STAR5
}

@Entity
public class Hotel{
  @Id
  private String id;
  private String name;

  @Enumerated(EnumType.STRING)  //EnumType.STRING : 매핑된 칼럼이 열거 타입의 상수 이름을 값으로 가질 때 사용
  private Grade grade;

}

매핑된 컬럼이 열거 타입의 상수 이름 대신 인덱스를 저장하는 경우에는 EnumType.ORDINAL을 사용한다. 열거 타입 상수 순서는 유지보수 과정에서 변할 수 있다. 따라서 상수의 순서보다는 이름을 사용하는 것이 유리하다. EnumType.ORDINAL이 기본 값이므로 별도 설정을 추가하지 않으면 EnumType.ORDINAL을 사용하여 매핑을 처리한다.

1.4 @Column 애노테이션과 이름 지정

필드/프로퍼티의 이름과 테이블의 칼럼 이름이 다를 경우 @Column 애노테이션을 사용해서 컬럼 이름을 지정한다.

1
2
3
4
5
6
7
8
9
10
11
12
@Entity
@Table(name="room_info")
public class Room{
  @Id
  private String number;
  private String name;

  @Column(name="description")
  private String desc;

  //...
}

1.5 @Column 애노테이션을 이용한 읽기 전용 매핑 설정

@Column을 사용하면 변경 내역이 DB에 반영되지 않는 읽기 전용 데이터를 설정할 수 있다.

예) 자동 증가하는 id 컬럼을 갖고 있는 경우 제약을 갖는다. 새로운 객체 생성시 insert 쿼리에서 제외되어야 하고, 이 필드를 수정해도 그 값이 DB 테이블에 반영되면 안 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
@Entity
@Table(name="room_info")
public class Room{
  @Id
  private String number;
  private String name;

  @Column(name="description")
  private String desc;

  @Column(name="id", insertable=false, updatable=false)
  private Long dbId;
}

insertable=false이면 엔티티 객체를 DB에 저장시 insert 쿼리에서 해당 값을 제외한다. (update도 마찬가지)

02. 접근 타입: 영속 대상 필드와 프로퍼티

@Id 애노테이션을 필드에 적용하면 JPA는 다음의 두 과정에서 데이터를 읽고 쓸 때 필드를 사용한다.

  • 앤티티 객체에서 값을 읽어와 DB에 반영할 때
  • DB에서 읽어온 값을 엔티티 객체에 적용할 때

매핑을 필드에만 설정할 수 있는 것은 아니다. 자바빈 방식의 프로퍼티 메서드 중 get 메서드에도 설정할 수 있다. 필드가 아닌 get 메서드에 @Id 애노테이션을 설정하면 JPA는 필드대신 get/set 메서드를 이용하여 데이터를 처리한다.

예) DB에서 데이터를 읽어와 엔티티 객체에 전달할 때는 set 메서드를 이용하고, 반대로 엔티티를 DB에 반영할 때는 get 메서드를 이용해서 엔티티에서 값을 읽어온다.

기본적으로 필드 접근 방식을 사용하고, 특정 영속 대상에 대해서만 프로퍼티 접근 방식을 사용해야 한다면 @Access 애노테이션을 사용한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Entity
@Table(name="room_info")
public class Room {
    @Id
    private String number;
    private String name;

    @Column(name="description")
    private String desc;

    @Column(name="id", insertable=false, updatable=false)
    //AccessType.PROPERTY은 dbId에 해당하는 데이터만 필드가 아닌 프로퍼티를 통해서 접근함을 의미한다.(get/set 메서드를 통해서만 접근)
    @Access(AccessType.PROPERTY)
    private Long dbId;

    //...

    public Long getDbId() {
        return dbId;
    }

    private void setDbId(Long dbId) {
        this.dbId = dbId;
    }
}

반대로 프로퍼티 접근 타입을 기본으로 사용하고 특정 영속 대상만 필드 접근 타입을 사용하고 싶다면 get 메서드에 @Access 애노테이션을 설정하면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@Entity
@Table(name="room_info")
public class Room {
    //...
    private Long dbId;

    @Id
    public String getNumber() {
        return number;
    }

    public void setNumber(String number) {
        this.number = number;
    }

    public String getName() {
        return name;
    }

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

    @Column(name="description")
    public String getDesc() {
        return desc;
    }

    public void setDesc(String desc) {
        this.desc = desc;
    }

    @Column(name="id", insertable=false, updatable=false)
    //접근 타입이 필드라서 setDbId() 메서드를 사용하지 않는다.
    @Access(AccessType.FIELD)
    public Long getDbId() {
        return dbId;
    }
}

또 클래스에 @Access 애노테이션을 적용해서 접근 타입을 지정할 수도 있다. 그러면 @Id 애노테이션을 프로퍼티 메서드에 설정해야 한다.

2.1 영속 대상에서 제외하기

필드 접근 타입을 사용하는데 영속 대상이 아닌 필드가 존재한다면 transient 키워드를 사용해서 영속 대상에서 제외할 수 있다. 또는 JPA에서 제공하는 @Transient 애노테이션을 사용해도 영속 대상에서 제외된다.

1
2
3
4
transient private long timestamp = System.currentTimeMillis();

@Transient
private long timestamp = System.currentTimeMillis();

03. 엔티티 클래스의 제약 조건

  1. 엔티티 클래스는 인자가 없는 기본 생성자를 제공해야 한다.
  2. 기본 생성자의 접근 범위는 public 이나 protected이어야 한다. private의 경우 JPA의 특정 기능이 올바르게 동작하지 않을 수 있다.
  3. 엔티티는 클래스여야 한다. 인터페이스나 열거 타입을 엔티티로 지정할 수 없다.
  4. 엔티티 클래스는 final이면 안 된다.
  5. 엔티티의 메서드나 영속 대상 필드도 final이면 안 된다.

04. 엔티티 목록 설정

엔티티 클래스를 작성했다면 persistence.xml 파일의 태그를 사용하여 엔티티 클래스를 영속 단위에 추가한다.

JPA는 영속 단위에 추가한 클래스를 엔티티로 사용한다.

1
2
3
4
5
6
7
8
9
10
  ....

<persistence-unit name="jpastart" transaction-type="RESOURCE_LOCAL">
        <class>jpastart.reserve.model.User</class>
        <class>jpastart.reserve.model.Room</class>
        <class>jpastart.reserve.model.Hotel</class>

        <exclude-unlisted-classes>true</exclude-unlisted-classes>

        ......

05. EntityManager의 엔티티 관련 기본 기능

5.1 find() 메서드

public <T> T find(Class<T> entityClass, Object primaryKey); 첫 번째 파라미터로 찾을 엔티티의 타입을 전달하고 두 번째 파라미터로 식별자를 전달한다. 식별자가 존재하는 경우 엔티티 객체를 리턴하고, 존재하지 않는 경우 null을 리턴한다.

5.2 getReference() 메서드

find() 메서드와 동일한 파라미터를 갖는다. find() 메서드와 다르게 데이터가 존재하지 않는경우 EntityNotFoundException을 발생한다.

1
2
3
4
//getReference()는 프록시 객체를 리턴하며, 이 시점에 쿼리를 실행하지 않는다.
Hotel hotel = entityManager.getReference(Hotel.class, "NON_HOTEL_ID");
//getName()을 통해 최초로 데이터에 접근할 때 쿼리를 실행한다.
String name = hotel.getName();

getReference() 메서드는 쿼리를 바로 실행하지 않고 대신 프록시 객체를 리턴한다. 이 프록시 객체는 최초로 데이터가 필요한 시점에 select 쿼리를 실행한다. 그리고 select 쿼리 결과가 존재하지 않으면 exception을 발생한다.

getReference() 메서드로 구한 프록시 객체는 최초에 데이터가 필요한 시점에 쿼리를 실행하기 때문에 EntityManager 세션이 유효한 범위에서 프록시 객체를 처음 사용해야 한다. 범위 밖에서 처음 사용하게 되면 필요한 커넥션을 구할 수 없어 exception이 발생한다.

5.3 persist() 메서드

새로운 엔티티 객체를 DB에 저장할 때에는 persist() 메서드를 사용한다. 이 메서드는 상태를 변경하므로 트랜잭션 범위 안에서 실행해야 한다.

트랜잭션 범위 안에서 persist()를 실행하면 JPA는 알맞은 insert 쿼리를 실행하는 데 insert 쿼리를 실행하는 시점은 엔티티 클래스의 식별자를 생성하는 규칙에 따라 달라진다. 직접 식별자를 설정하는 경우 트랜잭션을 커밋하는 시점에 insert 쿼리를 실행하고, auto_increment를 사용하는 경우 persist()를 실행하는 시점에 insert 쿼리를 실행한다.

1
2
3
4
5
6
7
8
9
10
@Entity
@Table(name="hotel_review")
public class Review{
  @Id
  @GerneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  //.....
}

GeneratedValue 애노테이션은 JPA가 식별자의 값을 생성한다는 것을 뜻한다.

5.4 remove() 메서드

remove() 메서드를 사용하면 엔티티 객체를 제거한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
try{
  transaction.begin();
  Room room = entityManager.find(Room.class, "R10");
  if(room != null) {
    entityManager.remove(room);  //영속 컨텍스트에 삭제 대상 엔티티 추가
  }
  transaction.commit(); //커밋 시점에 delete 쿼리 실행

} catch(Exception e) {
  transaction.rollback();
  throw ex;
} finally{
  entityManager.close();
}

5.5 엔티티 수정

JPA는 트랜잭션 범위에서 엔티티 객체의 상태가 변경되면 이를 트랜잭션 커밋 시점에 반영한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
try{
  transaction.begin();
  Room room = entityManager.find(Room.class, "R10");
  if(room != null) {
    room.changeName("카프리");
  }
  transaction.commit(); //트랜잭션 범위 내에서 변경된 영속 객체를 DB에 반영

} catch(Exception e) {
  transaction.rollback();
  throw ex;
} finally{
  entityManager.close();
}

06. 식별자 생성 방식

엔티티 식별자 생성 방식은 애플리케이션 코드에서 직접 생성하는 방식과 JPA가 생성(식별 칼럼 방식, 시퀀스 방식, 테이블 방식)하는 두 가지 방식이 존재한다.

6.1 직접 할당 방식

별도의 식별자 생성 규칙이 존재하는 경우에 적합하다. 식별자를 직접 생성하는 방식은 @Id 애노테이션을 사용해서 식별자로 사용할 영속 대상만 지정하면 끝난다.

6.2 식별 칼럼 방식

이 방식은 @Id 애노테이션 대상에 다음과 같은 설정을 추가한다.

  • @GeneratedValue 애노테이션 추가
  • 위 애노테이션 strategy 값으로 GenerationType.IDENTITY 설정

자동 증감 컬럼은 insert 쿼리를 생성해야 식별자를 알 수 있다.

6.3 시퀀스 사용 방식

@SequenceGenerator 애노테이션을 사용해서 시퀀스 기반 식별자 생성기를 설정한다. @GeneratedValue 애노테이션의 generator 값으로 앞서 설정한 식별자 생성기를 지정한다.

1
2
3
4
5
6
7
8
@Id
@SequenceGenerator(
  name="review_seq_gen",    //시퀀스 생성기의 이름 지정
  sequenceName="hotel_review_seq",    //시퀀스 이름 지정
  allocationSize=1    //몇 개의 식별자를 생성할지 결정
)
@GeneratedValue(generator="review_seq_gen")
private Long id;

시퀀스를 사용하는 경우 시퀀스만 사용해서 식별자를 생성할 수 있으므로 persist() 시점에 insert 쿼리를 실행하지 않고 시퀀스 관련 쿼리만 실행한다. 그리고 트랜잭션을 커밋하는 시점에 새로 추가한 엔티티를 저장하기 위한 insert 쿼리를 실행한다.

allocationSize 속성 설정

@SequenceGenerator 애노테이션을 사용할때 allocationSize 속성 값은 1로 설정해야 한다. 이 속성의 기본값은 50인데 이는 DB 시퀀스 사용시 문제를 유발한다.(94p 참고)

6.4 테이블 사용 방식

모든 DB에서 동일한 방식으로 식별자를 생성하길 원하는 경우 테이블 사용 방식을 사용하면 된다. 테이블 방식을 사용하면 먼저 식별자를 보관할 때 사용할 테이블을 생성(주요키 컬럼, 다음 식별자로 사용할 숫자 컬럼)한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
@Entity
public class City{
  @Id
  @TableGenerator(name="idgen", //테이블 생성기
      table="id_gen",           //테이블 지정
      pkColumnName="entity",    //테이블 주요키 컬럼
      pkColumnValue="city",     //주요키 컬럼에 사용할 값
      valueColumnName="nexrid", //생성할 식별자를 갖는 컬럼 지정
      initialValue=0,           //초기 값
      allocationSize=1)         //할당 크기
  @GeneratedValue(generator="idgen")
  private Long id;
}

시퀀스 사용방식과 마찬가지로 persist()를 실행하는 시점에는 식별자를 생성하기 위한 컬럼만 실행하고, 트랜잭션을 커밋하는 시점에 엔티티 객체를 저장하기 위한 insert 쿼리를 실행한다.

This post is licensed under CC BY 4.0 by the author.