JPA [1/4] : JPA 시작과 영속성 컨텍스트

2026. 6. 23. 12:52·🍃SpringBoot

1. JPA를 왜 사용하는가? (패러다임의 불일치와 ORM)

JPA(Java Persistence API)를 배우기 전 가장 먼저 이해해야 하는 것은 "왜 JPA가 등장했는가?"이다.
단순히 기능이나 어노테이션을 외우는 것보다, JPA가 해결하려는 고질적인 문제가 무엇인지 이해하는 것이 훨씬 중요하다.

1.1 SQL 중심 개발의 문제점

전통적인 JDBC나 MyBatis 기반의 개발에서는 객체를 데이터베이스에 저장하고 조회하기 위해 개발자가 직접 SQL을 작성해야 한다.

1) 무한 반복되는 CRUD 작업

객체에 필드가 하나만 추가되어도 이와 연관된 모든 SQL(`INSERT`, `SELECT`, `UPDATE` 등)을 일일이 수정해야 한다.

// 1. 기존 객체 구조
public class Member {
    private Long id;
    private String name;
}

// 2. 새로운 필드 'age'가 추가됨
public class Member {
    private Long id;
    private String name;
    private int age; // 추가
}

필드가 추가되는 순간 개발자는 다음과 같은 수많은 SQL을 직접 수정하는 수고를 들여야 한다.

  • `INSERT INTO member(id, name, age) VALUES (?, ?, ?)`
  • `SELECT id, name, age FROM member WHERE id = ?`
  • `UPDATE member SET age = ? WHERE id = ?`

이로 인해 애플리케이션 코드는 SQL에 종속적이게 되며, 개발자가 아닌 'SQL 데이터 매퍼'처럼 일하게 되는 생산성 저하 문제가 발생한다.

객체와 관계형 DB의 패러다임 불일치

JPA가 등장한 가장 근본적인 이유는 객체 지향 프로그래밍과 관계형 데이터베이스(RDB)의 패러다임 불일치를 해결하기 위해서다.
개발자는 객체 중심으로 설계하고 싶어 하지만, 데이터베이스는 테이블 중심으로 데이터를 저장한다.

2) 연관관계와 참조 방식의 차이

  • 객체: 참조(Reference)를 사용해 연관된 객체를 찾는다. (`member.getTeam()`) 방향성이 존재하며 한쪽으로만 이동한다.
  • 테이블: 외래 키(`FK`)와 `JOIN`을 사용해 연관된 테이블을 찾는다. 방향성 없이 양방향으로 조회가 가능하다.

3) 상속 구조의 표현 한계

객체에는 상속 관계가 존재하지만, RDB에는 자바의 상속과 똑같은 개념이 없다.
(`슈퍼타입-서브타입` 관계가 비슷하지만 다르게 작동한다.)

class Item {}
class Album extends Item {}

자바에서는 객체를 저장할 때 `list.add(album)` 한 줄이면 끝나지만, DB에 저장하려면 ITEM 테이블과 ALBUM 테이블 각각에 맞는 INSERT SQL을 2번 작성하고 조회할 때는 JOIN을 써야 하므로 코드가 매우 복잡해진다.

3) 처음 실행하는 SQL에 따른 탐색 범위 제한

객체는 자유롭게 객체 그래프를 탐색할 수 있어야 한다. 하지만 SQL을 직접 다루면 처음 실행한 SQL이 MEMBER와 TEAM만 조회했을 경우, `member.getOrder()`와 같이 다른 객체로의 탐색이 불가능하다. 즉, 데이터 식별을 위해 비즈니스 로직이 DAO/SQL 코드에 결합된다.

1.2 ORM과 JPA의 개념

이러한 패러다임 불일치 문제를 해결하기 위해 등장한 기술이 바로 ORM(Object-Relational Mapping)이다.

fig 1. JPA는 애플리케이션과 JDBC 사이에서 동작

1) ORM (객체 관계 매핑) 이란?

  • 객체는 객체대로 설계하고, 관계형 DB는 관계형 DB대로 설계한다.
  • 그 중간에서 ORM 프레임워크가 두 영역 사이를 자동으로 매핑해 준다.
  • 개발자는 대화 상대로 DB가 아닌 '자바 컬렉션'을 대하듯 객체를 다루고, SQL은 ORM이 대신 생성한다.

2) JPA(Java Persistence API)의 정체

JPA는 자바 진영의 ORM 기술 표준 명세(Specification)다.
즉, JPA는 인터페이스의 모음이며 이를 실제로 구현한 다양한 구현체들이 존재한다.

분류 설명
JPA (인터페이스) 자바에서 ORM을 어떻게 사용할지 정의한 표준 표준 명세
Hibernate (구현체) JPA 표준을 구현한 가장 대표적이고 대중적인 ORM 프레임워크

💡 애플리케이션 구조 흐름

  • [Java Application] ──> [JPA 표준 인터페이스] ──> [Hibernate (구현체)] ──> [Database]
 

3) JPA의 핵심 목표와 장점

많은 사람들이 "JPA는 SQL을 안 써도 되게 만들어주는 기술"로 오해하지만,
실제 목표는 개발의 중심을 SQL에서 객체로 전환하는 것이다.

  • 생산성 향상: 자바 컬렉션에 저장하듯 `em.persist(member)` 한 줄로 저장이 가능하며, 지루한 CRUD SQL 작성이 사라진다.
  • 유지보수성 향상: 필드가 추가되거나 변경되어도 SQL을 일일이 수정할 필요가 없다. JPA가 필드를 인식해 SQL을 알아서 빌드한다.
  • 패러다임 불일치 해결: 상속, 연관관계, 객체 그래프 탐색 등의 문제를 JPA가 내부적으로 해결해 주므로 객체지향적인 설계가 가능해진다.
  • 데이터베이스 독립성(Dialect 방언): 특정 DB에 종속되지 않는다. 데이터베이스를 MySQL에서 PostgreSQL로 변경하더라도 설정(방언)만 바꾸면 JPA가 해당 DB에 맞는 SQL을 알아서 생성한다.
  • 성능 최적화 기능: 내부적으로 1차 캐시, 쓰기 지연(Transactional Write-Behind), 변경 감지(Dirty Checking), 지연 로딩(Lazy Loading) 등의 최적화를 지원하여 데이터베이스와의 통신 횟수를 줄인다.

4) JPA의 단점과 주의할 점

장점이 강력한 만큼, 잘못 사용했을 때의 리스크도 존재한다.

  • 높은 학습 난이도: 단순히 매핑하는 법을 아는 것을 넘어 영속성 컨텍스트, 지연 로딩, N+1 문제 등 내부 메커니즘을 깊게 이해해야 한다.
  • 성능 저하의 위험: 메커니즘을 모른 채 코드를 작성하면, `member.getOrders()` 호출 한 줄 때문에 순식간에 수천 개의 연관 쿼리가 실행되는 성능 재앙(N+1 문제)을 맞이할 수 있다.
  • SQL 학습의 중요성: 실무에서는 복잡한 통계나 조회를 위해 JPQL, QueryDSL, Native Query 등을 필수적으로 혼용한다.
    따라서 JPA를 잘하려면 RDB와 SQL을 오히려 더 완벽하게 알고 있어야 한다.

JPA는 SQL 중심의 개발에서 벗어나 객체 중심으로 개발할 수 있도록 돕는 자바 ORM 표준 기술이며, 생산성과 유지보수성을 극대화하지만 그만큼 내부 동작 원리를 정확히 파악하고 써야 하는 기술이다.

2. JPA 시작하기 (Entity, EntityManager, 기본 CRUD)

이번 장에서는 JPA를 실제로 사용하는 가장 기본적인 흐름과 핵심 객체들을 살펴본다.
목표는 다음 3가지를 명확히 이해하는 것이다.

  1. 엔티티(Entity)가 무엇인가?
  2. EntityManager의 역할은 무엇인가?
  3. JPA에서 CRUD(기본 데이터 작업)가 어떻게 동작하는가?

2.1 엔티티(Entity)란?

엔티티는 데이터베이스(DB) 테이블과 매핑되는 자바 객체를 말한다.
단순히 데이터를 담는 객체(DTO)를 넘어, DB의 한 행(Row)과 1:1로 대응하는 특별한 객체다.

엔티티 매핑 예시

DB에 MEMBER 테이블이 존재할 때, 자바에서는 다음과 같이 클래스를 설계하고 어노테이션을 붙여 엔티티로 선언한다.

@Entity
@Table(name = "MEMBER") // 생략 시 클래스명과 동일한 테이블에 매핑
public class Member {

    @Id
    private Long id;

    @Column(name = "name") // 생략 시 필드명과 동일한 컬럼에 매핑
    private String name;
    
    // JPA 규칙: 기본 생성자가 반드시 필요하다 (접근 제어자는 public 또는 protected)
    protected Member() {} 
}
  • `@Entity`: 이 클래스가 JPA가 관리하는 엔티티임을 선언한다. 이 어노테이션이 붙어야 비로소 DB 테이블과 매핑된다.
  • `@Table`: 엔티티와 매핑할 테이블 이름을 지정한다. 생략하면 클래스 이름을 테이블 이름으로 사용한다.

2.2 기본 키(PK) 매핑 전략

JPA에서 관리하는 모든 엔티티는 데이터베이스의 기본 키(PK)와 매핑될 식별자가 반드시 필요하다.
이를 `@Id` 어노테이션으로 지정한다.

1) 직접 할당 방식 (@Id만 사용)

애플리케이션에서 개발자가 직접 `member.setId(1L)`과 같이 PK 값을 세팅하여 저장하는 방식이다.

2) 자동 생성 방식 (@GeneratedValue 추가)

실무에서는 주로 DB가 PK를 자동으로 생성해 주는 방식을 사용한다.

  • IDENTITY 전략: `GenerationType.IDENTITY`
    • 기본 키 생성을 데이터베이스에 위임한다. (예: MySQL의 AUTO_INCREMENT)
  • SEQUENCE 전략: `GenerationType.SEQUENCE`
    • 데이터베이스 시퀀스 오브젝트를 사용해 PK를 생성한다. (예: Oracle, PostgreSQL)

1.3 EntityManager의 역할

EntityManager는 JPA의 기능을 사용하기 위한 가장 핵심적인 객체다.
엔티티의 저장, 조회, 수정, 삭제 등 엔티티와 관련된 모든 데이터 작업을 담당한다.

  • [애플리케이션] ──> [EntityManager] ──> [데이터베이스]

쉽게 비유하자면, 전통적인 JDBC의 DB Connection과 유사한 역할을 수행한다. 개발자는 내부에 커넥션을 품고 있는 EntityManager를 통해 DB와 통신하게 된다.

1.4 JPA의 기본 CRUD 동작 방식

JPA는 데이터를 변경(저장, 수정, 삭제)할 때 반드시 트랜잭션(Transaction) 안에서 실행해야 한다.
트랜잭션 없이 데이터를 변경하면 예외가 발생한다.

1) 저장 (Create)

Member member = new Member();
member.setName("Kim");

em.persist(member); // 엔티티 저장

`em.persist(member)`를 호출하면 JPA가 객체를 분석하여 적절한 `INSERT INTO MEMBER ...` SQL을 자동으로 생성하고 실행한다.

2) 조회 (Read)

Member member = em.find(Member.class, 1L); // 타입과 PK 전달

`em.find()`는 가장 기본적인 단건 조회 메서드다.
조회하려는 엔티티의 클래스 타입과 PK 값을 넘기면 `SELECT * FROM MEMBER WHERE ID = 1` SQL을 실행하고 그 결과를 자바 객체로 변환해 반환한다.

3) 수정 (Update) ⭐ 매우 중요

JPA에는 `em.update()` 같은 수정 메서드가 존재하지 않는다.
대신 자바 컬렉션의 객체 값을 바꾸듯 처리한다.

// 1. 수정할 엔티티를 먼저 조회
Member member = em.find(Member.class, 1L);

// 2. 자바 객체의 값 변경
member.setName("Lee"); 

// 3. 커밋 (끝)
tx.commit(); 

데이터를 조회한 후 값만 변경하고 트랜잭션을 커밋(`tx.commit()`)하면, JPA가 엔티티의 변경 사항을 자동으로 감지하여 UPDATE SQL을 실행한다. 이를 변경 감지(Dirty Checking)라고 부른다.

4) 삭제 (Delete)

Member member = em.find(Member.class, 1L); // 삭제할 엔티티 조회
em.remove(member); // 엔티티 삭제

삭제할 엔티티를 먼저 조회한 후 `em.remove(member)`를 호출하면 `DELETE FROM MEMBER ...` SQL이 실행된다.

5) 전체 CRUD 실행 예제 코드

JPA의 순수 자바 구동 흐름은 정석적으로 다음과 같은 구조를 가진다.
예외 발생 시 롤백 처리와 리소스 반환(`close()`)이 필수적이다.

EntityManager em = emf.createEntityManager(); // EntityManager 생성
EntityTransaction tx = em.getTransaction();   // 트랜잭션 획득

try {
    tx.begin(); // [트랜잭션 시작]

    // 1. 저장 (C)
    Member member = new Member();
    member.setName("Kim");
    em.persist(member);

    // 2. 조회 (R)
    Member findMember = em.find(Member.class, member.getId());

    // 3. 수정 (U) - 변경 감지 기능 작동
    findMember.setName("Lee");

    // 4. 삭제 (D)
    // em.remove(findMember);

    tx.commit(); // [트랜잭션 커밋] -> 이 시점에 실제 SQL이 DB로 전송됨
} catch (Exception e) {
    tx.rollback(); // 예외 발생 시 롤백
    e.printStackTrace();
} finally {
    em.close(); // 엔티티 매니저 종료 (필수)
}

6) JPA 핵심 규칙 및 실무 유의 사항

  1. 기본 생성자 필수: JPA가 프로시저나 내부 리플렉션을 통해 객체를 동적으로 생성하므로, 엔티티 클래스에는 파라미터가 없는 public 또는 protected 기본 생성자가 반드시 있어야 한다.
  2. 식별자(PK) 필수: `@Id`가 없는 클래스는 엔티티로 등록할 수 없으며 컴파일 또는 구동 시점에 에러가 발생한다.
  3. 트랜잭션 중심: 모든 데이터 변경은 반드시 하나의 트랜잭션 단위 안에서 이루어져야 안전하게 반영된다.
  4. 수정 메서드 호출 금지: 무의식적으로 `em.update()`나 유사한 코드를 찾으려 하지 마라. 자바 객체의 값만 바꾸면 JPA가 다 알아서 처리한다.

💡 실무자의 시선: 핵심은 '영속성 컨텍스트'

실무에서 JPA를 처음 다루는 개발자들이 가장 낯설어하는 부분이 바로 이것이다.

  • `em.persist(member)`를 했는데, 왜 즉시 콘솔에 INSERT SQL이 안 보이지?
  • `member.setName("Lee")`만 했는데, 왜 자동으로 UPDATE SQL이 나가지?

이 모든 마법 같은 일들의 비밀은 바로 다음 장에서 다룰 영속성 컨텍스트(Persistence Context)라는 내부 메모리 공간에 핵심 원리가 숨겨져 있다. 사실상 JPA를 제대로 이해한다는 것은 영속성 컨텍스트를 완벽히 이해하는 것과 같다.

3. 영속성 컨텍스트(Persistence Context) 완벽 이해

JPA를 공부하면서 가장 중요한 단 하나의 챕터를 꼽으라면 단연 영속성 컨텍스트다. JPA의 핵심 기능인 1차 캐시, 동일성 보장, 쓰기 지연, 변경 감지, 지연 로딩 모두가 이 영속성 컨텍스트를 기반으로 동작한다. 즉, "JPA를 이해한다는 것은 영속성 컨텍스트를 이해하는 것이다."라고 해도 과언이 아니다.

3.1 영속성 컨텍스트란?

  • 공식 정의: 엔티티를 영구 저장하는 환경
  • 실무적 이해: 애플리케이션과 데이터베이스 사이에서 엔티티를 관리하는 눈에 보이지 않는 메모리 공간

`em.persist(member)`를 호출했을 때, 객체는 DB에 바로 저장되는 것이 아니라 영속성 컨텍스트라는 논리적 공간에 먼저 저장된다. 엔티티 매니저(EntityManager)를 통해 이 영속성 컨텍스트에 접근하고 관리할 수 있다.

3.2 엔티티의 생명주기 (4가지 상태)

엔티티는 영속성 컨텍스트와의 관계에 따라 4가지 상태를 가진다.

1) 비영속 (New)

JPA 및 영속성 컨텍스트와 전혀 관계가 없는 순수한 객체 상태다.
메인 메모리에만 존재하며 DB나 캐시에는 아무런 영향이 없다.

Member member = new Member();
member.setName("Kim"); // 현재 비영속 상태

2) 영속 (Managed)

엔티티 매니저를 통해 엔티티가 영속성 컨텍스트에 저장된 상태다.
이제부터 JPA의 관리를 받게 된다.

em.persist(member); // 비영속 -> 영속 상태 전환
⚠️ 주의: `em.persist()`가 호출된다고 해서 즉시 INSERT SQL이 DB로 날아가는 것이 아니다.
실제 SQL은 트랜잭션이 커밋되는 시점에 실행된다.

 

3) 준영속 (Detached)

영속성 컨텍스트에 저장되었다가 분리되어, 더 이상 JPA의 관리를 받지 않는 상태다.

em.detach(member); // 특정 엔티티만 준영속으로 전환
em.clear();        // 영속성 컨텍스트 전체 통째로 비우기

준영속 상태가 된 객체는 `member.setName("Lee")` 처럼 값을 변경해도 DB에 UPDATE SQL이 생성되지 않는다.

4) 삭제 (Removed)

엔티티를 영속성 컨텍스트와 데이터베이스에서 삭제하기로 마킹한 상태다.

em.remove(member); // 커밋 시점에 DELETE SQL 실행 예정

3.3 영속성 컨텍스트의 핵심 기능 4가지

영속성 컨텍스트가 존재함으로써 얻는 이점은 매우 강력하다.

1) 1차 캐시 (조회 성능 최적화)

영속성 컨텍스트 내부에는 자바의 Map 형태를 가진 1차 캐시가 존재한다.
`@Id` 값이 Key가 되고 엔티티 인스턴스 자체가 Value가 된다.

Member member = new Member();
member.setId(1L);
em.persist(member); // 1차 캐시에 저장됨

// 조회 요청
Member findMember = em.find(Member.class, 1L); 
  • `em.find()`를 호출하면 JPA는 DB를 보기 전에 1차 캐시를 먼저 탐색한다.
  • 값이 캐시에 존재하므로 DB에 SELECT SQL을 보내지 않고 메모리에서 바로 객체를 반환한다. (성능 이점)
  • 만약 1차 캐시에 없다면 그제야 DB를 조회하고, 조회된 데이터를 1차 캐시에 저장한 뒤 반환한다.

2) 영속 엔티티의 동일성(Identity) 보장

자바 컬렉션에서 같은 인덱스의 객체를 꺼내면 주소값이 같듯, JPA도 동일성을 보장한다.

Member a = em.find(Member.class, 1L);
Member b = em.find(Member.class, 1L);

System.out.println(a == b); // true (1차 캐시에 있는 동일한 인스턴스를 반환하기 때문)

REAPEATABLE READ 등급의 트랜잭션 격리 수준을 데이터베이스가 아닌 애플리케이션 차원에서 제공한다.

3) 트랜잭션을 지원하는 쓰기 지연 (Action Queue)

`em.persist()`를 할 때마다 SQL을 보내면 DB와의 네트워크 비용이 많이 발생한다. JPA는 이를 모아서 한 번에 처리한다.

em.persist(memberA);
em.persist(memberB);
// 이 시점까지는 DB에 SQL을 보내지 않고 내부 '쓰기 지연 SQL 저장소'에 쌓아둔다.

tx.commit(); // [트랜잭션 커밋 시점]에 모아둔 INSERT SQL 2개가 동시에 던져진다.

4) 변경 감지 (Dirty Checking)

JPA는 엔티티의 수정을 위한 별도의 메서드가 없다. 대신 영속성 컨텍스트가 처음에 읽어온 엔티티의 최초 상태(스냅샷)를 보관하고 있다가, 커밋 시점에 현재 엔티티 상태와 비교한다.

Member member = em.find(Member.class, 1L); // 영속 상태 진입 및 스냅샷 저장
member.setName("Lee"); // 값 변경

tx.commit(); // 커밋 시점에 스냅샷과 변경된 엔티티를 비교 후, 자동으로 UPDATE SQL 빌드 및 실행

3.4 플러시(Flush)란?

플러시는 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영(동기화)하는 작업이다.
쓰기 지연 저장소에 쌓여 있는 쿼리들을 DB에 보내는 역할을 한다.

플러시가 발생하는 시점

  1. `tx.commit()` 호출 시: 커밋 직전에 JPA가 자동으로 플러시를 수행한다.
  2. `em.flush()` 직접 호출: 강제로 변경 내용을 DB에 선반영하고 싶을 때 사용한다.
  3. JPQL 쿼리 실행 시: SQL로 변환되어 실행되기 전, 데이터 정합성을 위해 자동으로 플러시가 호출된다.
💡 핵심 오해 정정: 플러시(flush())는 영속성 컨텍스트의 변경 내용을 DB에 전송할 뿐이다. 영속성 컨텍스트를 비우는 것이 아니며, 트랜잭션을 끝내는 것도 아니다. 플러시 후에도 롤백(rollback())하면 DB 반영은 취소된다.

 

영속성 컨텍스트 핵심 포인트

Q. em.persist(member)를 호출하면 바로 INSERT SQL이 실행되나?

  • A. 아니다. 영속성 컨텍스트의 1차 캐시와 쓰기 지연 SQL 저장소에 보관되었다가,
    트랜잭션이 커밋(`commit()`)되거나 플러시(`flush()`)가 발생할 때 DB로 전송된다.

Q. JPA에서 엔티티를 수정할 때 em.update() 같은 메서드가 없는 이유는?

  • A. JPA는 변경 감지(Dirty Checking) 메커니즘을 사용하기 때문이다. 영속 상태의 엔티티는 트랜잭션 커밋 시점에 최초 로드된 상태(스냅샷)와 비교하여 변경된 부분이 있으면 자동으로 UPDATE 쿼리를 생성하여 실행한다.

Q. 준영속 상태와 비영속 상태의 차이는 무엇인가?

  • A. 비영속 상태는 영속성 컨텍스트에 한 번도 들어간 적이 없는 순수 객체 상태이고, 준영속 상태는 한 번 영속 상태가 되었다가 분리된 상태다. 따라서 준영속 상태는 식별자(PK)를 반드시 가지고 있다는 차이점이 있다.

영속성 컨텍스트는 애플리케이션과 DB 사이에서 엔티티를 관리하는 논리적 메모리 계층이며, 이를 통해 1차 캐시, 동일성 보장, 쓰기 지연, 변경 감지라는 강력한 최적화 기능을 제공한다.

'🍃SpringBoot' 카테고리의 다른 글

Spring IoC & DI 완전 정복: Chapter 1: "객체지향은 왜 등장했을까?"  (0) 2026.07.22
JPA [2/4] : 엔티티 매핑과 연관관계  (0) 2026.06.23
JUnit [2/2]: Service, Repository, Controller, 동시성 테스트  (0) 2026.06.22
JUnit [1/2]: 테스트 개념과 JUnit, Mockito 사용법  (0) 2026.06.22
카카오 로그인 구현(실습): Spring Boot + React 소셜 로그인 구현 및 JWT 인증 연동(Google, Github 포함)  (0) 2026.06.19
'🍃SpringBoot' 카테고리의 다른 글
  • Spring IoC & DI 완전 정복: Chapter 1: "객체지향은 왜 등장했을까?"
  • JPA [2/4] : 엔티티 매핑과 연관관계
  • JUnit [2/2]: Service, Repository, Controller, 동시성 테스트
  • JUnit [1/2]: 테스트 개념과 JUnit, Mockito 사용법
limdaeil
limdaeil
limdaeil 님의 블로그 입니다.
  • limdaeil
    limdaeil
    limdaeil
  • 전체
    오늘
    어제
    • 분류 전체보기 (83) N
      • 💭Retrospective (21)
      • 🥕FrontEnd (0)
      • 🐬MySQL (2) N
      • 🐍Python (5)
      • 🍃SpringBoot (33) N
      • ☕Java (1)
      • ♾️Devops (1)
      • 🌎Network (2)
      • 📚Read & 👨‍🏫Course (10)
      • Programmers (1)
      • 🧪Test (7)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    Python
    한빛미디어
    optimistic rock
    gradle
    jwt
    책
    나는리뷰어다
    한빛아카데미
    Concurrency
    distributed lock
    junit
    회고
    서평단
    Spring
    IoC
    Mockito
    spring boot
    DI
    MySQL
    redis
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.6
limdaeil
JPA [1/4] : JPA 시작과 영속성 컨텍스트
상단으로

티스토리툴바