본 게시글은 김영한님의 [자바 ORM 표준 JPA 프로그래밍 - 기본편]을 수강하며 작성한 글입니다.
1. 객체와 테이블 매핑
1) 엔티티 매핑 소개
- 객체와 테이블 매핑
: @Entity, @Table - 필드와 컬럼 매핑
: @Column - 기본 키 매핑
: @Id - 연관관계 매핑
: @ManyToOne, @JoinColumn
2) @Entity
- @Entity가 붙은 클래스는 JPA가 관리하는 엔티티이다.
- JPA를 사용해서 테이블과 매핑할 클래스는 @Entity 어노테이션을 필수로 붙여야 한다.
주의 사항
1. 기본 생성자 필수 (파라미터가 없는 public 또는 protected 생성자)
public Member() {} // 기본 생성자
문제는 다음과 같이 생성자를 하나 이상 만들면 자바는 기본 생성자를 자동으로 만들지 않는다. 이때는 기본 생성자를 직접 만들어야 한다.
public Member() {} // 기본 생성자
public Member(String name) {
this.name = name;
}
2. final 클래스, enum, interface, inner 클래스는 사용할 수 없다.
3. DB에 저장할 필드에는 final 키워드를 사용할 수 없다.
속성
- name
- JPA에서 사용할 엔티티 이름을 지정한다.
- 기본값: 클래스 이름을 그대로 사용한다. (예: Member)
- 같은 클래스 이름이 없으면 가급적 기본값을 사용한다. (다른 패키지에서 이미 Entity 이름이 사용 중일 때 등)
@Entity(name = "Member") public class Member { }
3) @Table
- 엔티티와 매핑할 테이블을 지정한다.
@Entity(name = "Member")
@Table(name = "MBR")
public class Member {
}
SQL 문에 자동으로 지정한 테이블 명으로 변경되어 적용된다.
속성
속성 | 기능 | 기본값 |
name | 매핑할 테이블 이름 | 엔티티 이름을 사용 |
catalog | 데이터베이스 catalog 매핑 | |
schema | 데이터베이스 schema 매핑 | |
uniqueConstraints(DDL) | DDL 생성 시에 유니크 제약 조건 생성 |
@Entity
@Table(name = "MBR", catalog = "catalog", schema = "schema")
public class Member() {
}
2. 데이터베이스 스키마 자동 생성
- JPA는 애플리케이션 실행 시점에 DDL을 자동으로 생성한다. 따라서 테이블을 따로 생성할 필요가 없다.
- 테이블 중심 → 객체 중심
- 데이터베이스 방언을 활용하여 데이터베이스에 맞는 적절한 DDL을 생성한다.
- 운영이 아닌 개발 서버에서만 사용을 권장한다.
1) hibernate.hbm2ddl.auto 속성
<!-- persistence.xml -->
<property name="hibernate.hbm2ddl.auto" value="create" />
옵션 | 설명 |
create | 기존 테이블 삭제 후 다시 생성 (DROP + CREATE) |
create-drop | create와 같으나 종료시점에 테이블 DROP |
(보통 테스트 케이스 실행 시 사용한다. 삭제는 수행하지 않는다.) | |
update | 변경분만 반영 |
(운영DB에서는 사용하면 안된다.) | |
validate | 엔티티와 테이블이 정상 매핑되었는지 확인 |
(엔티티와 테이블을 비교하여 다른 점이 있으면 에러가 발생한다.) | |
none | 사용하지 않음 |
💡 그 외의 하이버네이트 설정 옵션
- hibernate.show_sql: 실행한 SQL을 출력
- hibernate.format_sql: SQL을 보기 좋게 정렬
- hibernate.use_sql_comments: 쿼리 출력 시 주석도 함께 출력
- hibernate.id.new_generator_mappings: JPA 표준에 맞는 새로운 키 생성 전략을 사용
2) 주의해야할 점
운영 장비에는 절대 create, create-drop, update를 사용하면 안된다.
해당 옵션들은 운영 중인 데이터베이스의 테이블이나 컬럼을 삭제할 수 있다.
개발 환경에 따른 추천 전략은 다음과 같다.
- 개발 초기 단계는 create 또는 update
- 테스트 서버는 update 또는 validate
- 스테이징과 운영 서버는 validate 또는 none
💡 개발이나 테스트 서버에서도 가급적 본인이 직접 스키마를 작성하여 사용하는 것을 추천한다.
또한 해당 속성을 통해 출력되는 DDL문을 참고하여 다듬은 후 운영 서버에 반영하기도 한다.
3) DDL 생성 기능
- 제약조건 추가
@Entity
public class Member {
@Id
private Long id;
@Column(unique = true, length = 10)
private String name;
}
- 유니크 제약조건 추가
@Table(uniqueConstraints = {@UniqueConstraint( name = "NAME_AGE_UNIQUE", columnNames = {"NAME", "AGE"})})
DDL 생성 기능은 DDL을 자동 생성할 때만 사용되고, JPA의 실행 로직에는 영향을 주지 않는다. 따라서 스키마 자동 생성 기능을 사용하지 않고 직접 DDL을 만든다면 사용할 이유가 없다.
이 기능을 사용하면 애플리케이션 개발자가 엔티티만 보고도 손쉽게 다양한 제약 조건을 파악할 수 있는 장점이 있다.
3. 필드와 컬럼 매핑
요구사항 추가
1. 회원은 일반 회원과 관리자로 구분해야 한다.
2. 회원 가입일과 수정일이 있어야 한다.
3. 회원을 설명할 수 있는 필드가 있어야 한다. 이 필드는 길이 제한이 없다.
1) 매핑 어노테이션 정리
어노테이션 | 설명 |
@Column | 컬럼 매핑 |
@Temporal | 날짜 타입 매핑 |
@Enumerated | enum 타입 매핑 |
@Lob | BLOB, CLOB 매핑 |
@Transient | 특정 필드를 컬럼에 매핑하지 않음 |
2) @Column
속성 | 설명 | 기본값 |
name | 필드와 매핑할 테이블의 컬럼 이름 | 객체의 필드 이름 |
insertable, updatable | 등록, 변경 가능 여부 | TRUE |
nullable(DDL) | null 값의 허용 여부를 설정한다. false로 설정하면 DDL 생성시에 not null 제약조건이 붙는다. | |
unique(DDL) | @Table의 uniqueConstraints와 같지만 한 컬럼에 간단한 유니크 제약조건을 걸 때 사용한다. 유니크 제약조건의 값에 컬럼명이 그대로 들어가지 않아서 잘 쓰지 않는다. @Table의 uniqueConstrants을 선호한다. |
|
columnDefinition(DDL) | 데이터베이스 컬럼 정보를 직접줄 수 있다. ex) varchar(100) default ‘EMPTY’ DB 종속적인 정보도 넣을 수 있다. |
필드의 자바 타입과 방언 정보를 사용 |
length(DDL) | 문자 길이 제약조건, String 타입에만 사용한다. | 255 |
precision, scale(DDL) | BigDecimal 타입에서 사용한다.(Biginteger도 사용할 수 있다.) precision은 소수점을 포함한 전체 자릿수를, scale은 소수의 자릿수다. 참고로 double, float 타입에는 적용되지 않는다. 아주 큰 숫자나 정밀한 소수를 다루어야 할 때만 사용한다. |
precision=19, scale=2 |
@Column(name = "name", nullable = false)
private String username;
3) @Enumerated
자바 enum 타입을 매핑할 때 사용한다.
속성 | 설명 | 기본값 |
value | - EnumType.ORDINAL : enum 순서를 데이터베이스에 저장 - EnumType.STRING : enum 이름을 데이터베이스에 저장 |
EnumType.ORDINAL |
@Enumerated(EnumType.STRING)
private RoleType roleType;
🚨 EnumType.ORDINAL 사용X
요구사항이 변경(RoleType에 GUEST가 추가)되어 enum에 멤버가 추가됐을 때 문제가 발생한다.
요구사항 변경 전
// Member.java
@Enumerated(EnumType.ORDINAL)
private RoleType roleType;
// RoleType.java
public enum RoleType {
USER, ADMIN
}
// JpaMain.java
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Member member1 = new Member();
member1.setId(1L);
member1.setUsername("A");
member1.setRoleType(RoleType.USER);
Member member2 = new Member();
member2.setId(2L);
member2.setUsername("B");
member2.setRoleType(RoleType.ADMIN);
em.persist(member1);
em.persist(member2);
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
emf.close();
}
}
요구사항 변경 후
// RoleType.java
public enum RoleType {
GUEST, USER, ADMIN
}
// JpaMain.java
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Member member = new Member();
member.setId(3L);
member.setUsername("C");
member.setRoleType(RoleType.GUEST);
em.persist(member);
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
emf.close();
}
}
4) @Temporal
날짜 타입(java.util.Date, java.util.Calendar)을 매핑할 때 사용한다.
- LocalDate, LocalDateTime을 사용할 때는 @Temporal 어노테이션 생략이 가능하다. (최신 하이버네이트 지원)
속성 | 설명 | 기본값 |
value | - TemporalType.DATE : 날짜, 데이터베이스 date 타입과 매핑 (예: 2014-01-01) - TemporalType.TIME : 시간, 데이터베이스 time 타입과 매핑 (예: 11:11:11) - TemporalType.TIMESTAMP : 날짜와 시간, 데이터베이스 timestamp 타입과 매핑 (예: 2014-01-01 11:11:11) |
@Temporal(TemporalType.TIMESTAMP)
private Date createdDate;
@Temporal(TemporalType.TIMESTAMP)
private Date lastModifiedDate;
private LocalDate testLocalDate;
private LocalDateTime testLocalDateTime;
5) @Lob
BLOB, CLOB 타입을 매핑할 때 사용한다.
- @Lob에는 지정할 수 있는 속성이 없다.
- 매핑하는 필드 타입이 문자면 CLOB을 매핑, 나머지는 BLOB을 매핑한다.
- CLOB : String, char[], java.sql.CLOB
- BLOB : byte[], java.sql.BLOB
@Lob
private String description;
6) @Transient
필드 매핑을 안하고 싶을 때 사용한다.
- 데이터베이스에 저장하지 않고, 조회하지 않을 때 사용한다.
- 주로 메모리 상에서만 임시로 어떤 값을 보관하고 싶을 때 사용한다.
@Transient
private String temp;
3. 기본 키 매핑
1) 기본 키 매핑 어노테이션
- @Id
- @GeneratedValue
@Id @GeneratedValue(strategy = GenerationType.AUTO) // PK Mapping
private Long id;
2) 기본 키 매핑 방법
- 기본 키를 직접 할당: @Id만 사용
- 자동 생성 전략 사용: @GeneratedValue 추가 및 키 생성 전략 선택
- INDENTITY : 기본 키 생성을 데이터베이스에 위임한다.
- SEQUENCE : 데이터베이스 시퀀스를 사용해서 기본 키를 할당한다.
- TABLE : 키 생성 테이블을 사용한다.
- AUTO : 방언에 따라 자동 지정한다. 기본값
3) IDENTITY 전략
- 기본 키 생성을 데이터베이스에 위임한다.
- 주로 MySQL, PostgreSQL, SQL Server, DB2에서 사용한다. (예: MYSQL의 auto_increment)
- JPA는 보통 트랜잭션 커밋 시점에 INSERT SQL문을 실행한다.
- AUTO_INCREMENT는 데이터베이스에 INSERT SQL을 실행한 이후에 ID 값을 알 수 있다.
DB에 들어가야만 PK를 알 수 있다. 하지만 영속성 컨텍스트의 1차 캐시 내의 @Id가 DB의 PK이기 때문에 기본키는 JPA가 필수로 알고 있어야 하는 값이다. - em.persist() 호출 시점에 INSERT SQL을 즉시 DB에 전달한다. 따라서 쓰기 지연이 동작하지 않는다. JPA는 보통 em.persist() 호출 시점에 INSERT SQL을 생성하여 쓰기 지연 SQL 저장소에 저장하고, 트랜잭션 commit() 호출 시점에 INSERT SQL을 DB에 전달한다.
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
}
4) SEQUENCE 전략
- 유일한 값을 순서대로 생성하는 특별한 데이터베이스 오브젝트이다. (예: 오라클 시퀀스)
- 주로 오라클, PostgreSQL, DB2, H2 데이터베이스에서 사용한다.
- em.persist()를 수행하려면 pk가 필요하다. 따라서 sequence를 호출하여 값을 얻어와서 엔티티에 할당하고 엔티티를 영속성 컨텍스트에 저장한다. 이후 commit() 시점에 INSERT SQL을 날린다.
- 쓰기 지연이 동작한다.
@Entity
@SequenceGenerator(
name = "MEMBER_SEQ_GENERATOR",
sequenceName = "MEMBER_SEQ", // 매핑할 데이터베이스 시퀀스 이름
initialValue = 1, allocationSize = 1)
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "MEMBER_SEQ_GENERATOR")
private Long id;
}
속성 | 설명 | 기본값 |
name | 식별자 생성기 이름 | 필수 |
sequenceName | 데이터베이스에 등록되어 있는 시퀀스 이름 | hibernate_sequences |
initalValue | DDL 생성 시에만 사용됨. 시퀀스는 DDL을 생성할 때 처음 1 시작하는 수를 지정한다. | |
allocationSize | 시퀀스 한 번 호출에 증가하는 수 (성능 최적화에 사용된다.) call next value 최초 호출로 시퀀스 값을 세팅한다. 여러 웹 서버가 있어도 동시성 이슈 없이 저장이 가능하다. 데이터베이스 시퀀스 값이 하나씩 증가하도록 설정되어 있으면 이 값을 반드시 1로 설정해야 한다. |
50 |
catalog, schema | 데이터베이스 catalog, schema 이름 |
5) TABLE 전략
- 키 생성 전용 테이블을 하나 만들고 여기에 이름과 값을 사용할 컬럼을 만들어 데이터베이스 시퀀스를 흉내내는 전략이다.
- 테이블을 사용하므로 모든 데이터베이스에 적용 가능하다.
- LOCK 등의 성능 이슈로 운영 서버에서의 사용은 권장하지 않는 전략이다.
@Entity
@TableGenerator(
name = "MEMBER_SEQ_GENERATOR",
table = "MY_SEQUENCES",
pkColumnValue = "MEMBER_SEQ", allocationSize = 1)
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.TABLE, generator = "MEMBER_SEQ_GENERATOR")
private Long id;
}
속성 | 설명 | 기본값 |
name | 식별자 생성기 이름 | 필수 |
table | 키생성 테이블명 | hibernate_sequences |
pkColumnName | 시퀀스 컬럼명 | sequence_name |
valueColumnName | 시퀀스 값 컬럼명 | next_val |
pkColumnValue | 키로 사용할 값 이름 | 엔티티 이름 |
initialValue | 초기 값, 마지막으로 생성된 값이 기준이다. | 0 |
allocationSize | 시퀀스 한 번 호출에 증가하는 수 (성능 최적화에 사용된다.) |
50 |
catalog, schema | 데이터베이스 catalog, schema 이름 | |
uniqueConstraints(DDL) | 유니크 제약 조건을 지정할 수 있다. |
6) 권장하는 식별자 전략
데이터베이스 기본 키는 다음 3가지 조건을 모두 만족해야 한다.
- null 값을 허용하지 않는다.
- 유일해야 한다.
- 변하면 안 된다.
미래까지 이 조건을 만족하는 자연 키는 찾기 어렵다. 대신 대리 키(대체 키)를 사용하자.
대리 키는 비즈니스와 무관한 임의의 값이므로 요구사항이 변경되어도 기본 키가 변경되는 일은 드물다.
자연 키 vs 대리 키
- 자연 키
- 비지니스 적으로 의미가 있는 키
- 주민등록번호, 이메일, 전화번호
- 대리 키
- 비지니스와 관련 없는 임의로 만들어진 키, 대체 키.
- 오라클 시퀀스, auto_increment, 키생성 테이블 </aside>
→ 권장: Long 타입 + 대체 키 + 키 생성전략 사용
4. 실전 예제 1 - 요구사항 분석과 기본 매핑
1) 요구사항 분석
- 회원은 상품을 주문할 수 있다.
- 주문 시 여러 종류의 상품을 선택할 수 있다.
2) 기능 목록
회원 기능
- 회원등록
- 회원조회
상품 기능
- 상품등록
- 상품수정
- 상품조회
주문 기능
- 상품주문
- 주문내역조회
- 주문취소
3) 도메인 모델 분석
- 회원과 주문의 관계: 회원은 여러 번 주문할 수 있다. (일대다)
- 주문과 상품의 관계: 주문할 때 여러 상품을 선택할 수 있다. 반대로 같은 상품도 여러 번 주문될 수 있다. 주문상품 이라는 모델을 만들어서 다대다 관계를 일다대, 다대일 관계로 풀어냄
4) 테이블 설계
5) 엔티티 설계와 매핑
6) 데이터 중심 설계의 문제점
- 현재 방식은 객체 설계를 테이블 설계에 맞춘 방식으로 테이블의 외래키를 객체에 그대로 가져왔기 때문에 객체 그래프 탐색이 불가능
- 관계형 데이터베이스는 연관된 객체를 찾을 때 외래 키를 사용해서 조인하면 되지만, 객체는 조인이라는 기능이 없으며 연관된 객체를 찾을 때 참조를 사용해야 함
- 외래 키만 가지고 있으면 연관된 엔티티를 찾을 때 외래 키로 데이터베이스를 다시 조회해야하는 문제점이 발생
RDB의 외래 키를 가지고 조회하기
// 주문을 조회한 다음 주문과 연관된 회원을 외래 키를 가지고 조회하기
Order order = em.find(Order.class, 1L);
Long memberId = order.getMemberId();
// 외래 키로 다시 조회
Member member = em.find(Member.class, memberId);
객체에서 참조를 사용해서 조회하기
// 주문을 조회한 다음 주문과 연관된 회원을 객체에서 참조를 사용해서 조회하기
Order order = em.find(Order.class, 1L);
Member findMember = order.getMember();
⇒ JPA를 사용하여 객체의 참조와 테이블의 외래 키를 매핑해서 객체에서는 참조를 사용하고 테이블에서는 외래 키를 사용할 수 있도록 해야한다.
'Courses > JPA' 카테고리의 다른 글
[자바 ORM 표준 JPA 프로그래밍 - 기본편] 3. 영속성 관리 - 내부 동작 방식 (0) | 2023.10.02 |
---|---|
[자바 ORM 표준 JPA 프로그래밍 - 기본편] 2. JPA 시작하기 (1) | 2023.10.01 |
[자바 ORM 표준 JPA 프로그래밍 - 기본편] 1. JPA 소개 (0) | 2023.10.01 |