leehyeon-dv 님의 블로그
[UMC] JPA 활용 본문
⭐ 목차
- 영속성 컨텍스트란?
- 왜 영속상태가 좋을까
- 지연로딩(FetchType.LASY)
- N=1
- JPQL
- QueryDSL
- 실습
목표 = JPA의 영속성 컨텍스트의 개념, JPQL과 QueryDSL의 차이점, QueryDSL이 가지는 유리함에 대해 알기
📌영속성 컨텍스트란?
JPA의 핵심개념 중 하나로 엔티티 객체를 영구적으로 저장하고 관리하는 일종의 메모리 공간이다
쉽게 말하면, 애플리케이션 내부에 존재하는 가상의 데이터 베이스라고 이해할 수 있다
왜필요할까?
→ ORM은 객체와 관계형 데이터베이스를 자동으로 매핑해주는 기술이다. JPA는 이러한 ORM기술을 제공하며 영속성 컨텍스트는 JPA에서 엔티티 객체를 효율적으로 관리하기위해 존재한다
동작방식
데이터를 조회하거나 저장시, JPA는 다음과 같은 흐름으로 동작합니다
1. 엔티티를 조회할때
• 먼저 영속성 컨텍스트에서 해당 엔티티가 있는지 확인합니다
• 없으면 데이터베이스에 쿼리를 날려 조회 후 해당 엔티티를 영속성 컨텍스트에 저장합니다
2. 같은 엔티티에 다시 접근할때
영속성 컨텍스트에 이미 존재하면 데이터 베이스에 접근하지 않고 캐시된 엔티티를 반환합니다
영속성 컨텍스트의 주요 특징
• 1차 캐시 역할 : 동일한 트랜잭션 내에서 동일한 엔티티는 한번만 조회합니다
• 엔티티의 생명 주기 관리 : 엔티티의 생성, 변경, 삭제 등의 상태를 추적하고 반영합니다
• 변경 감지 : 트랜잭션이 끝나기 전까지 엔티티 객체의 변경사항을 추적해 DB에 자동으로 반영합니다
• 지연로딩 지원 : 필요할때만 DB에서 데이터를 로딩합니다
@EntityManager을 이용해 접근합니다
📌왜 영속상태가 좋을까?
1. EntityManager의 변경감지
Manager 객체의 이름 필드값을 수정하는 로직이 있다고 할때 트랜잭션이 끝나면 JPA는 영속성 컨텍스트 내부에서 변경된 내용을 자동으로 감지하고 DB에 UPDATE 쿼리를 날려 수정된 내용을 반영한다
// EntityManager을 통해 영속성 컨텍스트에 접근한다.
@EntityManager em = entityManagerFactory.createEntityManager();
em.getTransaction().begin(); // 트랜잭션 시작
// 영속성 컨텍스트 최초 저장: EntityManager을 이용하여 객체를 조회하고, 영속성 컨텍스트에 저장한다.
Member member = em.find(Member.class, 1L);//두번쨰 인자는 key값
// 1차 캐시: DB에 쿼리 발생하지 않고, 1차 캐시를 이용하여 동일 객체를 반환해준다.
Member sameMember = em.find(Member.class, 1L);
// 해당 멤버 객체의 필드값 수정
member.setName("UMC7기최고"); // 이 시점에서는 아직 DB에 저장되지 않은 상태
// 트랜잭션이 커밋될 때, 비로소 변경사항이 반영되어 DB가 업데이트 된다.
em.getTransaction().commit();
📌 지연로딩(FetchType.LASY)
(즉시로딩 = FetchType.EAGER) - DB에서 한번에 조회해 하나의 쿼리로 가져옴
지연로딩은 DB가 아닌 프록시에서 데이터를 가져온다
예를들어 아래와 같을 경우
@Entity
@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class MemberPrefer extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
private Member member;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "category_id")
private FoodCategory foodCategory;
}
MemberPrefer과 Member, FoodCategory가 각각 분리되어 MemberPrefer은 DB, Member과 FoodCategory는 프록시를 조회합니다 (지연로딩은 N+1문제가 발생할 수 있기 때문입니다 )
📌N+1
RDB와 객체 지향의 패러다임 간극에 의해 발생하는 문제
1개의 쿼리를 실행한 후 관련된 N개의 데이터를 각가 가져오기 위해 추가적으로 N번의 불필요한 쿼리가 실행되어 저하가 발생한다
객체는 연관관계를 통해 레퍼런스를 가지고 있으면언제든지 메모리 내에서 Random Access를 통해 연관 객체에 접근할 수 있지만 RDB의 경우 SELECT 쿼리를 통해서만 조회할 수 있기 때문입니다
📌JPQL
JPA의 일부로 쿼리를 데이터베이스의 테이블이 아닌 JPA엔티티 즉, 객체를 대상으로 작성하는 객체 지향 쿼리 언어이다
JPQL사용법
1. EntityManager 인터페이스 : Native SQL과 병행 사용이 가능하고 트랜잭션관리, 엔티티 작업관리에 탁월
2. repository 인터페이스 : 간결함과 일관성 유지에 탁월함
🤔member엔팉를 가지고 '베뉴'라는 이름을 가지고, ACTIVE 상태인 회원을 조회하고 싶다는 요구사항이 있을때 어떻게 해야할까
1. 메서드 이름으로 쿼리 생성
Spring Data JPA는 메서드 이름을 기반으로 쿼리를 생성해준다
즉 이름과 상태를 조건으로 내세운 findByNameAndStatus라는 메서드 이름을 정의하면 name필드와 status를 동시에 조건으로 하는 JPQL 쿼리를 생성한다
public interface MemberRepository extends JpaRepository<Member, Long> {
List<Member> findByNameAndStatus(String name, MemberStatus status);
}
List<Member> member = memberRepository.findByNameAndStatus("베뉴", MemberStatus.ACTIVE);
이렇게 하면 Spring Data JPA는 다음과 같은 쿼리를 자동으로 실행한다
SELECT * FROM member WHERE name = '베뉴' AND status = 'ACTIVE';
2. @Query 어노테이션
JPQL을 직접 작성하는 방법
더 복잡한 조건이나 커스터마이징이 필요한 쿼리를 작성할 때 유리하다
public interface MemberRepository extends JpaRepository<Member, Long> {
@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);
}
@Query : 이름과 상태를 조건으로 조회하는 JPQL 쿼리 작성
@Param : JPQL 쿼리에서 사용되는 :name과 :staus파라미터를 메서드의 인자와 연결
📌QueryDSL
타입 안전성을 보장하는 자바 기반의 쿼리 빌더 라이브러리
컴파일시점에 오류를 잡을 수 있고 동적 쿼리 작성이 편리하고 메서드 체이닝을 통한 복잡한 쿼리 작성에 유리합니다
실습]
QueryDSL적용하기
1. Q클래스를 자동으로 생성하기 위한 설정을 먼저함
build.gradle파일에 플러그인과 종속성을 명시해준다
plugins {
id 'java'
id 'org.springframework.boot' version '3.0.0'
id 'io.spring.dependency-management' version '1.0.15.RELEASE'
id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
}
group = 'com.example'
version = '1.0.0'
sourceCompatibility = '17'
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.hibernate.orm:hibernate-core:6.0.2.Final' // Hibernate 6.0.2 이상
implementation 'mysql:mysql-connector-java:8.0.33' // MySQL 드라이버 추가
// queryDSL
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.3'
implementation 'com.fasterxml.jackson.core:jackson-annotations:2.13.3'
implementation 'com.fasterxml.jackson.core:jackson-core:2.13.3'
compileOnly 'org.projectlombok:lombok:1.18.22'
annotationProcessor 'org.projectlombok:lombok:1.18.22'
implementation 'org.hibernate:hibernate-core:5.6.9.Final'
implementation 'org.springdoc:springdoc-openapi-ui:1.6.9'
implementation 'org.springdoc:springdoc-openapi-data-rest:1.6.9'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
sourceSets {
main {
java {
srcDirs = ['src/main/java', 'src/main/resources']
}
}
}
tasks.named('test') {
useJUnitPlatform()
}
// Querydsl 설정부
def generated = 'src/main/generated'
querydsl {
jpa = true
querydslSourcesDir = generated
}
sourceSets {
main.java.srcDir generated
}
compileQuerydsl{
options.annotationProcessorPath = configurations.querydsl
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
querydsl.extendsFrom compileClasspath
}
2. Q클래스 생성
build.gradle에 해당 QueryDSL을 위한 플러그인과 종속성 설치가 완료되면 각 엔티티에 대해 Q클래스가 자동으로 생성된다
Q클래스를 위한 디렉토리를 src/main/generated로 해줬기때문에 해당 경로에 현재 클래스가 작성된걸 볼 수 있다
3. QueryDSL설정 파일 만들기
쿼리를 작성하기전에 QueryDSL을 사용하려면 JPAQueryFactory를 통해 쿼리를 작성해야한다
따라서 JPAQueryFactory를 Bean으로 등록하는 과정이 필요하다
@Configuration
@RequiredArgsConstructor
public class QueryDSLConfig {
private final EntityManager entityManager;
@Bean
public JPAQueryFactory jpaQueryFactory(){
return new JPAQueryFactory(entityManager);
}
}
4. QueryDSL 쿼리 작성
동적 쿼리를 작성하는 방법에는 BooleanBuilder와 Where다중 파라미터 부분이 있습니다
BooleanBuilder는 QueryDSL에서 여러개의 조건을 조합하기 위해 제공되는 빌더 클래스입니다
즉 여러 조건을 동적으로 추가할 수 있다는 장점이 있기 때문에 name이나 score 파라미터가 있는지에 대한 여부를 반영해 조건이 됩니다
.and() 나 .or()등의 간단한 논리 연산으로 Where문에 조건을 연결할 수 있습니다
실습하면서 쿼리를 작성해봅시다 ( ̄︶ ̄)↗
1. Repository 패키지에 새로운 패키지 StoreRepository를 만듭니다(Store관련 respository인터페이스들과 구현 클래스들 패키지)
( StoreRepository )
( StoreRepositoryCustom)
(StoreRepositoryImpl)
이걸 서비스 단에서 호출하겠습니다
StoreQueryService인터페이스와 이를 구현한 StoreQueryServiceImpl구현체 클래스를 만듭시다
(StoreQueryService)
(StoreQueryServiceImpl)
이제 테스트를 위한 더미 데이터를 sql로 삽입하겠습니다
INSERT INTO region (id, name, created_at, updated_at)
VALUES (1, '서울', NOW(), NOW()),
(2, '부산', NOW(), NOW()),
(3, '인천', NOW(), NOW());
INSERT INTO store (id, name, address, score, region_id, created_at, updated_at)
VALUES (1, 'Store 1', '서울시 서대문구 이화여대길 52', 4.5, 1, NOW(), NOW()),
(2, 'Store 2', '서울시 마포구 연남동', 3.8, 1, NOW(), NOW()),
(3, 'Store 3', '서울시 동작구 흑석동', 2.2, 1, NOW(), NOW()),
(4, '요아정', '서울시 용산구 이태원동', 4.0, 1, NOW(), NOW()),
(5, '요아정', '서울시 서대문구 이화여대길 52', 3.2, 1, NOW(), NOW()),
(6, '요아정', '서울시 강남구 대치동', 4.5, 1, NOW(), NOW());
INSERT INTO mission (id, mission_spec, store_id, created_at, updated_at)
VALUES (1, 'Store 1-미션 1', 1, NOW(), NOW()),
(2, 'Store 1-미션 2', 1, NOW(), NOW()),
(3, 'Store 2-미션 1', 2, NOW(), NOW()),
(4, 'Store 3-미션 1', 3, NOW(), NOW());
INSERT INTO review (id, body, score, store_id, created_at, updated_at)
VALUES (1, '너무 좋아요!', 5.0, 1, NOW(), NOW()),
(2, '분위기 짱~', 3.0, 1, NOW(), NOW()),
(3, '서비스가 좋습니다', 4.8, 2, NOW(), NOW()),
(4, '음식이 맛있고 사장님이 친절해요', 4.5, 3, NOW(), NOW());
그러면 다음과같이 store테이블에 더미데이터가 잘 들어왔는지 확인할 수 있습니다
'백엔드 > spring Boot' 카테고리의 다른 글
[UMC] JPA활용 미션 (0) | 2025.05.07 |
---|---|
[UMC] JPA 기초 및 프로젝트 구조 (0) | 2025.04.28 |
데이터베이스 만들기 (0) | 2025.01.25 |
스프링 환경 갖추기 (0) | 2024.12.18 |
RESTful 웹 서비스 사용 (0) | 2024.11.25 |