1. JPA 개요
- JPA(Java Persistence API)는 객체와 관계형 데이터베이스 간 패러다임 불일치 문제를 해결하기 위해 등장.
- SQL 중심 개발을 객체 중심 개발로 전환하여 생산성 향상.
- 주요 구현체: Hibernate, EclipseLink, OpenJPA 등.
2. 영속성 컨텍스트 (Persistence Context)
- JPA에서 가장 중요한 개념!
EntityManager가 관리하는 1차 캐시 역할.- 같은 트랜잭션 내에서 동일한 엔티티는 동일한 객체로 관리됨.
-
주요 동작:
persist(): 영속 상태로 변경find(): 1차 캐시에서 조회 (없으면 DB 조회 후 캐싱)remove(): 삭제 처리merge(): 준영속 상태 객체를 영속 상태로 변경
3. 엔티티 생명주기
- 비영속 (Transient) →
new로 생성했지만 아직 JPA가 관리하지 않는 상태. - 영속 (Managed) →
persist()를 호출하면 영속성 컨텍스트에서 관리됨. - 준영속 (Detached) →
detach()호출하거나clear(),close()하면 관리 대상에서 제외됨. - 삭제 (Removed) →
remove()호출 시 DB에서 삭제됨.
4. 기본 매핑
@Entity: JPA에서 엔티티로 사용.@Id: 기본 키(primary key) 설정.-
@GeneratedValue: 자동 생성 전략.IDENTITY: DB가 자동 생성 (AUTO_INCREMENT)SEQUENCE: DB 시퀀스 사용 (Oracle 등)TABLE: 키 생성 전용 테이블 사용AUTO: DB에 따라 자동 선택
@Column(name="칼럼명"): 필드와 DB 컬럼 매핑.
5. 연관관계 매핑
JPA에서는 객체 간 연관관계를 외래 키가 아닌 객체 자체로 표현해야 함.
✅ 연관관계 종류
-
단방향 관계
@ManyToOne: 다대일 관계 (N:1)@OneToOne: 일대일 관계 (1:1)
-
양방향 관계
@OneToMany: 일대다 관계 (1:N)@ManyToMany: 다대다 관계 (연결 테이블 필요)
✅ 연관관계 설정 시 주의할 점
- 연관관계의 주인(owner)을 명확히 해야 함.
@ManyToOne이 주인이고,@OneToMany는mappedBy를 사용해 연관관계를 설정.
@Entity
class Member {
@Id @GeneratedValue
private Long id;
@ManyToOne
@JoinColumn(name = "team_id") // 외래키
private Team team;
}
@Entity
class Team {
@Id @GeneratedValue
private Long id;
@OneToMany(mappedBy = "team") // 읽기 전용
private List<Member> members = new ArrayList<>();
}6. 프록시와 지연 로딩
- JPA는 엔티티를 바로 조회하지 않고, 필요할 때 실제 데이터를 가져오는 지연 로딩(LAZY Loading)을 지원.
- 즉시 로딩(EAGER) 사용 시 성능 문제 발생 가능.
@OneToMany,@ManyToMany기본값: LAZY@ManyToOne,@OneToOne기본값: EAGER
@ManyToOne(fetch = FetchType.LAZY)
private Team team;⚠ 즉시 로딩(EAGER)은 지양하고, 필요할 때 fetch join이나 EntityGraph를 활용하자!
7. JPQL과 QueryDSL
JPA는 JPQL (Java Persistence Query Language)을 제공하여 SQL을 객체 중심으로 변환.
✅ JPQL 기본 문법
- SQL과 유사하지만 테이블이 아닌 엔티티(Member)를 대상으로 쿼리.
JOIN FETCH사용 시 연관된 엔티티도 한 번에 조회 가능.
SELECT m FROM Member m JOIN FETCH m.team⚡ QueryDSL을 활용하면 타입 안전한 쿼리 작성 가능.
JPAQueryFactory queryFactory = new JPAQueryFactory(entityManager);
QMember m = QMember.member;
List<Member> members = queryFactory.selectFrom(m)
.where(m.age.gt(18))
.fetch();8. 트랜잭션과 변경 감지 (Dirty Checking)
- JPA는 엔티티 변경 시 자동으로 업데이트 SQL 실행.
- 트랜잭션 내에서 엔티티 값을 변경하면
flush()시점에 SQL이 자동 실행됨.
@Transactional
public void updateMember(Long id) {
Member member = em.find(Member.class, id);
member.setName("변경됨"); // UPDATE SQL 실행됨 (Dirty Checking)
}⚠ 트랜잭션이 없으면 변경 사항이 반영되지 않으니 주의!
9. N+1 문제와 해결 방법
- 연관된 엔티티를
LAZY로딩할 때, 추가적인 쿼리가 반복 실행되는 문제. - 예제: 회원을 조회할 때
team을Lazy로딩하면, 회원 개수만큼SELECT쿼리가 실행됨.
✅ 해결 방법
-
Fetch Join 사용 (즉시 로딩처럼 한 번에 조회)
SELECT m FROM Member m JOIN FETCH m.team -
EntityGraph 사용
@EntityGraph(attributePaths = {"team"}) @Query("SELECT m FROM Member m") List<Member> findAllWithTeam(); -
Batch Size 설정 (
hibernate.default_batch_fetch_size)→ 한 번의 SQL로 여러 개의team데이터를 가져옴.spring.jpa.properties.hibernate.default_batch_fetch_size=100
10. 실무에서 주의할 점
- 즉시 로딩(EAGER)은 피하고, 지연 로딩(LAZY)과
fetch join을 적절히 사용하자. - JPA에서 엔티티 ID만 필요하면 조회 성능 최적화를 위해 DTO를 사용하자.
- N+1 문제는 Fetch Join, EntityGraph, Batch Size 등을 활용해 해결하자.
- 연관관계 매핑 시 양방향 관계를 신중하게 설계하고,
mappedBy설정을 올바르게 하자. - Dirty Checking을 활용해 엔티티 변경을 자동 반영하도록 설계하자.
결론
- JPA는 객체 중심 개발을 가능하게 하지만, 올바른 사용법을 익히지 않으면 성능 문제가 발생할 수 있음.
- 실무에서는 연관관계 설정, JPQL 최적화, 트랜잭션 관리, N+1 문제 해결이 핵심.
- 무조건 JPA를 사용하기보다는, 상황에 따라 Native Query, QueryDSL도 함께 활용하자!
이 링크를 통해 구매하시면 제가 수익을 받을 수 있어요. 🤗