-
Notifications
You must be signed in to change notification settings - Fork 1
JPA
Lee Do Kyung edited this page Mar 19, 2026
·
1 revision
자바 ORM 표준 — 객체와 관계형 DB를 매핑해 SQL을 객체 중심으로 다룬다.
Spring Boot에서는 기본 구현체로 Hibernate를 사용한다.
- JPA란?
- 핵심 개념: 엔티티
- 영속성 컨텍스트
- 연관관계 매핑
- 지연 로딩 vs 즉시 로딩
- N+1 문제와 해결
- Spring Data JPA
- 영속성 전이 · 고아 객체
- 상속 매핑 (참고)
- 실무 체크리스트
- 예제 프로젝트 API
| 항목 | 설명 |
|---|---|
| 정의 | ORM을 위한 자바 표준 API(명세). 구현체: Hibernate, EclipseLink 등 |
| 흐름 |
애플리케이션 → JPA → Hibernate → JDBC → DB
|
Java 애플리케이션 → JPA (인터페이스) → Hibernate (구현체) → JDBC → DB
💡 한 줄
JPA는 “테이블을 자바 객체로 표현하고, 영속성 컨텍스트로 변경을 추적해 트랜잭션 끝에 DB와 맞춘다”는 계약이다.
데이터베이스 테이블과 매핑되는 클래스다.
@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 |
방언에 따라 자동 선택 |
엔티티를 보관·관리하는 1차 캐시(논리적 환경) 이다.
EntityManagerFactory → EntityManager → 영속성 컨텍스트
비영속(new) → 영속(managed) → 준영속(detached)
↓
삭제(removed)
| 상태 | 의미 |
|---|---|
| 비영속 |
new Member() — 컨텍스트와 무관 |
| 영속 |
persist / 조회로 관리됨 |
| 준영속 |
detach 등으로 분리 |
| 삭제 |
remove — 삭제 예약 |
| 기능 | 설명 |
|---|---|
| 1차 캐시 | 동일 트랜잭션 내 동일 엔티티 재조회 시 DB 생략 가능 |
| 동일성 | 같은 식별자 → 같은 인스턴스 (==) |
| 쓰기 지연 | SQL을 모아 커밋·flush 시점에 전송 |
| 변경 감지 | 필드만 바꿔도 UPDATE 가능 (별도 save 불필요한 경우 많음) |
| 지연 로딩 | 연관은 실제 접근 시 로딩 |
@Transactional
public void updateMember(Long id, String newName) {
Member member = memberRepository.findById(id).orElseThrow();
member.setName(newName);
// 트랜잭션 커밋 시 스냅샷과 비교 후 UPDATE
}
⚠️ 주의
save()없이도 반영되는 것은 영속 상태이고 트랜잭션 안일 때가 전제다.
| 관계 | 어노테이션 | 예시 |
|---|---|---|
| 다대일 | @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);
}
}| 방식 | 동작 | 실무 |
|---|---|---|
| EAGER | 연관을 즉시 함께 조회 | 예상 밖 JOIN·성능 이슈 가능 |
| LAZY | 사용 시점에 조회(프록시) | 연관관계는 기본 LAZY 권장 |
@ManyToOne(fetch = FetchType.LAZY)
private Team team;💡 필요한 조회는 fetch join / EntityGraph / 배치 등으로 “한 번에 가져올 그래프”를 설계한다.
정의
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인터페이스만으로 기본 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("SELECT m FROM Member m JOIN FETCH m.team WHERE m.age >= :age")
List<Member> findMembersWithTeamByAge(@Param("age") int age);@Query(value = "SELECT * FROM member WHERE name LIKE %:keyword%", nativeQuery = true)
List<Member> searchByKeyword(@Param("keyword") String keyword);@OneToMany(mappedBy = "team", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Member> members;| 옵션 | 의미 |
|---|---|
CascadeType.ALL |
persist/remove 등 전파 |
PERSIST / REMOVE
|
일부만 전파 |
orphanRemoval |
부모와 연 끊긴 자식 삭제 |
⚠️ Cascade는 부모가 자식 생명주기를 완전히 책임질 때만 좁게 쓰는 것이 안전하다.
@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 |
비권장인 경우 많음 |
- 무분별
@Setter지양 → 의도 있는 메서드로 변경 -
protected기본 생성자 (JPA 스펙) - 양방향이면 편의 메서드로 양쪽 동기화
- 가능하면 단방향부터 설계
- 연관은 LAZY 기본
- N+1은 fetch join / EntityGraph / 배치로 대응
- 화면용 조회는 DTO 직접 조회 검토
- 조회 전용
@Transactional(readOnly = true)
| 값 | 용도 |
|---|---|
create / create-drop
|
로컬·테스트 |
update |
초기 개발 (운영 비권장) |
validate / none
|
스테이징·운영 |
⚠️ 운영에서create·무분별update는 데이터 손실·스키마 불일치 위험이 크다.
리포지토리의 jpa-example 기준.
| 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} |
삭제 |
| 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기준)
- Multi_Module
- MSA (Microservices)
- Architecture
- Backend Stack
- Etc