Spring 입문주차 -3

dev_joo·2026년 1월 30일

ORM (Object-Relational Mapping)

반복적이고 번거로운 애플리케이션 단에서의 SQL 작업을 줄여주기 위해서 객체와 DB의 관계를 매핑 해주는 도구

기존 JDBC 방식으로 DB를 직접 조작할 때 문제

1. 테이블을 직접 SQL에 따로 접속해 만들어야 한다.

2. SQL문에 의존적이라 수정이 어렵다.

객체의 형태가 바뀌었을 때, SQL문을 수정하거나 DTO 객체 변환 부분까지 수정해야한다.

JPA(Java Persistence API)

자바 ORM 기술에 대한 표준 명세
JPA를 사용하여 직접 SQL을 작성하지 않아도 DB에 데이터를 저장, 조회, 수정, 삭제 할 수 있다.

Hybernate

JPA를 구현한 사실상 표준

Entity

DB의 테이블과 매핑되어 JPA에 의해 관리되는 클래스

@ Entity

@Entity(name = "classname")
JPA가 관리할 수 있는 Entity 클래스로 지정 (default: 클래스명 소문자)
JPA가 Entity 클래스를 인스턴스화 할 때 기본 생성자를 사용한다.

@Table

@Table(name = "entityname")
매핑할 테이블을 지정 (default: Entity 명)


@Column

@Column(name = "username")
필드와 매핑할 테이블의 컬럼을 지정 (default: 객체의 필드명)

@Column 옵션

@Column(nullable = false) : 데이터의 null 값 허용 여부 (default: true)
@Column(unique = true) : 데이터의 중복 값 허용 여부를 (default: false)
@Column(length = 500) : 데이터 값(문자)의 길이 제약조건 (default: 255)

@Id

테이블의 기본 키를 지정
기본 키 - 영속성 컨텍스트에서 Entity를 구분하고 관리할 때 사용되는 식별자 역할

@GeneratedValue

@GeneratedValue(strategy = GenerationType.IDENTITY)
기본 키 생성을 DB에 위임

create table memo (
       id bigint not null auto_increment,
        contents varchar(500) not null,
        username varchar(255) not null,
        primary key (id)
);

영속성 컨텍스트

영속성 컨텍스트는 JPA가 Entity 객체를 효율적으로 관리하기 위해 객체(Entity)를 DB처럼 다루도록 만든 논리적인 공간이다.

영속성 컨텍스트에 Entity 객체를 저장·관리하면서 DB와의 동기화를 수행한다.

Entity Manager

영속성 컨텍스트를 통해 Entity를 관리하는 인터페이스다. (영속성 컨텍스트의 창구)

persist, find, remove, flush 등의 메서드를 제공하며
Entity의 생명주기를 제어한다.

EntityManagerFactory

DB 연결 정보와 JPA 설정을 기반으로 Spring이 자동으로 생성 & 관리되며
EntityManager를 만들어내는 팩토리 객체다.

일반적으로 애플리케이션 전체에서
DB 하나당 하나만 생성되어 공유된다.

EntityManagerFactory emf = Persistence.createEntityManagerFactory("app");
EntityManager em = emf.createEntityManager();

영속성 컨텍스트의 캐시 저장소 활용

영속성 컨텍스트는 1차 캐시를 가지고 있으며,
Map과 유사한 구조로 Entity를 관리한다.

Key에 저장한 식별자값을 사용하여 Entity 객체를 구분하고 관리한다.

key - Entity의 식별자(@Id)
value - Entity 객체

Entity 저장

EntityTransaction et = em.getTransaction();

    et.begin();

    try {

        Memo memo = new Memo();
        memo.setId(1L);
        memo.setUsername("Robbie");
        memo.setContents("1차 캐시 Entity 저장");

        em.persist(memo);

        et.commit();

    } catch (Exception ex) {
        ex.printStackTrace();
        et.rollback();
    } finally {
        em.close();
    }

    emf.close();


em.persist(memo); 메서드가 호출되면 memo Entity 객체를 캐시 저장소에 임시 저장한다.
이 시점에는 INSERT SQL이 DB로 전송되지 않는다.

Entity 조회

em.find(Memo.class, 1); 호출 시 캐시 저장소를 먼저 확인 한 후

해당 값이 없을 때 DB에 SELECT 조회 후 생성된 Entity를 캐시 저장소에 저장하고 반환한다.

만약 캐시에 값이 있다면 해당 Entity 객체를 반환한다.

캐시 저장소의 존재로 DB 조회 횟수를 줄여 좋다.

객체 동일성 보장

같은 영속성 컨텍스트 안에서는 같은 식별자를 가진 Entity는
항상 동일한 객체 인스턴스로 관리된다.

 Memo secondMemo = new Memo();
            secondMemo.setId(2L);
            secondMemo.setUsername("Robbert");
            secondMemo.setContents("객체 동일성 보장");
            em.persist(secondMemo);

            Memo memo1 = em.find(Memo.class, 1);
            Memo memo2 = em.find(Memo.class, 1);
            Memo memo3  = em.find(Memo.class, 2);

            System.out.println(memo1 == memo2); // true
            System.out.println(memo1 == memo3); //  false

            et.commit();

Entity 삭제

삭제할 Entity를 조회한 후 캐시 저장소에 없다면 DB에 조회해서 저장한다.

em.remove(memo); 호출 시 삭제할 Entity를 DELETED 상태로 만든 후

트랜잭션 commit 후 Delete SQL이 DB에 요청된다.

쓰기 지연 저장소

트랜잭션(Transaction)

트랜잭션은 여러 SQL 작업을 하나의 논리적 작업 단위로 묶어
원자성(Atomicity) 을 보장하는 개념이다.

START TRANSACTION;  -- 트랜잭션 시작

DML (INSERT, UPDATE, DELETE)
...

COMMIT;  -- 트랜잭션 커밋

트랜잭션 안의 작업은 모두 성공하거나,
하나라도 실패하면 전부 취소된다.

JPA에서 트랜잭션과 쓰기 지연

JPA에서는 트랜잭션 안에서 발생한 Entity 변경 사항을
즉시 DB에 반영하지 않는다.

대신, 변경으로 인해 생성된 SQL(INSERT, UPDATE, DELETE)을
쓰기 지연(Write-behind) SQL 저장소에 모아두고 관리한다.

트랜잭션을 종료하면서 변경 내용을 최종적으로 확정하고 DB에 반영하는 과정을
커밋(commit)이라고 한다.

+commit과 flush의 관계

트랜잭션이 커밋되는 시점에는 flush가 자동으로 발생한다.
이때, 쓰기 지연 SQL 저장소에 쌓여 있던 SQL들이 DB로 한 번에 전송된다.

commit
 └─ flush (SQL 전송)
     └─ DB commit (확정)

flush: SQL을 DB에 전송

commit: 전송된 변경 내용을 확정

데이터 변경 SQL을 DB에 요청하고 반영하기 위해서는
반드시 트랜잭션이 필요하다.

따라서 트랜잭션 없이 em.flush()를 호출하면,

TransactionRequiredException (no transaction is in progress) 오류가 발생한다.
flush는 트랜잭션 내부에서만 의미가 있다.

변경 감지 (Dirty Checking)

엔티티의 필드 값이 바뀌었는지 자동으로 감지해서 UPDATE 쿼리를 날려주는 기능

JPA는 Entity 변경 시마다 즉시 UPDATE SQL을 생성하지 않는다.
대신 변경 감지(Dirty Checking) 과정을 통해 필요한 경우에만 SQL을 생성한다.

변경 감지 과정

  1. Entity가 영속성 컨텍스트에 저장될 때
    최초 상태(Loaded State) 를 함께 저장한다.

  2. 트랜잭션이 커밋되거나 em.flush()가 호출된다.

  3. Entity의 현재 상태와 최초 상태를 비교한다.

  4. 변경이 감지되면 UPDATE SQL을 생성하여
    쓰기 지연 SQL 저장소에 저장한다.

  5. 쓰기 지연 저장소의 SQL이 DB로 전송된다.

  6. DB 트랜잭션이 commit되며 변경 사항이 확정된다.

entityInstance - Entity 객체의 현재 상태
entityEntry > loadedState - Entity가 처음 영속성 컨텍스트에 저장될 때의 최초 상태

em.flush()가 호출되면
영속성 컨텍스트는 Entity의 현재 상태와 최초 상태를 비교하고,
변경이 있는 경우 UPDATE SQL을 생성하여
쓰기 지연 SQL 저장소에 저장한 뒤 DB로 전송한다.

Entity의 상태

비영속 Transient

Memo memo = new Memo(); // 비영속 상태
memo.setId(1L);
memo.setUsername("Robbie");
memo.setContents("비영속과 영속 상태");

인스턴스화된 Entity 객체가 영속성 컨텍스트에 저장되지 않고, 때문에 JPA의 관리를 받지 않는 상태

영속 Managed

em.persist(entitiy);

EntityManager를 통해 영속성 컨텍스트에 저장하여 관리되고 있는 상태

준영속 Detached

em.detach(entity);

영속성 컨텍스트에 의해 관리되다가 분리된 상태
준영속 상태의 Entity는 dirty-checking이 동작하지 않는다.

  • em.detach(entity) - 영속 상태에서 준영속 상태로 변경
  • em.contains(entity) 해당 객체가 영속성 컨텍스트에 저장되어 관리되는 상태인지 확인 (true/false)
Memo memo = em.find(Memo.class, 1);            System.out.println("em.contains(memo) = " + em.contains(memo));
em.detach(memo);
System.out.println("em.contains(memo) = " + em.contains(memo));

// detach 상태이므로 dirty-checking이 동작하지 않아 UPDATE SQL이 실행되지 않는다.
System.out.println("memo Entity 객체 수정 시도");
memo.setUsername("Update");
memo.setContents("memo Entity Update");

et.commit();
  • em.clear() - 영속성 컨텍스트를 완전히 초기화(모든 Entity를 준영속 상태로 전환)


  • em.close() - 영속성 컨텍스트를 종료 (계속해서 영속성 컨텍스트를 사용할 수 없다.)

Memo memo1 = em.find(Memo.class, 1);
Memo memo2 = em.find(Memo.class, 2);

// em.contains(entity) : Entity 객체가 현재 영속성 컨텍스트에 저장되어 관리되는 상태인지 확인하는 메서드
System.out.println("em.contains(memo1) = " + em.contains(memo1));
System.out.println("em.contains(memo2) = " + em.contains(memo2));

System.out.println("close() 호출");
em.close();
Memo memo = em.find(Memo.class, 2); // Session/EntityManager is closed 메시지와 함께 오류 발생
System.out.println("memo.getId() = " + memo.getId());
/* 출력
em.contains(memo1) = true
em.contains(memo2) = true
close() 호출
java.lang.IllegalStateException: Session/EntityManager is closed
*/
  • em.merge(entity) - 파라미터로 전달받은 비영속/준영속 상태의 Entity의 식별자 값으로 새로운 Entity를 반환
     Memo memo = new Memo();
     memo.setId(3L);
     memo.setUsername("merge()");
     memo.setContents("merge() 저장");
     
     System.out.println("merge() 호출");
     Memo mergedMemo = em.merge(memo);
     
     System.out.println("em.contains(memo) = " + em.contains(memo)); // false
     System.out.println("em.contains(mergedMemo) = " + em.contains(mergedMemo)); // true
    merge(entity) 동작 1. 해당 Entity가 영속성 컨텍스트에 없을때:
         1. DB에서 새롭게 조회합니다.
         2. 조회한 Entity를 영속성 컨텍스트에 저장
         3. 전달 받은 Entity의 값을 사용하여 병합
         4. Update SQL이 수행 (DB 수정)
    2. 만약 DB에서도 없다면 ?
         1. 새롭게 생성한 Entity를 영속성 컨텍스트에 저장
         2. Insert SQL이 수행 (DB 추가)

삭제 Removed

em.remove(entity);

영속 상태의 Entity를 파라미터로 전달받아 삭제 상태로 전환

profile
풀스택 연습생. 끈기있는 삽질로 무대에서 화려하게 데뷔할 예정 ❤️🔥

0개의 댓글