728x90

1. 영속성 컨텍스트(Persistence Context)란?
영속성 컨텍스트는 엔티티 객체를 영구적으로 저장하고 관리하는 메모리 공간이다.
일종의 1차 캐시처럼 동작하며, JPA는 이를 통해 DB에 직접 접근하지 않고 객체를 효율적으로 관리한다.
주요 기능
- 1차 캐시
- 변경 감지(Dirty Checking)
- 지연 로딩(Lazy Loading)
예제: 변경 감지
Member member = em.find(Member.class, 1L);
member.setName("변경된 이름");
em.getTransaction().commit(); // UPDATE 쿼리 자동 실행
2. FetchType.EAGER vs LAZY
즉시 로딩 (EAGER)
- 연관된 모든 엔티티를 즉시 조인(fetch join)으로 가져온다.
- N+1 문제가 발생할 수 있다.
지연 로딩 (LAZY)
- 실제로 엔티티에 접근하는 순간에 쿼리가 발생한다.
- 성능 최적화에 유리하다.
@ManyToOne(fetch = FetchType.LAZY)
private Member member;
실무에서는 기본적으로 LAZY
를 사용하는 것이 좋다.
3. N+1 문제란?
1개의 쿼리를 실행한 뒤, 연관된 엔티티를 각각 가져오기 위해 N개의 추가 쿼리가 발생하는 문제다.
List<Member> members = memberRepository.findAll(); // 1번 쿼리
members.get(0).getMemberPreferList(); // +N번 쿼리 발생
해결 방법
- FetchType.LAZY 설정
- @EntityGraph 또는 fetch join 활용
4. JPQL (Java Persistence Query Language)
JPQL은 SQL과 유사하지만, 테이블이 아닌 엔티티를 대상으로 쿼리를 작성하는 언어다.
JPA는 이를 SQL로 변환해 DB와 통신한다.
방식 1. 메서드 네이밍
List<Member> findByNameAndStatus(String name, MemberStatus status);
실행되는 JPQL:
SELECT m FROM Member m WHERE m.name = :name AND m.status = :status
방식 2. @Query 어노테이션 사용
@Query("SELECT m FROM Member m WHERE m.name = :name AND m.status = :status")
List<Member> findByNameAndStatus(@Param("name") String name, @Param("status") MemberStatus status);
5. QueryDSL: 타입 안전한 동적 쿼리
QueryDSL은 컴파일 시점에 오류를 잡을 수 있는 자바 기반의 쿼리 빌더다.
복잡한 조건 조합이나 동적 쿼리를 작성할 때 유리하다.
동적 쿼리란?
실행 시점에 쿼리 조건이나 구조가 유동적으로 변하는 쿼리를 말한다.
예를 들어, 검색 조건을 사용자가 넣을 수도 있고 안 넣을 수도 있을 때 필요하다.
BooleanBuilder builder = new BooleanBuilder();
if (name != null) {
builder.and(store.name.eq(name));
}
if (score != null) {
builder.and(store.score.goe(score));
}
List<Store> result = queryFactory
.selectFrom(store)
.where(builder)
.fetch();
Spring Data JPA는 단순한 Repository 기반 CRUD에서 끝나는 것이 아니라,
영속성 컨텍스트와 성능 최적화 전략, JPQL과 QueryDSL을 활용한 복잡 쿼리 처리까지 이해하고 적용해야 제대로 쓸 수 있다.
관계형 데이터와 객체 지향 사이의 간극을 JPA가 어떻게 메꿔주는지,
성능 병목이 어디서 생기는지를 이해하는 것이 JPA 고급 활용의 핵심이다.
식당 미션 앱 Query DSL 실습
Q 클래스
QUserMission um = QUserMission.userMission;
QMission m = QMission.mission;
QStore s = QStore.store;
QRegion r = QRegion.region;
QUser u = QUser.user;
QFoodCategory fc = QFoodCategory.foodCategory;
QReview review = QReview.review;
1-1. 진행중 미션
List<Tuple> inProgress = queryFactory
.select(
m.missionContent,
m.rewardPoint,
s.storeName,
um.status
)
.from(um)
.join(um.mission, m)
.join(m.store, s)
.where(
um.status.eq(MissionStatus.IN_PROGRESS),
um.user.id.eq(userId)
)
.orderBy(um.updatedAt.desc())
.offset(page * size)
.limit(size)
.fetch();
1-2. 진행 완료 미션
List<Tuple> done = queryFactory
.select(
m.missionContent,
m.rewardPoint,
s.storeName,
um.status
)
.from(um)
.join(um.mission, m)
.join(m.store, s)
.where(
um.status.eq(MissionStatus.DONE),
um.user.id.eq(userId)
)
.orderBy(um.updatedAt.desc())
.offset(page * size)
.limit(size)
.fetch();
2-1. 완료 미션 수
Integer count = queryFactory
.select(u.completedMission)
.from(u)
.where(u.id.eq(userId))
.fetchOne();
2-2. 안암동에서 도전 가능한 미션 목록
List<Tuple> available = queryFactory
.select(
m.missionTitle,
m.missionContent,
m.rewardPoint,
m.endDate,
s.storeName,
fc.categoryName,
Expressions.numberTemplate(Integer.class, "DATEDIFF({0}, CURRENT_DATE)", m.endDate)
)
.from(m)
.join(m.store, s)
.join(s.region, r)
.leftJoin(s.category, fc)
.leftJoin(um).on(um.mission.eq(m).and(um.user.id.eq(userId)))
.where(
r.district.eq("안암동"),
um.mission.isNull() // 아직 도전 안 한 미션만
)
.orderBy(m.endDate.asc())
.offset(page * size)
.limit(size)
.fetch();
3. 마이페이지
Tuple myPage = queryFactory
.select(
u.name,
u.email,
u.isPhoneVarified,
u.point,
Expressions.stringTemplate("COALESCE({0}, '미인증')", u.phoneNum)
)
.from(u)
.where(u.id.eq(userId))
.fetchOne();
728x90
'Server > Spring' 카테고리의 다른 글
[Spring/프로젝트] Process Builder로 Python 코드 스케줄링 (0) | 2025.05.11 |
---|---|
[Spring/프로젝트] 카드뉴스 생성하기 Python Pillow + Open AI API (0) | 2025.05.09 |
[Spring/공부] Spring JPA와 프로젝트 구조 (0) | 2024.11.20 |
[Spring/공부] Spring Boot 코어 개념 정리 (0) | 2024.11.05 |