개발 학습 실습실

이 사이트의 실습은 자바스크립트를 켜야 움직입니다. 아래는 같은 내용을 글로 적은 것입니다.

공부 › JPA › 엔티티 매핑

엔티티 매핑과 기본 키

엔티티에 붙인 애너테이션은 테이블과 저장되는 값을 어떻게 바꾸나

JPA는 엔티티 클래스의 필드와 애너테이션을 읽어 테이블의 컬럼과 짝을 짓습니다. 이것을 매핑이라고 합니다.

실행으로 대조 · Hibernate 7.4.5와 H2에서 아흔여섯 가지 조합의 엔티티를 컴파일·실행해 create table 문과 저장된 값을 대조

이 실습에 나오는 개념

@Column
엔티티의 필드가 테이블의 어떤 컬럼과 짝지어지는지를 자세히 정하는 애너테이션이다. name으로 컬럼 이름을, nullable = false로 null 금지를, unique = true로 중복 금지를, length로 문자열의 최대 길이를 적는다. 붙이지 않아도 필드는 컬럼과 짝지어지며, 그때 문자열은 길이 255에 null과 중복을 허용한다. nullable, unique, length는 JPA가 테이블을 만들 때(create table 문) 반영되는 정보다. 이미 있는 테이블을 그대로 쓰는 경우 unique와 length는 실행 중에 아무것도 막아 주지 않는다.
@Enumerated
enum 타입의 필드를 DB에 어떤 값으로 저장할지 정하는 애너테이션이다. EnumType.ORDINAL은 enum에 적힌 순서 번호(0부터)를 숫자로 저장하고, EnumType.STRING은 상수의 이름을 글자로 저장한다. 붙이지 않았을 때의 기본값은 ORDINAL이다. ORDINAL로 저장한 뒤 enum의 중간이나 앞에 상수를 추가하면 번호가 밀려서, 이미 저장된 값이 다른 상수로 읽힌다. 예외가 나지 않아 알아차리기 어려우므로 STRING으로 쓰는 것이 안전하다.
@Transient
엔티티의 필드를 매핑에서 빼는 애너테이션이다. 이 필드에는 컬럼이 만들어지지 않고, 저장할 때도 조회할 때도 쓰이지 않는다. 엔티티의 필드는 따로 표시하지 않으면 모두 컬럼과 짝지어지므로, DB에 둘 필요가 없는 임시 값이나 계산용 값에는 이것을 붙여야 한다. DB에서 다시 읽어 온 객체에서는 이 필드가 비어 있다(참조형이면 null). 자바의 transient 키워드는 원래 직렬화에서 필드를 빼는 것이 목적이지만, JPA도 transient 필드는 저장하지 않는다.
기본 키 생성 전략(@GeneratedValue)
엔티티의 기본 키(@Id) 값을 누가 어떻게 만드는지 정하는 설정이다. @Id만 붙이면 코드에서 값을 직접 넣어야 하고, 넣지 않고 저장하면 예외가 난다. @GeneratedValue를 함께 붙이면 자동으로 만들어진다. IDENTITY는 DB의 자동 증가 컬럼에 맡기는 방식으로 MySQL에서 흔히 쓰고, SEQUENCE는 DB의 시퀀스에서 번호를 받아 오는 방식이다. 자동 생성으로 해 두고 값을 직접 넣어 em.persist로 저장하면 그것도 예외가 된다.
엔티티
데이터베이스 테이블의 행에 대응하는 자바 객체다. @Entity를 붙인 클래스로 정의하며 식별자(@Id)를 가진다. JPA는 엔티티의 상태 변화를 추적해 그에 맞는 SQL을 실행한다.

이렇게 바꿔 보세요

처음 설정으로 실행하면

  1. 애플리케이션이 시작될 때: Hibernate가 Member 클래스를 읽어 create table 문을 만듭니다(테이블을 자동으로 만드는 설정일 때). @Id가 붙은 id 필드는 기본 키 컬럼이 됩니다. @Id만 있으므로 평범한 not null 컬럼입니다. 값은 코드가 넣어 주어야 합니다.
  2. name 필드: @Column을 붙이지 않았으므로 기본값대로 만들어집니다. 길이는 255자이고, null도 중복도 허용합니다.
  3. grade 필드: ORDINAL은 enum에 적힌 순서 번호를 저장하는 방식이므로 작은 정수 컬럼(tinyint)이 됩니다. BASIC은 0, VIP는 1입니다. 번호가 0에서 1 사이인지 확인하는 조건(check)도 함께 붙었습니다.
  4. memo 필드: 애너테이션이 하나도 없지만 컬럼이 만들어집니다. 엔티티의 필드는 static이나 transient 필드, @Transient로 뺀 필드가 아니면 모두 컬럼과 짝지어집니다.
  5. em.persist(m)과 commit: INSERT가 나가 행이 하나 저장됩니다. id는 코드에서 넣은 100 그대로입니다.
  6. DB에 저장된 값: grade 컬럼에는 숫자 1이 들어 있습니다. Grade.VIP가 enum에서 두 번째(순서 번호 1)이기 때문입니다. "VIP"라는 글자는 DB 어디에도 없습니다. memo 컬럼에는 "오늘 가입"이 저장됐습니다.
  7. em2.find(Member.class, 100L): DB에서 다시 읽습니다. grade 컬럼의 1을 지금 enum의 순서 번호 1인 상수로 바꾸면 VIP입니다. memo는 "오늘 가입"입니다.

실행 결과

코드

enum Grade { BASIC, VIP }

@Entity
class Member {
    @Id
    Long id;

    String name;

    @Enumerated(EnumType.ORDINAL)
    Grade grade;

    String memo;
}

// 회원 한 명을 저장한다
Member m = new Member();
m.setId(100L);
m.setName("김철수");
m.setGrade(Grade.VIP);
m.setMemo("오늘 가입");
em.persist(m);
em.getTransaction().commit();

// 새 em으로 DB에서 다시 읽는다
Member r = em2.find(Member.class, 100L);
System.out.println(r.getGrade() + ", " + r.getMemo());

이 모형의 전제

아흔여섯 가지 조합마다 엔티티 클래스의 소스를 만들어 컴파일하고 실제 Hibernate 7.4.5와 H2 데이터베이스에서 실행해 대조했습니다. 대조한 것은 Hibernate가 만든 create table 문의 컬럼 정의 전부, DB에 저장된 행의 값, 새 em으로 다시 읽은 grade와 memo, 저장된 id 또는 예외의 종류입니다. 요약에 적은 다음 사실도 따로 실행해 확인했습니다: @Enumerated를 붙이지 않은 enum 필드가 ORDINAL과 똑같이 만들어지고 저장된다는 것, name이 null, 같은 이름, 21자인 회원을 저장했을 때의 성공과 실패(예외의 종류와 INSERT가 DB까지 나갔는지), 테이블을 Hibernate가 만들지 않았을 때 unique와 length가 아무 일도 하지 않는다는 것, 자동 생성 방식에서 id를 직접 넣으면 예외가 난다는 것. 컬럼 정의는 DB 종류(방언)와 Hibernate 버전에 따라 다릅니다. 여기 적힌 tinyint와 check 조건, enum 타입 컬럼, "generated by default as identity"는 Hibernate 7.4.5가 H2용으로 만든 것이며, MySQL에서는 자동 증가가 auto_increment로 적히는 식으로 달라집니다. MySQL의 표기와, 예전 Hibernate(5.x)가 STRING을 varchar(255)로 만들었다는 것은 문서를 따른 것이며 여기서 실행해 보지는 않았습니다. 그래도 "ORDINAL은 순서 번호를, STRING은 이름을 저장한다"는 점은 같습니다. 실제 create table 문에서는 컬럼이 화면과 다른 순서로 나오고, 스프링 부트를 쓰지 않으면 테이블 이름이 클래스 이름 그대로(Member) 적힙니다. 자동 생성은 IDENTITY 하나만 다루었고, INSERT가 나가는 시점의 차이는 영속성 컨텍스트 실습에 있습니다. 운영 환경에서는 테이블을 자동으로 만드는 설정을 쓰지 않는 것이 보통이며, 그때 @Column의 unique와 length는 문서 구실만 합니다.

앞 실습: 준영속과 병합 · 다음 실습: 값 타입과 임베디드 타입

이 실습에 나오는 개념

이 실습에서 보는 것

설정

코드


      
    

그림

실행 비교

이렇게 바꿔 보세요