본문 바로가기

Spring/JPA

1) Spring Data JPA 1부 - 설정 및 예제

반응형

한 10년전에 하이버네이트를 사용해서 도메인 모델링과 ORM 프로그래밍을 했었고 

여러가지 장단점을 많이 느꼈었다. 그떄 당시에는 하이버네이트보다는 iBatis를 많이 사용하는 추세였고

하이버네이트에 좀 부정적인 입장이였는데 최근에 보니 Spring Data JPA 나 하이버네이트 처럼 ORM 프레임워크를 

많이 사용하고 있는것을 알게 되어 다시 ORM에 대해 공부해 보려고 한다.

 

먼저 JPA는 왜 쓰는걸까? 에 대해 궁금하다. 어떤 장점이 있어서 사용할까?

 

기존의 SQL 중심 개발에 어느정도 벗어날수 있다.

기존의 ibatis의 경우는 SQL를 직접 만들고 해당 SQL이 변화면 해당 SQL의 DTO도 변경해야 되는데

JPA는 이 문제에서 어느정도 벗어나게 해준다. 반대인 경우도 field가 늘어나면 해당되는 모든 쿼리를 수정해야 한다.

여기서 어느정도라는것을 시스템을 설계하고 개발하다보면 모든 경우를 전부 JPA로 대체할수는 없다고 생각한다.

이유는 아주 복잡한 쿼리나 통계 데이터 같은 경우는 보다 DB에 최적화된 SQL문을 작성해서 데이터를 추출하는것이 

더 성능과 최적화 면에서 좋다고 보기 때문이다.

간단하게 위와 같은 구조의 Entity 모델링을 구현해 보자.

단순히 예제인 Sample 모델이므로 모델 구조적인 문제점들에 대해서는 넘어가자.

 

먼저 Spring JPA Data의 Entity 만드는 방법에 대해 알아 보자.

 

application.properties

spring.profiles.active=local
# H2 설정
spring.h2.console.enabled=true
spring.h2.console.path=/h2

# Datasource 설정

spring.datasource.hikari.jdbc-url=jdbc:h2:tcp://localhost:9092/./data/devDB
spring.datasource.hikari.driver-class-name: org.h2.Driver
spring.datasource.hikari.username=sa
spring.datasource.hikari.password=

spring.jpa.database-platform= H2
spring.jpa.hibernate.ddl-auto= update

# 로그 레벨
logging.level.org.hibernate=debug
logging.level.org.hibernate.type.descriptor.sql=trace
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true

 

build.gradle

plugins {
	id 'org.springframework.boot' version '2.4.3'
	id 'io.spring.dependency-management' version '1.0.11.RELEASE'
	id 'java'
}

group = 'com.devracoon.jpa'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'

repositories {
	mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-jdbc'
	
    runtimeOnly 'com.h2database:h2'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
	
    compileOnly 'org.projectlombok:lombok:1.18.6'
    annotationProcessor 'org.projectlombok:lombok:1.18.6'
}

test {
	useJUnitPlatform()
}

 

Product.java

package com.devracoon.jpa.entity;

import java.util.ArrayList;
import java.util.List;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.OneToMany;

import org.hibernate.annotations.GenericGenerator;

import lombok.Data;
import lombok.NonNull;

@Data
@Entity
public class Product {

    @Id
    @GeneratedValue(generator = "uuid2")
    @GenericGenerator(name = "uuid2", strategy = "org.hibernate.id.UUIDGenerator")
    @Column(name = "product_id", columnDefinition = "VARCHAR(255)")
    private String productId;
    
    @NonNull
    @Column(name = "product_name", columnDefinition = "VARCHAR(255)")
    private String productName;
    
    @OneToMany(mappedBy = "product" , targetEntity = Item.class , fetch = FetchType.LAZY ,cascade = CascadeType.PERSIST, orphanRemoval = true)
    private List<Item> items = new ArrayList<Item>();
    
}

@GeneratedValue

lombok 라이브러리는 Object 의 Getter Setter 등 생성자를 자동으로 만들어주는 라이브러리이다.

 

@Data : lombok 라이브러리의 @Getter @Setter , @RequiredArgsConstructor, @ToString, @EqualsAndHashCode

           를 전부 통합한 어노테이션이다. 

@Entity : JPA가 관리하는 테이블과 맵핑되는 객체가 된다.

             기본적으로 Class 이름이 테이블명이 됨.

@Id : 테이블의 primary key 가 된다.

@GenericGenerator : ID생성 하는 Generator를 지정

@GeneratedValue : 지정한 Generator로 ID를 생성.

@NonNull : lombok이 자동으로 NonNull이 붙은 properties들로 생성자를 만들어 준다.

 

@OneToMany : 1: N 관계를 명시한다. 각 속성값에 대해 알아 보자. 

 

# mappedBy - Item Class의 product 필드와 mapping 되었다라는 말인데 이게 무슨말일까? 

                    OneToMany는 Item.class 로의 단방향 관계를 명시를 한것인다. 

                    반대로 Item.class에서도 Product를 ManyToOne으로 바로보고 있다. 이것 또한 Item 입장에서의 

                    단방향이 만들어 진것인다. 

                    이 두개의 단방향을 서로 연결해서 하나로 만들어주는것이 mappedBy이다. 

                    말로하니까 이해가 하기 어려워지는데 결과를 보자 .

 

먼저 mappedBy가 없으면 무슨일이 생길까?

위 소스에서 mappedBy를 빼고 테이블을 생성하면 아래와 같이 Product와 Item을 맵핑시키는 맵핑테이블이 하나생긴다.

 

PRODUCT_ITEMS 테이블은 단순히 두 테이블의 맵핑관계를 위해 만들어진다. 

 

그럼 mappedBy를 넣으면 어떻게 될까?

위와 같이 맵핑테이블이 없어지고 아래와 같은 Constraints 가 걸리게 된다.

ALTER TABLE "PUBLIC"."ITEM" ADD CONSTRAINT "PUBLIC"."FKD1G72RRHGQ1SF7M4UWFVUHLHE" 
FOREIGN KEY("PRODUCT_ID") INDEX "PUBLIC"."FKD1G72RRHGQ1SF7M4UWFVUHLHE_INDEX_2" 
REFERENCES "PUBLIC"."PRODUCT"("PRODUCT_ID") NOCHECK

위 결과에서 알 수 있듯이 간단하게 생각하면

mappedBy를 없으면 두 테이블의 연관 관계를 맵핑테이블을 만들어서 연결하려고 하는것이고

mappedBy가 있으면 두 parent-child 와 같은 관계 구조가 만들어진다 라고 이해해도 될것 같다.

 

#targetEntity  : 연결된 Entitiy를 지정

#fetch : 데이터로딩방법에 대한 설정이다.

           EAGER : 즉시로딩 . Product 가 조회되면 연결된 Item들까지 전부 한번의 쿼리로 조회해 온다.

           LAZY :  게으른 로딩 . Product가 조회되고 나중에 Product에서 getItems 와 같이 items를 참조하게 되면 

                      그때 쿼리를 날려 데이터를 조회해 온다. 

아래 테스트 결과를 보자.

 

먼저 Lazy Loading 테스트에서 아래처럼 소스를 수정하고 실행하면 

Optional<Product> product = productRepo.findById(productId);
System.out.println(product.get().getItems().size());

쿼리 결과가 처음 product를 조회해 오고 그다음에 items 리스트를 조회해 온다.

select
        product0_.product_id as product_1_1_0_,
        product0_.product_name as product_2_1_0_ 
    from
        product product0_ 
    where
        product0_.product_id=?
Hibernate: 
    select
        product0_.product_id as product_1_1_0_,
        product0_.product_name as product_2_1_0_ 
    from
        product product0_ 
    where
        product0_.product_id=?
2021-03-12 09:40:35.076 TRACE 21732 --- [nio-8080-exec-4] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [f1d55f8e-5c3e-419b-a47f-3196fb4ccc35]
2021-03-12 09:40:35.078 DEBUG 21732 --- [nio-8080-exec-4] l.p.e.p.i.EntityReferenceInitializerImpl : On call to EntityIdentifierReaderImpl#resolve, EntityKey was already known; should only happen on root returns with an optional identifier specified
2021-03-12 09:40:35.078 TRACE 21732 --- [nio-8080-exec-4] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([product_2_1_0_] : [VARCHAR]) - [Product 0]
2021-03-12 09:40:35.078 DEBUG 21732 --- [nio-8080-exec-4] o.h.engine.internal.TwoPhaseLoad         : Resolving attributes for [com.devracoon.jpa.entity.Product#f1d55f8e-5c3e-419b-a47f-3196fb4ccc35]
2021-03-12 09:40:35.078 DEBUG 21732 --- [nio-8080-exec-4] o.h.engine.internal.TwoPhaseLoad         : Processing attribute `items` : value = NOT NULL COLLECTION
2021-03-12 09:40:35.078 DEBUG 21732 --- [nio-8080-exec-4] o.h.engine.internal.TwoPhaseLoad         : Attribute (`items`)  - enhanced for lazy-loading? - false
2021-03-12 09:40:35.078 DEBUG 21732 --- [nio-8080-exec-4] o.h.engine.internal.TwoPhaseLoad         : Processing attribute `productName` : value = Product 0
2021-03-12 09:40:35.078 DEBUG 21732 --- [nio-8080-exec-4] o.h.engine.internal.TwoPhaseLoad         : Attribute (`productName`)  - enhanced for lazy-loading? - false
2021-03-12 09:40:35.079 DEBUG 21732 --- [nio-8080-exec-4] o.h.engine.internal.TwoPhaseLoad         : Done materializing entity [com.devracoon.jpa.entity.Product#f1d55f8e-5c3e-419b-a47f-3196fb4ccc35]
2021-03-12 09:40:35.079 DEBUG 21732 --- [nio-8080-exec-4] .l.e.p.AbstractLoadPlanBasedEntityLoader : Done entity load : com.devracoon.jpa.entity.Product#f1d55f8e-5c3e-419b-a47f-3196fb4ccc35
2021-03-12 09:40:35.079 DEBUG 21732 --- [nio-8080-exec-4] o.h.e.t.internal.TransactionImpl         : committing
2021-03-12 09:40:35.083 DEBUG 21732 --- [nio-8080-exec-4] stractLoadPlanBasedCollectionInitializer : Loading collection: [com.devracoon.jpa.entity.Product.items#f1d55f8e-5c3e-419b-a47f-3196fb4ccc35]
2021-03-12 09:40:35.083 DEBUG 21732 --- [nio-8080-exec-4] org.hibernate.SQL                        : 
    select
        items0_.product_id as product_4_0_0_,
        items0_.item_id as item_id1_0_0_,
        items0_.item_id as item_id1_0_1_,
        items0_.item_name as item_nam2_0_1_,
        items0_.item_number as item_num3_0_1_,
        items0_.product_id as product_4_0_1_ 
    from
        item items0_ 
    where
        items0_.product_id=?
Hibernate: 
    select
        items0_.product_id as product_4_0_0_,
        items0_.item_id as item_id1_0_0_,
        items0_.item_id as item_id1_0_1_,
        items0_.item_name as item_nam2_0_1_,
        items0_.item_number as item_num3_0_1_,
        items0_.product_id as product_4_0_1_ 
    from
        item items0_ 
    where
        items0_.product_id=?

이것이 Lazy Loading 이다. 해당 필드가 참조 되었을때 그때 쿼리를 조회해 온다.

 

다음은 EAGER 테스트이다. 

select
        product0_.product_id as product_1_1_0_,
        product0_.product_name as product_2_1_0_,
        items1_.product_id as product_4_0_1_,
        items1_.item_id as item_id1_0_1_,
        items1_.item_id as item_id1_0_2_,
        items1_.item_name as item_nam2_0_2_,
        items1_.item_number as item_num3_0_2_,
        items1_.product_id as product_4_0_2_ 
    from
        product product0_ 
    left outer join
        item items1_ 
            on product0_.product_id=items1_.product_id 
    where
        product0_.product_id=?

쿼리를 확인해 보면 위와 같이 한번 쿼리로 items 까지 다 가져오게 된다.

 

여기까지 1부를 끝낸다. 

 

2부에서 casecade와 옵션에 따른 insert와 update, delete에 대한 옵션들과 그에 따른 결과를 확인해보자.