BackEnd/JPA

[JPA] 엔티티 매핑

샤아이인 2022. 4. 2.

내가 공부한것을 올리며, 중요한 단원은 저 자신도 곱씹어 볼겸 상세히 기록하고 얕은부분들은 가겹게 포스팅 하겠습니다.

 

1. 객체와 테이블 매핑

● @Entity

1. @Entity가 붙은 클래스를 엔티티 라고 하며, JPA가 관리한다.

2. JPA를 사용해서 테이블과 매핑할 클래스는 @Entity가 필수이다.

3. 주의사항

기본 생성자 필수(파라미터가 없는 public or protected)

- final 클래스, enum, interface inner 클래스 사용 x

- 저장할 필드에 final 사용 X

 

4. 속성 정리

- name 속성

JPA에서 사용할 엔티티의 이름을 지정할수가 있다.

만약 name 속성을 지정하지 않는다면, Class 이름을 기본으로 사용한다.

일반적으로 거의 기본값을 사용한다.

 

 

● @Table

1. @Table은 엔티티와 매핑할 테이블 지정한다.

2. 속성 정리

- name : 매핑할 테이블 이름

- catalog: 데이터베이스 catalog 매핑

- schema: 데이터베이스 schema 매핑

- uniqueConstraint(DDL): DDL 생성 시 유니크 제약 조건 생성

 

2. 데이터베이스 스키마 자동 생성

데이터베이스 스키마 자동 생성을 사용하면 어플리케이션 실행 시점에 DDL(Data Definition Language)를 자동생성 한다.

 

일반적으로 개발을 할때 Table을 다 생성한 후에 DB에 Insert문 등을 날린다.

하지만 JPA는 그럴필요 없이 객체 매핑을 다 해두면 어플리케이션이 실행될때 Table을 다 만들수 있다.

 

이렇게 생성된 DDL은 개발 장비에서만 사용해야 한다. 생성된 DDL은 운영서버에서 사용하면 안된다.

 

● hibernate.hbm2ddl.auto 속성

옵션
설명
create
기존테이블 삭제 후 다시 생성 (DROP + CREATE)
create-drop
create와 같으나 종료시점에 테이블 DROP
update
변경분만 반영(운영DB에는 사용하면 안됨)
validate
엔티티와 테이블이 정상 매핑되었는지만 확인
none
사용하지 않음

- create

예를 들어 create 를 사용해 보자.

<property name="hibernate.hbm2ddl.auto" value="create" />
 

persistence.xml 을 위와같이 변경한후 실행해 보자. 결과는 다음과 같다.

우선적으로 drop문으로 Member라는 Table을 삭제한후, CREATE 문으로 Member table을 만든다.

 

- create-drop

만약 속성을 create-drop로 지정했으면, table drop -> table 생성 -> table drop 이 진행된다.

 

- update

기존에 Entity의 컬럼은 다음과 같았다.

@Id
private Long id;
private String name;
 

여기다 추가로 age라는 정수를 추가해준 후,

@Id
private Long id;
private String name;
private Integer age;
 

update문을 적용하면 다음과 같다.

TABEL에 대한 alter 문이 먼저 수행되게 된다.

 

● 주의 사항

운영 장비에는 절대 create, create-drop, update 사용하면 안된다.

- 개발 초기 단계는 create 또는 update

- 테스트 서버는 update 또는 validate

- 스테이징과 운영 서버는 validate 또는 none

 

● DDL 생성 기능

다음과 같이 제약 조건을 추가해줄수가 있다.

@Column(nullable = false, length = 10)
private String name
 

회원의 이름은 필수이며, 길이는 최대 10글자 까지 가능하단 제약을 추가하였다.

 

이러한 DDL 생성 기능은 DDL을 처음 자동 생성할때 제약조건에 해당하는 쿼리문을 추가해줄 뿐 이다.

JPA의 실행 로직에는 영향을 주지 않는다.

 

3. 필드와 컬럼 맵핑

@Entity
public class Member2 {
    
    @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;
}
 

1. @Column

출처 - 인프런 김영한 JPA

2. @Enumerated

자바의 enum 타입을 데이터베이스에 저장하기위해 사용한다.

 

속성에는 2가지가 있다.

- EnumType.ORDINAL: enum 순서를 데이터베이스에 저장

- EnumType.STRING: enum 이름을 데이터베이스에 저장

 

거의 STRING 만 사용한다.

ORDINAL 같은 경우 enum타입이 추가,변경,삭제 되어 순서가 달라질 경우 사이드이펙트가 생긴다.

 

3. @Temporal

날짜 타입(java.util.Date, java.util.Calendar)을 매핑할 때 사용한다.

하지만 최신 JAVA8 이상에서 LocalDate, LocalDateTime을 사용할 때는 에노테이션 생략이 가능하다.

 

속성은 3가지가 있다.

- TemporalType.DATE: 날짜, 데이터베이스 date 타입과 매핑 (예: 2013–10–11)

- TemporalType.TIME: 시간, 데이터베이스 time 타입과 매핑 (예: 11:11:11)

- TemporalType.TIMESTAMP: 날짜와 시간, 데이터베이 스 timestamp 타입과 매핑 (예: 2013–10–11 11:11:11)

 

4. @Lob

데이터베이스의 BLOB, CLOB 타입과 매핑된다. (대용량의 데이터를 저장할때 사용)

매핑하는 필드 타입이 문자면 CLOB, 나머지는 BLOB매핑

 

CLOB: String, char[], java.sql.CLOB

BLOB: byte[], java.sql.BLOB

 

5. @Transient

데이터 베이스에 저장하고 싶지 않을때!, 단지 메모리 상에서만 사용하고 싶을때!

예를 들어 다음을 보자.

@Transient
private int tmp_number;
 

@Transient가 적용된 tmp_number는 데이터베이스에 저장되지 않는다. 컬럼 또한 생성되지 않는다.

그냥 Entity안에서 보관은 해야하는데, DB상에 저장하고 싶지 않은 값들을 위해 사용한다.

 

4. 기본 키 매핑

기본키 매핑 방식에는 2가지가 있다.

 

1. @Id (직접 할당)

이 방식은 사용자가 직접 ID를 지정할때 사용한다.

@Id
private Long id;
 

2. @GeneratedValue (자동 생성)

자동 생성 방식은 다시 4가지 종류로 나뉘어 진다.

 

1) IDENTITY

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
 

이 전략은 기본키 생성을 데이터베이스에게 위임한다.

 

새로운 객체를 추가할때 insert문에 기본키가 null로 넘어가며, DB가 이를 알맞은 PK로 바꿔준다.

 

즉, DB까지 가서야 자신의 PK 값을 알게되는 것 이다.

 

하지만 영속성 컨텍스트에서 관리되려면 무조건 PK값이 있어야 1차 캐시에 저장될수가 있다.

즉, IDENTITY는 PK값을 DB에 들어가봐야 알수 있는것 이다.

 

이런경우 persist()를 통해 영속화가 될수 없는 이상한 현상이 생긴다.

PK값이 있어야만 1차 캐시에 key : value 쌍으로 저장이 되는데 PK값이 DB에 접근해야 알수있게 되는것이다.

 

이러한 문제를 해결하기 위해 IDENTITY 전략에서만 persist()를 호출하자마자 바로 DB에 쿼리가 전송된다. (지연쓰기 사용X)

이후 DB로부터 식별자 값을 얻어올수 있고, 이를 사용한다.

(ps. 원래는 일반적으로 JPA는 commit 시점에 INSERT 쿼리를 DB에 날리게 된다.)

 

다음 코드를 통해 확인해 보자.

Member member = new Member();
member.setUsername("A");

System.out.println("==========");
entityManager.persist(member);
System.out.println("member ID : " + member.getId());
System.out.println("==========");

transaction.commit();
 

persist()를 호출하자 마자, 우선 INSERT 쿼리문이 생성되어 DB에 전달되게 된다.

DB에 전달된 후 PK값을 얻었기 때문에 바로 member의 ID를 출력할수가 있다.

 

또한 PK값을 얻어오는 SELECT 쿼리는 만들어 지지 않았는데, 이는 내부적으로 JDBC 드라이버에서 INSERT 쿼리를 보낸후 바로 PK값을 return 받는 구조로 되어있기 때문이다.

 

주로 MySQL, PostgreSQL 에서 사용된다 (예: MySQL의 AUTO_ INCREMENT)

 

2) 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;

   // ... 생략
}
 

대표적으로 Oracle, H2 에서 사용하며, 이 전략은 데이터베이스 시퀀스 오브젝트를 필요로한다.

@SequenceGenerator 를 사용하지 않으면 기본적으로 제공하는 hibernate_sequence 를 사용하게 된다.

 

"MEMBER_SEQ_GENERATOR" 라는 @SequenceGenerator "MEMBER_SEQ"라는 시퀀스 오브젝트를 만들기 위해

@GeneratedValue의 generator 속성으로 generator 이름을 지정해 주었다.

 

시퀀스 전략을 사용하여 실행하면 다음과 같다.

우선 CREATE 로 했기 때문에 기존꺼를 모두 drop 한 후, 신규로 생성하였다.

이때 빨간박스 안처럼 "MEMBER_SEQ" 라는 시퀀스 오브젝트를 생성하는것을 확인할수 있다.

H2 데이터 베이스를 보면 다음과 같이 시쿼스가 생성되어 있다.

뒤에 붙어있는 "start with 1 increment by 1" 은 시퀀스는 1부터 시작하며, 1씩 증가시키라는 의미이다.

 

SEQUENCE 전략 또한 PK값이 있어야 persist()하여 1차 캐시에 저장할수 있다.

따라서 먼저 시쿼스에서 나의 PK값을 얻어와야 한다. 따라서 생성된 MEMBER_SEQ 에서 시퀀스 값을 가져온다.

 

이를 코드로 보면 다음과 같다.

Member member = new Member();
member.setUsername("A");

System.out.println("==========");
entityManager.persist(member);
System.out.println("member ID : " + member.getId());
System.out.println("==========");

transaction.commit();
 

call next value for 해서 쿼리가 전송되는것을 볼수가 있다.

즉 persist()가 호출될때 SEQUENCE 전략임이 확인되면, 우선 DB에서 시퀀스 값을 얻은 후, 얻어온 값으로 ID값을 지정한 후, 영속화가 진행된다.

 

이후 최종적으로 commit하는 시점에 INSERT 쿼리가 날라간다.

@SequenceGenerator 의 속성들은 다음과 같다.

시쿼스 방식은 성능 향상을 위해 allocationSize의 조율이 필요하다.

 

● allocationSize와 성능 향상

생각해보면 member를 등록할때마다 DB의 시퀀스 객체에 접근해서 값을 얻어오는 것은 비효율적이다.

미리 적정량의 숫자를 시퀀스 객체에서 증가시켜두고, 이를 어플리케이션에서 사용하면 네트워크를 통신에 의한 비용이 줄어든다.

 

예를 들어 allocationSize = 50 과 같이 지정해 두면, DB의 시퀀스 객체 51번까지 증가시켜둔다.

따라서 사용자는 1 ~ 50번까지 쭉 50개를 사용하면 된다.

다시 51번 이 필요할때 시퀀스 객체에 다시 50개를 할당하라 요청하는 것 이다.

 

이는 1개씩 시퀀스 객체에 요청할때보다 효율적이다. 다음 코드를 살펴보자.

Member member1 = new Member();
member1.setUsername("A");

Member member2 = new Member();
member1.setUsername("B");

Member member3 = new Member();
member1.setUsername("C");

System.out.println("===============");
// em.persist(member1); 아직 등록 안함!!!!
// em.persist(member2);
// em.persist(member3);

System.out.println("member1 = " + member1.getId());
System.out.println("member2 = " + member2.getId());
System.out.println("member3 = " + member3.getId());

System.out.println("===============");

transaction.commit();
 

실행하면 다음과 같은 부분을 볼수있다.

1부터 시작하고 50개씩 늘리겠다는 의미이다.

 

또한 위 코드에서 member1을 만들고 아직 persist는 하지 않았다.

따라서 DB의 시퀀스를 확인해보면 다음과 같이 나온다.

현재 값이 -49로 되어있다. 이후 member1을 영속화 하면 그때 -49 에서 1로 바뀌게 된다. 처음 호출했을때 1값을 반환해주기 위해 초기 시작값은 음수로 되어있다.

직접 member1을 등록해 보자.

System.out.println("===============");
em.persist(member1); // 등록!!
// em.persist(member2);
// em.persist(member3);

System.out.println("member1 = " + member1.getId());
System.out.println("member2 = " + member2.getId());
System.out.println("member3 = " + member3.getId());

System.out.println("===============");
 

실행시 결과는 다음과 같다.

call next value for MEMBER_SEQ가 2번 호출된다.

1번 호출 에서는 DB_SEQUENCE = 1 까지 확보한것 이다.

2번 호출 에서는 DB_SEQUENCE = 51 까지 확보한것 이다.

 

이후 member2, 3을 등록할때는 call next 를 더이상 호출하지 않는다. 메모리 상에서 값을 받아온다.

DB의 시퀀스 객체를 확인해 보면 다음과 같이 51로 증가해 있다.

그럼 그 메모리는 무엇일까? (여기서 부터는 수업에 없는 내용)

바로 BasicHolder Class가 해당 역할을 담당하게 됩니다.

 

● 동시성 문제

그럼 sequece 전략을 사용하면 서버가 여러대일 경우 문제가 발생하지 않을까? => 문제가 생기지 않는다!! 걱정말고 사용하자.

A, B 라는 서버가 2대 있을때, A는 처음 할당시 1 ~ 51 까지 할당받게 되고, B는 52 ~ 101 까지 할당되기 때문에 번호가 겹치지 않게 된다.

 

물론 A서버의 4번 보다 B 서버의 51이 먼저 사용될수는 있다.

서버가 2대 이상이라면 DB상에는 시퀀스 순서가 다르게 들어갈 수 있는것 이다! (중복이 되지는 않습니다.)

 

하지만 가급적 시퀀스는 순서를 보장하는데 쓰지 말고, 순서에 대한 부분은 별도의 날짜 컬럼을 사용하시는 것을 권장한다고 한다.

 

3) TABLE

TABLE은 키 생성 전용 테이블을 하나 만들어서 데이터베이스 시퀀스를 흉 내내는 전략이다.

장점으로는 모든 데이터베이스에 적용가능하지만, 단점으로는 성능이 좋지 못하다.

@Entity
@TableGenerator(
        name = "MEMBER_SEQ_GENERATOR",
        table = "MY_SEQUENCES",
        pkColumnName = "MEMBER_SEQ", allocationSize = 1)
public class Member2 {

    @Id
    @GeneratedValue(strategy = GenerationType.TABLE, generator = "MEMBER_SEQ_GENERATOR")
    @Column(name = "MEMBER_ID")
    private Long id;
...
}
 

위 코드와 같이 TableGenerator를 지정하여 실행하면 다음과 같이 나온다.

"MY_SEQUENCES"와 같이 테이블이 만들어 진다. 삽입된 결과는 다음과 같다.

계속 member가 삽입될때마다 NEXT_VAL 이 증가하게 된다.

 

- 키 생성 용 테이블 사용, 모든 DB에서 사용

- 키 생성 전용 테이블을 하나 만들어서 데이터베이스 시퀀스를 흉내내는 전략

- 모든 데이터 베이스에서 사용할 수 있지만, 성능이 떨어진다.

- @TableGenerator 필요

 

@TableGenerator 속성

4) AUTO

사용하는 데이터 베이스의 방언에 따라 자동 지정, 기본값

 

● 권장하는 식별자 전략

- 기본 키 제약 조건: not null, unique, 변하면 안된다.

- 위 조건을 계속 만족하는 자연키는 찾기 힘들기 때문에 대리키(대체키)를 사용하자.

 

- 예를 들면 주민등록번호도 기본 키로 적절하지 않다.

-> 기본 키가 주민등록번호가 되면 연관매핑을 맺은 다른 테이블에서도 외래키로 주민번호를 사용하기에 여기저기에 개인정보가 퍼지게 된다.

 

- 권장: Long형 + 대체키 + 키 생성전략 사용.

=> 결론 AUTO_ INCREMENT 나 Sequence를 사용하자, 아니면 UUID도 가능

'BackEnd > JPA' 카테고리의 다른 글

[JPA] 프록시와 연관관계 관리  (0) 2022.04.06
[JPA] 고급 매핑  (0) 2022.04.05
[JPA] 다양한 연관관계 매핑  (0) 2022.04.04
[JPA] 연관관계 매핑 기초  (0) 2022.04.03
[JPA] 영속성 관리 - 내부 동작 방식  (0) 2022.04.02

댓글