이전 장에서는 매핑한 엔티티를 실제로 사용하는 부분을 다뤘다면,
이번 장에서는 엔티티를 매핑하는 부분을 다루게 됩니다.
매핑을 하기 위해서는 어노테이션과 xml을 사용하게 되는데 어노테이션을 사용하는 것이 더 쉽고 직관적이므로
이 책에서는 어노테이션을 이용해서 매핑을 설명합니다.
먼저 객체와 테이블을 매핑하는 어노테이션에 대해 알아보겠습니다.
4.1 @Entity
JPA를 사용할 때는 테이블과 매핑할 클래스에 @Entity를 붙여야 합니다.
Entity의 속성으로는
📍name
JPA에서 사용할 엔티티 이름을 지정하는 것입니다.
설정하지 않으면 클래스 이름 그대로 사용하게 됩니다.
(보통은 기본으로 클래스 이름을 사용합니다)
다른 패키지에서 이름이 같은 엔티티가 있지 않게 이름을 구분하여 만들어주어야 합니다.
🏠 주의해야 할 점
- 기본 생성자는 필수로 있어야 함
- final 클래스, enum, interface, inner 클래스에는 사용할 수 없습니다.
- 저장할 필드에 final을 사용하면 안됩니다.
이 때 첫번째 사항인 기본 생성자가 필수로 있어야 함은 엔티티 객체 생성 시 기본 생성자를 사용하기 때문에 있어야 합니다.
물론, 생성자가 없는 경우에는 기본 생성자를 자동으로 만들어주지만,
다른 생성자를 만들어둔 경우라면, 기본 생성자가 만들어지지 않기 때문에 유의해야합니다.
4.2 @Table
@Table은 엔티티와 매핑할 테이블을 지정해주는 어노테이션입니다.
@Entity처럼 name값을 지정하지 않는 경우에는 클래스명을 테이블명으로 사용하게 됩니다.
📍name
매핑할 테이블 이름을 지정할 수 있음.
따로 지정하지 않으면, 클래스명을 테이블 명으로 사용함.
📍catalog
catalog 기능이 있는 데이터베이스에서 catalog를 매핑함
📍schema
schema 기능이 있는 데이터베이스에서 schema를 매핑함
📍uniqueConstraints
DDL을 생성할 때 유니크 제약조건을 만드는 데 이때 여러 개의 복합 유니크 제약조건도 만들 수 있음.
(스키마 자동 생성 기능 사용 시에만 적용)
4.3 다양한 매핑 사용
1장의 회원 관리 프로그램에서 요구사항이 추가되었습니다!
1. 기존에는 단지 회원으로만 구분되었지만, 이제 일반 회원과 관리자로 롤타입을 지정하고,
2. 회원 가입일 / 수정일이 존재해야 합니다.
3. 회원을 설명하는 필드가 있되, 이 필드의 길이 제한을 두면 안됩니다.
멤버 엔티티에서 추가한 사항만 아래에 정리해보았습니다.
@Enumerated(EnumType.String)
private RoleType roleType;
@Temporal(TemporalType.TIMESTAMP)
private Date createdDate;
@Temporal(TemporalType.TIMESTAMP)
private Date lastModifiedDate;
@Lob
private String description;
먼저 일반 회원과 관리자를 구분하기 위해서 Enum을 사용했습니다.
public enum RoleType{
ADMIN, USER
}
enum을 사용할 때는 @Enumerated 어노테이션으로 매핑해야합니다.
생성일자, 수정일자를 column으로 추가해주었는데 날짜의 경우에는 @Temporal를 사용해서 매핑하게 됩니다.
3번째 요구사항인 멤버 설명 필드에는 길이제한이 없음을 충족시키기 위해 @Lob을 사용했습니다.
@Lob을 사용할 때는 CLOB, BLOB을 매핑할 수 있습니다.
일반적인 String의 경우에는 VARCHAR 타입으로 저장하지만 길이제한이 없다는 조건 때문에 @Lob을 사용한 것입니다.
4.3 데이터베이스 스키마 자동 생성
JPA는 클래스의 매핑 정보와 데이터베이스 방언을 통해서 데이터베이스 스키마를 생성합니다.
https://www.ibm.com/kr-ko/topics/database-schema
데이터베이스 스키마란 무엇인가요? | IBM
데이터베이스 스키마는 관계형 데이터베이스 내에서 데이터가 구성되는 방식을 정의합니다.
www.ibm.com
데이터베이스 스키마는
관계형 데이터베이스에서 데이터가 어떻게 구성되는지 테이블 이름, 필드, 데이터 형식, 연관관계 등을 포함한 것입니다.
<property name="hibernate.hbm2ddl.auto" value="create" />
위의 속성을 persistence.xml에 지정하면 실행할 때 데이터베이스 테이블을 자동 생성해줍니다.
그리고 higernate.show_sql을 true로 설정하면 ddl을 콘솔에서 확인가능합니다.
위에서 수정한 엔티티를 적용한 어플리케이션 실행시에는
이전의 테이블을 삭제 후, 수정한 내용을 통해 다시 테이블을 생성합니다.
그리고 자동 생성될 때 DDL은 데이터베이스 방언에 따라 변동됩니다.
이렇게 자동 생성된 테이블을 사용하면 직접 테이블을 만들지 않아도 되기 때문에
편리하기는 하지만
실제 운영환경에서 사용할 정도로 완벽하다고 볼 수는 없습니다.
hibernate.hbm2ddl.auto를 앞에서는 create로 지정해서 확인해보았는데,
다른 옵션도 살펴보겠습니다.
📍create
기존 테이블 삭제 후 생성
📍create-drop
create와 동일하지만 어플리케이션 종료 시 테이블도 삭제함
📍update
테이블과 엔티티 매핑 정보를 비교 후 변경된 사항만 수정합니다.
📍validate
테이블과 엔티티 매핑 정보를 비교하고 차이에 대한 경고를 알려줍니다.
(테이블 수정 x)
📍none
자동 생성을 사용하지 않습니다.
HBM2DDL 주의사항
- ddl을 수정하는 옵션(update, create, create-drop)은 개발, 테스트 때에만 사용할 것!
개발 초기 : create , update
개발자 환경 , CI 서버 : create, create-crop
테스트 서버 : update, validate
운영서버 : validate, none
이름 매핑 전략
- 필드 명은 카멜케이스 적용, 데이터베이스는 언더스코어 방식을 주로 사용하는데, 이렇게 일일히 column name을 지정해주면 귀찮으니까 hibernate.ejb.naming_strategy를 사용하면 자동으로 데이터베이스에 언더스코어 방식 적용해줌
<property name="hibernate.ejb.naming_strategy" value="org.hibernate.cfg.ImproveNamingStrategy" />
4.4 DDL 생성 기능
헉 또 요구사항이 추가되었습니다...!
1. 회원 이름 필수 입력
2. 회원 이름은 10자 초과하면 안됨
@Column(name = "NAME", nullable = false, length = 10)
private String username;
위와 같이 not null 제약조건과 length 속성 값을 지정해서 문자의 크기를 지정할 수 있습니다.
즉, 생성된 DDL을 살펴보면
create table member {
ID varchar(255) not null,
NAME varchar(10) not null,
...
primary key (ID)
}
name column의 크기가 10으로 지정되었고 not null인 것을 볼 수 있습니다.
이뿐만이 아니라 유니크 제약조건도 추가할 수 있습니다.
@Table(name = "MEMBER", uniqueConstraints = {@UniqueConstraint(
name = "NAME_AGE_UNIGUE",
columnNames = {"NAME", "AGE"})})
위와 같이 지정해주게 되면
ALTER TABLE MEMBER
ADD CONSTRAINT NAME_AGE_UNIQUE UNIQUE (NAME, AGE)
이렇데 유니크 제약조건을 추가하는 DDL이 생성됩니다.
length나 nullable. unique 제약조건의 경우에는 ddl 사용 시에만
적용이 되는 것이고
JPA를 실행할 때 영향이 없습니다.
그러므로 직접 DDL을 입력하고 실행하면 되는 거 아닌가?
쓸모 없지 않나? 생각할 수도 있습니다.
그러나, 코드 상에 해당 내용이 명확히 기록되어 있기 때문에
개발자들이 데이터베이스에 접근하지 않고도 해당 테이블에 대한 제약조건을 확인할 수 있습니다.
4.6 기본 키 매핑
이번에는 PK 매핑을 알아보겠습니다!
@Id를 통해서 엔티티의 기본 키를 할당해주었습니다.
@Entity
public class Member {
@Id
@Column(name = "ID")
private String id;
...
}
이렇게 해주면 직접 엔티티를 생성할 때마다
키를 직접 지정해주어야 하는데요.
MySQL의 AUTO_INCREMENT와 같이 데이터베이스가 자동으로
키를 생성해주는 기능은 JPA에서 사용할 수 없을까요..?
JPA에서 기본 키를 생성하는 방식은 크게 두개가 있습니다.
💬직접 할당
기본 키를 직접 입력해야 함
위와 같이 @Id만 하면 됨
💬자동 생성
대리 키 사용
@Id도 하고 @GeneratedValue 사용
- IDENTITY : 기본 키 생성을 데이터베이스 보고 하라고 시킴
- SEQUENCE : 데이터베이스 시퀀스를 사용해 기본 키 할당
- TABLE : 키 생성 테이블을 사용
자동 생성 시에 3가지 전략이 있는 이유 데이터베이스마다 지원하는 방식이 다르기 때문입니다.
(MySQL : 시퀀스 제공 / 오라클 : 시퀀스 제공 X
시퀀스나 아이덴티티는 데이터베이스에 따라 적용이 달라지지만
테이블은 모든 데이터베이스에서 사용 가능)
📍기본 키 직접 할당 전략
@Id를 적용할 수 있는 자바 타입
1. 자바 기본형
2. 자바 래퍼형
2. String
3. java.util.Date
4. java.sql.Date
5. java.math BigDecimal
6. java.math.BigInteger
기본키를 직접 할당하는 방법
: 엔티티 저장 전에 기본키를 직접 할당해주면 됨
(자유로운 우리를 봐... 자유로워 이 느낌의 설명이지만 실제로 그러니까...)
Board board = new Board();
board.setId("id1");
em.persist(board);
📍IDENTITY 전략
자동생성 전략의 하나고 주로 MySQL, PostgreSQL, SQL server DB2에서 사용합니다.
MySQL의 AUTO_INCREMENT의 경우에는 아래와 같이 사용할 수 있습니다.
CREATE TABLE BOARD (
ID INT NOT NULL AUTO_INCREMENT PRIVARY KEY,
DATA VARCHAR(255)
}
INSERT INTO BOARD(DATA) VALUES('A');
INSERT INTO BOARD(DATA) VALUES('B');
이런 식으로 지정을 해두면
ID | DATA |
1 | A |
2 | B |
ID를 null로 해도 자동으로 1, 2, 3 ... 순서대로 ID가 증가됩니다.
IDENTITY 전략은 데이터베이스에 값을 저장하고 난 후에 기본 키 값을 구하는 경우에 사용합니다.
이제 이러한 전략을 JPA에서 사용하는 방식에 대해서 알아보겠습니다.
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
필드에 @GeneratedValue(strategy = GenerationType.IDENTITY)로
지정하게 되면 이제 IDENTITY 전략을 사용할 수 있게 됩니다.
Board board = new Board();
em.persist(board);
System.out.println("board.id = " + board.getId());
JPA를 통해서 보드 객체를 저장하게 되면
위에서 id값을 지정하지 않더라도 id 값이 자동으로 생성됨에 따라
순차적으로 지정된 id가 출력됩니다.
만약에 위의 코드에서 이전에 생성된 board가 없었다면
1이 출력될 것입니다.
IDENTITY 전략과 최적화
아이덴티티 전략을 이용하면, 데이터베이스에 데이터를 insert하고 난 후 기본 키 값을 조회하고
Statement.getGeneratedKey() 메서드를 사용하면서 데이터를 저장함과 동시에 기본 키값을 조회할 수 있어
데이터베이스와 두 번 통신하는 게 아니라 한 번만 통신하므로 최적화할 수 있습니다!
* 이 때는 데이터베이스를 저장해야 식별자 값을 저장할 수 있으므로 em.persist하는 순간 플러시 됨 -> 쓰기 지연 동작 안 함
📍SEQUENCE 전략
시퀀스 : 유일한 값을 순서대로 생성하는 특별한 데이터베이스 객체
시퀀스 전략은 시퀀스를 사용해서 기본 키를 생성하게 되는데
oracle, postgreSQL, db2, h2에서 이러한 방식을 사용할 수 있습니다.
CREATE TABLE BOARD {
ID BIGINT NOT NULL PRIMARY KEY,
DATA VARCHAR(255)
}
CREATE SEQUENCE BOARD_SEQ START WITH 1 INCREMENT BY 1;
이렇게 시퀀스를 만들어줄 수 있습니다!
@GeneratedValue(strategy = GenerationType.SEQUENCE,
generator = "BOARD_SEQ_GENERATOR")
private Long id;
JPA에서는 위와 같이 BOARD_SEQ_GENERATOR라는 시퀀스 생성기를 등록하고
sequenceName 속성으로 BOARD_SEQ를 지정하고 실제 데이터베이스의 시퀀스에 매핑합니다.
Board board = new Board();
em.persist(board);
System.out.println("board id = " + board.getId());
코드 자체는 같지만,
시퀀스 전략은 em.persist 호출 전에 시퀀스를 이용해 식별자를 조회합니다.
그 후 식별자를 엔티티에 할당하고 엔티티를 영속성 컨텍스트에 저장합니다.
그리고 트랜잭션을 커밋할 때에서 플러시가 일어나서 디비에 저장합니다.
아이덴티티 전략의 경우에는 em.persist 될 때 디비에 저장되고 그 다음에 식별자 조회 후 엔티티 식별자에 할당하므로
식별자 조회하는 시점이 다르다는 점이 포인트입니다...
@SequenceGenerator
📍name(필수) : 식별자 생성기 이름
📍sequenceName : 데이터베이스에 등록되어 있는 시퀀스 이름
📍initialValue : DDL 생성 시 사용되고 DDL 생성할 때 처음 시작하는 수
📍initialValue : DDL 생성 시 사용되고 DDL 생성할 때 처음 시작하는 수
📍allocationSize : 시퀀스 한 번에 호출 시 증가하는 수인데 기본 값 50
📍catalog, schema : 데이터베이스 catalog, schema 이름
📍TABLE 전략
키 생성 전용 테이블을 만든 다음에 이 테이블에 이름과 값으로 사용하는 칼럼을 만들어서
시퀀스를 모방하는 전략입니다.
시퀀스나 아이덴티티는 특정 데이터베이스에서만 사용할 수 있었지만
테이블 전략은 모든 데이터베이스에서 사용할 수 있습니다.
create table my_sequences (
sequence_name varchar(255) not null,
next_val bigint,
primary key ( sequence_name )
)
seqeunce_name을 시퀀스 이름으로 사용하고
next_val을 시퀀스 값으로 사용합니다.
@Id
@GeneratedValue(strategy = GenerationType.TABLE,
generator = "BOARD_SEQ_GENERATOR")
private Long id;
위와 같이 전략을 table로 변경하고
generator를 BOARD_SEQ_GENERATOR로 등록하고
MY_SEQUENCES 테이블을 키 생성용 테이블로 매핑하면 이제부터 테이블 키 생성이 자동으로 됩니다...!
이를 이용해서 객체를 영속성 컨텍스트에 저장하게 되면 시퀀스와 비슷한 형태로 실행됩니다.
테이블을 살펴보면,
next_val이 증가하면서 자동으로 할당되는 모습을 볼 수 있습니다.
@TableGenerator
속성 | 기능 | 기본값 |
name | 식별자 생성기 이름 | 필수 |
table | 키 생성 테이블 명 | hibernate_sequences |
pkColumnName | 시퀀스 컬럼명 | sequence_name |
valueColumnName | 시퀀스 값 컬럼명 | next_val |
pkColumnValue | 키로 사용할 값 이름 | 엔티티 이름 |
initialValue | 초기 값, 마지막으로 생성된 값이 기준이다. | 0 |
allocationSize | 시퀀스 한 번 호출에 증가하는 수 | 50 |
catalog, schema | 데이터베이스 catalog, schema 이름 | |
uniqueConstraints | 유니크 제약 조건 지정 |
Table 전략 & 최적화
table 전략은 값을 조회하는 과정에서 select, 다음 값으로 증가시키기 위한 update 쿼리를 사용함에 따라 sequence와 다르게 데이터베이스와 한 번 더 통신하게 됩니다.
이러한 점을 개선하기 위해서 @TableGenerator.allocationSize를 사용할 수 있습니다.
📍AUTO 전략
선택한 데이터베이스 방언에 따라 IDETITY, SEQUENCE, TABLE 중 하나를 선택합니다.
오라클은 SEQUENCE, MySQL은 IDENTITY를 선택합니다.
@Id
@GeneratedValue(strategy = GeneratinoType.AUTO)
private Long id;
✅AUTO 전략의 장점
데이터베이스를 변경해도 코드를 수정할 필요가 없다.
(키 생성 전략이 아직 확정되지 않은 단계에서 편리함)
AUTO 사용 시에 SEQUENCE나 TABLE 전략이 선택되면 시퀀스 나 키 생성 테이블을 미리 만들어야 합니다.
📍기본 키 매핑 정리
✅ 직접 할당
em.persist 호출 전에 직접 식별자 값을 할당해야 함
✅ SEQUENCE
데이터베이스 시퀀스에서 식별자 값 획득하고 나서 영속성 컨텍스트에 저장
✅ TABLE
데이터베이스 시퀀스 생성용 테이블에서 식별자 값 획등 후에 영속성 컨텍스트에 저장
✅ IDENTIDY
데이터베이스에서 엔티티를 저장해서 식별자 값을 획등하고
영속성 컨텍스트에 저장
식별자의 조건
1. null 값 허용 x
2. 유일해야 함(unique)
3. 변하면 됨
기본 키의 선택 전략
1. 자연키
: 비즈니스에 의미가 있는 키
ex: 주민등록번호, isbn
2. 대리키
: 비지니스와 관련 없는 임의로 만들어진 키
(=대체키)
ex: 오라클 시퀀스, auto_increment, 키 생성 테이블 사용
자연키보다는 대리키를 사용하자!
비즈니스 환경은 변하므로 자연키도 변할 수 있음.
대리 키를 기본키로 사용하고 자연 키의 후보를 유니크 인덱스로 설정하면 좋습니다.
또한, JPA는 모든 엔티티에 일관된 방식으로 대리 키 사용을 권장합니다!
4.7 필드와 컬럼 매핑 : 레퍼런스
📍@Coulmn
✅name
필드와 매핑할 테이블의 컬럼 이름
✅nullable
null 값의 허용 여부를 설정한다.
false로 설정 시 DDL 생성 시에 not null 제약조건 붙고
기본 값은true입니다.
@Column(nullable = false)
private String data;
data varchar(255) not null;
✅unique
@Table의 uniqueConstraints와 같으나
한 컬럼에만 해당합니다.
@Column(unique = true)
private Stringn username;
alter table Tablename
add constraint UK_Xxx unique (username)
✅columnDefinition
데이터베이스 컬럼 정보를 직접 줄 수 있습니다.
@Column(columnDefinition = "varchar(100) default 'EMPTY'"
private String data;
data varchar(100) default 'EMPTY'
✅ length
문자 길이 제약조건으로 String 타입만 쓸수 있고 기본은 255입니다.
@Column(length = 400)
private String data;
data varchar(400)
✅precision, scale
BigDecimal 타입에서 사용하고
precision은 소수점을 포함한 전체 자릿수,
scale은 소수의 자릿수를 사용할 때 사용합니다.
@Column(precision = 10, scale = 2)
private BigDecimal cal;
cal numeric(10, 2) // H2 PostgreSQL
cal numneric(10, 2) // oracle
cal decimal(10, 2) // MySQL
📍@Enumerated
enum을 매핑할 때 사용합니다.
✅EnumType.ORDINAL
enum 순서를 데이터베이스에 저장합니다.
✅EnumType.STRING
enum 이름을 데이터베이스에 저장합니다.
실제 예시
enum RoleType{
ADMIN, USER
}
이렇게 된 enum으로 매핑을 진행해봅시다.
@Enumerated(EnumType.STRING)
private RoleType roleType;
member.setRole(RoleType.ADMIN);
이렇게 STRING으로 저장하면 데이터베이스에 ADMIN 그대로 저장이 되지만
ORDINAL로 저장하게 되면 ADMIN은 0, USER는 1로 저장하게 됩니다.
ORINAL의 경우에는 저장되는 데이터 크기가 작지만 enum이 변경되는 경우를 대비할 수 없어서
STRING이 더 권장되는 방법입니다.
📍@Temporal
✅TemporalType.DATE
날짜.
(2025-03-30)
✅ TemporalType.TIME
시간과 매핑
(11:11:00)
✅TemporalType.TIMESTAMP
날짜 + 시간
(2024-02-10 12:22:00)
📍@Lob
@Lob에서는 지정할 수 있는 속성은 없지만
필드 타입이 String이면 CLOB, 아니면 BLOB으로 매핑합니다.
📍@Transient
매핑하지 않는 필드에 해당합니다.
조회나 저장이 불가하고
임시로 어떤 값을 보관하고 싶을 때 사용합니다.
📍@Access
JPA가 엔티티 데이터에 접근하는 방식을 지정합니다.
✅필드 접근
AccessType.FIELD로 지정
필드에 직접 접근하여 private여도 접근 가능
✅프로퍼티 접근
AccessType.PROPERTY로 지정
접근자 (Getter) 사용
실전 예제!
요구사항 분석
1. 회원은 상품을 주문 가능
2. 주문 시 여러 종류의 상품을 선택 가능
회원 기능 - 등록 / 조회
상품 기능 - 등록 / 수정 /조회
주문 기능 - 상품 주문, 주문 내역 조회, 주문 취소
도메인 모델 분석 및 엔티티 설계
회원 - 주문: 회원이 주문을 여러번할 수 있으니 일대다 관계입니다.
주문 - 상품 : 주문할 때 상품을 여러번 넣을 수 있고 상품도 여러번 주문될 수 있으므로
다대다 이지만 다대다는 관계형 데이터베이스나 엔티티에서 거의 사용되지 않으므로
주문상품이라는 연결엔티티를 추가하여 일대다, 다대일로 변경해주었습니다.
// Member
@Entity
@Getter
@Setter
public class Member{
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String name;
private String city;
private String zipcode;
}
@Entity
@Table(name = "ORDERS")
@Getter
@Setter
public class Order{
@Id @GeneratedValue
@Column(name ="ORDER_ID")
private Long id;
@Column(name = "MEMBER_ID")
private Long memberId;
@Temporal(Temporal.Type.TIMESTAMP)
private Date orderDate;
@Enumerated(EnumType.STRING)
private OrderStatus status;
}
@Entity
@Table(name = "ORDER_ITEM")
@Getter
@Setter
public class OrderItem{
@Id @GeneratedValue
@Column(name = "ORDER_ITEM_ID")
@Column(name = "ITEM_ID")
private Long itemId;
@Column(name = "ORDER_ID")
private Long orderId;
private int orderPrice;
private int count;
}
@Entity
@Getter
@Setter
public class Item{
@Id @GeneratedValue
@Column (name = "ITEM_ID")
private Long id;
private String name:
private int price;
private int stockQuantity;
}
이렇게 설계하는 게 이상하다고 느끼면 객체지향 설계를 의식하는 개발자라는데
나는 정말 아무생각이 없었는데 해당 부분을 읽고 나서야 깨달았다..
내가 이제까지 많이 해온 참조가 없다는 걸 그제서야 깨달았다 ㅋㅋ
이렇게 외래키만 있으면 다시 외래키로 데이터베이스를 조회해야하는 문제가 생긴다!!
다음 장에서 참조를 배우니까 더 나은 방식으로 매핑할 수 있다!!
'BE > 자바 ORM 표준 JPA 프로그래밍' 카테고리의 다른 글
[JPA STUDY] 03. 영속성 관리 (0) | 2025.03.22 |
---|---|
[ JPA STUDY ] 2장.JPA 시작 (0) | 2025.03.15 |
[ JPA STUDY ] 1장. JPA 소개 (0) | 2025.03.15 |