
JPA를 사용하는 데 가장 중요한 일은 엔티티와 테이블을 정확히 매핑하는 것이다.
따라서 매핑 어노테이션을 숙지하고 사용해야 한다. JPA는 다양한 매핑 어노테이션을 지원하는데 크게 4가지로 분류할 수 있다. 아래는 대표 어노페이션들이다.
- 객체와 테이블 매핑: @Entiry, @Table
- 기본키 매핑: @Id
- 필드와 컬럼 매핑:@Column
- 연관관계 매핑:@ManyToOne, @JoinColumn
※매핑 정보는 XML이나 어노테이션 중에 선택해서 기술하면 되는데 각각 장단점이 있지만 어노테이션을 사용하는 쪽이 좀더 쉽고 직관적이다 앞으로는 어노테이션을 사용해서 매팽정보를 구성하는 방법만 설명한다.
@Entity
JPA를 사용해서 테이블과 매핑할 클래스는 @Entity 어노테이션을 필수로 붙여야 한다. @Entity가 붙은 클래스는 JPA가 관리하는 것으로, 엔티티라 부른다
@Entity 적용시 주의사항은 다음과 같다.
- 기본 생성자는 필수다(파라미터가 없는 public 또는 protected 생성자)
- final 클래스, enum, interface, inner 클래스에는 사용할 수 없다.
- 저장할 필드에 final을 사용하면 안 된다.
JPA가 엔티티 객체를 생성할 때 기본 생성자를 사용하므로 이 생성자는 반드시 있어야 한다. 자바는 생성자가 하나도 없으면 다음과 같은 기본 생성자를 자동으로 만든다
public Member(){}//기본 생성자
문제는 다음과 같이 생성자를 하나 이상 만들면 자바는 기본 생성자를 자동으로 만들지 않는다. 이때는 기본 생성자를 직접 만들어야 한다.
public Member() {} 직접 만든 기본 생성자
//임의의 생성자
public Member(String name){
this.name = name
}
@Table
@Table은 인테테와 매핑할 테이블을 지정한다. 생략하면 매핑한 엔티티 이름을 테이블 이름으로 사용한다.
다양한 매핑 사용
앞서 개발하던 회원 관리 프로그램에 다음 요구사항이 추가되었다.
- 회원은 일반 회원과 관리자로 구분해야 한다.
- 회원 가입일과 수정일이 있어야 한다.
- 회원을 설명할 수 있는 필드가 있어야 한다. 이 필드는 길이 제한이 없다.
요구사항을 만족하도록 회원 엔티티에 기능을 추가하자
@Data
@Entity
@Table(name = "MEMBER")
public class Member {
@Id
@Column(name = "ID")
private String id;
@Column(name = "NAME")
private String username;
//매핑 정보가 없는 필드
private Integer age;
//==추가==
@Enumerated(EnumType.STRING)
private RoleType roleType;
@Temporal(TemporalType.TIMESTAMP)
private Date createdDate;
@Temporal(TemporalType.TIMESTAMP)
private Date lastModifiedDate;
@Lob
private String description;
}
- roleType : 자바의 enum을 사용해서 회원의 타입을 구분했다. 일반 회원은 USER, 관리자는 ADMIN이다. 이처럼 자바의 enum을 사용하려면 @Enumerated 어노테이션으로 매핑해야 한다.
- createdDate, lastModifiedDate: 자바의 날짜 타입은 @Temporal을 사용해서 매핑한다.
- description: 회원을 설명하는 필드는 길이 제한이 없다. 따라서 DB의 VARCHAR 타입 대신에 CLOB 타입으로 저장해야한다 @Lob을 사용하면 CLOB, BLOB 타입을 매핑할 수 있다.
지금까지는 테이블을 먼저 생성하고 그 다음에 엔티티를 만들었지만, 이번에는 DB 스키마 자동 생성을 사용해서 엔티티만 만들고 테이블은 자동 생성 되도록 해보자
데이터베이스 스키마 자동 생성
JPA는 DB 스키마를 자동으로 생성하는 기능을 지원한다. 클래스의 매핑정보를 보면 어떤 테이블에 어떤 컬럼을 사용하는지 알 수 있다. JPA는 이 매핑정보와 DB 방언을 사용해서 DB 스키마를 생성한다.
persistence.xml에 다음 속성을 추가하여 스키마 자동 생성 기능을 사용해보자
<property name="hibernate.hbm2ddl.auto" value="create"/>
이 속성을 추가하면 애플리케이션 실행 시점에 DB 테이블을 자동으로 생성한다. 참고로 hibernate.show_sql 속성을 true로 설정하면 콘솔에 실행되는 테이블 생성 DDL(Data Definition Language)을 출력할 수 있다.
Hibernate:
drop table if exists MEMBER cascade
Hibernate:
create table MEMBER (
age integer,
createdDate timestamp(6),
lastModifiedDate timestamp(6),
ID varchar(255) not null,
NAME varchar(255),
roleType varchar(255) check (roleType in ('ADMIN','USER')),
description clob,
primary key (ID)
)
실행된 결과를 보면 기존 테이블을 삭제하고 다시 생성한 것을 알 수 있다. 그리고 방금 추가한 roleType은 VARCHAR 타입으로, createdDate, lastModifiedDate는 TIMESTAMP 타입으로, description은 CLOB 타입으로 생성되었다. 자동 생성되는 DDL은 지정한 DB 방언에 따라 달라진다
스키마 자동 생성 기능을 사용하면 애플리케이션 실행 시점에 DB 테이블이 자동으로 생성되므로 개발자가 테이블을 직접 생성하는 수고를 덜 수 있다. 하지만 스키마 자동 생성 기능이 만든 DDL은 운영 환경에서 사용할 만큼 완벽하지는 않으므로 개발 환경에서 사용하거나 매핑을 어떻게 해야 하는지 참고하는 정도로만 사용하는 것이 좋다.
다음은 hibernate.hbm2ddl.auto 속성이다.
※운영 서버에서 create, create-drop, update처럼 DLL을 수정하는 옵션은 절대 사용하면 안 된다.
오직 개발 서버나 개발 단계에서만 사용해야 한다. 이 옵션들은 운영 중인 DB의 테이블이나 컬럼을 삭제할 수 있다.
※ 이름 매핑 전략 변경하기
단어와 단어를 구분할 때 자바 언어는 관례상 roleType과 같이 카멜 표기법을 주로 사용하고, DB는 관례상 role_type과 같이 언더스코어를 주로 사용한다. 앞서 살펴본 예제에서 회원 엔티티를 이렇게 매핑하려면 @Column.name 속성을 명시적으로 사용해서 이름을 지어주어야한다. 예를 들어 다음과 같이 매핑해야 한다.
@Column(name="role_type")//언더스코어로 구분
String roleType //카멜 표기법으로 구분
hibernate.ejb.naming_strategy 속성을 사용하면 이름 매핑 전략을 변경할 수 있다. 직접 이름 매핑 전략을 구현해서 변경해도되지만, hibernate는 org.hibernate.cfg.ImprovedNamingStrategy 클래스를 제공한다. 이 클래스는 테이블 명이나 컬럼 명이 생략되면 자바의 카멜 표기법을 테이블의 언더스코어 표기법으로 매핑한다.
DDL 생성 기능
회원 이름은 필수로 입력되어야 하고, 10자를 초과하면 안 된다는 제약조건이 추가되었다. 스키마 자동 생성하기를 통해 만들어지는 DDL에 이 제약조건을 추가해보자.
@Column(name = "NAME", nullable = false, length = 10)//추가
private String username;
@Column 매핑정보의 nullable 속성 값을 false로 지정하면 자동 생성되는 DDL에 not null 제약조건을 추가할 수 있다. 그리고 length 속성 값을 사용하면 자동 생성되는 DDL에 문자의 크기를 지정할 수 있다. nullable=false, length=10으로 지정해보자
이번에는 유니크 제약조건을 만들어주는 @Table의 uniqueConstraints 속성을 알아보자
@Table(name = "MEMBER", uniqueConstraints = {@UniqueConstraint(
name = "NAME_AGE_UNIQUE",
columnNames = {"NAME", "AGE"}
)})
이런 기능들은 단지 DDL을 자동으로 생성할 때만 사용되고 JPA의 실행 로직에는 영향을 주지 않는다.
따라서 스키마 자동생성 기능을 사용하지 않고 직접 DDL을 만든다면 사용할 이유가 없다.
그래도 이 기능을 사용하면 애플리케이션 개발자가 엔티티만 보고도 손쉽게 다양한 제약조건을 파악할 수 있는 장점이 있다.
기본 키 매핑
지금까지 @Id 어노테이션만 사용해서 회원의 기본 키를 애플리케이션에서 직접 할당했다. 기본 키를 애플리케이션에서 직접 할당하는 대신에 DB가 생성해주는 값을 사용하려면 어떻게 매핑해야 할까? DB마다 기본 키를 생성하는 방식이 서로 다르므로 이 문제를 해결하기는 쉽지 않다. JPA는 이런 문제들을 어떻게 해결하는지 알아보자.
JPA가 제공하는 DB PK 생성 전략을 다음과 같다
- 직접 할당: 기본 키를 애플리케이션에서 직접 할당한다
- 자동 생성: 대리 키 사용 방식
- IDENTITY: 기본 키 생성을 DB에 위임한다.
- SEQUENCE: DB 시퀀스를 사용해서 기본 키를 할당한다.
- TABLE: 키 생성 테이블을 사용한다.
자동 생성 전략이 이렇게 다양한 이유는 DB 벤더마다 지원하는 방식이 다르기 때문이다.
기본 키를 직접 할당하려면 @Id만 사용하면 되고, 자동 생성 전략을 사용하려면 @Id에 @GeneratedValue를 추가하고 원하는 키 생성 전략을 선택하면 된다.
ㅈ
※키 생성 전략을 사용하려면 persistence.xml에 hibernate.id.new_generator_mappings=true 속성을 반드시 추가해야 한다. hibernate는 더 효과적이고 JPA 규격에 맞는 새로운 키 생성 전략을 개발했는데 과거 버전과의 호환성을 유지하려고 기본 값을 false로 두었다 기존 hibernate 시스템을 유지보수하는 것이 아니라면 반드시 true로 설정하자.
기본 키 직접 할당 전략
기본 키를 직접 할당하려면 다음 코드와 같이 @Id로 매핑하면 된다.
@Id 적용 가능 자바 타입은 다음과 같다.
- 자바 기본형
- 자바 래퍼형
- String
- java.util.Date
- jave.sql.Date
- java.math.BigDecimal
- java.math.BigInteger
기본 키 직접 할당 전략은 em.persist()로 엔티티를 저장하기 전에 애플리케이션에서 기본 키를 직접 할당하는 방법이다.
Board board = new Board();
board.setId("id1")//기본키 직접 할당
em.persist(board);
IDENTITY 전략
IDENTITY는 기본 키 생성을 DB에 위임하는 전략이다. 주로 MySQL, PostgreSQL, SQL Server, DB2에서 사용한다. IDENTITY 전략은 DB에 값을 저장하고 나서야 기본 키 값을 구할 수 있을 때 사용한다. 지금처럼 식별자가 생성되는 경우에는 @GenratedValue 어노테이션을 사용하고 식별자 생성 전략을 선택해야 한다. @GeneratedValue의 strategy 속성 값을 GenerationType.IDENTITY로 지정하면 된다.
@Id
@Column(name = "ID")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private String id;
※엔티티가 영속 상태가 되려면 식별자가 반드시 필요하다 그런데 IDENTITY 식별자 생성 전략은 엔티티를 DB에 저장해야 식별자를 구할 수 있으므로 em.persist()를 호출하는 즉시 INSERT SQL이 DB에 전달된다. 따라서 이 전략은 트랜잭션을 지원하는 쓰기 지연이 동작하지 않는다.
※IDENTITY 전략과 최적화
IDENTITY 전략은 데이터를 DB에 INSERT한 후에 기본 키 값을 조회할 수 있다. 따라서 엔티티에 식별자 값을 할당하려면 jpa는 추가로 DB를 조회해야 한다. Statement.getGeneratedKeys()를 사용하면 데이터를 저장하면서 동시에 생성되는 기본 키 값도 얻어 올 수 있다. hibernate는 이 메소드를 통해서 DB와 한 번만 통신한다.
SEQUENCE 전략
DB 시퀀스는 유일한 값을 순서대로 생성하는 특별한 DB 오브젝트다. SEQUENCE 전략은 이 시퀀스를 사용해서 기본 키를 생성한다. 이 전략은 시퀀스를 지원하는 오라클, PostgreSQL, DB2, H2 DB에서 사용할 수 있다.
@Entity
@SequenceGenerator(
name = "BOARD_SEQ_GENERATOR",
sequenceName = "BOARD_SEQ", //매핑할 데이터베이스 시퀀스 이름
initialValue = 1, allocationSize = 1
)
public class Board{
@Id
@Column(name = "ID")
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "BOARD_SEQ_GENERATOR")
private String id;
}
@SequenceGenerator를 사용해서 BOARD_SEQ_GENERATOR라는 시퀀스 생성기를 등록했다. 그리고 sequenceName 속성의 이름으로 BOARD_SEQ를 지정했는데 JPA는 이 시퀀스 생성기를 실제 DB의 BOARD_SEQ 시퀀스와 매핑한다.
다음으로 키 생성 전략을 GenerationType.SEQUENCE로 설정하고 generator = "BOARD_SEQ_GENERATOR"로 방금 등록한 시퀀스 생성기를 선택했다. 이제부터 id 식별자 값은 BOARD_SEQ_GENERATOR 시퀀스 생성기가 할당한다.
시퀀스 사용 코드는 IDENTITY 전략과 같지만 내부 동작 방식은 다르다. SEQUENCE 전략은 em.persist()를 호출할 때 먼저 DB 시퀀스를 사용해서 식별자를 조회한다. 그리고 조회한 식별자를 엔티티에 할당한 후에 엔티티를 영속성 컨텍스트에 저장한다. 이후 트랜잭션을 커밋해서 플러시가 일어나면 엔티티를 DB에 저장한다. 반대로 이전에 설명했던 IDENTITY 전략은 먼저 엔티티를 DB에 저장한 후에 식별자를 조회해서 엔티티의 식별자에 할당한다.
TABLE 전략
TABLE 전략은 키 생성 전용 테이블을 하나 만들고 여기에 이름과 값으로 사용할 컬럼을 만들어 데이터베이스 시퀀스를 흉내내는 전략이다. 이 전략은 테이블을 사용하므로 모든 DB에 적용할 수 있다.
create table MY_SEQUENCES (
sequence_name varchar(255) not null,
next_val bigint,
primary key (sequence_name)
)
sequence_name 컬럼을 시퀀스 이름으로 사용하고 next_nal컬럼을 시퀀스 값으로 사용한다.
@Data
@Entity
@TableGenerator(
name = "BOARD_SEQ_GENERATOR",
table = "MY_SEQUENCES", //매핑할 데이터베이스 시퀀스 이름
pkColumnValue = "BOARD_SEQ", allocationSize = 1
)
public class Board {
@Id
@Column(name = "ID")
@GeneratedValue(strategy = GenerationType.TABLE, generator = "BOARD_SEQ_GENERATOR")
private Long Id;
}
먼저 @TableGenerator를 사용해서 테이블 키 생성기를 등록한다. 여기서 BOARD_SEQ_GENERATOR라는 이름의 테이블 키 생성기를 등록하고 방금 생성한 MY_SEQUENCES 테이블을 키 생성용 테이블로 매핑했다. 다음으로 TABLE 전략을 사용하기 위해 GenerationType.TABLE을 선택했다. 그리고 @GeneratedValue.generator에 방금 만든 테이블 키 생성기를 지정했다. 이제부터 id 식별자 값은 BOARD_SEQ_GENERATOR 테이블 키 생성기가 할당한다.
TABLE 전략은 시퀀스 대신에 테이블을 사용한다는 것만 제외 하면 SEQUENCE 전략과 내부 동작방식이 같다.
MY_SEQUENCES 테이블을 보면 @TableGenerator.pkColumnValue에서 지정한 "BOARD_SEQ"가 컬럼명으로 추가된 것을 확인할 수 있다. 이제 키 생성기를 사용할 때마다 next_val 컬럼 값이 증가한다.
@TableGenerator를 분석해보자
※Table 전략은 값을 조회하면서 SELECT 쿼리를 사용하고 다음 값으로 증가시키기 위해 UPDATE 쿼리를 사용한다. 이 전략은 SEQUENCE 전략과 비교해서 DB와 한 번 더 통신하는 단점이 있다. TABLE 전략을 최적화하려면 @TableGenerator.allocationSize를 사용하면 된다.
AUTO 전략
DB의 종류도 많고 PK를 만드는 방법도 다양하다. GenerationType.ATUO는 선택한 DB 방언에 따라 IDENTITY,SEQUENCE, TABLE 전략 중 하나를 자동으로 선택한다.
@Entity
public class Board {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
}
@GeneratedValue.strategy의 기본값은 AUTO다. 따라서 다음과 같이 사용해도 결과는 같다.
@Id @GeneratedValue
private Long id;
AUTO 전략의 장점은 DB를 변경해도 코드를 수정할 필요가 없다는 것이다. 특 히 키 생성 전략이 아직확정되지 않은 개발 초기 단계나 프로토타입 개발 시 편리하게 사용할 수 있다.
AUTO를 사용할 때 SEQUENCE나 TABLE 전략이 선택되면 시퀀스나 키 생성용 테이블을 미리 만들어 두어야 한다. 만약 스키마 자동 생성 기능을 사용한다면 hibernate가 기본값을 사용해서 적절한 시퀀스나 키 생성용 테이블을 만들어 줄 것이다.
기본키 매핑 정리
영속성 컨텍스트는 엔티티를 식별자 값으로 구분하므로 엔티티를 영속 상태로 만들려면 식별자 값이 반드시 있어야 한다. em.persist()를 호출한 직후에 발생하는 일을 식별자 할당 전략별로 정리하면 다음과 같다.
- 직접 할당 : em.persist()를 호출하기 전에 애플리케이션에서 직접 식별자 값을 할당해야 한다. 만약 식별자 값이 없으면 예외가 발생한다.
- SEQUENCE : DB 시퀀스에서 식별자 값을 획득한 후 영속성 컨텍스트에 저장한다.
- TABLE : DB 시퀀스 생성용 테이블에서 식별자 값을 획득한 후 영속성 컨텍스트에 저장한다.
- IDENTITY : DB에 엔티티를 저장해서 식별자 값을 획득한 후 영속성 컨텍스트에 저장한다(IDENTITY 전략은 테이블에 데이터를 저장해야 식별자 값을 획득할 수 있다.)
※권장하는 식별자 선택 전략
데이터베이스 기본 키는 다음 3가지 조건을 모두 만족해야 한다.
- null값은 허용하지 않는다.
- 유일해야 한다.
- 변해선 안된다
테이블의 기본 키를 선택하는 전략은 크게 2가지가 있다.
- 자연 키
- 비즈니스에 의미가 있는 키
- ex)주민등록번호, 이메일, 전화번호
- 대리 키
- 비즈니스와 관련 없는 임의로 만들어진 키, 대체 키로도 불린다
- ex)오라클 시퀀스, auto_increment, 키생성 테이블 사용
- 자연 키보다는 대리 키를 권장한다
자연 키와 대리 키는 일장 일단이 있지만 될 수 있으면 대리 키의 사용을 권장한다. 예를 들어 자연키인 전화번호를 기본 키로 선택한다면 그 번호가 유일할 수 는 있지만, 전화번호가 없을 수도 있고 전화번호가 변경될 수도 있따. 따라서 기본 키로 적당하지 않다. 문제는 주민등록번호처럼 그럴듯하게 보이는 값이다. 이 값은 null이 아니고 유일하며 변하지 않는다는 3가지 조건을 모두 만족하는 것 같다. 하지만 현실과 비즈니스 규칙은 생각보다 쉽게변한다. 주민등록번호조차도 여러 가지 이유로 변경될 수 있다.
필드와 컬럼 매핑: 레퍼런스
JPA가 제공하는 필드와 컬럼 매핑용 어노테이션들을 레퍼런스 형식으로 정리해보았다.
Column
@Column은 객체 필드를 테이블 컬럼에 매핑한다. 가장 많이 사용되고 기능도 많다. 속성 중에 name, nullable이 주로 사용되고 나머지는 잘 사용되지 않는 편이다.
@Enumerated
자바의 enum 타입을 매핑할 때 사용한다.
@Enumerated 사용 예
enum 클래스는 다음과 같다
enum RoleType{
ADMIN, USER
}
- EnumType.ORDINAL은 enum에 정의된 순서대로 ADMIN은 0, USER는 1 값이 DB에 저장된다.
- 장점:DB에 장되는 데이터 크기가 작다.
- 단점:이미 저장된 ENUM의 순서를 변경할 수 없다.
- EnumType.STRING은 이름 그대로 ADMIN은 "ADMIN", USER는 "USER"라는 문자로 DB에 저장된다
- 장점:저장된 enum의 순서가 바뀌거나 enum이 추가되어도 안전하다.
- 단점:DB에 저장되는 데이터 크기가 ORDINAL에 비해서 크다.
@Temporal
날짜 타입을 매핑할 때 사용한다
@Temporal을 생략하면 자바의 Date와 가장 유사한 timestamp로 정의된다. 하지만 tiemstamp 대신에 datetieme을 예약어로 사용하는 DB도 있는데 DB 방언 덕분에 코드를 변경하지 않아도 된다.
@Lob
DB BLOB, CLOB 타입과 매핑한다
@Lob에는 지정할 수 있는 속성이 없다. 대신에 매핑하는 필드 타입이 문자면 CLOB으로 매핑하고 나머지는 BLOB으로 매핑한다.
@Transient
이 필드는 매핑하지 않는다. 따라서 DB에 저장하지 않고 조회하지도 않는다. 객체에 임시로 어떤 값을 보관하고 싶을때 사용한다.
@Access
JPA가 엔티티 데이터에 접근하는 방식을 지정한다.
- 필드 접근 : AccessType.FIELD로 지정한다. 필드에 직접 접근한다. 필드 접근 권한이 private이어도 접근할 수 있다.
- 프로퍼티 접근 : AccessType.PROPERTY로 지정한다. 접근자 (Getter)를 사용한다.
출처
김영한, "자바 ORM 표준 JPA 프로그래밍(2015)", 에이콘출판사
'Database' 카테고리의 다른 글
JPA - 프록시 개념과 활용 방법 (0) | 2024.07.28 |
---|---|
연관관계 매핑 기초 (0) | 2024.02.04 |
영속성 관리 (1) | 2024.01.21 |
JPA를 활용한 애플리케이션 개발해보기 (1) | 2024.01.21 |
JPA - 객체와 관계형 데이터베이스를 매핑하는 표준 인터페이스 (0) | 2024.01.21 |

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!