[JPA] 2.엔티티 매핑
Updated:
엔티티 매핑
JPA를 제대로 사용하기 위해서 객체지향과 관계형DB 2가지 모두를 잘 이해해야 한다. 이번에는 JPA 사용시 가장 중요한 2가지 포인트 중 하나인 엔티티와 테이블 매핑에 대해 알아본다
엔티티와 테이블 매핑을 위해 JPA는 아래의 다양한 어노테이션을 제공한다
- 객체와 테이블 매핑:
@Entity,@Table - 필드와 컬럼 매핑:
@Column - 기본키 매핑:
@Id - 연관관계 매핑:
@ManyToOne,@OneToMany,@JoinColumn
1. 객체와 테이블 매핑: @Entity, @Table
@Entity
- JPA를 이용하여 테이블과 매핑할 클래스임을 명시
@Entity가 붙은 클래스는 JPA가 관리한다
- 기본 생성자(public 또는 protected 생성자)가 반드시 있어야 한다
- 저장할 필드에는 final을 사용할 수 없다
- final, enum, interface, inner 클래스에는 붙일 수 없다
@Table
- 엔티티와 매핑할 테이블 관련한 설정을 명시
name속성을 이용하여 매핑할 테이블 이름을 직접 정해줄 수 있다- 매핑할 테이블 이름의 default는 매핑된 엔티티 이름이다
- 이외에도 다른 속성들이 있으니 아래 그림을 참조하자

JPA의 DB 스키마 자동 생성 기능
application.properties 파일에 spring.jpa.hibernate.ddl-auto 옵션을 통해 설정한다. 개발 시에는 보통 기존 테이블을 삭제하고 새로 생성하는 create로 설정한다. application.properties의 spring.jpa.database-platform을 통해 설정한 DB dialect(방언)에 따라 적절한 DDL을 자동으로 생성해준다. spring.jpa.hibernate.ddl-auto 옵션을 더 알고 싶다면 아래 그림을 참조하자.

[주의점]
- 운영 시에는 spring.jpa.hibernate.ddl-auto 옵션을 none 또는 validate 둘 중 하나로만 사용할 것
- JPA의 DB 스키마 자동생성으로 만들어진 DDL을 실제 운영 시에 사용하고 싶다면, 적절히 다듬어 사용할 것
DDL 생성 기능
@Column을 이용하여 제약조건을 추가(nullable, length 속성 등…)하거나 unique 제약조건을 추가하는 것을 DDL 생성 기능이라고 하는데, 이는 DDL을 자동 생성할 때만 사용되고 @Table의 name속성을 지정해주는 것과 달리 JPA의 실행 로직에는 영향을 주지 않는다. DB에 영향이 있을 뿐
2. 필드와 컬럼 매핑: @Column

@Column은 객체의 필드를 테이블의 컬럼에 매핑할 때 사용된다. 이해를 위해 아래의 코드를 살펴보자
@Entity
public class Member {
@Id
private Long 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;
}
@Column

- 테이블의 컬럼과 객체의 필드명과 다르게 설정하고 싶다면
name속성을 사용한다 - 특정 컬럼값을 NOT NULL로 설정하고 싶다면
nullable속성을 이용한다 - UNIQUE 제약조건을 걸기 위해
unique속성을 사용할 수 있지만 UNIQUE 명을 지정해줄 수 없기 때문에@Table의uniqueConstraints옵션을 사용한다 columnDefinition속성을 통해 직접 DB 컬럼 정보를 설정 가능하다
@Enumerated
- Java의 enum타입을 사용하는 경우에 명시
@Enumerated의 속성에는EnumType.ORDINAL과EnumType.STRING2가지가 있는데 전자는 enum순서(숫자)를, 후자는 enum이름(문자)을 DB에 저장한다- 여기서 유의할 점은, 속성값으로 EnumType.STRING을 사용하자는 것이다. 만약 default값인
EnumType.ORDINAL사용시 enum에 새로운 값이 추가되었을 때 문제가 발생할 수 있다
@Temporal
- Java의 날짜 타입을 사용하는 경우에 명시
@Temporal의 속성에는TemporalType.Date,TemporalType.TIME,TemporalType.TIMESTAMP3가지가 있다.TemporalType.TIMESTAMP는 날짜와 시간이 모두 들어간, 나머지 둘은 날짜와 시간만 각각 들어간 타입을 사용한다고 보면된다
- Java8 이상을 사용한다면, LocalDate나 LocalDateTime을 사용하므로 @Temporal 없이도 JPA가 날짜 타입임을 스스로 인식하기 때문에 @Temporal을 명시할 필요가 없다
@Lob
- varchar보다 큰 content값을 넣어줘야 하는 경우에 사용
- 매핑하는 필드 타입이 String, char[] 등의 문자면
CLOB매핑을, byte[] 등 문자를 제외한 나머지일 경우에는BLOB을 매핑한다
@Transient
- 메모리상에 어떤 값을 보관해야 하지만 DB에 저장하기 싫은 경우에 사용
3. 기본키 매핑: @Id, @GeneratedValue
기본키를 application에서 직접 할당한다면 @Id만 사용해도 되지만, 자동할당을 원한다면 @GeneratedValue를 함께 써줘야 한다
@GeneratedValue의 속성으로 IDENTITY, SEQUENCE, TABLE, AUTO 4가지가 존재하며 AUTO 속성은 나머지 3개 속성 중 SQL dialect와 부합하는 방식으로 선택한다. 나머지 3가지 속성은 아래에서 자세히 살펴본다
a. IDENTITY 전략
@Entity
public class Member{
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
...
}
- 기본키 생성을 DB에 위임하는 전략으로 개발자가 직접 값을 넣으면 안된다
- 대표적으로 MySQL의
AUTO_INCREMENT가 있다 - 주로 MySQL, PostgreSQL, SQL Server, DB2에서 사용된다
- 이 방식은 DB에 데이터가 들어간 이후에 기본키 값이 정해지므로 JPA에서 1차캐시에 해당 엔티티를 저장하는 시점에는 기본키 값이 존재하지 않는다. 이는
em.persist()수행 시에 엔티티를 1차 캐시에 저장해야하지만 INSERT 쿼리는 트랜잭션 COMMIT 시점에 DB로 날아가기 때문에 발생하는 문제이다 - 이를 어떻게 해결하는지 알아보기 위해 아래 코드를 살펴보자
private static void test(EntityManager em){
Member member = new Member();
em.persist(member);
System.out.println("member.id = "+member.getId());
}
- 위의 코드를 보고 “어떻게 트랜잭션을 커밋하지 않고도 실행될 수 있냐” 라는 의문이 들 수 있다
- IDENTITY 전략에서는 JPA가 em.persist() 메서드가 쓰기지연을 하지 않고 바로 INSERT 쿼리를 DB에 보낸다. 이렇게 알아낸 기본키 값을 1차 캐시에서 @Id값으로 사용한다.
- 즉, IDENTITY 전략에서는 쓰기지연 기능이 수행되지 않지만 사실 이게 커다란 성능 차이를 발생시키지는 않는다
b. SEQUENCE 전략
@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;
...
}
- 위의 코드는
MEMBER_SEQ를 만들어 클래스 내부에서MEMBER_SEQ_GENERATOR라는 seq명으로 사용하겠다는 의미로@GeneratedValue의generator속성과 일치해야 한다 em.persist()수행 시에 JPA에서 SEQUENCE 전략임을 인지하고 DB에 접근하여 seq값을 알아낸후 해당 값을 기본키로 설정하여 해당 엔티티를 1차 캐시에 저장한다em.persist()수행 시에 기본키 값이 존재하지 않는 문제는 IDENTITY 전략과 동일하다- INSERT 쿼리를 DB에 날려서 생성된
AUTO_INCREMENT값을 가져와@Id로 설정후 1차 캐시에 저장하는 것이 IDENTITY 전략이다 - 반면, SEQUENCE 전략은 INSERT 쿼리를 날리지 않고 DB에 접근하여 seq값만을 알아낸 후에 @Id로 설정하여 1차 캐시에 저장하는 방식을 취한다는 차이점이 있다
- 그래서 SEQUENCE 전략은 IDENTITY 전략과 다르게 쓰기 지연이 가능하다
- INSERT 쿼리를 DB에 날려서 생성된
- 다만, seq를 받기 위해 DB에 접근하고 다시 받아오는 과정이 성능 최적화에 문제를 일으킬 수 있기 때문에 @SequenceGenerator의 allocationSize 속성을 이용한다
allocationSize로 지정한 숫자만큼의 seq를 사용하는 동안 DB에 접근하여 seq를 가져올 필요가 없도록 한다- 예를 들어,
allocationSize를 50으로 설정하면 1부터 50까지의 seq를 사용하는 동안에는 seq값을 가져오기 위해 DB에 접근하는 행위를 수행하지 않는다
c. TABLE 전략
- 키를 생성하는 전용 테이블을 하나 만들어서 DB의 시퀀스 역할을 수행하도록 하는 전략이다
- 모든 DB에서 사용가능하지만 테이블을 만들어 사용하기 때문에 성능이 떨어질 수 있다
-
보통 각 DB에 맞는 전략을 사용하지 이 전략을 사용하지 않는다
- 이 역시
@TableGenerator의allocationSize속성을 이용하여 성능 최적화를 할 수 있다
권장하는 전략
일단, 기본키 제약조건을 살펴보면 NOT NULL, UNIQUE, 변화X 3가지다. 이 중에서 충족시키기 어려운 것이 변화가 없어야 한다는 점인데, 문제는 미래까지 이 조건을 만족하는 자연키(비즈니스적으로 의미 있는 키 eg)주민등록번호를 찾기는 어렵다.
=> 결론적으로 권장하는 방식은 Long형 + 대체키(UUID) + 키 생성전략(Identity 또는 Sequence)을 사용하는 것이다!!!
Leave a comment