728x90
반응형

[JPA] save()가 INSERT 대신 SELECT를 날리는 경우

환경 : Spring Boot 3.x, JPA, Hibernate, Kotlin 1.9

 


증상

// memberRepository.save(member) 호출 시
// INSERT가 아니라 SELECT 쿼리가 먼저 실행됨
Hibernate: select m1_0.id ... from member m1_0 where m1_0.id=?
Hibernate: insert into member ...

 

1. 원인

Spring Data JPA의 save()는 내부적으로 persistmerge를 구분함

// SimpleJpaRepository 내부 구현
@Transactional
override fun <S : T> save(entity: S): S {
    if (entityInformation.isNew(entity)) {
        em.persist(entity) // INSERT
        return entity
    } else {
        return em.merge(entity) // SELECT + INSERT 또는 UPDATE
    }
}

isNew() 판단 기준:

  • ID가 null이면 새 엔티티 → persist
  • ID가 null이 아니면 기존 엔티티 → merge (SELECT 먼저 실행)

문제는 ID를 직접 할당하는 경우. ID가 이미 세팅되어 있으니 JPA가 기존 엔티티로 판단해서 merge를 호출함

// ID 직접 할당 → isNew() = false → merge → SELECT 발생
@Entity
class Product(
    @Id
    val id: String = UUID.randomUUID().toString(), // 이미 값이 있음
    val name: String
)

 

2. 해결 방법

2.1) @GeneratedValue 사용 (가장 간단)

DB에서 ID를 자동 생성하면 save() 시점에 ID가 null이므로 항상 persist

@Entity
class Member(
    val name: String,
    val email: String,
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0
)

 

2.2) Persistable 인터페이스 구현

ID를 직접 할당해야 하는 경우, Persistable을 구현해서 isNew() 판단 로직을 직접 제어

@Entity
class Product(
    @Id
    val id: String = UUID.randomUUID().toString(),
    val name: String,
    @CreatedDate
    var createdAt: LocalDateTime? = null
) : Persistable<String> {

    override fun getId(): String = id

    override fun isNew(): Boolean = createdAt == null
}

createdAtnull이면 아직 DB에 저장 안 된 것 → persist 호출. @CreatedDate는 저장 시점에 자동 세팅되므로 한 번 저장된 엔티티는 isNew() = false

@CreatedDate 사용하려면 @EnableJpaAuditing 필요

@EnableJpaAuditing
@SpringBootApplication
class Application

 

2.3) @Version 필드 활용

@Version 필드가 null이면 새 엔티티로 판단

@Entity
class Product(
    @Id
    val id: String = UUID.randomUUID().toString(),
    val name: String,
    @Version
    val version: Long? = null // null이면 isNew() = true
)

 

3. persist vs merge 동작 비교

// persist — INSERT 1번
em.persist(member)
// SQL: INSERT INTO member (name, email) VALUES (?, ?)

// merge — SELECT 1번 + INSERT 또는 UPDATE 1번
em.merge(member)
// SQL: SELECT ... FROM member WHERE id = ?
//      INSERT INTO member ... (새 엔티티인 경우)
//      UPDATE member SET ... (기존 엔티티인 경우)

merge는 항상 SELECT가 선행됨. 대량 INSERT 시 성능 차이가 큼

3.1) 대량 INSERT 성능 차이

// 나쁜 예 — ID 직접 할당 + saveAll → merge 1000번 = SELECT 1000 + INSERT 1000
val products = (1..1000).map { Product(id = "P-$it", name = "상품$it") }
productRepository.saveAll(products)
// 총 쿼리: 2000번

// 좋은 예 — Persistable 구현 후 saveAll → persist 1000번
// 총 쿼리: 1000번 (SELECT 없음)

※ 대량 INSERT 시에는 Persistable 구현 + spring.jpa.properties.hibernate.jdbc.batch_size=50 설정까지 하면 더 효과적

 

728x90
반응형
728x90
반응형

[JPA] MultipleBagFetchException 해결

환경 : Spring Boot 3.x, JPA, Hibernate



에러 메시지

org.hibernate.loader.MultipleBagFetchException:
  cannot simultaneously fetch multiple bags:
  [com.example.Member.orders, com.example.Member.addresses]


1. 원인

2개 이상의 컬렉션(List)을 동시에 fetch join 하면 발생. Hibernate가 카테시안 곱 결과를 올바르게 매핑할 수 없기 때문

@Entity
class Member(
    @OneToMany(mappedBy = "member")
    val orders: List<Order> = mutableListOf(),

    @OneToMany(mappedBy = "member")
    val addresses: List<Address> = mutableListOf()
)

// 이 쿼리에서 터짐
@Query("SELECT m FROM Member m JOIN FETCH m.orders JOIN FETCH m.addresses")
fun findAllWithOrdersAndAddresses(): List<Member>


2. 해결 방법

2.1) List를 Set으로 변경

가장 간단한 방법. Set은 중복을 허용하지 않아 카테시안 곱 문제가 발생하지 않음

@Entity
class Member(
    @OneToMany(mappedBy = "member")
    val orders: Set<Order> = mutableSetOf(),

    @OneToMany(mappedBy = "member")
    val addresses: Set<Address> = mutableSetOf()
)

※ 순서가 필요하면 LinkedHashSet 사용


2.2) @BatchSize로 분리 조회

fetch join 대신 @BatchSize로 IN 절 조회. 쿼리 2~3번으로 해결

@Entity
class Member(
    @OneToMany(mappedBy = "member")
    @BatchSize(size = 100)
    val orders: List<Order> = mutableListOf(),

    @OneToMany(mappedBy = "member")
    @BatchSize(size = 100)
    val addresses: List<Address> = mutableListOf()
)
# 또는 전역 설정
spring:
  jpa:
    properties:
      hibernate:
        default_batch_fetch_size: 100

2.3) fetch join을 나눠서 실행

한 번에 하나의 컬렉션만 fetch join하고 나머지는 Hibernate 초기화에 맡기기

@Query("SELECT m FROM Member m JOIN FETCH m.orders")
fun findAllWithOrders(): List<Member>

// addresses는 @BatchSize 또는 별도 조회로 처리

728x90
반응형
728x90
반응형

작성일자 : 2022.06.19

환경 : vue 2, webpack-dev-server 4.8.1

시나리오 : 프록시를 활용하여 CORS 해결

 

관련 포스팅 : https://osc131.tistory.com/118

 

[Spring Boot] CORS 해결

[Spring Boot] CORS 해결 작성일자 : 2019.07.07 환경 : Spring Boot 2.1.6 다른 도메인에서의 자원을 호출하는 행위에 제한이 없을 경우 안전하지 않습니다. CORS (Cross-Origin Resource Sharing)는 이렇게..

osc131.tistory.com

 

1. webpack-dev-server 확인

 

package-lock.json 

"webpack-dev-server": {
	"version": "4.8.1"
    ...
    ...
}

사용하고 있지 않을 시 모듈 추가 필요

 

 

2.Proxy 설정 추가

 

vue.config.js

module.exports = {
  devServer: {
    proxy: {
	// /api 및 /api/* 요청에 대해 프록시 설정
      '/api': {	
        target: 'http://localhost:8080', // 프록시를 설정할 도메인
        changeOrigin: true,
      }, 
    },
  },
}

 

위 설정 이후 http://localhost:8080/api/* 에 대해 CORS 가 허용됨

 

 

* 주의 : 프록시 설정 이후 요청할 때 도메인 정보는 생략해야함

ex)

    const uri = '/api'; // 'http://localhost:8080/api' 로 작성 시 프록시 적용 X
    fetch(uri, {method: 'get'})
        .then(response => response.json())
        .then(response => {
            alert(response);
        })
728x90
반응형
728x90
반응형

전체 로그 :

  [Vue warn]: You are using the runtime-only build of Vue where the template compiler is not available. Either pre-compile the templates into render functions, or use the compiler-included build.

 

조치 :

  프로젝트 바로 아래(package.json 위치)에 vue.config.js 생성 후 아래 옵션 추가

module.exports = {
    runtimeCompiler: true
}

 

728x90
반응형
728x90
반응형

작성일자 : 2020.06.16

 

IntelliJ 에서 Git 관련 작업이 무한 홀딩되는 상황 발생

 

 

상세 내용 : 

 

1. IntelliJ 에서 git repository 연결 Test 시 무한 홀딩

2. IntelliJ 에서 git 을 사용하여 소스 Clone 시 무한 홀딩

3. Spring 기반 자바 프로젝트를 IntelliJ 로 Open 시 무한 홀딩 (Vue 프로젝트는 열렸었음)

4. IntelliJ 설정에서 git 연결 확인 시 무한 홀딩 ( Versiont Control > Git > Test )

 

 

조치 내용 : 

 

IntelliJ 설정 ( File > Setting OR Ctrl+Alt+S ) > Version Control > Git > Path to Git excutable 수정

  

기존 - C:\Program Files\Git\git-cmd.exe 

수정 - C:\Program Files\Git\cmd\git.exe

 

Git 경로를 수정해준 뒤 Test 를 누르니, 정상적으로 버전 창 팝업 확인

 

 

reference :

https://intellij-support.jetbrains.com/hc/en-us/community/posts/360001211119-IntelliJ-hangs-on-Git-Identifying-Git-Version-

728x90
반응형
728x90
반응형

작성일자 : 2020.06.13

 

Stream 을 활용하여 List to Map 변환하는 과정에서 발생할 수 있는 키 중복 예외

 

예외 발생 예시

List<Integer> list = new ArrayList<Integer>(){{add(0);add(1);add(1);add(2);}};
// { 0, 1, 1, 2 }
        
        
Map<Integer, Integer> map = list.stream()
       .collect(Collectors.toMap(vo->vo, vo->vo));
// { {0,0}, {1,1}, {1,1}, {2,2} } 형태로 반환 -> key 1 중복 !!

 

수정

List<Integer> list = new ArrayList<Integer>(){{add(0);add(1);add(1);add(2);}};
// { 0, 1, 1, 2 }
        
        
Map<Integer, Integer> map = list.stream()
       .collect(Collectors.toMap(vo->vo, vo->vo, (oldValue, newValue) -> oldValue));
// { {0,0}, {1,1}, {2,2} } 형태로 반환 

// 기존 값을 유지할 경우
// .collect(Collectors.toMap(vo->vo, vo->vo, (oldValue, newValue) -> oldValue));
// 새로운 값을 유지할 경우
// .collect(Collectors.toMap(vo->vo, vo->vo, (oldValue, newValue) -> newValue));
728x90
반응형
728x90
반응형

// gradle.properties

org.gradle.jvmargs = -Xms512m -Xmx2048m

 

728x90
반응형
728x90
반응형

특정 Port 사용중인 프로세스 종료

 

작성일자 : 2019.12.26

시나리오 : WIndow에서 특정 포트번호를 사용중인 프로세스 종료

 

1. netstat -ano | findstr 'PortNumber'

2. PID 확인 ( 1. 명령어 결과의 우측 끝 5번째 인자 )

3. taskkill /F /PID 'PID'

 

 

728x90
반응형
728x90
반응형

[Spring Boot] CORS 해결

 

작성일자 : 2019.07.07

환경 : Spring Boot 2.1.6

 

다른 도메인에서의 자원을 호출하는 행위에 제한이 없을 경우 안전하지 않습니다. CORS (Cross-Origin Resource Sharing)는 이렇게 시스템 수준에서 타 도메인 간 자원 호출을 승인하거나 차단하는 것을 결정하는 것입니다. 여기서 Access-Control-Allow-Origin 란 CORS 의 헤더를 의미합니다.

Same Origin Policy에 의해 Script에 의한 cross-site http requests는 허용되지 않습니다. 즉 Ajax를 사용하여 통신을 하고 Front와 Back을 구별하여(ex Spring Boot + Vue.js) 개발하는 구조의 웹 서비스는 개발단계에서 서버와의 통신을 위해 추가로 설정이 필요합니다. 

 

GpConfig.java

package com.example.demo;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
@EnableWebMvc
public class GpConfig implements WebMvcConfigurer{
    @Override
    public void addCorsMappings(CorsRegistry cr) {
        cr.addMapping("/**")
            .allowedOrigins("http://127.0.0.1:8081")  // 허용할 주소 및 포트
            .allowedOrigins("http://localhost:8081");  // 허용할 주소 및 포트
    }

}

 

ProjectApplication.java 와 동일하거나 그 하위에 위치한 폴더에 작성

728x90
반응형
728x90
반응형

Failure to transfer commons-cli:commons-cli:pom:1.0 from http://repo.maven.apache.org/maven2 was cached in the local repository, resolution will not be reattempted until the update interval of central has elapsed or updates are forced. Original error: Could not transfer artifact commons-cli:commons-cli:pom:1.0 from/to central (http://repo.maven.apache.org/maven2): Failed to create a selector. to http://repo.maven.apache.org/maven2/commons-cli/commons-cli/1.0/commons-cli-1.0.pom

 

1. .m2/repository 폴더를 삭제 후 ( default User/.m2 )

2. STS 프로젝트 우클릭 > Maven > Update Project... > Force update of Snapshots/Releases 

 

728x90
반응형

+ Recent posts