LUMI_dev

4. 엔티티 매핑 본문

JPA/자바 ORM 표준 JPA 프로그래밍 - 기본편

4. 엔티티 매핑

luminous_dev 2025. 3. 23. 13:06

 

 

엔티티 맵핑

 

객체 & 테이블 > 필드 & 컬럼 > 기본 키 > 연관관계 

 

 

1. 객체와 테이블 매핑 

 

JPA로 테이블과 매핑할 클래스는 @Entity 필수

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

final, enum,interface,inner 클래스 사용 x (@Entity로 맵핑할 수 없음) 

저장할 필드에 final 사용 x 

 

 

@Entity 속성 정리 

name

 

 

@Table (name = "MBR")

엔티티와 매핑할 테이블 지정

지정하면 insert in to MBR -> 지정한 테이블로 쿼리가 나감 

 

 

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

DDL을 애플리케이션 실행 시점에 자동 생성

: 애플리케이션 생성 시점에 create 문으로 테이블 생성하는 기능 지원

테이블 -> 객체 중심 

생성된 DDL은 개발 장비에서만 사용 (운영서버에서는 사용하지 않거나, 적절히 다듬은 후 사용) 

 

보통 개발할 때 테이블 먼저 만들고 객체에서 돌아가서 개발함

JPA는 그럴 필요 없음

객체에서 해두고 매핑 해두면 쓸 때 필요하면 테이블 다 만듬

 

그럼 운영서버에서는 어떻게 데이터스키마를 써야할까?

더보기

운영 서버에서는 스키마 자동 생성 기능을 비활성화하고,

대신 명시적이고 제어된 방식으로 데이터베이스를 관리해야 합니다.

2.1. hibernate.hbm2ddl.auto 설정을 validate 또는 none으로 설정

운영 서버에서는 hibernate.hbm2ddl.auto 설정을 validate나 none으로 설정하는 것이 일반적입니다.

  • validate: 애플리케이션이 시작될 때, 현재 엔티티 클래스와 데이터베이스의 스키마가 일치하는지 검증만 하고, 변경하지 않습니다.
  • none: Hibernate가 스키마와 관련된 작업을 아예 수행하지 않도록 합니다. 이 설정은 운영 환경에 가장 안전합니다.
properties
복사
hibernate.hbm2ddl.auto=validate # 또는 none

2.2. 수동으로 DDL 관리

운영 환경에서는 데이터베이스 스키마를 직접 관리하는 것이 바람직합니다. 보통 다음과 같은 방법을 사용합니다.

  • 마이그레이션 도구 사용: Flyway나 Liquibase와 같은 데이터베이스 마이그레이션 도구를 사용하여 스키마 버전 관리를 합니다. 이 도구들은 SQL 스크립트를 통해 데이터베이스 변경사항을 명확히 기록하고, 변경을 관리할 수 있도록 도와줍니다.
    • Flyway와 Liquibase는 버전 관리된 SQL 파일을 사용하여 데이터베이스 변경 사항을 점진적으로 적용합니다.
    • 예를 들어, 각 스크립트에는 버전 번호가 매겨지고, 실행된 스크립트는 기록되어 이후에 변경된 사항만 적용됩니다.

2.3. 운영 환경에서의 DDL 생성 주기

운영 서버에서 데이터베이스 변경은 테스트와 검토가 충분히 된 후에 적용되어야 합니다. 일반적으로 운영 서버에서의 DDL 변경은 주기적으로 또는 새로운 기능 배포 시에만 발생하도록 계획합니다.

2.4. 롤백 전략 마련

운영 환경에서 스키마 변경을 진행할 때는 롤백 전략을 마련해두어야 합니다. 만약 스키마 변경 중 문제가 발생하면, 변경을 되돌릴 수 있는 방법을 준비하는 것이 중요합니다.

 



persistence.xml 

 

value에 create이 있으면,

1. drop table Member if exists (테이블이 있으면 지움)

2. create table 함 

 

 

create-drop : create하고 종료 시점에 테이블 drop (테스트 케이스 실행시키고 깔끔하게 처리하려고 할 때 쓰임) 

 

update : 테이블을 alter하고 싶을 때 (운영 DB에 사용하면 x) /지우는 거 안됨, 추가만 됨 

validate : 엔티티와 테이블이 정상적으로 매칭되었는지? 

none : 사용하지 않음 

 

 

운영 장비에선 절대!!!!! create, create-drop,update를 사용하면 안됨 

개발 초기  create, update
테스트 서버 update, validate
스테이징, 운영 서버 validate, none

 

DDL 생성 기능

 

Member.java 

@Entity
@Table(name="MBR") //: 쿼리가 나가는 테이블 지정
public class Member {
    @Id //PK가 뭔지 알려주는 것 Jakarta Persistence로 넣기
    private Long id;

    @Column(unique=true, length=10)
    private String name;
    private int age;

 

@Column~ (유니크 제약 조건 넣는 것들) 

→ DDL 생성 기능은 DDL 자동 생성 시에만 사용되고 JPA 실행 로직에는 영향 x 

alter 테이블 제약조건 쿼리 날아감

+ JPA가 실행될 때 이 어노테이션을 보고 런타임이 막 바뀌지 않음 / DDL 생성까지만 딱 도와줌 

 

@Table로 바꾸는 건 런타임에 영향을 줌 (인서트 쿼리, 업데이트 쿼리)

 

필드와 컬럼 맵핑

RoleType이라는 Enum 만들기 

 

Member.java 

package hellojpa;

import RoleType.RoleType;

import javax.persistence.*;
import java.util.Date;

@Entity
public class Member {
    @Id //PK가 뭔지 알려주는 것 Jakarta Persistence로 넣기
    private Long id;

    @Column(name= "name") //DB 컬럼명은 name이고 여기서는 username을 쓰고 싶다면 다음과 같이 쓰면 됨
    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 //varchar을 넘어서는 큰 컨텐츠를 넣고 싶으면 @Lob
    private String description;

    @Transient//만약 DB쿼리 날리기 싫은 것이 있다면
    private int temp;

    //Getter,Setter~
    public Member(){

    }
}

 

enum의 RoleType.java

package RoleType;

public enum RoleType {
    USER, ADMIN
}

 

@Lob 한 데이터가 문자형 큰 데이터 타입 = CLOB  / 숫자형 큰 데이터 타입 = BLOB

 

 

@Transient

: DB쿼리 날리기 싫은 것이 있을 때 (DB에선 사용하지 않음 / 메모리에 임시로 계산해두고 캐시 데이터로 넣거나 할 때 

 

@Column의 속성

 

 

@Column(name= "name",updatable = false) //DB 컬럼명은 name이고 여기서는 username을 쓰고 싶다면 다음과 같이 쓰면 됨

updatable = false

ㄴ 컬럼이 절대 변경되지 않음 

nullable = false

ㄴ null 허용 x 

columDefinition 

ㄴDDL에 그대로 입력됨 

@Column(name= "name",nullable = false, columnDefinition = "varchar(100) default 'EMPTY'") //DB 컬럼명은 name이고 여기서는 username을 쓰고 싶다면 다음과 같이 쓰면 됨

 

 

 

큰 숫자는 precision,scale을 씀

precision :소수점을 포함한 전체 자릿수 

scale : 소수의 자릿수

(double, float 타입에는 적용되지 않음) 

 

 

EnumType 

EnumType.ORDINAL : enum 순서를 DB에 저장 (사용할 때는 기본형이므로 그냥 @EnumType이라고만 쓰기 

EnumType.STRING : enum 이름을 DB에 저장  (이걸 필수로 쓰기)

 

주의) ORDINAL은 사용 XXX

ex) 만약 Guest가 추가된다고 해보자.

 

package RoleType;

public enum RoleType {
    GUEST,USER, ADMIN
}

 

그럼 인서트가 다음과 같이 진행됨 

 

EnumType.STRING을 쓰면 다음과 같이 이름으로 저장됨 (오류 적음)

 

@Temporal (요즘은 잘 안 사용함) -> JAVA8에 LocalDate, LocalDateTime이 들어와서 

 

Member.java 

@Temporal(TemporalType.TIMESTAMP)
private Date createdDate;

@Temporal(TemporalType.TIMESTAMP)
private Date lastModifiedDate;

private LocalDate testLocalDate;
private LocalDate testLocalDateTime;

 

@Lob (BLOB,CLOB) - 지정할 수 있는 속성이 없음 

CLOB (매핑하는 필드 타입 - 문자) : String,char[], java.sql.CLOB

BLOB (매핑하는 필드 타입 - 문자 x) : byte[], java, sql,BLOB

 

@Transient

- 필드 매핑 x / 데이터베이스 저장 x, 조회 x

- 주로 메모리상에서만 임시로 어떤 값을 보관하고 싶을 때 사용 

 

@Transient
private Integer temp

 

총정리

Member.java 

package hellojpa;

import RoleType.RoleType;

import javax.persistence.*;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.Date;

@Entity
public class Member {
    @Id //PK가 뭔지 알려주는 것 Jakarta Persistence로 넣기
    private Long id;

    @Column(name= "name",nullable = false, columnDefinition = "varchar(100) default 'EMPTY'") //DB 컬럼명은 name이고 여기서는 username을 쓰고 싶다면 다음과 같이 쓰면 됨
    private String username;

    private Integer age;

    @Enumerated(EnumType.STRING) //열거형을 쓰고 싶을 때
    private RoleType roleType;

    @Temporal(TemporalType.TIMESTAMP)
    private Date createdDate;

    @Temporal(TemporalType.TIMESTAMP)
    private Date lastModifiedDate;

    private LocalDate testLocalDate;
    private LocalDate testLocalDateTime;


    @Lob //varchar을 넘어서는 큰 컨텐츠를 넣고 싶으면 @Lob
    private String description;

    @Transient//만약 DB쿼리 날리기 싫은 것이 있다면
    private int temp;

    //Getter,Setter~
    public Member(){

    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public RoleType getRoleType() {
        return roleType;
    }

    public void setRoleType(RoleType roleType) {
        this.roleType = roleType;
    }

    public Date getCreatedDate() {
        return createdDate;
    }

    public void setCreatedDate(Date createdDate) {
        this.createdDate = createdDate;
    }

    public Date getLastModifiedDate() {
        return lastModifiedDate;
    }

    public void setLastModifiedDate(Date lastModifiedDate) {
        this.lastModifiedDate = lastModifiedDate;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public int getTemp() {
        return temp;
    }

    public void setTemp(int temp) {
        this.temp = temp;
    }
}

 

기본키 매핑

 

직접 할당 : @Id

자동 생성 : @GeneratedValue

 

여러가지 데이터를 조합해서 내가 직접 아이디 만들어 할당하겠다 = @Id

AutoIncrement (MySql) , Sequence(오라클) = @Generated Value 

 

@GeneratedValue(strategy = GenerationType.AUTO)

 

auto 설정은 다음 세가지 중에서 데이터베이스 환경에 맞춰서  하나가 선택됨 

 

GenerationType.class

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package javax.persistence;

public enum GenerationType {
    TABLE,
    SEQUENCE,
    IDENTITY,
    AUTO;

    private GenerationType() {
    }
}

 

IDENTITY

: DB 너가 알아서 해! 

: 기본 키 생성을 데이터베이스에 위임 / em.persist() 시점에 즉시 인서트 쿼리 실행

 

identify를 persistence.xml에서 mysql 방언으로 바꾸면 auto increment로 나옴 

<!--            <property name="hibernate.dialect" value="org.hibernate.dialect.MySQL5Dialect"/>-->

 

none으로 바꾸고 실행하기 

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

 

id값을 null로 넣고 그냥 설정하지 않고 persist 시점에 DB에 넣어버림

 

2) SEQUENCE (oracle)

create a sequence로 Sequence Object를 만듦 

Sequence Object를 통해서 값을 가져오고 이 값을 JpaMain.java에 설정

 

Member.java 

@Entity
public class Member {
   @Id //PK가 뭔지 알려주는 것 Jakarta Persistence로 넣기
   @GeneratedValue(strategy = GenerationType.SEQUENCE)
   private Long id;

   @Column(name= "name",nullable = false, columnDefinition = "varchar(100) default 'EMPTY'") //DB 컬럼명은 name이고 여기서는 username을 쓰고 싶다면 다음과 같이 쓰면 됨
   private String username;

   private Integer age;

 

private Long id;

쓰다가 10억 단위로 애초에 넘어갈 변수들은 데이터 타입을 Long으로 쓰기 

 

기본 시퀀스 하이버네이트가 만드는 기본 시퀀스를 사용하고 있음

 

각 테이블마다 시퀀스를 따로 관리하고 싶을 때 시퀀스 제네레이터 가지고 맵핑하면 됨 

 

 

시퀀스 제네레이터  

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

 

sequenceName = "MEMBER_SEQ": 실제 데이터베이스에서 사용되는 시퀀스의 이름

 

generator = "MEMBER_SEQ_GENERATOR"

: 내가 원하는 Generator를 사용할 때는 맵핑 걸면 멤버 시퀀스로 설정됨 

 

 

시퀀스 고급 전략 

allocation size / initial value 

 

 

 

@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라는 시퀀스 오브젝트를 만듦

 

로그보면

 

increment by 1이 있음 (1부터 시작해서 1씩 증가시켜라) 

 

DB는 아래와 같이 뜸

 

값을 애플리케이션으로 가져와야 함

 

em.persist();하려면 항상 PK가 있어야 함

그럼 먼저 sequence에서 내 PK를 가져와야 함

 

로그

 

call next value for MEMBER_SEQ

다음에 호출하면 점점 숫자가 커질 것임 

 

이 값을 가지고 em.persist로 영속성 컨텍스트에 넣으려고 보니까 시퀀스 전략 

> DB에서 값을 얻어와서 이 멤버에 아이디 값을 넣어줌 

> 그 다음에 영속성 컨텍스트에 저장

> 아직 DB의 인스턴트 쿼리는 안 날아가고 영속성 컨텍스트에 쌓여있음

(이유 : PK값만 얻고 필요하면 버퍼링 이런 것들 해야되니까)

> 실제 트랜잭션을 커밋하는 시점에 인서트 쿼리가 호출이 됨 

 

 

시퀀스 방식은 버퍼링 가능

쭉 모았다가 한번에 라이트하는 것이 가능함

 

성능 고민 : 자꾸 네트워크 왔다갔다해야하지 않나? 그냥 인서트 쿼리 날리지

-> allocationSize으로 성능 증가시킬 수 있음 (맨 하단 확인)


Table 전략 :

  • 테이블 하나를 만들어서 키를 계속 생성하는 것
  • 키 생성 전용 테이블을 만드는 것 / 데이터베이스 시퀀스를 흉내내는 전략
  • 장점 : 어떤 데이터베이스는 auto increment, 어떤 데이터베이스는 시퀀스가 있는데 모든 데이터베이스에 적용 가능 
  • 단점 : 테이블에 직접 사용하니까 별도의 테이블이라도 락이 걸릴 수 있고, 성능에 이슈가 생김   
    시퀀스 오브젝트 같은 것은 숫자 생성에 최적화되어 있는데 이건 최적화 안되어있어서 성능 떨어짐 

 

@Entity
@TableGenerator(
name = "MEMBER_SEQ_GENERATOR",
table = "MY_SEQUENCES",
pkColumnValue = “MEMBER_SEQ", allocationSize = 1)
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.TABLE,
generator = "MEMBER_SEQ_GENERATOR")
private Long id;

 

@TableGenerator 맵핑 전략을 테이블로 잡음

 

MySequence라는 테이블이 생성됨 

 

운영 - 테이블 전략 쓰기 부담스러움 -> DB에서 관례로 쓰는 것들을 그냥 쓰는 식별자 전략 쓰는 것 추천

 

 

allocation size / initial value -> 시퀀스 전략에서의 성능 최적화와 같은 내용  

 

식별자

주민번호 xxx

10억 넘어도 괜찮아야 하니까 Long 

대체키로 sequence나 UUID 권장 

결론: Auto_increment나 Sequence-object

 

 

 

아이덴티티 전략

아이덴티티 전략은 데이터베이스에 인서트를 해봐야 알 수 있음

 

내가 아이디에 값을 넣으면 안됨 + DB에 인서트 해야함 

DB에서 null로 인서트 쿼리가 날라오면 그때 DB에서 값을 세팅

 

단점) DB에 값이 들어가야 아이디 값을 알 수 있음

JPA는 영속성 컨텍스트 관리하려면 무조건 PK값이 있어야 함 

근데 DB에 들어가야 PK를 볼 수 있음

 

그래서 아이덴티티 전략만 예외적으로 em.persist(); 시점에 insert 쿼리가 날라가는 것을 볼 수 있음

 

Member.java 

 

JpaMain

package hellojpa;

import RoleType.RoleType;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;

public class JpaMain {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");//persistenceUnitName을 넘기라고 함 - persistence.xml의  <persistence-unit name="hello"> 부분임
        //이걸 연결하는 순간 데이터베이스랑 연결됨

        //create-entity-manger 꺼내기 - 데이터에서 커넥션 하나 받은 것임
        EntityManager em = emf.createEntityManager();

        EntityTransaction tx = em.getTransaction();
        tx.begin();

        try {
            //영속
            Member member = new Member();
            member.setUsername("C");

            em.persist(member);
            //커밋
            tx.commit();
        } catch (Exception e) {
            tx.rollback();
        } finally {
            em.close();
        }
        emf.close();
    }
}

 

이때 인서트 쿼리에선 null로 나오지만 DB에 들어가면 값이 존재함 

 

 

 

.persist() 한 시점 이후에 member.id를 조회할 수 있음

select 쿼리 날릴필요 없음 -> 값을 넣고 리턴 받을 수 있기 때문 

 

 

아이덴티티 전략에서는 em.persist()할 때마다 쿼리가 나가서 모아서 인서트하는 것이 불가능함 

 

데이터베이스 인서트 쿼리를 실행한 이후에야 id값을 알 수 있음 


allocationSize으로 성능 증가시키기 - 네트워크 오다가면 

 

Member.java

package hellojpa;

import RoleType.RoleType;

import javax.persistence.*;

import javax.persistence.*;
import java.math.BigDecimal;
import java.util.Date;
@Entity
@SequenceGenerator(
        name = "MEMBER_SEQ_GENERATOR",
        sequenceName = "MEMBER_SEQ", //매핑할 데이터베이스
        initialValue = 1, allocationSize = 50
)
public class Member {
    @Id //PK가 뭔지 알려주는 것 Jakarta Persistence로 넣기
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private Long id;

 

 

allocationSize 기본값은 50으로

= dba를 미리 올려놓고 메모리에서 그 개수만큼 쓰는 방식 

= Next call 한번 할 때 미리 50개 사이즈를 DB에 올려놓고 메모리에서는 1씩 쓰는 것 

( 다 담으면 Next call 또하고 그럼 51부터 100까지 DB에 미리 올려두고 다시 1씩 씀 )

= 여러 웹서버가 있어도 동시성 이슈 없이 다양한 문제들을 해결할 수 있음

 

 

start with 1 increment by 50

 

현재 값이 -49, 증가는 50라고 되어있음

 

왜 -49일까?

call next value 한번 호출해서 나온 값을 쓰는데 

한번 호출했을 때 처음에 1이 되기를 기대하는 것임 

실행해보면 1 값을 세팅하고 동작함 

 

 

em.persist를 member1만 하고 있음

call next value for 멤버 시퀀스가 두번 노출되고 있음 

 

JpaMain.java 

package hellojpa;

import RoleType.RoleType;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;

public class JpaMain {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");//persistenceUnitName을 넘기라고 함 - persistence.xml의  <persistence-unit name="hello"> 부분임
        //이걸 연결하는 순간 데이터베이스랑 연결됨

        //create-entity-manger 꺼내기 - 데이터에서 커넥션 하나 받은 것임
        EntityManager em = emf.createEntityManager();

        EntityTransaction tx = em.getTransaction();
        tx.begin();

        try {
            //영속

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


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


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

            //DB SEQ =1    |1 씀
            //DB SEQ = 51  |2 씀
            //DB SEQ = 51  |3 씀 -> 여기서부터는 DB SEQ는 같음

            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("======================");



            //커밋
            tx.commit();
        } catch (Exception e) {
            tx.rollback();
        } finally {
            em.close();
        }
        emf.close();
    }
}

 


처음 호출 했을 때는 DB SEQ = 1 | 1씀

//난 50개씩 써야 하는데 처음 호출해봤더니 1인 것임 -> 그래서 문제 있네?하고 한번 더 호출한 것임 

두번째 호출 했을 때는 DB SEQ = 51 | 2 씀

세번째 호출할 때는 DB SEQ = 51 | 3씀  (여기서부터 DB SEQ는 같음)

 

 

em.persist(member1); //1번은 그냥 더미를 호출한 것 + 51개로 맞춘 다음
//em.persist(member2); //여기서부터는 메모리에서 됨
//em.persist(member3);

 

 

member1에 두번 호출되고 member2,3부터는 호출 x

 

allocationSize는 50~100정도가 적당함


테이블 전략에도 Initial Value와 Allocation Size가 있음 

Allocation Size는 위랑 비슷한 것

 

서버가 여러대면 문제가 없나 고민될 수 있음 -> BUT 문제 없음

미리 값을 올려두는 방식이기 때문에 여러 대가 동시에 붙더라도,

50개를 동시 호출하더라도 자기가 숫자를 미리 확보하고 되기 때문에 크게 문제 없음