자바 ORM 표준 JPA 프로그래밍 | 김영한 | 에이콘출판- 교보ebook

스프링 데이터 예제 프로젝트로 배우는 전자정부 표준 데이터베이스 프레임, <b>★ 이 책에서 다루는 내용 ★</b> ■ JPA 기초 이론과 핵심 원리 ■ JPA로 도메인 모델을 설계하는 과정을 예제 중심

ebook-product.kyobobook.co.kr

소스

https://github.com/rkwhr0010/jpa

 

  • 객체 테이블 매핑
    @Entity, @Table
  • 기본 매핑
    @Id
  • 필드 컬럼 매핑
    @Column
  • 연관고나계 매핑
    @ManyToOne( 다수), @JoinColumn

 

매핑은 XML이나 애노테이션 하나로 기술한다.

요즘은 거의 애노테이션만 사용하는 추세다.

 

@Entity

필수 어노테이션, 값을 기준으로 JPA 관리할 엔티티를 식별한다.

 

필수 규칙

  • protect 범위 이상 기본 생성자 필수
  • final 클래스, enum, interface, inner 클래스에 사용 불가
  • 저장 필드에 final 사용 불가

 

기본 생성자 규칙은 쉽게 실수 있는 부분이다.



@Entity
public class Member {
        @Id
        @Column(name = "MEMBER_ID")
        private Long id;
        private String name;
        
        // 생성자가 아무 것도 없으면, 컴파일러가 기본 생성자를 생성해준다.
        // 이렇게 생성자를 만들면, 컴파일러는 기본 생성자를 안만들어 준다.
        public Member(Long id, String name, Team team) {
                this.id = id;
                this.name = name;
                this.team = team;
        }

다른 곳에서도 기본 생성자는 필수 때가 있다. 무조건 기본 생성자는 만들어도 나쁠 없다.

 

@Table

엔티티와 테이블을 매핑할 사용한다.

생략이 가능하다. 생략 @Entity 붙은 클래스 이름과 같은 테이블에 매핑한다.

속성 기능 기본값
name 매핑할 테이블 이름 엔티티 이름
catalog catalog 있는 DB에서 catalog 매핑
schema schema 있는 DB에서 schema 매핑
uniqueConstraints 유니크 제약조건을 추가한다. 자동 DDL 생성에서 사용한다. 실제 실무에선 Table 직접 만드므로 사용할 일이 없다.

 

매핑 정보



import java.util.Date;


import jakarta.persistence.*;


@Entity
@Table(name = "MEMBER")
public class Member {
        @Id
        @Column(name = "ID")
        private String id;
        
        @Column(name = "NAME")
        private String username;
        
        private Integer age;
        
        @Enumerated(EnumType.STRING)
        private RoleType roleType;
        
        @Temporal(TemporalType.TIMESTAMP)
        private Date createdDate;
        
        private Date lastModifiedDate;
        
        @Lob
        private String description;


        public String getId() {
                return id;
        }


        public void setId(String 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 enum RoleType {
        ADMIN, USER
}

 

DB 스키마 자동 생성

persistence.xml 설정 정보



<?xml version="1.0" encoding="utf-8"?>
<persistence xmlns="https://jakarta.ee/xml/ns/persistence"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence https://jakarta.ee/xml/ns/persistence/persistence_3_0.xsd"
    version="3.0">
    <persistence-unit name="studyjpa" transaction-type="RESOURCE_LOCAL">
        <properties>
            <!-- DB 연결 정보 -->
            <property name="jakarta.persistence.jdbc.driver"
                value="com.mysql.cj.jdbc.Driver" />
            <property name="jakarta.persistence.jdbc.url"
                value="jdbc:mysql://localhost:55555/jpa?characterEncoding=utf8" />
            <property name="jakarta.persistence.jdbc.user" value="root" />
            <property name="jakarta.persistence.jdbc.password" value="root" />
            <property name="hibernate.hikari.poolName" value="pool" />
            <property name="hibernate.hikari.maximumPoolSize" value="10" />
            <property name="hibernate.hikari.minimumIdle" value="10" />
            <property name="hibernate.hikari.connectionTimeout" value="1000" />
            <!--  MySql 방언 -->
            <property name="hibernate.dialect"
                value="org.hibernate.dialect.MySQLDialect" />
            <!-- SQL 출력 -->
<!--            <property name="hibernate.show_sql" value="true" />-->
            <!-- SQL 이쁘게 출력 -->
            <property name="hibernate.format_sql" value="true" />
            <!-- 주석도 함께 출력 -->
            <property name="hibernate.use_sql_comments" value="true" />
            <!-- JPA 표준에 맞춘 새로운 키 생성 전략 사용 -->
            <property name="hibernate.id.new_generator_mappings" value="true" />
            <!-- 실습에서만 사용할 것, @Entity에 따라 DDL 명령을 자동 실행해 준다. -->
            <property name="hibernate.hbm2ddl.auto" value="create" />
        </properties>
    </persistence-unit>
</persistence>

 

hibernate.hbm2ddl.auto 옵션을 키면, @Entity 정보를 기반으로 DDL 생성한다.

DDL hibernate.dialect 속성에 설정한 방언에 따라 DB 맞게 생성된다.

 

DDL 자동 생성 기능은 개인적으로 학습할 때나, 신규 기능으로 테이블은 만들기 전에 테스트 용도로만 사용해야 한다.

잘못하면, 개발DB 운영 DB 전부 날릴 수도 있다.

 

옵션 설명
create 기존 테이블 삭제, 새로 생성
create-drop create 기능 + 종료 생성된 DDL 제거
update DB 테이블과 엔티티 비교 변경 분만 수정
validate DB 테이블과 엔티티를 비교 다르면, 실행을 하지 않는다
none 사용 안함, 속성 값을 지우는 것과 동일

 

이름 매핑 전략 변경

애플리케이션은 일반적으로 카멜케이스 표기법을 DB 언더스코어 표기법을 많이 사용한다. 이를 자동으로 매핑해주는 속성이 있다.

 

과거 org.hibernate.cfg.NamingStrategy 속성으로 엔티티와 테이블 이름 매핑 전략을 설정했다. 현재는

hibernate.implicit_naming_strategy, hibernate.physical_naming_strategy 분리됐다.

implicit 애플리케이션 , physical DB

분리가 됐다는 것은 과서 org.hibernate.cfg.NamingStrategy 설정이 개의 책임을 가졌다는 것을 의미한다. 현재는 제거된 속성이다.

https://docs.jboss.org/hibernate/orm/5.0/userguide/html_single/Hibernate_User_Guide.html#naming

 

 

 

 

DDL 생성 기능

어디까지나 자동 DDL 생성 기능을 활용한 것이다.

절대 운영이나 개발에선 사용하면 안된다.

 

 

유니크 제약조건 생성하기

 

 

 

 

주의할 것은 @Column(name = "NAME", nullable = false, length = 10) 에서 nullable, length DDL 자동 생성 사용되는 정보지 JPA 실행 로직에는 영향을 주지 않는다.

 

DDL 자동 생성 기능은 그냥 실제 개발할 사용하지말고 개인적인 학습 용도로 사용하는 것이 좋다.

기본 매핑

  • 직접할당
  • 자동할당

 

자동 생성 방식이 많은 이유는 DB마다 생성 방식이 다르기 때문이다.

오라클 시퀀스, Mysql AUTO_INCREMENT 등…

 

SEQUENCE, IDENTITY는 DB 의존적인 옵션이다

 

자동 생성 전략을 위해선 persistence.xml 파일에 아래와 같은 설정을 반드시 해야 한다.

 

키가 있는 타입

@Id

  • 기본형
  • 기본형의 래퍼 클래스
  • String
  • java.util.Date
  • java.sql.Date
  • java.math.BigDecimal
  • java.math.BigInteger

2023.11 현재도 그대로다.

 

없지만, mysql 기준 enum 된다.

 

 

 

IDENTITY 전략

생성을 DB에게 위임

Mysql

Oracle

 

 

 

전략은 키를 생성하기 위해 무조건 DB 질의를 해야한다.

, 쓰기 지연 SQL 저장되지 않고, 바로 DB INSERT 한다.

영속성 컨텍스트에 저장하기 위해선 무조건 식별자가 필수라는 것을 상기하자

 

SEQUENCE 전략

시퀀스를 사용하여 기본 키를 생성

시퀀스를 지원하는 DB에서 사용한다.

 

 

 

 

 

 

실습 중인 Mysql 시퀀스를 지원 안해 테이블이 만들어진다.

 

오라클로 테스트

 

 

 

persistence.xml



<?xml version="1.0" encoding="utf-8"?>
<persistence xmlns="https://jakarta.ee/xml/ns/persistence"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence https://jakarta.ee/xml/ns/persistence/persistence_3_0.xsd"
    version="3.0">
    <persistence-unit name="studyjpa" transaction-type="RESOURCE_LOCAL">
        <properties>
            <!-- DB 연결 정보 -->
            <property name="jakarta.persistence.jdbc.driver"
                value="oracle.jdbc.OracleDriver" />
            <property name="jakarta.persistence.jdbc.url"
                value="jdbc:oracle:thin:@localhost:54321:XE" />
            <property name="jakarta.persistence.jdbc.user" value="scott" />
            <property name="jakarta.persistence.jdbc.password" value="tiger" />
            <property name="hibernate.hikari.poolName" value="pool" />
            <property name="hibernate.hikari.maximumPoolSize" value="10" />
            <property name="hibernate.hikari.minimumIdle" value="10" />
            <property name="hibernate.hikari.connectionTimeout" value="1000" />
            <!--  Oracle 방언 -->
            <property name="hibernate.dialect"
                value="org.hibernate.dialect.OracleDialect" />
            <!-- SQL 출력 -->
<!--            <property name="hibernate.show_sql" value="true" />-->
            <!-- SQL 이쁘게 출력 -->
            <property name="hibernate.format_sql" value="true" />
            <!-- 주석도 함께 출력 -->
            <property name="hibernate.use_sql_comments" value="true" />
            <!-- JPA 표준에 맞춘 새로운 키 생성 전략 사용 -->
            <property name="hibernate.id.new_generator_mappings" value="true" />
            <!-- 실습에서만 사용할 것, @Entity에 따라 DDL 명령을 자동 실행해 준다. -->
            <property name="hibernate.hbm2ddl.auto" value="create" />
<!--            <property name="hibernate.implicit_naming_strategy" value="jpa" />-->
<!--            <property name="hibernate.physical_naming_strategy" -->
<!--                value="org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy" />-->
        </properties>
    </persistence-unit>
</persistence>

 

 

오라클은 시퀀스를 지원해 생성된다.

 

다시 돌아와서 시퀀스 전략의 문제는 다시 영속성 컨텍스트 식별자 필수 제약과 연관있다.

영속성 컨텍스트에 저장되기 위해선 무조건 시퀀스가 있어야 한다.

 

따라서 영속성 컨텍스트에 저장하기 위한 시퀀스 조회를 수행하게 된다.

em.persist() 호출 시점에 시퀀스 조회

영속성 컨텍스트에 저장 INSERT 쿼리는 쓰기 지연 SQL 저장된다.

이후 플러시 시점에 INSERT 쿼리가 실행된다.

 

결과적으로 DB 통신하게 된다. 네트워크 통신은 가장 병목 시점이다.

 

다른 문제로 시퀀스가 50 증가하는 증상은 아래와 같은 설정으로 변경할 있다.

명시적으로 @SequenceGenerator 어노테이션으로 시퀀스 생성기를 등록하고, @GeneratedValue.generator 속성으로 시퀀스 생성기를 선택한다.

 

@SequenceGenerator 속성

속성 기능 기본값
name 시퀀스 생성기 이름 필수
sequenceName DB 시퀀스 이름
initialValue 최초 생성 시작 , 따라서 DDL 자동 생성 사용 사용하는 속성 1
allocationSize 시퀀스 호출 증가하는 50
catalog 카탈로그 지원하는 DB 카탈로그
schema 스키마 지원하는 DB 스키마

 

 

 

Caused by: org.hibernate.MappingException: The increment size of the [MEMBER_SEQ] sequence is set to [20] in the entity mapping while the associated database sequence increment size is [1].
        at org.hibernate.id.enhanced.SequenceStyleGenerator.configure(SequenceStyleGenerator.java:218)
        at org.hibernate.id.factory.internal.StandardIdentifierGeneratorFactory.createIdentifierGenerator(StandardIdentifierGeneratorFactory.java:217)
        ... 19 more

DDL 자동 생성 기능을 꺼도, allocationSize는 사용되는 유효한 값이다.

allocationSize 기본 값이 50 이유

시퀀스 전략은 INSERT DB 통신한다고 했다.

이를 줄이기 위한 최적화 방법으로 시퀀스는 50 증가시킨다.

시퀀스를 조회하고, 다음 시퀀스 증가 크기 만큼 메모리에서 시퀀스를 처리한다.

, 50 INSERT 마다 시퀀스를 조회한다.

 

Member member = null;
for (int i = 0; i < 50; i++) {
        member = new Member();
        member.setUsername("홍길동" + i);
        em.persist(member);
}

 

 

번의 시퀀스 조회로 50 까지 식별자를 메모리에서 할당한 것을 있다.

보통은 allocationSize 값을 1 주고 쓴다.

 

테이블 전략

생성 전용 테이블을 만들고 시퀀스를 흉내낸다.

 

 

 

 

테이블 전략도 allocationSize 속성 값으로 최적화를 진행할 있다.

 

 

 

로우 하나가 하나의 시퀀스 오브젝트를 흉내낸다.

 

 

@TableGenerator

속성 기능 기본값
name 테이블 식별자 생성기 이름 필수
table DB 테이블
pkColumnName 시퀀스 컬럼명, 시퀀스 오브젝트 이름이라고 봐도 된다.
valueColumnValue 시퀀스 컬럼명
initialValue 초기 0
allocationSize 호출 증가값 50
catalog, schema

uniqueConstraints 유니크 제약 조건 지정(테이블 이라 가능한 속성)

 

 

AUTO 전략

선택한 DB 방언에 따라 TABLE, SEQUENCE, IDENTITY 하나를 자동으로 선택

 

 

 

 

참고로 기본 값은 AUTO

 

참고

 

 

기본 매핑 정리

직접 할당 DB 통신은 플러시 시점 번만 이뤄 진다.
SEQUENCE 시퀀스 번을 위해 persist() 시점에 DB 통신, 플러시 시점에 DB 통신
TABLE 시퀀스와 똑같은 이유로 DB 통신
IDENTITY DB에게 위임 Mysql 경우 무조건 데이터 저장시 키가 생성되기에 persit() 시점에 INSERT 이뤄진다.

 

 

기본 조건

  • null 불가
  • 유일성
  • 변하면 안됨

 

기본 선정 전략

  • 자연
    비즈니스
    데이터를 사용한
  • 대리
    비즈니스
    데이터와 관계 없는 임의
    자동
    생성 전략은 전부 대리키다.
     

 

있으면 대리 사용을 권장, 비즈니스는 언젠간 변하게 되어있다.

반면에 대리 키는 변할 여지가 없다.

 

대리 키를 기본 키로 잡고, 자연 후보들로 유니크 인덱스를 잡아서 사용하는 것을 권장한다.

 

필드와 컬럼 매핑

@Column 컬럼 매핑
@Enumerated 자바 열거형 매핑
@Temporal 날짜 타입 매핑
@Lob BLOB, CLOB 매핑
@Transient 매핑 무시할 필드

 

@Column

 

 

 

속성 기능 기본값
name 컬럼 매핑 필드 이름
insertable 필드 저장 가능 여부, 읽기 전용으로 만들 가끔 사용 true
updatable 필드 수정 가능 여부, 읽기 전용으로 만들 가끔 사용 true
table 하나의 엔티티를 이상 테이블에 매핑할 사용 현재 클래스와 매핑된 테이블
nulable DDL 생성 null 가능 여부 true
unique DDL 생성 해당 컬럼의 유니크 제약 조건 설정
2 이상 복합 유니크 제약은 클래스 레벨에서 @Table uniqueConstaints 사용해야 한다. 물론 하나도 가능
false
length DDL 생성 길이 제한, String 타입만 가능하다 255
precision DDL 생성 BigDecimal, BigInteger 에서 사용, 소수점 포함 전체 자리수 38
scale DDL 생성 BigDecimal에서 사용, 소수점 2

 

 

기본 참고

자바 기본형은 전부 자동으로 not null 붙는다.

굳이 null 가능하게 설정할 수도 있다.

하지만, 자바 단에서 기본값이 존재하기 때문에 DB null 절대로 들어갈 없다.

 

이런 불일치 위험 때문에 기본형은 nullable = false 명시적으로 주는 것이 안전하다

아니면 기본형의 래퍼 클래스를 사용하도록 한다.

 

 

@Enumerated

자바 enum 열거형을 매핑할 사용한다.

 

 

기본 값이 ORDINAL 되어 있다.

무조건 STRING으로 사용한다.

 

이유는 간단하다. ORDINAL 말그대로 순서로 순서에 의존하게 된다.

의존성을 없앨 있는 선택지가 있는데 의존성을 굳이 가져가는 것은 좋지 않다.

 

 

@Temporal

날짜 타입을 매핑하기 위해 사용한다.

 

java.sql.Date 년월일 정보만 담을 있다.

java.sql.Time 시분초 정보만 담을 있다.

java.sql.Timestamp 년월일시분초

 

해당 타입으로 매핑은 하더라도, 설정된 정보 만큼만 담긴다.

 

 

java.sql.* 패키지의 날짜 타입만 빼고, 어느 것이든 자기가 있는 사이트에서 사용하는 것을 사용하도록 한다.

@Temporal 생략해도 기본 동작이 "년월일시분초"라는 것만 알고 있으면 된다.

 

 


@Lob

DB BLOB, CLOB 매핑

  • CLOB
    String, char[], java.sql.CLOB
  • BLOB
    byte[], java.sql.BLOB
     

 

 

 

 

 

@Transient

매핑을 무시할 필드를 선정한다.

 

 

 

 

@Access

JPA 엔티티 데이터의 접근하는 방식을 지정한다.

 

 

 

필드 접근 방식은 필드가 private 이여도 상관 없다.

 

 

@Access 명시적으로 없으면, @Id 위치로 접근 방식이 결정된다.

 

@Id 필드에 있으므로, 필드 전략을 사용

 

서로 다른 전략을 사용할 수도 있다.

 

 

실전 예제

코드만 기술

@Entity
@Data
public class Member {
        @Id
        @GeneratedValue
        @Column(name = "MEMBER_ID")
        private Long id;
        
        private String name;
        private String city;
        private String strret;
        private String zipcode;
}
@Entity
@Table(name = "ORDERS")
@Data
public class Order {
        @Id
        @GeneratedValue
        @Column(name = "ORDER_ID")
        private Long id;
        @Column(name = "MEMBER_ID")
        private Long memberId;
        
        private LocalDateTime orderDate;
        
        @Enumerated(EnumType.STRING)
        private OrderStatus status;
}


public enum OrderStatus {
        ORDER, CANCEL
}


@Entity
@Table(name = "ORDER_ITEM")
@Data
public class OrderItem {
        
        @Id
        @GeneratedValue
        @Column(name = "ORDER_ITEM_ID")
        private Long id;
        
        @Column(name = "ITEM_ID")
        private Long itemId;
        @Column(name = "ORDER_ID")
        private Long orderId;
        
        private int orderPrice;
        private int count;
}


@Entity
@Data
public class Item {
        @Id
        @GeneratedValue
        @Column(name = "ITEM_ID")
        private Long id;
        
        private String name;
        private int price;
        private int stockQuantity;
}

 

객체지향설계는 객체가 맡은 역할과 책임을 가지고, 객체 참조를 통해 협업하도록 설계한다.

예제 코드는 객체 설계를 테이블에 맞춘 방법이다. 외래 부분이 그대로 데이터 타입인 것을 있다. 테이블은 문제가 안되는 데이터로 조인을 하면 된다. 하지만 객체는 조인이 불가능하다. 연관된 객체의 참조가 있어야 한다.

'개발 > JPA' 카테고리의 다른 글

06 다양한 연관관계 매핑  (0) 2024.04.14
05 연관관계 매핑 기초  (1) 2024.03.22
03 영속성 관리  (1) 2024.02.19
02 JPA 소개  (0) 2023.12.01
01 JPA 환경 설정  (0) 2023.11.23

+ Recent posts