객체와 테이블 매핑
📌 엔티티 매핑(@Entity)
클래스에 @Entity를 선언하면 JPA가 관리하는 엔티티가 된다. 이로 인해 JPA는 애플리케이션 로딩 및 실행 시점에 매핑할 테이블을 자동 생성한다.
즉, create, insert 등의 DDL문을 자동으로 생성한다는 것이다. 다음과 같은 제약 조건을 줄 수 있다.
@Entity
public class Member {
@Id
private Long id;
@Column(nullable = true, length = 10) //null 허용x, 길이 10 이하
private String name;
private int age;
...
JPA는 동적으로 객체를 생성하기 때문에 기본 생성자가 필수이며, 변경이 불가능한 필드를 선언해서는 안된다.
- final class
- enum class
- interface
- inner class
- final field
🔎 JPA에서 기본 생성자를 사용하는 이유
JPA는 자바에서 제공하는 리플렉션 API(reflection API)를 활용하여 런타임 시점에 동적으로 객체를 생성한다.
Java Reflection API는 구체적인 클래스 타입을 알지 못하더라도 해당 클래스 이름을 통해 메서드, 타입, 변수 등에 접근할 수 있도록 해주는 API다. 이것은 기본 생성자를 통해 객체를 생성하여 setter를 사용하지 않고도 데이터를 넣을 수 있다. 또한, private 메서드에 접근할 수 있다.
기본 생성자는 public 또는 protected로 선언해야 한다. 이유는 지연 로딩 때문이다. 지연 로딩은 원본 객체를 상속받은 프록시 객체를 사용하는데, 자식 클래스가
부모 클래스를 호출하려면 public 또는 protected인 기본 생성자가 필요하다.
Reference:
https://ittrue.tistory.com/298
🔎 Naming
@Entity의 name 속성을 사용하여 엔티티의 이름을 지을 수 있다. 기본 이름은 해당 클래스명이고, 권장하는 이름이다. 이 의미는 HQL(Hibernate Query Language)에서 해당 엔티티를 식별하는 이름을 지정하는 것이다.
다른 애노테이션으로 @Table이 있다. @Table은 마찬가지로 네이밍을 위한 name 속성이 있는데, 이는 데이터베이스에서 생성할 테이블의 이름을 지정하는 것이다.
따라서, @Table은 @Entity의 테이블 네이밍이라는 부분 기능을 가진 것이다.
@Entity는 엔티티와 테이블 모두 영향을 미치고, @Table은 테이블만 영향을 미친다.
Reference:
https://www.inflearn.com/questions/75556/table%EA%B3%BC-entity-%EC%B0%A8%EC%9D%B4%EC%A0%90
📌 컬럼 매핑
@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;
publicMember() {
}
}
@Column을 사용하여 데이터베이스 컬럼과 객체의 필드를 매핑한다. 다음과 같은 다양한 속성이 있다.
- name : 테이블의 컬럼 이름 (default : 객체의 필드 이름)
- insertable/updatable : 등록/변경 가능 여부 (default : true)
- nullable(DDL) : null 값 허용 여부 (default : false, 제약 조건에 not null 추가됨)
- length(DDL) : 문자 길이 제약 조건 (String 타입에만 사용)
- columnDefinition(DDL) : DB 컬럼 정보를 직접 정의 (DB 방언을 이용하여 특정 SQL에 종속적인 명령도 수용)
- unique(DDL) : @Table의 uniqueConstraint와 같지만 한 컬럼에 간단히 걸 때 사용
- 제약 조건 이름이 아래처럼 랜덤하게 붙으므로 운영 환경에는 적합하지 않음
@Enumerated는 자바 enum 타입을 매핑할 때 사용한다. 주의할 점은 EnumType으로 STRING 사용을 권장하는 것이다.
- EnumType.ORDINAL : enum 순서를 DB에 저장(default)
- EnumType.STRING : enum 이름을 DB에 저장
순서는 동적으로 변경되어 정렬되지 않는다. 따라서, 순서를 사용하는 경우 식별자 중복이 발생할 가능성이 높다.
@Temporal은 날짜 타입을 매핑할 때 사용한다. 현재는 Java 8에서 추가된 LocalDate나 LocalDateTime을 사용한다.
@Lob는 가변 길이의 큰 데이터를 저장할 때 사용한다. 일반적인 데이터베이스에서 저장하는 255 길이보다 긴 문자를 저장하고 싶을 때 사용한다.
- 매핑하는 타입이 문자 : CLOB
- 그 외 : BLOB
📌 기본 키 매핑
@Id 애노테이션을 통해 JPA 엔티티 객체의 기본키를 지정할 수 있다.
기본 키는 매번 엔티티가 생성될 때마다 직접 할당하지 않는다. 이를 위해 @GeneratedValue 애노테이션을 사용하여 기본 키 생성 전략을 지정할 수 있다.
아래와 같은 전략들이 있다.
🔎 IDENTITY
기본 키 생성을 데이터베이스에 위임하는 전략으로 개발자가 직접 값을 넣을 수 없다. 하지만 해당 전략은 쓰기 지연 SQL 저장소를 사용하지 않는 단점이 있다.
해당 전략은 MySQL의 AUTO_INCREMENT와 비슷한데, 이는 데이터베이스에 INSERT 쿼리를 실행한 이후에 ID 값을 알 수 있다. 그런데, JPA는 보통 트랜잭션
커밋 시점에 INSERT 쿼리가 실행된다. 결과적으로 커밋 시점이 되어야 ID 값을 알 수 있다는 것이다.
JPA에서 persist로 영속화하는 작업은 1차 캐시에 엔티티를 담을 뿐, 데이터베이스에 변경이 반영되는 것이 아니다. 이러한 이유로 해당 전략은 persist 시점에
즉시 INSERT 쿼리를 실행하고 데이터베이스에서 식별자를 조회한다. 따라서, 쓰기 지연 SQL 저장소를 사용하지 않는 것이다.
public class JpaMain {
public static void main(String[] args) {
...
try {
Member member = new Member();
member.setUsername("C");
System.out.println("=====================");
em.persist(member);
System.out.println("member.id = " + member.getId());
System.out.println("=====================");
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
emf.close();
}
}
🔎 SEQUENCE
데이터베이스 시퀀스 오브젝트를 사용하는 방법이다.
데이터베이스 시퀀스는 유일한 값을 순서대로 생성하는 특별한 데이터베이스 오브젝트다.
쓰기 지연 SQL 저장소를 사용하며, @SequenceGenerator가 필요하다.
sequenceName 속성을 사용하면 테이블마다 시퀀스 오브젝트를 따로 관리할 수 있다.
시퀀스를 사용하는 DB에서 기본으로 사용하는 전략이다. (ex. Oracle, PostgreSQL, DB2, H2 등)
이 전략은 ID 값을 설정하지 않고(Null) generator에 매핑된 Sequence 전략("member_seq")에서 ID 값을 얻어온다. 해당 시퀀스 객체는 DB가 관리하기 때문에 DB에서 매번 ID 값을 가져와야 한다.
위의 IDENTITY 전략과 달리, persist 전에 PK 값을 해당 객체의 ID에 넣는다. 즉, INSERT 쿼리가 나가기 전에 영속성 컨텍스트에 쌓여 있을 때에도
PK 값을 알 수 있다. 따라서, 쓰기 지연 SQL 저장소를 사용하고 트랜잭션 커밋 시점에 INSERT 쿼리가 날아간다.
또 하나의 이점은 INSERT 전에 PK를 알기 때문에 필요한 경우 버퍼링이 가능하다.
그런데, 시퀀스 전략은 매번 네트워크를 통해 DB에서 ID 값을 가져와야 하기 때문에 성능 상 저하를 가져올 수 있다. 이를 해결하기 위한 성능 최적화 방법으로 allocationSize 옵션을 사용한다. 해당 옵션을 통해 미리 시퀀스를 필요한 만큼 DB에 올려놓고 메모리 상에서 1개씩 쓰는 것이다.
이론적으로 allocationSize를 크게 잡을수록 좋지만, 얼마 사용하지 않고 서버를 내릴 경우 그만큼 시퀀스가 낭비된다. DB는 한번 할당한 시퀀스로 돌아가지 않으므로,
중간에 시퀀스 구멍이 생긴다. 그래서 적당한 50 ~ 100으로 설정하는 것이 좋다.
Reference:
https://gmlwjd9405.github.io/2019/08/12/primary-key-mapping.html
🔎 Table
키 생성용 테이블 사용
모든 DB에서 사용 가능한 장점이 있지만 숫자 생성용 테이블을 생성하여 시퀀스를 받는 방법으로 SEQUENCE 전략과 내부 동작 방식이 동일하다. 하지만 성능에서 차이를 보인다. 해당 전략은 값을 조회하면서 SELECT 쿼리, 숫자를 생성하면서 UPDATE 쿼리를 날리기 때문에 DB와 한번 더 통신해야 하며 쿼리도 한번 더 나가야 한다.
그래서 잘 사용하지 않는다.
🔎 AUTO
@GeneratedValue의 기본 전략으로, 데이터베이스 방언에 따라 TABLE, SEQUENCE, IDENTITY 세 개 중 하나가 자동으로 지정된다.
- H2 ➡ SEQUENCE
- MySQL ➡ IDENTITY
SEQUENCE 전략은 영속화 시점에 INSERT 쿼리가 나가지 않아도 식별자를 알 수 있어 로그에 나타나지 않는다.
Reference:
https://velog.io/@gkdud583/JPA-GeneratedValue-AUTO%EC%99%80-IDENTITY-%EC%B0%A8%EC%9D%B4%EC%A0%90
🔎 정리
기본 키 제약 조건은 null이면 안되고, 유일해야 하며 변하면 안 된다. 따라서, 미래까지 이 조건을 만족하는 자연키를 찾기 어렵기 때문에
대리키(대체키)를 사용하는 것을 권장한다.
기본 키 = Long type + 대체키 + 키 생성 전략 사용하기
키 생성 전략은 AUTO나 SEQUENCE 권장 또는 회사 내 규정 전략
💡 자연키 vs 대리키(대체키)
자연키 : 비즈니스적으로 의미 있는 키 (ex. 주민등록번호, 전화번호 등)
대리키(대체키) : 비즈니스와 전혀 상관없는 기본 키 속성을 가진 키 (ex. 시퀀스, 랜덤값, AUTO 등)
Reference:
https://www.inflearn.com/course/ORM-JPA-Basic/dashboard