[JPA] 엔티티 매핑

Swim Lee·2021년 1월 28일
0

JPA

목록 보기
5/10
post-thumbnail
post-custom-banner

엔티티 매핑 소개

🧐 JPA에서 가장 중요한 2가지

  1. 객체와 관계형 데이터베이스 매핑 (Object-Relational Mapping)
  2. 영속성 컨텍스트

이번에는 바로 객체 관계형 DB 매핑에 대해 알아볼 것!
객체와 관계형 DB 매핑은 설계적인 것, 정적인 내용에 해당된다.

🤔 종류

  • 객체와 테이블 매핑 : @Entity, @Table
  • 필드와 컬럼 매핑 : @Column
  • 기본키 매핑 : @Id
  • 연관관계 매핑 : @ManyToOne, @JoinColumn

객체와 테이블 매핑

@Entity

  • @Entity가 붙은 클래스는 JPA가 관리, 엔티티라 한다.
  • JPA를 사용해서 테이블과 매핑할 클래스는 @Entity 필수!
  • 주의
    • 기본 생성자 필수 (파라미터가 없는 public 또는 protected 생성자)
      • JPA가 내부적으로 사용하는 라이브러리들이 리플렉션을 사용해서 프록싱 등을 할 때 필요하기 때문
    • final 클래스, enum, interface, inner 클래스 사용 X
      • 해당 클래스는 @Entity를 붙여서 DB Table과 매핑할 수 없음
    • 저장할 필드에 final 사용 X

name 속성

  • JPA에서 사용할 엔티티 이름을 지정
  • 기본값 : 클래스 이름을 그대로 사용
  • 같은 클래스 이름이 없으면 가급적 기본값 사용
  • 따라서 일반적으로 사용할일 없음

@Table

  • @Table은 엔티티와 매핑할 테이블을 지정

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

JPA는 사실 매핑 정보만 보면, 어떤 쿼리를 만들어야하는지, 어떤 테이블이 있는지 다 알 수 있다. 따라서 JPA에서는 아예 애플리케이션 로딩 시점에 DB 테이블을 생성하는 기능도 제공해준다.

물론 운영환경에서 절대 사용하면 안되고, 개발 환경이나 로컬 환경에서 도움된다.

  • DDL을 애플리케이션 실행 시점에 자동 생성
  • 테이블 중심 ➡ 객체 중심
    • 보통 개발할 때 테이블 먼저 만들어두고, 객체로 돌아와서 개발하는데 JPA는 그럴 필요 없다.
    • 그냥 객체를 만들어두고 객체 테이블 매핑 다 해두면, 애플리케이션 로딩 시점에(실행 시점에) 필요하면 테이블을 다 만들어준다.
  • 데이터베이스 방언을 활용해서 데이터베이스에 맞는 적절한 DDL 생성!
  • 이렇게 생성된 DDL은 개발 장비에서만 사용
  • 생성된 DDL은 운영서버에서 사용하지 않거나, 적절히 다듬은 후 사용

속성

주의

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

  • 개발 초기 단계는 create 또는 update
  • 테스트 서버는 update 또는 validate
    • 여러 개발자가 함께 테스트해보는 서버
    • 다른사람이 테스트 해보는 데이터 다 날려버리면 안되니까 create는 사용 X
    • 하지만 개발서버도 가급적이면 update 사용 안하는 것이 좋다
  • 스테이징과 운영서버는 validate 또는 none
    • 운영서버에서 데이터 수백만건 있는 상태에서 alter를 잘못치거나 하면 시스템이 중단상태가 될 수도 있다
    • 굉장히 위험한 것이다
    • 5분만 시스템 멈춰도 대장애가 발생하는 것임
    • 따라서 alter table 스크립트 직접 만든 것을 잘동작하는지 테스트 서버 DB에 반영해봐야한다
    • 따라서 직접 스크립트 짜서 적용해보는 것을 권장

결론은 로컬 PC에서는 자유롭게 쓰되, 여러 명이 쓰는 개발 서버나 운영 서버에서는 사용하지 말아라

로컬 PC에서 나온 DDL 스크립트를 다듬어서 운영 서버에 적용하면 된다.
물론 일일이 꼼꼼히 따져봐야한다!

DDL 생성 기능

  • 제약조건 추가 : 예) 회원 이름은 필수, 10자 초과 X

    • @Column(nullable = false, length = 10)
  • 유니크 제약조건 추가

    • @Table(uniqueConstraints = {@UniqueConstraint(name ="NAME_AGE_UNIQUE", columnNames = {"NAME", "AGE"})})

DDL 생성기능은 DDL을 자동 생성할 때만 사용되고 JPA의 실행 로직에는 영향을 주지 않는다.

왜냐하면 제약조건 추가하는 것은 DB에 영향주는 것이지 애플리케이션 실행에는 영향주는 것이 아니기때문

테이블 명, 컬럼명 바꾸는 것은 INSERT 쿼리 날리는 것이 바뀌기 때문에 JPA 실행 로직에 영향을 주지만, 제약 조건 추가는 그냥 alter 문 하나 날리는게 끝이고 애플리케이션 실행에는 영향을 주지 않는다.

필드와 컬럼 매핑

@Column

  • @Column의 unique 제약조건은 제약조건 이름이 이상하게 랜덤으로 생성된다.
  • @Column의 unique 제약조건보다는 @Table의 unique 제약조건 사용한다. 이름까지 지정할 수 있기 때문

@Enumerated

자바 enum 타입 매핑시 사용 (DB에는 enum 타입이 없으므로 매핑해줘야함)

주의! ORDINAL 사용하지 않는다!

@Temporal

날짜 타입 매핑에 사용 (java.util.Date, java.util.Calendar)

LocalDate, LocalDateTime 사용시에는 생략가능 (최신 하이버네이트 지원)

@Lob

데이터베이스의 BLOB과 CLOB 타입 매핑

  • @Lob에는 지정할 수 있는 속성이 없다.
  • 매핑하는 필드 타입이 문자면 CLOB, 나머지는 BLOB
    • CLOB : String, char[], java.sql.CLOB
    • BLOB : byte[], java.sql.BLOB

@Transient

  • 필드 매핑 X
  • 데이터베이스에 저장 X, 조회 X
  • 주로 메모리 상에서만 임시로 어떤 값 보관하고 싶을 때 사용

기본키 매핑

  • @Id
  • @GeneratedValue

기본키 매핑 방법

  • 직접 할당 : @Id 사용
  • 자동 생성 : (@GeneratedValue)
    • IDENTITY : 데이터베이스에 위임, MYSQL
    • SEQUENCE : 데이터베이스 시퀀스 오브젝트 사용, ORACLE
      • @SequenceGenerator 필요
    • TABLE : 키 생성용 테이블 사용, 모든 DB에서 사용
      • @TableGenerator 필요
    • AUTO : 방언에 따라 자동 지정, 기본값 (위 3가지중 DBDialect에 알맞은 거 선택되는 옵션)

기본키 자동 생성 전략은 결과적으로 3가지 전략이 있다.

SEQUENCE 전략

  • 데이터베이스 시퀀스는 유일한 값을 순서대로 생성하는 특별한 데이터베이스 오브젝트 (예: 오라클 시퀀스)
  • 오라클, PostgreSQL, DB2, H2 데이터베이스에서 사용

create sequence로 데이터베이스에 sequence 오브젝트 생성한다. 시퀀스 오브젝트 1부터 시작하고 1씩 증가.

sequence object를 통해 값을 가져온다음 insert쿼리에 해당 값을 세팅해서 날린다.
into Member(name, id) values (?,?)에서 value(?,여기)에 시퀀스 오브젝트에서 가져온 값이 세팅되서 날라간다

위 사진에서는 따로 이름을 주지 않았기 때문에, 하이버네이트가 만드는 기본 시퀀스를 사용한다.

그런데 테이블 마다 시퀀스 따로 관리하고 싶다. 그렇다면 @SequenceGenerator라는 것을 통해 시퀀스 오브젝트랑 매핑하면 된다.

매핑

@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;
}

위의 결과로 DB에 MEMBER_SEQ라는 시퀀스 오브젝트가 생성된다.
name은 Id필드에 어떤 시퀀스 오브젝트를 시퀀스 제네레이터로 사용할지 매핑하기 위해 필요한 속성
아래 사진으로 확인

Sequence 전략의 경우 DB에 엔티티 insert 하려면 키 값이 필요함
따라서 시퀀스 오브젝트에서 먼저 키 값을 조회한 후, 해당 키 값으로 세팅해서 영속성 컨텍스트에 저장한다. (따라서 em.persist() 시점에는 아직 쿼리 안날라감)
트랜잭션이 커밋되는 시점에 쓰기지연SQL 저장소에 저장되있는 insert 쿼리 날린다. (버퍼링 가능)

@SequenceGenerator

주의 : allocationSize 기본값 = 50

Sequence 전략 사용하면, 매번 키 값 세팅하기 위해서 DB에서 Select 쿼리 날려야하는데 (next call로 하나씩 가져오면), 그러면 네트워크를 계속 타야하니 성능상 문제있지 않나?

그래서 JPA에서는 allocationSize를 미리 50개 올려놓고, 메모리에서 1씩 사용한다. (매번 1씩 안올려줘도 된다.) 그러다가 50개 올려놓은 것에 도착하면, 또 next call 한번 호출해준다.

이게 기가막힌다! 여러 웹서버 올려놔도 동시성 문제없이 잘 동작한다.

▶ 시작은 1 증가는 50씩
저렇게 세팅하고 확인해보면 현재 시퀀스 값이 -49이다. 왜 저렇게 되있냐면 다음 번 호출때 50증가하면 1 가지도록 하기 위해서
call next value 해주면 50 증가해서 현재값 1가진다.

해당 코드 실행하면 call next value가 2번 호출된다.
성능 최적화 하기 위해서 allocationSize = 50으로 하는 건데, 50개씩 메모리에 쓰려고 했는데 처음 호출하면 값이 1이니까 한번 더 호출해서 51까지 늘려준다.

DB에는 51개까지 늘려주고, 애플리케이션 메모리에서는 1부터 차례대로 증가시킨다.
맨 처음에만 DB 호출해주고, 이후에는 51되기 전까지 메모리에서 호출한다.
1,2,3 다 1차 캐시에 등록됐는데 DB에서 call next value는 똑같이 2번 호출됐다.

Table 전략

  • 키 생성 전용 테이블을 하나 만들어서 데이터베이스 시퀀스를 흉내내는 전략
  • 장점 : 모든 데이터베이스에 적용 가능
    • 어떤 DB는 시퀀스가 있고 (Oracle), 어떤 DB는 AutoIncrement가 있다. (MySQL)
    • 둘중 하나 선택해야한다.
    • 하지만 테이블 전략은 구분없이 사용가능하다 (테이블을 그냥 만들어서 키를 계속 뽑는 것이니까)
  • 단점 : 성능
    • 테이블 직접사용하다보니, 별도 테이블이라고 해도 테이블 lock 걸릴 수도 있고 성능 이슈가 있다
    • 시퀀스 오브젝트같은 것들은 숫자뽑는데 최적화되어있는데, 그냥 테이블은 최적화되있지 않기 때문

매핑

거의 시퀀스와 비슷하게 쓴다.

create table MY_SEQUENCES (
	SEQUENCE_NAME VARCHAR(255) NOT NULL,
    NEXT_VAL BIGINT,
    PRIMARY KEY (SEQUENCE_NAME)
)
@Entity
@TableGenerator(
    name = "MEMBER_SEQ_GENERATOR", //테이블 제네레이터 이름
    table = "MY_SEQUENCES", //테이블 이름
    pkColumnValue = "MEMBER_SEQ", allocationSize = 1) //PK컬럼 이름
public class Member {
    
    @Id
    @GeneratedValue(strategy = GenerationType.TABLE,
    		generator = "MEMBER_SEQ_GENERATOR")
    private Long id;
}

위의 쿼리 날리고 종료 (시퀀스 테이블에서 next value select해오고, 시퀀스 테이블 업데이트하고, 가져온 키값으로 member insert)

위에서 생성된 테이블 보면 이해된다.
MEMBER_SEQ의 경우 다음 키 값이 1인 상태이다.
또 다른 테이블의 SEQ 생성하고 싶다면 시퀀스 네임을 USER_SEQ 이런식으로 줘서 사용하면 된다.

하지만 운영에서 TABLE 전략 사용하기 부담스러움

기존 DB에서 관례로 사용하던게 있기 때문에, 기존 DB 전략 사용을 권장

속성

SEQUENCE 전략과 마찬가지로 initialValue와 allocationSize가 있다. 최적화 가능하다.

서버 여러 대여도 이런 식의 성능 최적화 문제없나??

문제 없다! 왜냐면 DB에 미리 값을 올려두는 방식이기 때문이다. 동시에 호출되더라도 각자 숫자 확보하고 값만 쭉 올라간다.

IDENTITY 전략

  • 기본 키 생성을 데이터베이스에 위임
  • Id값 null로 insert하고 DB에게 생성을 위임한다
  • DB에서는 null로 insert쿼리가 날라오면, 그때 DB에서 id값 세팅해준다.
  • 주로 MySQL, PostgreSQL, SQL Server, DB2 에서 사용
    (예: MySQL의 AUTO_INCREMENT)
  • JPA는 보통 트랜잭션 커밋 시점에 INSERT SQL을 실행
  • AUTO_INCREMENT는 데이터 베이스에 INSERT SQL을 실행한 이후에 ID 값을 알 수 있음
  • IDENTITY전략은 em.persist() 시점에 즉시 INSERT SQL을 실행하고 DB에서 식별자 조회
  • 위의 두 전략(SEQUENCE, TABLE) 같은 경우, DB에서 다음 키 값을 가져온 후에 INSERT를 한다.
  • 하지만 IDENTITY 전략은 null로 먼저 INSERT를 해야 다음 키 값을 알 수 있다.

문제점

  • IDENTITY 전략은 DB에 값이 들어가봐야 Id값을 알 수 있다.
  • JPA에서 영속성 컨텍스트에서 관리되려면 무조건 PK값이 있어야한다.
  • 그런데 IDENTITY전략은 PK값이 DB에 들어가봐야 안다.
  • JPA는 보통 트랜잭션 커밋 시점에 INSERT SQL을 실행하지만!
  • IDENTITY 전략에서만 울며 겨자먹기로 em.persist() 시점에 즉시 INSERT SQL을 실행하고, DB에서 PK값을 조회한다. 👿
    null로 insert하면 db에서 id값 세팅되서 들어간 후, jpa는 내부적으로 생성된 id값 select해서 영속성 컨텍스트에 등록한다.
    select 쿼리 왜 안나감? jdbc 드라이버에 이런 경우 값을 insert했을때 바로 해당 키값을 리턴받는게 다 짜여져있다. 그래서 db에 insert하는 시점에 생성된 키값을 바로 알 수 있음 (리턴받아서)

결론 : SQL 버퍼링 (모아서 INSERT)하는 것이 IDENTITY 전략에서는 불가능하다.

권장하는 식별자 전략

  • 기본키 제약 조건 : null아님, 유일, 변하면 안된다. (NOT NULL, UNIQUE)
  • 미래까지 이 조건을 만족하는 자연키는 찾기 힘들다. 대체키를 사용하자
    • 자연키 : 비즈니스적으로 의미있는 키, 주민번호, 전화번호 같은 것
    • 대체키 : 비즈니스적으로 의미 없는 것, 상관없는 것, 랜덤값
  • 예를 들어 주민등록번호도 기본키로 적절하지 않다.
  • 권장 : Long형 + 대체키 + 키 생성전략 사용

해당 게시글은 인프런 김영한님의 <자바 ORM 표준 JPA 프로그래밍 - 기본편>을 듣고 정리한 내용입니다.

profile
백엔드 꿈나무 🐥
post-custom-banner

0개의 댓글