1. JPA 엔티티 (Entity)
1) 엔티티(Entity) 클래스란?
- 정의: 데이터베이스의 테이블과 1:1로 매핑되는 자바 객체 클래스.
- 주요 어노테이션:
- @Entity: JPA가 관리할 클래스임을 선언(명시)한다.
- ⚠️ 주의점: 이 어노테이션이 없으면 EntityManager가 해당 클래스를 인식하지 못해 영속성 컨텍스트에 담을 수 없다. 기본 생성자(접근 제어자가 public 또는 protected인 인자 없는 생성자)가 반드시 함께 있어야 함.
- @Id: DB 테이블의 Primary Key(PK)와 매핑할 필드를 지정한다.
- ⚠️ 주의점: 데이터베이스의 모든 테이블은 식별자(PK)가 필요하듯, JPA 엔티티도 영속성 컨텍스트(1차 캐시)에서 객체를 구분할 고유 ID가 필수. @Id가 지정된 필드가 없으면 프로젝트 빌드 시점에 에러가 발생한다.
- @Table: 매핑할 DB 테이블 이름을 지정한다 (생략 시 클래스 이름 사용).
- @Column: 컬럼명 변경이나 제약조건 추가 시 사용한다. (생략 시 필드 이름을 그대로 컬럼명으로 사용하며, 특정 제약 조건이나 컬럼명 변경이 필요할 때만 명시한다)
- @Entity: JPA가 관리할 클래스임을 선언(명시)한다.
더보기
💡 @Column 추가 설명
- 기본 원칙: 자바 필드명과 DB 컬럼명이 같고 별다른 제약 조건이 없다면 과감하게 생략
- JPA에서는 지정해주지 않아도 기본적으로 @Column이 생략된 것으로 간주하고 데이터베이스 테이블의 컬럼과 자동으로 매핑해 준다.
- 생략 시 기본값(Default) 적용: JPA는 자바 필드 명과 타입을 기반으로 다음과 같은 기본값을 적용한다.
- 컬럼 이름: 자바 필드 이름을 그대로 사용한다.
- 예시: private String name; ➡️ DB의 NAME 컬럼과 매핑
- 데이터 타입 및 길이: 자바 타입을 기반으로 표준 DB 타입(예: VARCHAR(255))으로 자동 매핑된다.
- null 허용 여부: 기본적으로 NULL을 허용(nullable = true)한다.
- 컬럼 이름: 자바 필드 이름을 그대로 사용한다.
- 기본 규칙 외에 특별한 제약 조건이나 매핑 설정이 필요할 때만 명시적으로 붙여주면 된다. 주로 다음과 같은 상황:
- ① DB 컬럼명과 자바 필드명이 다를 때
- ② 필수 값(Not Null) 제약 조건을 넣고 싶을 때 (JPA가 DDL을 자동 생성할 때 NOT NULL이 붙는다.)
2) 상속 및 슈퍼클래스 필드 주의점
- 자바의 모든 클래스는 기본적으로 Object 클래스를 상속받지만, 상속받은 일반 속성은 DB 테이블의 컬럼으로 매핑되지 않는다.
- 부모 클래스의 필드를 공통 컬럼으로 매핑하려면 @MappedSuperclass 처리가 필요하다.
2. JPA Entity와 영속성 컨텍스트 (Persistence Context)
1) 개념
- JPA Entity란?: DB 테이블과 1:1로 매핑되는 자바 객체 클래스.
- 영속성 컨텍스트란?:
- "엔티티를 영구 저장하는 환경"을 의미하는 논리적인 영역.
- EntityManager를 통해 엔티티를 저장하거나 조회하면, JPA는 이 영속성 컨텍스트 안에 엔티티 인스턴스들을 보관하고 상태를 관리한다.
- 상속 및 필드 매핑 주의점:
- 자바의 모든 클래스는 암시적으로 Object 클래스를 상속받지만, 상속받은 속성(필드/메서드)은 DB 테이블의 컬럼으로 매핑되지 않는다.
- 슈퍼클래스의 필드를 테이블 컬럼으로 매핑하려면 @MappedSuperclass 같은 특정 어노테이션 처리가 필요하다.
- 낙관적 잠금 (Optimistic Lock):
- 동시성 제어를 위한 JPA의 메커니즘 중 하나.
더보기
ℹ️ 엔티티 비교 (JDBC vs JPA)
1) 기존 엔티티(Entity)의 의미 (JDBC 시절 포함)
- 데이터베이스 설계(DB 모델링)에서 엔티티는 단순히 데이터베이스 테이블 그 자체 또는 테이블에 저장되는 독립된 데이터 단위를 뜻하는 개념적인 용어이다.
- JDBC에서의 사용:
- 엔티티를 자바 객체로 표현: 개발자들은 DB 테이블(엔티티)의 구조를 본떠서 자바 클래스를 만듦. 보통 DTO(Data Transfer Object) 또는 VO(Value Object)라고 부른다.
- 한계:
- JDBC는 자바 객체와 DB 테이블을 자동으로 연결해 주지 못함.
- 이때의 객체는 단순히 DB 데이터를 임시로 담아두는 역할에 불과하며, JDBC 입장에서는 이게 엔티티인지 일반 클래스인지 알 방법이 없었음.
- 개발자가 일일이 SQL 문을 짜서 데이터를 집어넣어 주어야 했다.
2) JPA에서의 '엔티티(Entity)'
- JPA에서 말하는 엔티티는 단순한 개념이나 데이터를 담는 그릇 역할을 넘어, "JPA라는 프레임워크가 생명주기를 직접 관리하고 DB 테이블과 완벽하게 동기화하는 특별한 객체"를 의미한다.
- 기술적인 JPA 엔티티는 영속성 컨텍스트라는 메모리 공간에서 적극적인 관리를 받는 특별한 자바 객체를 뜻함.
3) 📊 JDBC 객체 vs JPA 엔티티 비교
- 가장 큰 차이점은 영속성 컨텍스트(Persistence Context)의 관리 여부. JPA는 '영속성 컨텍스트'라는 중간 계층을 통해 1차 캐시, 쓰기 지연, 변경 감지를 자동으로 처리해 준다.
| JDBC 시절의 객체 (DTO/VO) | JPA의 엔티티 (@Entity) | |
| 정의 | 개발자가 수동으로 DB 구조를 흉내 낸 클래스 | JPA가 인식하고 DB 테이블과 1:1 매핑한 클래스 |
| 상태 관리 | 없음 (데이터를 담고 있는 일반 자바 객체) | 영속성 컨텍스트가 상태를 추적함 (비영속, 영속 등) |
| 수정 방식 | 값을 바꾸고 반드시 수동으로 UPDATE SQL 실행 | 객체의 값만 바꾸면 변경 감지(Dirty Checking)로 자동 수정 |
| ID 식별 | 자바 입장에서는 일반 변수 중 하나일 뿐 | @Id를 통해 영속성 컨텍스트 내의 고유 식별자로 쓰임 |
2) 엔티티의 생명주기 (Entity Lifecycle)
- EntityManager가 영속성 컨텍스트를 관리하며 엔티티의 생명주기(상태 변화)를 다룬다.
- 비영속 (Transient): 객체를 생성만 한 상태로, 영속성 컨텍스트와 전혀 관련이 없는 상태. (영속성 컨텍스트가 관리하지 않음)
- 영속 (Managed): em.persist(entity) 또는 em.find()를 통해 영속성 컨텍스트에 저장되어 JPA의 관리를 받는 상태.
- 준영속 (Detached): 영속성 컨텍스트에 저장되어 있다가 분리된(em.detach(), em.clear()) 상태.
- 삭제 (Removed): DB 및 영속성 컨텍스트에서 엔티티를 삭제하기로 예약한 상태.
[ 비영속 (Transient) ]
│ em.persist()
▼
[ 영속 (Managed) ] ─── em.detach() / em.clear() ───► [ 준영속 (Detached) ]
│
│ em.remove()
▼
[ 삭제 (Removed) ]
3. 영속성 컨텍스트의 핵심 기능 (내부 동작 원리/이점)
개발 생산성과 객체지향적 설계를 가능하게 해주는 내부 기능들.
① 1차 캐시 (First Level Cache / Entity Map)
- 개념: 영속성 컨텍스트 내부에 존재하는, 영속 상태의 엔티티 인스턴스를 보관하는 메모리 저장소.
- 조회 동작 방식 (예시):
- 조회 요청(em.find() 호출) 시 DB보다 먼저 1차 캐시를 확인한다.
- 1차 캐시에 해당 엔티티가 존재하면: DB 조회 없이 4번 동작으로. (메모리에서 바로 반환)
- 1차 캐시에 해당 엔티티가 없으면(조회 ➡️ 객체 생성 ➡️ 저장):
- 행 데이터 조회: JDBC API를 통해 DB에서 데이터를 조회한다.
- 엔티티 객체 생성(변환): DB에서 가져온 로우(Row) 데이터를 바탕으로 자바 엔티티 객체를 만든다
- 1차 캐시 저장 & 복사본(스냅샷) 생성: 이 객체를 영속성 컨텍스트의 1차 캐시에 집어넣으면서(*리플렉션 활용), 그 집어넣는 순간의 최초 상태를 그대로 복사해서 별도의 공간에 보관한다. 이 복사본을 스냅샷이라고 한다.
- 엔티티 반환: 모든 준비가 끝난 엔티티 객체를 호출한 비즈니스 로직(애플리케이션)으로 반환한다.
- 개발자는 반환받은 객체를 활용해 데이터를 조회하거나 값을 수정(변경 감지)하는 비즈니스 로직을 수행하게 됨.
- .em.clear()를 호출하면 1차 캐시가 모두 초기화되어 이후 조회 시 다시 DB SELECT 쿼리가 나간다.
더보기
💡 JDBC vs 영속성 컨텍스트 데이터의 형태
JDBC가 데이터베이스에서 꺼내오는 데이터와 영속성 컨텍스트가 받아서 저장하는 데이터의 형태가 다르다. 아래는 과정별로 나누어 보면 비교 & 설명한다.
엔티티를 통한 비교는 위의 [ℹ️ 엔티티 비교 (JDBC vs JPA)] 에서 설명하고 있다.
1. JDBC API가 조회하는 데이터
데이터베이스 테이블의 로우(Row) 데이터 (가공되지 않은 데이터)
- JDBC는 엔티티라는 개념을 모른다.
- 개발자가 SELECT * FROM POST라는 SQL을 날리면, JDBC는 그저 데이터베이스 테이블에 있는 가공되지 않은 순수한 레코드(텍스트, 숫자 등의 데이터 묶음)를 ResultSet 형태로 조회해 올 뿐.
2. '엔티티'가 되는 시점
- 조회한 로우 데이터를 자바 객체로 변환(매핑)한 후
- JPA는 JDBC가 던져준 가공되지 않은 데이터를 받아서, 리플렉션(Reflection) 기술을 사용해 개발자가 정의한 엔티티 클래스(예: Post)의 인스턴스를 새로 생성하고 값을 채워 넣는다.
- 이렇게 자바 객체로 변환 완료된 것이 바로 엔티티(Entity).
3. 1차 캐시에 저장되는 형태 (엔티티 객체, 스냅샷)
- 이 변환된 엔티티를 1차 캐시에 저장할 때, 스냅샷과 엔티티가 동시에 만들어져 각각 보관된다. (별개의 메모리 영역)
- 내부 구조(1차 캐시): Map 구조 (Key: @Id로 지정된 PK 값, Value: 엔티티 클래스 인스턴스 참조값).
- 저장 형태:
- Key: 엔티티의 식별자 PK (예: Post(1L)).
- Value: 실제 자바 엔티티 객체의 메모리 주소(참조값).
- 엔티티 객체: 실제로 비즈니스 로직에서 쓰고 값을 수정할 진짜 객체. (현재 객체)
- 스냅샷 (Snapshot): 엔티티가 최초로 1차 캐시에 저장되는 순간(조회/등록 시점)의 고정된 초기 상태를 별도의 공간에 그대로 복사하여 보관.
- 개발자가 자바 객체 값을 아무리 수정해도 스냅샷의 초기값은 절대 변하지 않는다.
- 나중에 변경 감지할 때 비교 기준점으로 쓰임 + 읽기 전용
더보기
✨ 리플렉션(Reflection) 활용이란?
1) JPA는 왜 리플렉션을 사용할까?
- JPA(Hibernate) 프레임워크 입장에서 전 세계 수많은 개발자가 어떤 엔티티 클래스(예: Post, Member, Product 등)를 만들지 미리 알 수 없음.
- 하지만 DB에서 데이터를 조회해 오면, 어떻게든 개발자가 만든 그 클래스의 객체를 생성해서 값을 채워주어야 한다. 이때 컴파일 시점이 아닌, 프로그램이 실행되는 도중에(런타임) 동적으로 객체를 조립하기 위해 사용하는 기술이 바로 리플렉션
- 구체적인 클래스 타입을 알지 못해도, 런타임에 클래스의 메서드, 타입, 변수들에 접근할 수 있도록 해준다.
- 요약: DB에서 가져온 진짜 데이터를 가지고 진짜 객체를 생성/조립할 때 쓰는 필수 기술 (1차 캐시 저장 시점)
2) 1차 캐시 저장 시점의 리플렉션 동작 방식 (em.find()로 DB를 거쳐올 때)
- DB에서 로우 데이터 조회: em.find() 등을 통해 DB에서 실제 행(Row) 데이터를 한 줄 읽어온다
- 리플렉션을 통한 객체 생성:
- JPA는 리플렉션 기술을 사용해 해당 엔티티의 기본 생성자를 강제로 호출하여 빈 인스턴스를 만듦.
- 💡 깨알 지식: JPA 엔티티에 인자 없는 기본 생성자가 필수인 이유가 바로 리플렉션이 이 기본 생성자를 통해 객체를 생성하기 때문. (빈 인스턴스를 만들고 나서 값을 채움)
- 필드 값 주입 및 1차 캐시 저장:
- 생성된 빈 객체의 필드(변수)에 DB에서 가져온 진짜 데이터들을 직접 집어넣어 진짜 엔티티 객체를 완성한다.
- 완성된 진짜 객체와 최초 스냅샷을 영속성 컨텍스트 내의 1차 캐시에 저장하고 반환.
3) 💡 연관 개념: 프록시(Proxy)와 지연 로딩
- 개념: 데이터베이스 조회를 당장 하지 않고 뒤로 미루기 위해, 진짜 객체 대신 임시로 세워두는 가짜 대리자(껍데기) 객체
- 등장 시점: 1차 캐시에 진짜 객체를 채워 넣는 시점이 아니라, 연관된 데이터를 당장 조회하지 않고 미루는 지연 로딩(Lazy Loading) 시점에 등장한다.
- 1차 캐시에 넣을 진짜 객체를 아직 만들고 싶지 않을 때(DB 조회를 미루고 싶을 때) 등장
- 프록시 동작 방식 (예시): Post(게시글) 엔티티를 조회할 때 작성자 정보인 User 엔티티가 연관되어 있다고 가정. 게시글 본문만 읽으면 되는데 작성자 정보까지 DB에서 매번 가져오면 무겁다.
- 상황 및 지연 로딩 설정: 당장 화면에는 게시글 제목만 필요하고 작성자 정보는 필요하지 않은 상황.
- JPA의 행동 (가짜 객체 삽입): JPA는 User 자리에 진짜 객체를 리플렉션으로 만들지 않고, 껍데기만 똑같이 생긴 가짜 프록시 객체를 생성해서 꽂아둔 채로 Post를 반환한다. (DB 조회를 미룸)
- 실제 데이터가 필요한 시점 (프록시 초기화): 이후 비즈니스 로직 코드에서 user.getName()처럼 진짜 작성자 데이터가 필요한 순간이 오면, JPA가 비로소 DB를 조회하고 리플렉션을 가동해 진짜 데이터를 프록시 객체 내부에 채워 넣는다.
② 영속 엔티티의 동일성(Identity) 보장
- 같은 트랜잭션 내에서 동일한 @Id로 조회한 엔티티는 Java의 == 비교 시 true를 반환한다.
Post a = em.find(Member.class, "post1");
Post b = em.find(Member.class, "post1");
System.out.println(a == b); // true (동일성 보장)
③ 쓰기 지연 버퍼 (Transactional Write-Behind Queue / ActionQueue)
- 개념: em.persist()를 호출하는 즉시 DB에 SQL을 보내지 않고, 영속성 컨텍스트 내부의 쓰기 지연 SQL 저장소(ActionQueue)에 쿼리를 먼저 모아두는 메커니즘 (트랜잭션을 지원한다)
- JPA나 Hibernate 같은 ORM 기술에서 아주 중요한 역할을 한다.
- 실행 시점: 트랜잭션을 커밋(tx.commit())하거나 플러시(em.flush())하는 시점에 모아둔 SQL이 데이터베이스로 한꺼번에 일괄(Batch) 전달된다.
- em.persist()를 호출할 때(데이터의 변경 사항(등록, 수정, 삭제)을 발견할 때마다) SQL을 즉시 DB에 보내지 않고 SQL 저장소(ActionQueue)에 쌓아둔다.
- 트랜잭션을 commit() 하거나 em.flush() 하는 시점에 모아둔 SQL이 DB로 한꺼번에 전송되어 실행된다.
- 💡 쓰기 지연을 사용하는 이유와 장점
- 네트워크 비용 감소 (Batch Insert): 쿼리가 발생할 때마다 매번 DB와 통신(Network I/O)하면 성능이 떨어진다. 쿼리를 모았다가 한 번에 보냄으로써 JDBC 일괄 처리(Batch, 배치) 기능을 극대화할 수 있다.
- 애플리케이션 수준의 최적화: 한 트랜잭션 내에서 데이터를 여러 번 수정하거나 생성 후 바로 삭제하는 경우, 영속성 컨텍스트가 최종 상태만 계산하여 꼭 필요한 쿼리만 압축해 보낼 수 있다.
- 일반적인 방식(예시): INSERT ➡️ UPDATE ➡️ UPDATE ➡️ DELETE 총 4번의 쿼리가 실행됨.
- 쓰기 지연 방식: 극한의 경우 쿼리가 하나도 안 나가거나 꼭 필요한 쿼리만 압축해서 보낼 수 있다.
- 비즈니스 로직 집중: 개발자는 데이터베이스 트랜잭션 시점에 맞춰 SQL이 나가는 것을 신경 쓰지 않고, 순수 자바 객체를 다루듯 비즈니스 로직에만 집중할 수 있다.
- 트랜잭션 커밋 시점에 성공하면 다 같이 반영되고, 실패하면 다 같이 안 되는 원자성(Atomicity)을 보장받기가 훨씬 수월해 짐.
더보기
💡 어떻게 동작하는지? (동작 원리)
만약 코드에서 새로운 게시글(Post) 3개를 연속으로 저장(persist)한다고 가정했을 때:
- em.persist(post1);
- 영속성 컨텍스트의 '1차 캐시'에 post1을 저장한다.
- 동시에 이 객체를 DB에 넣기 위한 INSERT SQL을 생성해서 '쓰기 지연 SQL 저장소'라는 곳에 차곡차곡 쌓아둔다. (아직 DB로는 안 보냄)
- em.persist(post2); ...
- 마찬가지로 1차 캐시에 저장하고, INSERT SQL을 쓰기 지연 저장소에 계속 쌓는다.
- tx.commit(); (트랜잭션 커밋 시점)
- 트랜잭션을 커밋하면, JPA가 내부적으로 flush()를 먼저 호출한다.
- 이때 쓰기 지연 SQL 저장소에 모여있던 3개의 INSERT SQL이 한번에 데이터베이스로 전송됨.
- 마지막으로 데이터베이스의 진짜 트랜잭션이 커밋되면서 데이터가 최종 반영된다.
💡 요약: 쓰기 지연은 데이터베이스와의 불필요한 통신을 줄이기 위해 영속성 컨텍스트라는 '버퍼(Buffer)'를 두고 쿼리를 모아서 던지는 최적화 메커니즘.
④ 변경 감지 (Dirty Checking)
- 개념: 영속 상태의 엔티티 값이 변경되었을 때, JPA가 이를 자동으로 감지하여 데이터베이스에 반영하는 메커니즘.
- 원리: JPA는 엔티티가 영속 상태가 될 때의 최초 상태를 복사한 스냅샷을 1차 캐시에 함께 저장해 둔다.
- 수정 동작 방식 (프로세스)
- 엔티티 값 수정: 영속 상태인 엔티티 객체의 값을 변경한다.
- 예시: post1.setTitle("제목 수정합니다!");
- 1:1 상태 비교 (플러시/커밋 시점): 트랜잭션이 커밋되거나 플러시(em.flush())가 호출되는 시점에, JPA가 엔티티의 현재 상태와 최초 1차 캐시에 저장되었던 스냅샷을 서로 비교(Dirty Checking)한다.
- 영속성 컨텍스트가 엔티티의 "처음 상태(스냅샷)"를 기억하고 있다가, 트랜잭션이 끝날 때 "현재 상태"와 자동으로 비교해 주는 것.
- 때문에 별도의 update()나 save() 메서드를 호출하지 않아도 됨. 개발자가 수동으로 "수정되었다"고 알려주지 않아도 자바 객체의 변경 사항을 스스로 추적할 수 있는 것.
- UPDATE 쿼리 자동 생성 및 실행: 비교 결과 변경사항이 감지되면, JPA가 자동으로 UPDATE SQL을 생성하여 쓰기 지연 SQL 저장소에 등록한다. 이후 트랜잭션 커밋 또는 플러시(em.flush()) 시점에 저장소에 모여있던 UPDATE SQL이 데이터베이스로 한꺼번에 전송되어 최종 반영(실행)된다.
- 엔티티 값 수정: 영속 상태인 엔티티 객체의 값을 변경한다.
4. 실습 코드 흐름 요약 (Post 엔티티)
// 1. 비영속 상태 (일반 자바 객체 단계, 영속성 컨텍스트가 아직 모름)
Post post1 = new Post(1L, "첫 번째 글", "안녕하세요.");
Post post2 = new Post(2L, "두 번째 글", "반갑습니다.");
// 2. 영속 상태 (1차 캐시 등록 + INSERT 쿼리 쓰기 지연 큐 등록)
em.persist(post1);
em.persist(post2);
// 3. 변경 감지 (Dirty Checking)
// 객체 값만 변경하면, 커밋 시점에 자동으로 UPDATE 쿼리가 생성되어 실행됨
// → 별도의 update()를 부르지 않아도 됨
post1.setTitle("제목 수정합니다!");
// 4. 삭제 대상 조회 및 삭제 (DELETE 쿼리가 쓰기 지연 큐에 등록됨)
Post deleteTarget = em.find(Post.class, 2L);
em.remove(deleteTarget); // DELETE 쿼리가 쓰기 지연 큐에 등록됨
// 5. 플러시 및 트랜잭션 커밋
// 이 시점에 쓰기 지연 큐에 쌓여있던 쿼리들(INSERT 1개, UPDATE 1개, DELETE 1개)이 DB로 한꺼번에 전송된다.
tx.commit()
'Tech Stack > Database' 카테고리의 다른 글
| 🔌JDBC 요약 정리 (0) | 2026.07.11 |
|---|---|
| 🗃️JPA 엔티티 매핑 기본 전략 (Identity, Sequence) (0) | 2026.07.10 |
| 🗃️ [JPA-03] JPA 아키텍처 구조 및 주요 API 동작 원리 (0) | 2026.07.09 |
| 🍃 JDBC, JPA 환경에 사용할 데이터베이스 설치 및 환경 설정(의존성) (0) | 2026.07.09 |
| 🔌[JDBC] JDBC 개요 및 데이터베이스 연결 원리 (0) | 2026.07.09 |