Skip to content
Lee Do Kyung edited this page Mar 19, 2026 · 1 revision

JPA (Java Persistence API)

자바 ORM 표준 — 객체와 관계형 DB를 매핑해 SQL을 객체 중심으로 다룬다.

Spring Boot에서는 기본 구현체로 Hibernate를 사용한다.


목차

  1. JPA란?
  2. 핵심 개념: 엔티티
  3. 영속성 컨텍스트
  4. 연관관계 매핑
  5. 지연 로딩 vs 즉시 로딩
  6. N+1 문제와 해결
  7. Spring Data JPA
  8. 영속성 전이 · 고아 객체
  9. 상속 매핑 (참고)
  10. 실무 체크리스트
  11. 예제 프로젝트 API

1. JPA란?

항목 설명
정의 ORM을 위한 자바 표준 API(명세). 구현체: Hibernate, EclipseLink 등
흐름 애플리케이션JPAHibernateJDBCDB
Java 애플리케이션  →  JPA (인터페이스)  →  Hibernate (구현체)  →  JDBC  →  DB

💡 한 줄
JPA는 “테이블을 자바 객체로 표현하고, 영속성 컨텍스트로 변경을 추적해 트랜잭션 끝에 DB와 맞춘다”는 계약이다.


2. 핵심 개념: 엔티티

데이터베이스 테이블과 매핑되는 클래스다.

@Entity
@Table(name = "member")
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 30)
    private String name;

    private int age;
}
어노테이션 역할
@Entity JPA 관리 대상
@Id / @GeneratedValue 기본키 및 생성 전략
@Column 컬럼명·제약 (생략 시 관례 적용)
@Table 테이블명 지정 (생략 시 엔티티명)

기본키 생성 전략

전략 설명
IDENTITY DB에 위임 (예: MySQL AUTO_INCREMENT)
SEQUENCE 시퀀스 (Oracle, PostgreSQL 등)
TABLE 키 전용 테이블
AUTO 방언에 따라 자동 선택

3. 영속성 컨텍스트

엔티티를 보관·관리하는 1차 캐시(논리적 환경) 이다.

EntityManagerFactory  →  EntityManager  →  영속성 컨텍스트

생명주기

비영속(new)  →  영속(managed)  →  준영속(detached)
                    ↓
                삭제(removed)
상태 의미
비영속 new Member() — 컨텍스트와 무관
영속 persist / 조회로 관리됨
준영속 detach 등으로 분리
삭제 remove — 삭제 예약

이점 요약

기능 설명
1차 캐시 동일 트랜잭션 내 동일 엔티티 재조회 시 DB 생략 가능
동일성 같은 식별자 → 같은 인스턴스 (==)
쓰기 지연 SQL을 모아 커밋·flush 시점에 전송
변경 감지 필드만 바꿔도 UPDATE 가능 (별도 save 불필요한 경우 많음)
지연 로딩 연관은 실제 접근 시 로딩

변경 감지 (Dirty Checking)

@Transactional
public void updateMember(Long id, String newName) {
    Member member = memberRepository.findById(id).orElseThrow();
    member.setName(newName);
    // 트랜잭션 커밋 시 스냅샷과 비교 후 UPDATE
}

⚠️ 주의
save() 없이도 반영되는 것은 영속 상태이고 트랜잭션 안일 때가 전제다.


4. 연관관계 매핑

관계 어노테이션 예시
다대일 @ManyToOne 회원 → 팀
일대다 @OneToMany 팀 → 회원 목록
일대일 @OneToOne 회원 ↔ 프로필
다대다 @ManyToMany 가능하나 실무에서는 중간 엔티티로 푸는 경우가 많음

연관관계의 주인

  • 외래키(FK)를 가진 쪽이 주인이다.
  • 주인만 FK를 등록·수정한다. 반대편은 mappedBy로 읽기 위주.
// Member — 주인 (FK: team_id)
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "team_id")
private Team team;

// Team — 반대편
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();

연관관계 편의 메서드 (양방향)

public void changeTeam(Team team) {
    if (this.team != null) {
        this.team.getMembers().remove(this);
    }
    this.team = team;
    if (team != null) {
        team.getMembers().add(this);
    }
}

5. 지연 로딩 vs 즉시 로딩

방식 동작 실무
EAGER 연관을 즉시 함께 조회 예상 밖 JOIN·성능 이슈 가능
LAZY 사용 시점에 조회(프록시) 연관관계는 기본 LAZY 권장
@ManyToOne(fetch = FetchType.LAZY)
private Team team;

💡 필요한 조회는 fetch join / EntityGraph / 배치 등으로 “한 번에 가져올 그래프”를 설계한다.


6. N+1 문제와 해결

정의
1번의 목록 조회 후, 각 행마다 연관을 또 조회해 쿼리가 N번 추가되는 현상.

팀 3건 조회 (1) + 멤버 조회 (3) = 총 4번 …

해결 패턴

① 페치 조인 (가장 흔함)
@Query("SELECT DISTINCT t FROM Team t JOIN FETCH t.members")
List<Team> findAllWithMembers();
② @EntityGraph
@EntityGraph(attributePaths = {"members"})
@Query("SELECT t FROM Team t")
List<Team> findAllWithMembers();
③ @BatchSize / 글로벌 배치
@BatchSize(size = 100)
@OneToMany(mappedBy = "team")
private List<Member> members;
# application.yml
spring:
  jpa:
    properties:
      hibernate.default_batch_fetch_size: 100

7. Spring Data JPA

인터페이스만으로 기본 CRUD·쿼리 메서드를 제공한다.

기본 CRUD

public interface MemberRepository extends JpaRepository<Member, Long> {
    // save, findById, findAll, deleteById ...
}

쿼리 메서드 (이름 규칙)

List<Member> findByName(String name);
List<Member> findByAgeGreaterThanEqual(int age);
List<Member> findByNameContainingAndAgeGreaterThan(String name, int age);
List<Member> findByTeamName(String teamName);
키워드 의미(예)
And / Or 복합 조건
Between, LessThan, GreaterThanEqual 범위·비교
Like, Containing 문자열 패턴
OrderBy 정렬

@Query (JPQL)

@Query("SELECT m FROM Member m JOIN FETCH m.team WHERE m.age >= :age")
List<Member> findMembersWithTeamByAge(@Param("age") int age);

네이티브 SQL

@Query(value = "SELECT * FROM member WHERE name LIKE %:keyword%", nativeQuery = true)
List<Member> searchByKeyword(@Param("keyword") String keyword);

8. 영속성 전이 · 고아 객체

@OneToMany(mappedBy = "team", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Member> members;
옵션 의미
CascadeType.ALL persist/remove 등 전파
PERSIST / REMOVE 일부만 전파
orphanRemoval 부모와 연 끊긴 자식 삭제

⚠️ Cascade는 부모가 자식 생명주기를 완전히 책임질 때만 좁게 쓰는 것이 안전하다.


9. 상속 매핑 (참고)

@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(name = "dtype")
public abstract class Item {
    @Id @GeneratedValue
    private Long id;
    private String name;
    private int price;
}

@Entity
@DiscriminatorValue("BOOK")
public class Book extends Item {
    private String author;
}
전략 특징
JOINED 테이블 분리·정규화, JOIN 증가
SINGLE_TABLE 한 테이블, NULL 컬럼 다수 가능
TABLE_PER_CLASS 비권장인 경우 많음

10. 실무 체크리스트

설계

  • 무분별 @Setter 지양 → 의도 있는 메서드로 변경
  • protected 기본 생성자 (JPA 스펙)
  • 양방향이면 편의 메서드로 양쪽 동기화
  • 가능하면 단방향부터 설계

성능

  • 연관은 LAZY 기본
  • N+1은 fetch join / EntityGraph / 배치로 대응
  • 화면용 조회는 DTO 직접 조회 검토
  • 조회 전용 @Transactional(readOnly = true)

ddl-auto

용도
create / create-drop 로컬·테스트
update 초기 개발 (운영 비권장)
validate / none 스테이징·운영

⚠️ 운영에서 create·무분별 update데이터 손실·스키마 불일치 위험이 크다.


11. 예제 프로젝트 API

리포지토리의 jpa-example 기준.

Team

Method URL 설명
POST /api/teams 생성
GET /api/teams/{id} 단건
GET /api/teams 전체
GET /api/teams/with-members 팀 + 멤버 (페치 조인)
PUT /api/teams/{id} 수정 (변경 감지)
DELETE /api/teams/{id} 삭제

Member

Method URL 설명
POST /api/members 생성
GET /api/members/{id} 단건
GET /api/members 전체
GET /api/members/by-team?teamName= 팀명 검색
GET /api/members/with-team?age= 나이 + 팀 (페치 조인)
PUT /api/members/{id} 수정
DELETE /api/members/{id} 삭제

실행

cd jpa-example
./gradlew bootRun
# Windows: gradlew.bat bootRun
  • H2 콘솔: http://localhost:8080/h2-console (URL은 application.yml 기준)

📂 목차

Clone this wiki locally