본문 바로가기

개발/JPA

[JPA 기본] 4. 엔티티 매핑

객체와 테이블 매핑 방법

@Entity

정의

JPA를 사용해서 테이블과 매핑할 클래스

 

Entity 사용시 주의사항

- public이나 protected 접근제어자가 붙은 기본 생성자는 필수 (리플렉션 사용에 필요)

- final 클래스, enum, interface, inner 클래스 사용 불가.

- 저장할 필드에 final 사용 불가

 

name 속성

- 기본 값: 클래스 이름을 그대로 사용한다. 

- 별도의 이름을 지정하고 싶을 경우 name 속성 사용

// Member 그대로 사용
@Entity
public class Member {
....
}

// 별도의 이름 지정
@Entity(name="Users")
public class Member {
....
}

@Table

정의

엔티티와 매핑할 테이블 지정

 

속성

- name

- catalog

- schema

- uniqueConstraints


데이터베이스 스키마 자동 생성

스키마 자동 생성이란?

- DDL을 애플리케이션 실행 시점에 자동 생성한다. 

- 데이터베이스 방언에 맞게 적절한 DDL이 생성된다.

- 생성된 DDL은 운영서버에서는 사용하지 않고 개발에서만 사용하거나 ,적절히 다듬어 사용

** DDL (= Data Definition Language, 데이터 정의어)

: 테이블과 같은 데이터 구조를 정의하는데 사용되는 명령어. (CREATE, ALTER, DROP, RENAME, TRUNCATE)

 

코드 적용방법

persistence.xml 내 프로퍼티 추가

<property name="hibernate.hbm2ddl.auto" value="create-drop"/>

 

속성

- create: 기존 테이블 drop 후 다시 생성 

- create-drop: create와 기본 로직 같다. 단, 종료 시점에 테이블 drop

- update: 변경분만 적용 (운영DB에 사용 금지)

   -> 필드 추가시 ALTER 명령어 실행

   -> 필드 삭제할 경우에는 아무 일도 일어나지 않음. 

- validate: 존재하지 않는 필드일 경우 에러. 

- none: 아무것도 실행되지 않는다. 

 

** create-drop 예시

앱 실행시 drop table Member if exists가 실행된다. 

데이터 조회하면 기존의 데이터가 모두 삭제된 것을 알 수 있다. 

주의사항

운영 서버에서는 절대로 create, create-drop, update 사용 금지

- 개발 초기 단계: create, update

- 테스트 서버: update, validate

- 스테이징, 운영서버: validate, none

** 강사님 추천: 로컬을 제외한 스테이징, 운영에선 update 옵션을 사용하지 말아라.

대신, ALTER 쿼리 스크립트를 DB에 직접 적용하는 것을 권장. (운영은 DBA 점검을 받고 적용하자)

 

DDL 생성 기능

- 제약 조건 설정

=> DDL 생성 기능은 DDL 자동 생성에만 영향을 끼치며, JPA의 런타임 로직에는 영향이 없다. 

// 예1. 회원 이름 필수. 10자 초과 금지
@Colum(nullable = false, length = 10)
private String name;

// 예2. 유니크 제약조건 추가
@Table(uniqueConstraints = {@UniqueConstraints(name=
NAME_AGE_UNIQUE", columnNames={"NAME","AGE"} )})

필드와 컬럼 매핑

매핑 에노테이션 

- @Column: 컬럼

- @Temporal: 날짜

- @Enumerated: enum 타입 

** 주의: EnumType의 option값은 STRING으로 주자. ORDINARY는 사용하지 말 것. 

** 이유: ORDINARY는 정수(예. 1,2,3,...)의 형태로 DB에 들어간다.

타입이 추가될 경우 장애가 발생할 수가 있으므로, DB 용량을 좀 더 차지하더라도, STRING만 사용하자 

** 예시: Role이 GUEST, MEMBER 두가지만 존재하다가, OWNER, GUEST, MEMBER가 될 경우, GUEST 의 정수 1을 OWNER가 대체하여 장애 발생함. 

- @Lob: BLOB, CLOB 

- @Transient: DB에 매핑하지 않고, 애플리케이션 단에서만 가지고 있음. (예. 캐싱 데이터)

** Transient는 영속대상에서 제외하는 것. 

(보충 설명: https://gmoon92.github.io/jpa/2019/09/29/what-is-the-transient-annotation-used-for-in-jpa.html)

 

@Column 속성

- name: 테이블 컬럼명

- insertable, updatable: class 필드 등록/수정시, DB에 반영 가능 여부

- nullable: not null 허용 여부. false일 경우 not null

- unique: 한 개의 컬럼에 유니크 제약조건 설정

- columnDefinition: DB 컬럼 정보

- length: 문자 길이 제약 조건 

- precision: bigDecimal, bigInteger 타입에서 사용

  : precision -> 소수점 포함한 전체 자릿수 / scale: 소수의 자릿수

** double, float 타입에는 적용되지 않음. 


기본 키 매핑

키 매핑용 애노테이션

- @Id

- @GeneratedValue

** 참고: id에는 어떤 데이터 타입을 쓸까?

Long을 사용하자. (int와 Integer를 사용하지 않는 이유는 아래를 참고)

- int : 부적합 => 데이터가 없을 경우 0이 들어간다. 데이터만 보고, 의도적으로 0을 넣은 것인지 판별하기 어렵다.

- Integer : 부적합 => 대용량 데이터일 경우 범위를 벗어날 수 있다. 

 

키 매핑 방법 2가지

1. 직접 할당: @Id만 사용

2. 자동 생성: @GeneratedValue

 

GeneratedValue의 속성값

1) IDENTITY

- 기본 키 생성을 데이터베이스에 위임

- em.persist 시점에 INSERT SQL 실행하고, DB에서 식별자를 얻어온다. 

   cf) AUTO_INCREMENT: commit 시점에 INSERT SQL 실행. 

2) SEQUENCE

- DB 시퀀스 오브젝트 사용

- DB에서 allocationSize 양만큼 시퀀스를 미리 확보.

   persist 호출시 메모리에 저장된 시퀀스를 다 사용하기 전까지는 DB에 다시 접근하지 않는다. 

// 예시 (allocationSize는 50으로 지정된 상태
Member member1 = new Member();
Member member2 = new Member();
Member member3 = new Member();
member1.setUsername("A");
member2.setUsername("B");
member3.setUsername("C");

System.out.println("=============");
em.persist(member1);
em.persist(member2);
em.persist(member3);
System.out.println("=============");
## 터미널 출력 결과
## 총 2번 SEQ GEERATOR 호출. 두번째에서 2~51번 시퀀스 확보. 
## 52번의 저장이 필요할 때 3번째 generator가 호출된다. 
=============
Hibernate: 
    call next value for NUMBER_SEQ_GENERATOR
Hibernate: 
    call next value for NUMBER_SEQ_GENERATOR
=============
Hibernate: 
    /* insert hellojpa.Member

- 주요 속성

  -> initialValue: DDL 생성시, 처음 시작하는 수 지정. 

  -> allocationSize: 시퀀스 한 번 호출에 증가하는 수. 성능 최적화에 사용

3) TABLE

- 키 생성용 테이블 사용

-> 주요속성: initalValue, allocationSize (동작 방식은 SEQUENCE의 속성과 동일하다)

4) AUTO

- 방언에 맞게 자동 지정. (default)

 

** 트러블 슈팅

- 이슈: strategy를 identity로 바꿀 경우 null 로 id를 지정할 수 없다고 오류 메시지가 아래처럼 뜬다. 

ERROR: NULL not allowed for column "ID"; SQL statement:
/* insert hellojpa.Member */ insert into Member (id, name) values (null, ?) [23502-202]

- 해결 방법: 로컬의 h2 버전을 1.4.200으로 다운그레이드 

(출처: https://www.inflearn.com/questions/374987)

 

식별자를 정하는법

1. 기본키: null 아님. 유일할것. 

2. 자연키 대신 대리키(=대체키)를 사용하자. 

** 자연키를 사용하는 경우 생기는 문제 예시

주민등록번호를 PK로 설정, 이후 "DB에는 사용자 주민등록번호가 있으면 안된다"는 법에 따라 PK를 지울 경우, PK를 외래키로 잡은 타 테이블도 모두 수정해야하므로 공수가 크다. 

<결론> 권장되는 키 전략: Long형 + 대체키 + 키 생성 전략

 


실전 예제 코드

** Entity 정의시 주의사항: Entity에 DB의 제약조건을 함께 명시해준다.

테이블 명세를 따로 확인해야하는 번거로움을 덜어준다. 

//예. 컬럼 최대 길이 명시 
@Column(Length=10)
private String name;

 

데이터 중심 설계의 문제점

객체 설계를 테이블 설계에 그대로 맞출 경우, 객체 지향 코드 작성이 어렵다. 

// 예) 아래 코드를 살펴보면, team을 조회하기 위해 member에서 memberId를 빼내야함 
// -> team에게 member를 물어보는게 더 낫다.

Member member = em.find(Member.class, 1L);
Long teamId = member.getTeamId();
Team team = em.find(Team.class, teamId);