잡다한 프로그래밍 :: 잡다한 프로그래밍
반응형

이번 시간에는 JPA에서 엔티티의 기본 키(@Id) 를 어떻게 매핑하고 생성 전략을 어떻게 선택할지에 대해 알아보자.

기본 키 매핑이란?

JPA에서 @Entity 클래스는 반드시 식별자(PK) 를 가져야 하며, 이 식별자를 통해 엔티티를 구분하고 영속성 컨텍스트에서 관리한다.

기본적으로 다음과 같은 방식으로 식별자를 지정할 수 있다.

@Entity
public class Member {
    @Id
    private Long id; // 직접 지정 (직접 할당 방식) 거의 사용하지 않음
}

 

기본 키 자동 생성 전략 (@GeneratedValue)

IDENTITY DB에 위임 (MySQL 등)
SEQUENCE DB 시퀀스 오브젝트 사용 (Oracle, Postgres 등), @SequenceGenerator 필요
TABLE 키 생성 전용 테이블 사용 (모든 DB 지원, but 느림), @TableGenerator 필요
AUTO 방언(Dialect)에 따라 자동 선택 (기본값)
 

IDENTITY 전략 (DB 너가 알아서 해줘!)

 
@Entity
public class Member {
 @Id
 @GeneratedValue(strategy = GenerationType.IDENTITY)
 private Long id;

 

  • DB에 ID 생성 위임 (AUTO_INCREMENT)
  • 대표 DB: MySQL, MariaDB, SQL Server

특징:

  • em.persist() 즉시 INSERT SQL 실행됨 → PK값을 DB에서 받아오기 때문
    • DB에  ID값 생성을 위임 하기 때문에 최초에 ID값이 없어 영속성 컨텍스트에 먼저 저장할 수 없음
    • 따라서 INSERT 쿼리를 먼저 날린후 해당 ID값 으로 영속성 컨텍스트에 저장
    • 따라서 batch insert 지원하지 않음 (하지만 같은 트랜잭션 내에서 성능차이가 그렇게 크진않음..)

 

SEQUENCE 전략

@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의 시퀀스 오브젝트 사용 → 성능이 우수함
  • 시퀀스 제너레이터를 통해서 시퀀스를 만들어낼 수 있고, 테이블별로 만드는걸 권장함
  • 대표 DB: Oracle, PostgreSQL, DB2, H2

동작 방식:

  1. 먼저 select nextval('member_seq') 로 시퀀스 값을 조회
  2. 해당 값으로 INSERT 실행
  3. 같은 트랜잭션에서 insert 여러 번 하더라도 미리 받은 값에서 순차 사용 가능
  • allocationSize=50이 기본 → 메모리 미리 할당
  • 최초에 조회 후 같은 트랙잭션 내에서 insert 여러번 실행시 50개의 PK를 가지고 있으므로 bulk insert 가능

@SequenceGenerator 주요 속성 정리

속성명 설명 기본값 필수여부
name 식별자 생성기 이름 (@GeneratedValue(generator = "...")와 연결) 없음 O
sequenceName 매핑할 DB 시퀀스 객체 이름 hibernate_sequence X
initialValue 시퀀스 시작 값 (DDL 생성 시에만 사용) 1 X
allocationSize 한 번에 미리 증가시킬 수량 (성능 최적화용, JPA는 메모리 캐싱함) 50 X
  → DB 시퀀스가 1씩 증가하도록 설정되어 있다면 반드시 1로 맞춰야 함    
catalog 시퀀스가 속한 catalog 이름 DB 기본값 X
schema 시퀀스가 속한 schema 이름 DB 기본값 X

 

TABLE 전략

  • 키 생성용 테이블을 직접 만들어서 시퀀스처럼 사용하는 방식
  • 장점: 모든 DB에서 사용 가능
  • 단점: 성능이 매우 느림 → 실무에선 거의 사용 X

이런 방식은 레거시 DB 호환성 때문에 어쩔 수 없을 때만 고려

 

AUTO 전략 (기본값)

  • 방언(Dialect)에 따라 적절한 전략 자동 선택

예:

  • MySQL → IDENTITY
  • Oracle → SEQUENCE
  • H2 → SEQUENCE
 

권장하는 식별자 전략

 

  • PK는 절대 변하지 않는 대리키(대체키)를 Long 타입으로 사용
  • 자연키(주민번호, 이메일 등)는 PK로 부적합 (변경 가능성, 유출 위험 등)
  • 전략 선택:
    • Oracle, PostgreSQL 등 → SEQUENCE + allocationSize 1
    • MySQL → IDENTITY
    • 운영 DB 성능 최적화 필요 → 직접 관리 (e.g., UUID 전략, UUID+time 등)

 

반응형
반응형

지난 시간에는 엔티티와 테이블 매핑을 배웠다면,
이번에는 엔티티의 필드와 데이터베이스 컬럼을 어떻게 매핑하는지에 대해 정리합니다.

 

실습 요구사항

아래 조건에 맞는 회원(Member) 엔티티를 만들어 봅니다.

  1. 회원은 일반 회원과 관리자 두 가지 역할이 있다.
  2. 회원 가입일과 수정일을 저장해야 한다.
  3. 회원을 설명할 수 있는 필드가 있으며, 길이 제한은 없다.

회원 Entity

 
import javax.persistence.*;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Date;
@Entity
public class Member {
	@Id
	private Long id;
 
	@Column(name = "name") // 필드는 username 이지만, 컬럼명은 name일 때
	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
 	private String description;
}

 

생성되는 테이블 예시

create table Member {
	id bigint not null,
    age integer,
    createdDate timestamp,
    description clob,
    lastModifiedDate timestamp,
    roleType varchar(255),
    name varchar(255)
    primary key (id)
}

 

매핑 어노테이션 정리

1. @Column

  • 기본적으로 필드명 = 컬럼명 이지만, @Column(name="...")으로 커스텀 가능
  • 주요 속성:
속성
name 컬럼명 지정
nullable false면 NOT NULL 제약조건 생성 (default true)
unique 간단한 유니크 제약조건 부여 (운영에선 비권장, 유니크의 이름이 랜덤값으로 생성되어 알아보기 불편함, 멀티 컬럼 선택 불가 등)
insertable/updatable INSERT, UPDATE 가능 여부
columnDefinition 직접 DDL 정의 (예: "varchar(100) default 'EMPTY'")
length 문자열 길이 제한 (기본 255)
precision, scale BigDecimal 등 정밀 수치 지정 (precision=19, scale=2)

2. @Enumerated

  • Enum 타입 매핑 시 사용
  • 옵션:
    • EnumType.ORDINAL: enum 순서를 저장 (비추천)
    • EnumType.STRING: enum 이름을 저장 (필수 권장)

ORDINAL 사용 시 문제 예시

public enum RoleType {
    USER, ADMIN
}

이렇게 사용중일 때 DB에 USER = 0, ADMIN = 1로 저장

 

public enum RoleType {
    GUEST, USER, ADMIN
}

만약 이후에 GUEST라는 타입을 추가했다면

GUEST = 0, USER = 1, ADMIN = 2

기존 USER가 GUEST로 해석될 수 있음 ⇒ 데이터 망가짐

 

3. @Temporal (최신 자바 미사용)

  • java.util.Date, java.util.Calendar 타입 매핑 시 사용

4. @Lob

  • 큰 데이터(CLOB, BLOB)를 저장할 때 사용
  • 필드 타입에 따라 자동 결정:
    • String, char[] → CLOB
    • byte[] → BLOB

@Lob은 속성이 없음. 타입에 따라 자동으로 CLOB/BLOB 선택됨.

📌 그럼 CLOB vs TEXT 차이?

  • CLOB: 표준 ANSI SQL의 대용량 문자 타입
  • TEXT: DBMS에 따라 제공되는 비표준 타입 (MySQL 등)
  • JPA는 CLOB으로 매핑하며, 방언에 따라 TEXT 등으로 변환됨

5. @Transient

  • 해당 필드를 DB 컬럼으로 매핑하지 않음
  • 계산용, 로그 출력용 등 JPA에서 관리하지 않아야 할 필드에 사용
반응형

'프로그래밍 > JPA' 카테고리의 다른 글

[JPA] 단방향 연관관계  (0) 2025.06.24
[JPA] 기본키 매핑  (0) 2025.06.19
[JPA] 데이터베이스 스키마 자동 생성  (0) 2025.06.19
[JPA] 객체와 테이블 매핑  (0) 2025.06.19
[JPA] JPA의 영속성 관리2  (0) 2025.06.10
반응형

이전글에서 객체와 테이블의 매핑에 대해서 배웠는데,

이번에는 JPA가 애플리케이션을 실행할 때, 엔티티 클래스 정의를 기반으로 DB 테이블을 자동 생성하는 기능에 대해 정리한다.

 

쉽게말해, 엔티티를 작성하면 JPA가 알아서 테이블을 생성해준다!

 

객체 중심 개발 패러다임

과거에는 테이블 설계(DDL) → 객체 매핑 순이었다면,
JPA는 반대로 객체 정의(Entity) → 테이블 생성(DDL) 흐름을 지원합니다.

즉, "객체 중심 설계"로 전환하면서, JPA가 엔티티 정의를 읽고 적절한 테이블 스키마를 생성해줍니다.

 

DB 방언(Dialect)에 따라 DDL 자동 생성

JPA는 사용하는 DBMS에 맞는 방언(Dialect) 을 기반으로
적절한 SQL DDL을 자동 생성합니다.

예:

  • Oracle: varchar2
  • MySQL: varchar

 

⚙️ DDL 생성 전략 설정 (hibernate.hbm2ddl.auto)

DDL 자동 생성 기능의 핵심 설정은 다음 속성으로 제어합니다.

spring.jpa.hibernate.ddl-auto=create
설정 값 설명
create 기존 테이블 삭제 후, 매번 새로 생성 (애플리케이션 시작 시 DROP → CREATE)
create-drop create와 동일하지만, 애플리케이션 종료 시 테이블을 DROP
update 변경된 부분만 반영 (기존 데이터 보존) → 주의: 운영에서 사용 금지
validate 테이블과 엔티티 매핑이 일치하는지 검사만 수행 (DDL 실행 ❌)
none 아무 작업도 수행하지 않음 (기본값 또는 운영에서 권장)

 

언제 어떤 옵션을 써야 할까?

개발 환경 create, update 빠른 테스트 및 개발을 위해 테이블 자동 생성/수정
테스트 서버 update, validate 데이터 보존하면서 매핑 검증
운영 환경 validate, none DDL 실행 ❌, 수동 스키마 관리 필수


참고: DDL 자동 생성의 역할

  • @Table, @Column(length = 100), @UniqueConstraint, @Index 등
    엔티티에 설정한 메타데이터를 기반으로 테이블/컬럼/제약조건 생성
  • JPA 실행 로직에는 영향을 주지 않음
    → DDL은 시작 시점에만 수행되고, 이후 쿼리 동작과는 무관
반응형

'프로그래밍 > JPA' 카테고리의 다른 글

[JPA] 기본키 매핑  (0) 2025.06.19
[JPA] 필드와 컬럼 매핑  (1) 2025.06.19
[JPA] 객체와 테이블 매핑  (0) 2025.06.19
[JPA] JPA의 영속성 관리2  (0) 2025.06.10
[JPA] JPA 의 영속성 관리  (0) 2025.06.10
반응형

지난 시간에는 JPA의 영속성 컨텍스트(Persistence Context)와 1차 캐시에 대해 살펴보았습니다.

 

이번 글에서는 실제 설계된 도메인 객체를 RDB 테이블과 매핑하는 방법,

즉 JPA 엔티티 매핑의 기본 개념과 어노테이션 사용법을 정리 하겠습니다.

 

1. JPA 엔티티(@Entity)란?

@Entity

  • JPA가 관리하는 영속성 대상 클래스임을 선언합니다.
  • 이 어노테이션이 붙은 클래스만이 JPA의 CRUD, 변경 감지, 지연 로딩 등의 기능을 활용할 수 있습니다.
import javax.persistence.Entity;
import javax.persistence.Id;

@Entity
public class User {
    @Id
    private Long id;
    private String name;
    // ... getter, setter, 기본생성자 등
}

 

엔티티 클래스 제약 사항

  1. 기본 생성자
    • 파라미터가 없는 public 또는 protected 생성자가 반드시 필요합니다. (프록시 객체를 만들기 위해)
  2. final 클래스, enum, interface, inner 클래스 사용 불가
    • 엔티티는 런타임에 프록시로 확장될 수 있어야 하므로 final 클래스나 enum/interface/내부 클래스(inner class)로 선언할 수 없습니다.
  3. final 필드 사용 불가
    • JPA는 필드 값을 변경 감지하기 위해 리플렉션을 사용하므로, final 필드는 사용할 수 없습니다.
더보기

JPA의 지연 로딩과 프록시 객체

  1. 지연 로딩(Lazy Loading) 개념
    • 엔티티 조회 시, 연관된 다른 엔티티를 즉시 함께 가져오지 않고
    • 실제로 해당 연관 데이터를 사용할 때(getter 호출 시) 한 번 더 DB 조회를 수행하는 방식
@Entity
public class Post {
    @Id
    @GeneratedValue
    private Long id;

    private String title;

    @ManyToOne(fetch = FetchType.LAZY) // ← 지연 로딩 설정
    private User author;               // ← 이 부분이 지연 로딩 대상
}

프록시 객체(Proxy Object)

  • 지연 로딩을 구현하기 위해 JPA가 생성하는 “가짜” 엔티티 객체
  • 실제 엔티티 클래스를 상속해 만들고, 초기에는 최소한의 자리만 차지하다가
  • 필요한 시점에 원본 데이터를 로딩

- final class가 안 되는 이유

  • 프록시 객체를 만들려면 엔티티 클래스를 상속해야 하는데,
  • final class는 상속이 금지되어 있어 프록시 생성이 불가
  • 결과적으로 지연 로딩 기능 자체를 사용할 수 없으므로,
  • JPA 엔티티에는 final class를 허용하지 않음

- final 필드가 안 되는 이유

  • JPA는 리플렉션과 기본 생성자 + setter 호출을 통해 프록시 객체의 내부 값을 채움
  • final 필드는 한번 초기화된 뒤 변경이 불가능하므로,
  • 리플렉션을 통한 값 설정이 실패하고 정상 동작할 수 없음
  • 따라서 엔티티의 속성(컬럼 매핑 필드)에도 final 키워드를 사용하지 말아야 함

 

※ JAVA record 클래스와 JPA

Java 14에 도입된 record는 불변 데이터 캐리어(Immutable Data Carrier) 로, 컴파일러가 자동으로 equals/hashCode/toString/모든 필드를 초기화하는 생성자 등을 생성해 줍니다. 일견 JPA 엔티티처럼 데이터 보관용 DTO로 유용해 보이지만, 다음 이유로 엔티티로 사용할 수 없습니다.

  1. 불변성(final 필드)
    record의 모든 구성 요소(component)는 암묵적으로 private final 필드로 선언됩니다.
    → JPA는 영속성 컨텍스트에서 변경 감지를 위해 필드 값을 바꿀 수 있어야 하지만, final 필드이므로 불가능합니다.
  2. 기본 생성자 부재
    record는 전체 필드를 인자로 받는 생성자만 제공하고, 파라미터 없는 생성자를 생성하지 않습니다.
    → JPA 사양에서는 “파라미터 없는 기본 생성자”를 요구합니다.
  3. 프록시(subclass) 생성 불가
    JPA 구현체(예: Hibernate)는 프록시 생성이나 바이트코드 조작을 위해 엔티티 클래스를 상속하거나 바이트코드를 재작성해야 합니다.
    record는 final로 간주되어 상속이 불가능합니다.

 

2. 테이블 매핑 어노테이션: @Table

@Entity만 선언하면 엔티티 이름과 같은 테이블에 매핑됩니다. 테이블명, 스키마, 카탈로그, 제약 조건 등을 세밀하게 지정하려면 @Table 을 함께 사용합니다.

@Entity
@Table(
    name = "users",            // 매핑할 테이블명
    schema = "public",         // 스키마
    uniqueConstraints = {
        @UniqueConstraint(columnNames = {"email"})  // 이메일 컬럼에 유니크 제약 걸기
    }
)
public class User {

 

옵션 종류

 

  • name: 매핑할 테이블명
  • schema / catalog: DB 스키마/카탈로그
  • uniqueConstraints: DDL 생성 시 추가할 복합 유니크 제약 조건

 

 

반응형

'프로그래밍 > JPA' 카테고리의 다른 글

[JPA] 필드와 컬럼 매핑  (1) 2025.06.19
[JPA] 데이터베이스 스키마 자동 생성  (0) 2025.06.19
[JPA] JPA의 영속성 관리2  (0) 2025.06.10
[JPA] JPA 의 영속성 관리  (0) 2025.06.10
[JPA] JPA 동작 방식  (2) 2025.06.08
반응형

0. 개요

지난 시간에는 JPA의 핵심 구조인 영속성 컨텍스트엔티티 생명주기에 대해 알아봤습니다.
이번에는 영속성 컨텍스트가 우리에게 어떤 실질적인 장점을 제공하는지 하나씩 살펴보겠습니다.

 

1. 1차 캐시 (First-Level Cache)

영속성 컨텍스트는 엔티티를 관리하기 위해 메모리 캐시(1차 캐시) 를 내부에 가지고 있습니다.
이 캐시는 같은 트랜잭션 범위 안에서 반복되는 조회를 줄여주는 역할을 합니다.

// 엔티티를 생성한 상태 (비영속)
Member member = new Member();
member.setId("member1");

// 영속 상태 (1차 캐시에 저장)
em.persist(memeber);

// 1차 캐시에서 조회 (DB에서 조회하지않음)
Member findM = em.find(Member.class, "member1");

 

  • persist() 시점에서 1차 캐시에 저장됨
  • 이후 find()를 하면 DB가 아니라 캐시에서 조회
  • 복잡한 도메인 로직에서 중복 조회를 줄여주는 데 유용

 

2.  엔티티 동일성 보장 (Identity Guarantee)

JPA는 같은 트랜잭션 범위 내에서 같은 엔티티는 동일한 인스턴스로 보장합니다.

Member m1 = em.find(Member.class, 1);
Member m2 = em.find(Member.class, 1);

System.out.println(a == b); // true 동일성 보장
  • 두 객체는 equals() 비교가 아닌 == 비교에서도 true
  • 즉, 같은 1차 캐시 객체를 반환
  • 이는 도메인 모델을 안전하게 다루고, ORM의 객체 그래프 관리에 있어 중복 문제를 방지해줌

3. 트랜잭션을 지원하는 쓰기 지연

JPA는 persist() 시점에 곧바로 DB에 INSERT 쿼리를 날리지 않습니다.
대신 내부의 쓰기 지연 저장소(write-behind queue) 에 SQL을 모아뒀다가,
commit() 시점에 한꺼번에 DB에 반영합니다.

transaction.begin(); // 트랜잭션 시작

em.persist(member1);
em.persist(member2); // 영속 상태 (쿼리 날리지 않음)

// 커밋하는 시점에 INSERT SQL을 보냄
trasaction.commit();

 

  • INSERT 쿼리를 모아서 한번에 처리
  • DB와의 통신 횟수를 줄여줌 → 성능 최적화
  • Hibernate는 이걸 Batch Insert로 활용할 수도 있음 (설정 필요)

 

4. 변경 감지 (Dirty Checking)

JPA는 영속성 컨텍스트 안의 엔티티가 변경되면
자동으로 UPDATE SQL을 생성해서 DB에 반영합니다.

trasaction.begin();

// 영속 엔티티 조회
Member m = em.find(Member.class, 1);

// 영속 엔티티 수정
m.setUsername("name2");

trasaction.commit();
  • 영속성 컨텍스트에 엔티티가 등록될 때 스냅샷(초기 상태 복사본) 을 저장
  • 트랜잭션 종료 전 현재 값과 스냅샷을 비교
  • 변경된 필드가 있다면 → UPDATE SQL 생성
  • 이 쿼리도 마찬가지로 쓰기 지연 저장소에 모았다가 커밋 시 DB 반영

이걸 Dirty Checking이라고 부릅니다.

 

 

5. 지연로딩

추후 작성 예정

반응형

'프로그래밍 > JPA' 카테고리의 다른 글

[JPA] 필드와 컬럼 매핑  (1) 2025.06.19
[JPA] 데이터베이스 스키마 자동 생성  (0) 2025.06.19
[JPA] 객체와 테이블 매핑  (0) 2025.06.19
[JPA] JPA 의 영속성 관리  (0) 2025.06.10
[JPA] JPA 동작 방식  (2) 2025.06.08
반응형

0. 개요

지난 시간에 아래와 같이 member 엔티티를 DB에 자동으로 insert 하는 내용을 학습했다.

그런데 JPA는 어떻게 이걸 자동으로 처리할까?
em.persist()가 곧바로 insert 쿼리를 DB로 쏘는 함수일까?

 

오늘은 JPA가 엔티티를 어떻게 관리하고 DB와 상호작용하는지의 핵심인
“영속성 컨텍스트(Persistence Context)”와 엔티티 생명주기를 정리한다.

    public static void main(String[] args) {

        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();
        tx.begin();

        try {
            Member member = new Member();
            member.setId(1L);
            member.setName("HelloA");
            em.persist(member);

            tx.commit();
        } catch (Exception e) {
            tx.rollback();
        } finally {
            em.close();
        }

        emf.close();
    }

 

 

1. JPA에서 가장 중요한 두가지

  • 객체와 관계형 데이터베이스의 매핑 (설계에 가까운 부분으로, 후에 설계 방법도 설명할 예정입니다)
    (→ 객체 모델을 어떻게 RDB 테이블에 맞게 설계·매핑할 것인가)
  • 영속성 컨텍스트(Persistence Context)
    (→ JPA 내부 동작 원리, 실제 DB와의 상호작용 및 엔티티 생명주기 관리의 핵심)

 

2. 영속성 컨텍스트란?

 

  • 한마디로 “엔티티를 영구 저장하는 논리적 환경(캐시)”
  • 실제 DB에 바로 저장되는 게 아니라,
    먼저 영속성 컨텍스트에 저장·관리된다.
  • EntityManager를 통해서만 영속성 컨텍스트를 간접적으로 다룬다.
중요:
em.persist(entity)를 호출하면
바로 DB에 insert가 되는 게 아니라,
일단 영속성 컨텍스트(1차 캐시)에 저장됨!

 

더보기

📌 Spring JPA에서 EntityManager는 왜 N:1 구조인가?

 

예를들어 아래처럼 사용자의 요청이 와서 한트랜잭션 서비스 내에서 2개의 repo를 호출하고 있다고 가정합니다.

이때 각 repo는 각각의 em을 사용하고 있다고 가정하구요.

 

이럴때 두개의 em은 같은 영속성 컨텍스트를 사용하게 됩니다.

왜일까요? 

 

MembrerRepoA

@Repository
@RequiredArgsConstructor
public class MemberRepositoryA {

    private final EntityManager em;

    public void saveA(String name) {
        System.out.println("RepoA EntityManager: " + em); // 프록시 주소 출력
        Member member = new Member();
        member.setName("A_" + name);
        em.persist(member);
    }
}

 

MemberRepoB

@Repository
@RequiredArgsConstructor
public class MemberRepositoryB {

    private final EntityManager em;

    public void saveB(String name) {
        System.out.println("RepoB EntityManager: " + em); // 프록시 주소 출력
        Member member = new Member();
        member.setName("B_" + name);
        em.persist(member);
    }
}

 

 

MemberService

@Service
@RequiredArgsConstructor
public class MemberService {

    private final MemberRepositoryA repoA;
    private final MemberRepositoryB repoB;

    @Transactional
    public void register(String name) {
        repoA.saveA(name);
        repoB.saveB(name);
    }
}

 

spring에서 em을 주입받아 사용했으니, 당연히 같은 객체 (entityManager) 아니야? 라고 생각할 수 있습니다.

실제로 둘을 비교하면 같은 객체로 나오구요

❗️하지만 진실은?

이건 프록시 객체입니다.
실제로는 내부에서 **현재 트랜잭션에 연결된 "진짜 EntityManager"**를 찾아서 위임하고 있는 구조 입니다.

그리고 이 "진짜 EntityManager"는 트랜잭션마다 다릅니다.

즉, 한 요청/트랜잭션 내에서는 같은 컨텍스트를 공유하지만,
다른 요청에서는 다른 컨텍스트가 사용됩니다.

 

🔍 그럼 왜 N:1?

여러 개의 클래스(RepoA, RepoB 등)에서 EntityManager를 주입받는다면 N개의 주입 지점이 생깁니다.
이 주입된 EntityManager들은 모두 같은 프록시 인스턴스를 공유하지만,

 

실제론 현재 트랜잭션에 맞는 하나의 영속성 컨텍스트로 위임되고 있습니다.

그래서 우리는 이를 N (여러 주입 지점) : 1 (현재 트랜잭션에 묶인 영속성 컨텍스트)로 부릅니다.

 

 

✅ 진짜 EntityManager를 싱글톤으로 주입하면 안 되는 근본 이유

EntityManager는 스레드에 안전하지 않다 (not thread-safe)
그래서 트랜잭션/스레드마다 분리된 객체가 필요하다
즉, 요청마다 다른 컨텍스트가 필요하기 때문에
싱글톤 EntityManager를 공유하면 큰 문제 발생

2. 엔티티 생명주기 4단계

1) 비영속(Transient)

  • 아직 JPA가 모르는 상태 (영속성 컨텍스트와 전혀 관계가 없는 상태)
  • 단순히 new로 객체를 만든 상태 (아직 em.persist() 호출 전)
 
            // 객체를 생성함 (비영속)
            Member member = new Member();
            member.setId(1L);
            member.setName("HelloA");

 

2) 영속(Managed)

  • 영속성 컨텍스트에서 관리되는 상태 (메모리내 1차 캐시 등록)
  • 아직 실제 DB insert는 아님 (commit 전까지는 DB에 적용 X)
  • 동일 트랜잭션 내에서 1차 캐시된 객체를 “동일성 비교(==)”로 찾을 수 있음 (추가로 설명하기...)
            Member member = new Member();
            member.setId(1L);
            member.setName("HelloA");
            
            // 영속(영속성 컨텍스트에서 관리를 함)
            em.persist(member);
            
            // 실제 DB insert 쿼리는 트랜잭션을 커밋하는 시점에 동작함
            tx.commit();

 

3) 준영속(Detached)

  • 영속 상태였다가 영속성 컨텍스트에서 분리된 엔티티
  • EntityManager가 더 이상 변경 추적, dirty checking, flush 등 관리를 하지 않음
em.detach(member); // 엔티티를 영속성 컨텍스트에서 분리

em.clear(); // 영속성 콘텍스트를 비움

em.close(); // 영속성 콘텍스트를 종료
그럼 detach로 분리한순간 영속성 컨텍스트에서 바로 삭제될까?

정답 : 삭제한다

 

❗️ 비영속과 준영속의 미묘한 차이

  • 비영속: 처음부터 영속성 컨텍스트에 등록된 적이 없는 상태(new로 생성만 한 객체)
  • 준영속: 한때 관리받다가 detach/clear/close로 “분리된” 상태

 

4) 삭제(Removed)

  • 삭제 예약 상태
  • em.remove(entity) 호출
    → 영속성 컨텍스트에선 제거됨
    → flush/commit 시 실제 DB에서 DELETE 쿼리 실행
em.remove(member);

 

반응형

'프로그래밍 > JPA' 카테고리의 다른 글

[JPA] 필드와 컬럼 매핑  (1) 2025.06.19
[JPA] 데이터베이스 스키마 자동 생성  (0) 2025.06.19
[JPA] 객체와 테이블 매핑  (0) 2025.06.19
[JPA] JPA의 영속성 관리2  (0) 2025.06.10
[JPA] JPA 동작 방식  (2) 2025.06.08
반응형

1. 캐시 스탬피드 현상이란?

캐시 만료 시점에 대량의 요청이 한꺼번에 DB 또는 외부 API 등 원본 소스로 몰리는 현상입니다.

“동시에 캐시가 비어, 수많은 트래픽이 원본 시스템을 두드려
서비스 전체의 가용성과 응답속도를 위협하는 상황”

 

일반적인 캐시 동작 플로우

 

1. 클라이언트가 데이터를 요청

2. 서버는 캐시에서 해당 데이터를 조회

3. 캐시에 존재하면 그 값을 바로 반환

4. 캐시에 없으면 (Miss) → DB나 외부 API 등 원본 시스템에서 조회 후 캐시에 저장, 응답

 

애플리케이션 동작 흐름

 

🚨문제가 생기는 경우: 캐시 만료 시점

예를 들어 캐시가 만료된 시점에 동시에 N건의 요청이 들어오면 어떻게 될까?

  • 캐시 데이터가 만료되었고, 이 때 100만명의 사용자가 요청을 진행함
  • 100만건 요청 모두 DB or API 서버로 데이터를 요청
  • 특별한 조치가 없을 경우, 백엔드 서버는 100만번 캐시에 기록 후 사용자에게 데이터를 전달

이런 경우 백엔드, 캐시 모두 부하(장애, 응답 지연, 전체 다운 위험)를 일으키는 원인이 되며

이러한 현상을 캐시 스탬피드라고 합니다.

2. 해결 방법

2-1. TTL (Time To Live) 증가

캐시 만료 주기를 늘리면, 실제 리소스에 접근하는 횟수를 줄이고 위험도 줄어듭니다. (가장 간단한 방법)

 

단점:

  • 실시간성이 중요한 경우 사용자 경험이 나빠질 수 있음
  • 예) 어떤 상품이 오전 10시부터 판매일때, 10시 2분에도 아직 오래된 캐시로 사용자에게 판매 대기중으로 보일 수 있음

 

2-2. Lock을 통한 동시성 제어

하나의 요청에 Lock을 걸어 동시에 캐시를 쓰지 않도록 하여 단한번의 캐시 쓰기 작업만 이루어지도록 한다

더보기

AOP 구현

@Aspect
@Component
@RequiredArgsConstructor
@Slf4j
public class DistributedLockAop {

    private static final String LOCK_PREFIX = "LOCK:";

    private final RedissonClient redissonClient;

    @Around("@annotation(distributedLock)")
    public Object lock(ProceedingJoinPoint joinPoint, DistributedLock distributedLock) throws Throwable {
        String key = LOCK_PREFIX + parseKey(joinPoint, distributedLock.key());
        RLock lock = redissonClient.getLock(key);

        boolean locked = false;
        try {
            locked = lock.tryLock(
                distributedLock.waitTime(),
                distributedLock.leaseTime(),
                distributedLock.timeUnit()
            );
            if (!locked) {
                // 락 획득 실패시 예외 또는 커스텀 반환
                log.warn("분산락 획득 실패: {}", key);
                throw new IllegalStateException("Lock acquisition failed: " + key);
            }
            // 비즈니스 로직 실행
            return joinPoint.proceed();
        } finally {
            if (locked && lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }

    private String parseKey(ProceedingJoinPoint joinPoint, String keyExpression) {
        // 파라미터를 SpEL로 파싱하는 유틸 (아래 참고)
        return CustomSpringELParser.getDynamicValue(
            ((MethodSignature) joinPoint.getSignature()).getParameterNames(),
            joinPoint.getArgs(),
            keyExpression
        ).toString();
    }
}

 

 

캐시 조회 / 갱신 서비스 구현

@Service
@RequiredArgsConstructor
public class ProductService {

    private final RedisTemplate<String, Object> redisTemplate;
    private final ProductRepository productRepository;

    @DistributedLock(key = "'product:' + #productId")
    public Product getProductWithCache(Long productId) {
        String cacheKey = "product:" + productId;
        Product product = (Product) redisTemplate.opsForValue().get(cacheKey);

        if (product == null) {
            // 캐시 미스 → 락 잡은 1명만 이 코드에 진입
            product = productRepository.findById(productId)
                    .orElseThrow(() -> new RuntimeException("상품 없음"));
            redisTemplate.opsForValue().set(cacheKey, product, 5, TimeUnit.MINUTES);
        }
        return product;
    }
}

 

단점

  • 결국 Lock걸린 작업이 끝날때까지 대기해야한다.

 

2-3. 캐시 웜업 (캐시 사전로딩)

별도의 배치(30초, 1분 등)를 통해서 많은 트래픽이 발생할 수 있는 캐시를 캐시가 만료되기 전에 미리 갱신해주는 방법

만료되기전 미리 갱신하므로 캐시에 있는 데이터만으로 조회하고, 트래픽 부하에 대비하고 응답지연현상도 방지할 수 있음

 

문제점

- 캐시로 넣어야하는 데이터를 미리 예측하지 못할경우 누락 가능

- 대량 데이터 갱신시, 부하 가능성 존재

🚩 카카오 프로모션 캐시 운영의 실제 이슈

카카오에서는 상품 프로모션 정보를 약 22,000개 운영 중입니다.
만약 이 모든 데이터를 캐시 웜업(warm-up) 방식으로 미리 메모리에 올리면

  • 최악의 경우 30GB라는 엄청난 캐시 공간이 소모되고
  • 전체 웜업에도 오랜 시간이 걸립니다.

 

2-4. PER (확률적 조기 갱신) 방식

PER 동작 원리

  • 캐시의 실제 TTL보다 조금 더 일찍, 일정 확률로 갱신
  • 예를 들어 TTL 10분짜리 캐시라면 9분부터 10분 사이에 들어오는 요청들은 확률적으로 캐시를 갱신함
  • 이때 "얼마나 확률적으로 조기갱신 할 것인가"를 수학적 "PER 공식"으로 계산

 

  • currentTime: 현재시간 (currentTimeMillis)
  • timeToCompute(Recompute time interval, delta): 캐시된 값을 다시 계산하는데 걸리는 시간
  • beta: 기본적으로 1. 0보다 큰 값 설정 가능. 갱신이 자주 일어나길 바랄 경우 beta를 수정하여 확률을 높일 수 있다.
  • rand(): 0과 1 사이의 랜덤 값을 반환하는 함수. log(rand())¹의 값은 0부터 마이너스 무한대까지의 범위를 가짐.
  • expiry (that is, time() + pttl)
  •  

 

@RestController
@RequiredArgsConstructor
public class ProductController {
    private final ProductService productService;

    @GetMapping("/products/{productId}")
    public ResponseEntity<ProductDto> getProduct(@PathVariable Long productId) {
        ProductDto product = productService.getProductWithPERCache(productId);
        return ResponseEntity.ok(product);
    }
}

 

 

@Service
@RequiredArgsConstructor
public class ProductService {
    private final CacheManager cacheManager;
    private final ProductRepository productRepository;

    // PER 파라미터
    private final long TIME_TO_COMPUTE_MS = 100; // DB조회 예상시간 (ms)
    private final double BETA = 1.0;

    public ProductDto getProductWithPERCache(Long productId) {
        Cache cache = cacheManager.getCache("productCache");
        ProductCacheValue cached = cache.get(productId, ProductCacheValue.class);

        long now = System.currentTimeMillis();

        if (cached != null) {
            // PER 공식 적용
            long expiry = cached.getExpiry(); // 캐시 만료 시각
            double rand = Math.random(); // 0~1
            double lhs = now + TIME_TO_COMPUTE_MS * BETA * Math.log(rand);

            // 캐시 만료 전이라도, 확률적으로 갱신
            if (lhs < expiry) {
                return cached.getProduct();
            }
            // 아니면 DB에서 새로 조회
        }

        // DB 조회 및 캐시 갱신
        ProductDto product = productRepository.findById(productId)
                .orElseThrow(() -> new EntityNotFoundException("Not Found"));

        // 캐시 저장 (예: TTL 5분)
        long newExpiry = now + 5 * 60 * 1000;
        cache.put(productId, new ProductCacheValue(product, newExpiry));

        return product;
    }

}

 

  • 캐시에 데이터가 있으면
    PER 수식 계산
    now + timeToCompute * beta * log(rand()) < expiry
    만족하면 캐시 리턴, 아니면 새로 갱신
  • 캐시에 없으면 무조건 DB 조회 후 캐시 저장
  • TTL(만료시각)은 value에 함께 기록, 실전에서는 Redis 등과 조합하면 더 견고
  • 스탬피드 현상 최소화
  • Hot data 예측/분산 효과
  • “적은 비용으로 큰 효과” (특히 대형 트래픽 서비스에서)

단점/주의

  • 확률식과 TTL/요청도에 따라 실제 조기갱신이 과도하거나 부족할 수 있음
  • 퍼포먼스 튜닝 필요

....

 

[실제 테스트 결과 분석] 미반영

1. 응답시간

 

CPU

 

Cache Hit

 

Cache Miss

 

CacheHit Rate

 

반응형
반응형

 

1. JPA란

**JPA(Java Persistence API)**는 자바에서 관계형 데이터베이스를 객체지향적으로 다룰 수 있도록 도와주는 ORM(Object-Relational Mapping) 표준 명세입니다.

즉, 복잡한 SQL 없이도 자바 객체와 데이터베이스 테이블 간의 매핑과 쿼리 작성을 가능하게 해줍니다.

Hibernate, EclipseLink, OpenJPA 등은 JPA의 구현체입니다.
Spring Data JPA는 이 위에 얹혀진 추상화 프레임워크입니다.

 

2. JPA 동작 방식 (PURE JAVA 환경)

1. Persistence가 persistence.xml을 읽고 EntityManagerFactory를 생성

  • EntityManagerFactory는 애플리케이션 전체에서 하나만 생성해야함

2. EntityManagerFactory로부터 EntityManager를 생성

  • EntityManager는 데이터베이스와의 실제 작업을 담당
  • 한 트랜잭션당 하나씩 생성해서 사용 후 반드시 close
  • 스레드간 공유 X

3. EntityTransaction을 통해 트랜잭션 시작 및 종료

[코드 예시]

persistence.xml

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.2" xmlns="http://xmlns.jcp.org/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd">
    <persistence-unit name="hello">
        <properties>
            <!-- 필수 속성 -->
            <property name="jakarta.persistence.jdbc.driver" value="org.h2.Driver"/>
            <property name="jakarta.persistence.jdbc.user" value="user"/>
            <property name="jakarta.persistence.jdbc.password" value="password"/>
            <property name="jakarta.persistence.jdbc.url" value="jdbc:h2:tcp://localhost/~/test"/>
            <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/>

            <!-- 옵션 -->
            <property name="hibernate.show_sql" value="true"/>
            <property name="hibernate.format_sql" value="true"/>
            <property name="hibernate.use_sql_comments"  value="true"/>
<!--            <property name="hibernate.hbm2ddl.auto" value="create" />-->
        </properties>
    </persistence-unit>

</persistence>

 

memeber 엔티티

package hellojpa;

import jakarta.persistence.Entity;
import jakarta.persistence.Id;

@Entity
public class Member {

    @Id
    private Long id;
    private String name;

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

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

 

main.java

    public static void main(String[] args) {

        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();
        tx.begin();

        try {
            Member member = new Member();
            member.setId(1L);
            member.setName("HelloA");
            em.persist(member);

            tx.commit();
        } catch (Exception e) {
            tx.rollback();
        } finally {
            em.close();
        }

        emf.close();
    }

 

[Spring boot 환경 예시]

- persitence.xml에 설정한 내용을 yaml파일로 쉽게 대체 가능하다.

- trasaction 작업을 어노테이션으로 대체 가능하다.

 

application.yaml

spring:
  datasource:
    url: jdbc:h2:tcp://localhost/~/test
    driver-class-name: org.h2.Driver
    username: sa
    password: 
  
  # 기존xml을 yaml 설정으로 대체가능
  jpa:
    hibernate:
      ddl-auto: update 
    show-sql: true
    properties:
      hibernate:
        format_sql: true

 

 

service

@Service
public class MemberService {
    @PersistenceContext
    private EntityManager em;

    @Transactional
    public void createMember() {
        Member member = new Member();
        member.setId(1L);
        member.setName("SpringBootMember");
        em.persist(member);
    }
}
반응형

'프로그래밍 > JPA' 카테고리의 다른 글

[JPA] 필드와 컬럼 매핑  (1) 2025.06.19
[JPA] 데이터베이스 스키마 자동 생성  (0) 2025.06.19
[JPA] 객체와 테이블 매핑  (0) 2025.06.19
[JPA] JPA의 영속성 관리2  (0) 2025.06.10
[JPA] JPA 의 영속성 관리  (0) 2025.06.10

+ Recent posts