이제 JPA에서 역시 제일 중요한 부분 중 하나인 객체와 테이블 매핑에 대해서 알아보자. 이 부분은 복잡한 내용은 없고 간단하게 어노테이션을 이용해서 객체를 테이블과 매핑할 수 있다.
Member Entity
package org.example.entity;
import javax.persistence.*;
import java.time.LocalDateTime;
@Entity
@Table(uniqueConstraints = {
@UniqueConstraint(name = "UniqueName", columnNames = { "name" }),
@UniqueConstraint(name = "UniqueEmail", columnNames = { "email" })
})
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
// ! strategy는 크게 4가지가 있다.
// ! IDENTITY = DB에게 기본키 생성을 위임
// ! SEQUENCE = DB에 Sequence 오브젝트를 만들어내서 그 오브젝트에서 다음 값, 또 다음 값을 꺼내서 할당
// ! TABLE = 키 생성 전용 테이블을 하나 만들어서 데이터베이스 시퀀스를 흉내내는 전략
// ! AUTO = 위 3개중에 아무거나 하나로 자동 지정
private Long id;
//! 컬럼 매핑
@Column(nullable = false)
private String email;
//! 컬럼 매핑
@Column(nullable = false)
private String name;
//! 날짜 타입 매핑
private LocalDateTime createdAt;
//! 날짜 타입 매핑
private LocalDateTime lastModifiedAt;
//! Enum Type (일반적으로 DB엔 ENUM이 없음)
//! 기본이 EnumType이 ORDINAL인데 이건 데이터베이스에 ENUM의 순서를 저장하기 때문에 좋지 않음 STRING으로 쓸 것
@Enumerated(EnumType.STRING)
private RoleType roleType;
//! VARCHAR 보다 더 큰 값이 들어가야 할 때
@Lob
private String description;
//! 특정 필드를 컬럼으로 생성하는게 아니라 그냥 객체 안에서 메모리에서만 관리하고자 하는 경우
@Transient
private String temp;
}
코드를 먼저 보면 위처럼 생겼다. 만약 Member 라는 테이블을 만들고 싶다면 이처럼 Member 클래스를 하나 만들고 @Entity 라는 어노테이션을 붙여주면 JPA는 아 이 클래스가 테이블과 매핑되기를 원하는구나라고 인식한다.
@Column
가장 간단한 @Column 어노테이션부터 살펴보면, 이 어노테이션이 붙으면 필드를 컬럼과 매핑하겠다는 의미이다.
컬럼안에 옵션으로 nullable, unique, length 등 여러 옵션을 줄 수 있고, 이 옵션은 객체와는 상관없이 데이터베이스에만 영향을 주는 것들이다.
Timestamp
데이터베이스에 시간 관련 필드를 만들고자 하면 자바에서는 그냥 LocalDateTime 타입을 사용하면 된다.
@Enumerated
자바에서 ENUM 클래스를 사용해서 필드를 만들고 싶을 때가 더러 있다. 그러나 DB는 일반적으로 ENUM 타입은 존재하지 않고 그럴 때 이렇게 @Enumerated 라는 어노테이션을 붙이면 ENUM 클래스를 필드로 매핑할 수 있다.
여기서 중요한 건 EnumType.STRING 옵션이다. 왜 이게 중요하냐면 기본적으로 EnumType은 ORDINAL이 기본값인데 이것을 사용하면 안된다. 이 값은 데이터베이스에 ENUM의 순서를 값으로 사용한다는 뜻인데 이건 큰 문제가 하나 있다.
다음 코드를 보자.
package org.example.entity;
public enum RoleType {
USER, ADMIN
}
나는 두개의 ENUM Data가 있다. 이것을 데이터베이스에 필드로 선언하고 유저를 생성하는 코드를 짜보자.
package org.example;
import org.example.entity.Member;
import org.example.entity.RoleType;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
import java.util.List;
public class Main {
public static void main(String[] args) {
// ! EntityManagerFactory 이 녀석은 서비스가 띄워지면 딱 한개만 생성되어야 한다.
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
// ! EntityManager 이 녀석은 어떤 디비에 작업을 할 때마다 하나씩 만들어지고 작업이 끝나면 버려져야 한다.
EntityManager em = emf.createEntityManager();
// ! 모든 데이터베이스에 대한 변경 작업은 트랜잭션 안에서 일어나야 한다. (조회는 꼭 트랜잭션 안에서가 아니더라도 상관없다)
// ! 하나의 트랜잭션에서 원하는 작업을 끝내고 그 트랜잭션안에서 커밋을 해줘야 변경이 적용된다.
EntityTransaction tx = em.getTransaction();
// ! Transaction 시작
tx.begin();
try {
/* CREATE */
Member member = new Member();
member.setName("helloC");
member.setRoleType(RoleType.USER);
em.persist(member);
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
emf.close();
}
}
}
RoleType.USER로 생성할 멤버의 롤 타입을 지정해서 유저를 생성했다. 데이터베이스에는 어떻게 들어갈까?
보다시피 0이라는 값으로 들어간다. 이번엔 ADMIN으로 생성해보자.
ADMIN은 1로 생성이 된다. 이 0과 1은 ENUM의 순서를 의미한다. 근데 만약, 비즈니스 요구사항이 변경된다면 ?
"GUEST라는 롤도 추가해주세요"
이런 요구사항이 들어와서 이렇게 코드를 수정했다.
package org.example.entity;
public enum RoleType {
GUEST, USER, ADMIN
}
이제 GUEST를 추가하면 어떤 일이 일어날까?
그렇다. 0으로 추가된다. 왜냐하면 GUEST를 맨 앞에 두었으니까. 이게 ORDINAL의 가장 큰 문제다. 이를 방지하기 위해서 STRING 타입으로 꼭 변경해야 한다.
@Lob
VARCHAR(255)보다 큰 매우 큰 문자열이 필요한 경우 @Lob을 사용한다.
@Transient
데이터베이스에 컬럼으로 추가하고 싶진 않지만 객체 메모리 상에서 관리할 데이터가 있을 때 이 어노테이션을 사용하면 데이터베이스에는 컬럼으로 추가되지 않는다.
@UniqueConstraint
이건 뭐냐면 일반적으로 필드에 unique 속성을 추가하고 싶다면 그냥 @Column(unique = true)라고 추가하면 되는데 이게 주는 문제는 제약의 이름이 마구잡이로 생성된다. 한번 예시를 보자.
@Column(nullable = false, unique = true)
private String name;
이렇게 하고 데이터베이스 테이블을 만들어보면 다음과 같은 SQL문이 출력된다.
보다시피 알 수 없는 이름으로 제약조건이 생성된다. 난 이게 싫다. 로그를 보고 알아 볼 수 있어야 문제가 생겼을 때도 수월하게 해결이 가능하니까. 이를 해결하기 위해서 @UniqueConstraint 어노테이션으로 제약 조건의 이름을 직접 명시한다. 바로 아래처럼.
@Table(uniqueConstraints = {
@UniqueConstraint(name = "UniqueName", columnNames = { "name" }),
@UniqueConstraint(name = "UniqueEmail", columnNames = { "email" })
})
저기서 name이 바로 제약조건의 이름이 된다. columnNames는 컬럼명을 의미한다.
이렇게 하고 다시 테이블을 만들게 해보면 다음과 같은 로그가 찍힌다.
@Id, @GeneratedValue
가장 중요한 부분인 기본키 생성과 전략이다. 우선 모든 테이블은 PK가 필요한데 그 PK를 생성하기 위해 @Id 라는 어노테이션을 붙인다. 그럼 JPA는 아 이 필드가 기본키가 될 녀석이구나라고 인식한다. 그럼 @GeneratedValue는 무엇일까? 기본키를 어떻게 생성할지에 대한 정보이다.
@GeneratedValue 어노테이션은 기본키 생성 전략을 가지는데 그 전략은 크게 4가지가 있다.
- IDENTITY: 데이터베이스에게 기본키 생성을 위임
- SEQUENCE: 데이터베이스 시퀀스를 생성하고 그 시퀀스의 값을 기본키로 지정
- TABLE: 기본키를 위한 테이블을 하나 만들고 데이터베이스 시퀀스를 흉내내는 전략
- AUTO: 위 3가지 중 하나를 임의로 지정
여기서 중요한건 IDENTITY와 SEQUENCE다.
IDENTITY는 데이터베이스에게 완전 위임을 하는 것이기 때문에 데이터베이스에 데이터가 추가가 되기 전까지 INSERT 대상의 객체 기본키를 알 수 없다. 그럼 여기서 의문이 생긴다.
분명히 엔티티 매니저가 persist()를 호출하면 영속 컨텍스트에 영속시키고 그 영속시킬 때 1차 캐시에 기본키를 저장하는데 INSERT가 되기 전까지 기본키를 알 수 없는 IDENTITY 전략은 어떻게 하지?
그래서! 이 IDENTITY 전략은 persist()를 할 때 영속시키기 전 데이터베이스에 추가한다. 로그로 바로 확인해볼 수 있다.
package org.example;
import org.example.entity.Member;
import org.example.entity.RoleType;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
import java.util.List;
public class Main {
public static void main(String[] args) {
// ! EntityManagerFactory 이 녀석은 서비스가 띄워지면 딱 한개만 생성되어야 한다.
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
// ! EntityManager 이 녀석은 어떤 디비에 작업을 할 때마다 하나씩 만들어지고 작업이 끝나면 버려져야 한다.
EntityManager em = emf.createEntityManager();
// ! 모든 데이터베이스에 대한 변경 작업은 트랜잭션 안에서 일어나야 한다. (조회는 꼭 트랜잭션 안에서가 아니더라도 상관없다)
// ! 하나의 트랜잭션에서 원하는 작업을 끝내고 그 트랜잭션안에서 커밋을 해줘야 변경이 적용된다.
EntityTransaction tx = em.getTransaction();
// ! Transaction 시작
tx.begin();
try {
/* CREATE */
Member member = new Member();
member.setName("helloCCC");
member.setId(1L);
member.setRoleType(RoleType.GUEST);
member.setEmail("test33");
System.out.println("BEFORE");
em.persist(member);
System.out.println("AFTER");
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
emf.close();
}
}
}
위 코드를 실행해보자. 지금 전략은 아무것도 지정하지 않았다. 즉, 내가 직접 지정한다는 의미이다. 이 상태에서는 절대 절대 트랜잭션 커밋이 일어나기 전까지 SQL문이 보이지 않을 것이다.
로그는 BEFORE, AFTER가 찍히고 난 후 INSERT문이 실행됐다. 당연하다. persist()는 영속 컨텍스트에 영속시킬 뿐 데이터베이스에 뭔가를 작업하는 단계가 아니니까.
근데 여기서 전략을 IDENTITY로 해보면 어떨까? persist()를 호출할 때 INSERT문이 보일것이다.
보다시피 BEFORE와 AFTER 사이에 INSERT문이 실행됐다. 즉, 영속 컨텍스트에 객체를 담기 위해선 기본키가 필요하고 IDENTITY 전략은 데이터베이스에 추가되기 전까지 기본키 값을 알 수 없기 때문에 내부 메커니즘이 IDENTITY일 경우 persist()를 호출하면 데이터베이스에 항상 SQL문을 날리는 것이다. 이 말은 INSERT 문에 한하여 쓰기 지연 SQL문은 IDENTITY에서 없다는 뜻이다.
반면, SEQUENCE는 데이터베이스의 시퀀스를 만들고 그 시퀀스를 가지고 기본키를 적용하는 전략이다. 그럼 생각해보자. 역시나 영속 컨텍스트에 객체를 보관하기 위해 기본키가 필요하고 전략이 SEQUENCE라면 persist()가 호출될 때 데이터베이스의 시퀀스를 조회하지 않을까? 맞다.
아래 사진은 전략을 SEQUENCE로 변경 후 새로운 멤버를 추가했을 때 로그 출력 결과다.
persist()가 호출될 때 시퀀스로부터 다음값을 먼저 불러온다. 왜냐하면 그래야 persist(member)가 호출될 때 이 멤버의 기본키를 알 수 있기 때문이다.
그럼 이 SEQUENCE 전략에선 이런 생각을 할 수 있다.
그럼 시퀀스를 가져오는 것 한 번, 데이터베이스에 INSERT 쿼리를 날리는 것 한 번해서 쓸데없이 두번 왔다 갔다 해야 하잖아? 성능에 좋지 않겠다.
어느 정도 합리적이다. 그러나 이에 대한 성능 최적화를 위해 allocationSize라는 속성이 있다.
@Entity
@SequenceGenerator(
name = "MEMBER_SEQ_GENERATOR",
sequenceName = "MEMBER_SEQ",
initialValue = 1,
allocationSize = 100
)
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "MEMBER_SEQ_GENERATOR")
private Long id;
}
이렇게 시퀀스를 생성할 때 이름도 지정하고 기본값들을 지정할 수 있는 @SequenceGenerator 어노테이션이 있는데 여기서 allocationSize를 100으로 지정하면 메모리가 미리 시퀀스 100개를 가지게 되는 것과 동일하다.
그러면 영속 컨텍스트에 객체를 보관하기 위해 시퀀스 값을 가져오는 횟수는 100개가 될 때까지 딱 2번이다.
왜 두번이냐면 100개씩 시퀀스를 가져오는 범위가 다음과 같다. 1, 2-101, 102 - 201, …
initialValue가 1인데 처음에는 애플리케이션 입장에선 데이터베이스에 저장된 시퀀스 현재값이 뭔지 모르기 때문에 현재값 뭐야?!라는 1번 그리고 가져왔더니 1이네? 또 한번 persist()가 호출되면 그때 100개 가져와야지! 해서 이후에 persist() 호출 시에 100개 가져오는 거 1번해서 총 2번을 100개의 시퀀스동안 가져온다. 그 다음부턴 이제 102번째에 100개 가져오고 하는것.
그래서 멤버가 100명이 될 때까지 시퀀스를 땡겨오지 않는다. 이것도 역시 로그로 직접 확인해보자.
package org.example;
import org.example.entity.Member;
import org.example.entity.RoleType;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
import java.util.List;
public class Main {
public static void main(String[] args) {
// ! EntityManagerFactory 이 녀석은 서비스가 띄워지면 딱 한개만 생성되어야 한다.
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
// ! EntityManager 이 녀석은 어떤 디비에 작업을 할 때마다 하나씩 만들어지고 작업이 끝나면 버려져야 한다.
EntityManager em = emf.createEntityManager();
// ! 모든 데이터베이스에 대한 변경 작업은 트랜잭션 안에서 일어나야 한다. (조회는 꼭 트랜잭션 안에서가 아니더라도 상관없다)
// ! 하나의 트랜잭션에서 원하는 작업을 끝내고 그 트랜잭션안에서 커밋을 해줘야 변경이 적용된다.
EntityTransaction tx = em.getTransaction();
// ! Transaction 시작
tx.begin();
try {
/* CREATE */
System.out.println("BEFORE");
Member member = new Member();
member.setName("helloCCC");
member.setRoleType(RoleType.GUEST);
member.setEmail("test33");
Member member2 = new Member();
member2.setName("helloBBB");
member2.setRoleType(RoleType.GUEST);
member2.setEmail("test22");
Member member1 = new Member();
member1.setName("helloAAA");
member1.setRoleType(RoleType.GUEST);
member1.setEmail("test11");
em.persist(member);
em.persist(member2);
em.persist(member1);
System.out.println("AFTER");
tx.commit();
} catch (Exception e) {
System.out.println(e.getMessage());
tx.rollback();
} finally {
em.close();
emf.close();
}
}
}
자, 위 사진에서 볼 수 있듯 시퀀스의 값은 두 번만 호출했다. 이제 100개의 시퀀스가 채워질 때 까지 더 이상 시퀀스를 데이터베이스로부터 가져오지 않는다. 이렇게 성능 최적화도 가능하다.
'JPA(Java Persistence API)' 카테고리의 다른 글
[JPA] Part 7. 다대일, 일대다, 일대일, 다대다 (0) | 2023.10.19 |
---|---|
[JPA] Part 6. 객체 지향형 모델링, 단방향 양방향 연관관계 주인 (0) | 2023.10.18 |
[JPA] Part 4. 영속성 컨텍스트 (0) | 2023.10.17 |
[JPA] Part 3. JPA를 뿌리부터 시작해보기 (0) | 2023.10.17 |
[JPA]: Part 2. Hibernate 정의와 JPA와 Hibernate의 차이점 (2) | 2023.10.11 |