이지은님의 블로그
250213 - Java Spring 숙련: JDBC와 JPA(영속성과 영속성 컨텍스트, Hibernate, JPQL) 본문
250213 - Java Spring 숙련: JDBC와 JPA(영속성과 영속성 컨텍스트, Hibernate, JPQL)
queenriwon3 2025. 2. 13. 20:32▷ 오늘 배운 것
과제와 강의를 병행하다보면 많은 지식을 배우더라도 까먹을 때가 많다. 뇌용량의 제한은 늘 존재해왔다. 따라서 TIL이 계속 며칠째 반복되고 있는 것 같지만 오늘도 JPA와 영속성 컨텍스트 등에 대해 학습하면서 복기 중에 다시 또 재복기를 하는 시간을 블로그 작성을 통해 가져보도록 하겠다.
오늘은 스탠다드반 세션을 보고 내용을 정리해보도록 하겠다.
<<목차>>
1. JDBC와 JPA
1) JDBC
2) JPA
2. Hibernate
1) Hibernate
2) ORM(Object-Relational Mapping)
3. 영속성
1) 영속성
2) JPA의 영속성(Persistence) - 영속성 컨텍스트
3) 영속성 컨텍스트
4. JPQL와 JOIN
1) JPA에서 JOIN을 직접 사용할 수 없는 이유
2) JPQL을 사용하여 JOIN(코틀린 문법)
3) QueryDSL
1. JDBC와 JPA
데이터 베이스에 접근하는 기술은
JDBC -> SQL MAPPER -> ORM 순서로 발전하고 출시해왔다.
앞서 배웠던 JDBC에는 쿼리를 호출함으로써 데이터 값을 가져올 수 있었지만, ORM, JPA에서는 쿼리를 작성하는 것을 생략하고, JPA와 Spring Data JPA가 제공하는 추상화 및 자동 쿼리를 생성해준다.
1) JDBC
JDBC를 사용할 때 다음과 같은 과정이 필요하다.
Connection connection = DriverManager.getConnection(DB_URL, USER, PASSWORD);
: DB에 연결
PreparedStatement statement = connection.prepareStatement(sql);
: SQL 문을 미리 컴파일하고 실행 준비
ResultSet resultSet = statement.executeQuery();
: select 쿼리문을 실행하고 결과를 저장한다.
그리고 이 모든 과정은 .close()를 사용해 리소스 해제또한 해주어야한다.(메모리 누수 방지)
Entity 하나를 생성하기 위해서는 ResultSet이 필요하게 되고, 하나씩 setter를 사용해서 값을 저장해야한다.
그리고 직접 SQL쿼리를 작성하고실해야해야하며, 트랜잭션도 다음과 같이 트랜잭션을 관리해야한다.
connection.setAutoCommit(false);
try {
statement.executeUpdate();
connection.commit();
} catch (SQLException e) {
connection.rollback();
}
2) JPA
그러나 JPA는
- Spring Data JPA의 메서드 명명 규칙에 따라 findByActiveTrue()라는 메서드를 작성하면,
내부적으로 "SELECT * FROM users WHERE active = true" 쿼리가 자동생성이 된다. - 또한 자동으로 엔티티와 매핑되기 때문에, Entity와 테이블의 칼럼이 자동으로 매핑된다.
- 자동 트랜잭션 관리, JPA는 @Transactional이 기본적으로 적용되어 있어, 별도로 트랜잭션을 관리할 필요가 없다. 그래서 Spring Data JPA가 내부적으로 트랜잭션을 시작하고, 메서드 실행이 끝나면 자동으로 커밋 또는 롤백 수행할 수 있는 기능이 있다.
명명규칙에따라 JPA메서드를 사용하면 자동으로 쿼리를 만들어주는 것도 있으나, 만약 조회해야하는 코드가 복잡할 경우 다음과 같이 @Query를 사용할 수 있다.
@Query("SELECT u FROM User u WHERE u.registrationDate BETWEEN :startDate AND :endDate")
List<User> findByRegistrationDateBetween(@Param("startDate") Date startDate, @Param("endDate") Date endDate);
2. Hibernate
1) Hibernate
Hibernate는 "자바에서 데이터베이스와 객체(엔티티)를 쉽게 연결해주는 ORM(Object-Relational Mapping) 프레임워크"
JPA와 Hibernate는 다음과 같은 차이가 있다.
JPA | 자바에서 ORM을 쉽게 할 수 있도록 제공하는 표준 인터페이스 |
Hibernate | JPA의 구현체 중 하나로, 실제로 동작하는 ORM 프레임워크 |
구분 | JPA | Hibernate |
정체 | 인터페이스(명세) | JPA 구현체 |
역할 | ORM 표준 정의 | JPA 기능 실제 구현 |
SQL 자동 생성 | x | SQL 자동 생성 |
캐싱 및 성능 최적화 | x | 1,2차 캐싱 |
트랜잭션 | x | 자동 트랜잭션 관리 |
Spring Boot에서 사용 | 단독 사용 불가 | JPA와 함께 사용 |
—> JPA = 개념(명세) / Hibernate = 실제 코드 실행하는 도구
→ Hibernate는 자바 개발자가 SQL쿼리문을 덜 사용하고, 더 쉽게 DB를 다룰 수 있도록 도와주는 ORM 프레임워크. JPA를 사용한다고 해도, 내부적으로 Hibernate가 동작한다.
2) ORM(Object-Relational Mapping)
객체 지향을 설계하면 추상화, 캡슐화, 정보은닉, 상속, 다형성이 중요하다. 하지만 SQL에 의존적으로 설계하면 이것들을 놓칠때가 많아 오히려 프로그래밍이 어려워질 수 있다.
이를 보완하고 데이터베이스를 객체 지향적으로 사용하기 위해 객체의 관계로 설계하는 것을 돕기 위해 ORM을 사용한다.(말 그대로Object-Relational Mapping...)
3. 영속성
1) 영속성
일반적으로 우리가 변수를 선언해서 데이터를 저장하면 RAM(휘발성 메모리)에 저장된다. 그럼 중요한 데이터를 유지하기 위해서는 파일이나 데이터베이스(DB)에 저장해될 것이다.
영속성이란?
"데이터가 프로그램이 종료되더라도 사라지지 않고 지속되는 것" 을 의미한다.
DB에 저장된 데이터는 프로그램이 종료되어도 유지되므로 영속적이라고 할 수 있다.
2) JPA의 영속성(Persistence) - 영속성 컨텍스트
JPA는 영속성 컨텍스트(Persistence Context) 라는 곳에 데이터를 관리함
JPA에서 엔티티 객체를 관리하는 "논리적 저장소"
영속성 컨텍스트는 Entity(객체)를 저장 관리해주는 공간이다.(DB의 데이터를 RAM(메모리)에서 관리할 수 있도록 해주는 공간)
JPA는 단순히 데이터를 DB에 저장하는 게 아니라, 한번 가져온 데이터를 영속성 컨텍스트에 보관해서 같은 데이터를 여러 번 조회해도 DB에 다시 접근하지 않도록 최적화 하고, 데이터 변경 시 자동으로 DB에 반영 될 수 있도록 관리
이 영속성 컨텍스트를 관리 할 수 있는 것이 EntityManager
- EntityManager를 생성하면 그 안에 영속성 컨텍스트 있음
- EntityManagerFactory통해 요청이 올 때 마다 EntityManager 생성
- EntityManager는 내부적으로 Connection 사용하여 DB 접근
- spring에서 EntityManager 여러 개, 영속성 컨텍스트 1개 존재
3) 영속성 컨텍스트
비영속 (Transient) | JPA와 관련 없이 단순히 생성된 객체 | 아직 영속성 컨텍스트에 포함되지 않음 |
영속 (Persistent) | EntityManager.persist(entity) 호출 후 관리됨 | 영속성 컨텍스트에서 영속된 상태 |
준영속 (Detached) | EntityManager.detach(entity)로 관리 대상에서 제외됨 | 영속성 컨텍스트에서 분리됨 |
삭제 (Removed) | EntityManager.remove(entity) 호출 후 삭제됨 | 영속성 컨텍스트에서 삭제됨 |
1️⃣ 1차 캐시
: 엔티티를 영속성 컨텍스트가 캐싱하여 동일한 트랜잭션 내에서 DB 쿼리를 최소화
첫 번째 조회 시 DB에서 데이터를 가져오지만, 이후에는 메모리에서 조회(DB까지 가지 않고 1차 캐시에서 처리)
2️⃣ 쓰기 지연
: JPA의 트랜잭션이 끝나기 전까지 INSERT, UPDATE, DELETE 같은 SQL을 바로 실행하지 않고,영속성 컨텍스트에 모아두었다가 한 번에 실행하는 방식
EntityManager.persist()를 호출해도 즉시 DB에 반영되지 않고, 트랜잭션이 끝날 때 flush()가 발생하며 한 번에 실행됨.
3️⃣ 변경 감지 (Dirty Checking)
: 트랜잭션이 끝날 때 변경된 값이 자동으로 감지되어 UPDATE 쿼리 실행(스냅샷으로 처음상태 저장해둠)
flush 전 스냅샷과 현 상태 비교 후 UPDATE 쿼리를 쓰기 지연 저장소에 추가
4️⃣ 지연 로딩 (Lazy Loading)
연관된 엔티티는 필요할 때만 데이터를 가져오는 특징
4. JPQL와 JOIN
1) JPA에서 JOIN을 직접 사용할 수 없는 이유
엔티티 간의 관계(@OneToMany, @ManyToOne)를 설정하면, JPA가 내부적으로 필요한 쿼리를 생성하지만, 우리가 직접 SQL의 JOIN을 작성할 수는 없다. (객체 지향적인 설계)
—> 해결방법: JPQL, FETCH JOIN
2) JPQL을 사용하여 JOIN(코틀린 문법)
1️⃣ JPQL을 이용한 JOIN 쿼리
(코틀린 문법)
val query = entityManager.createQuery(
"SELECT s FROM Student s JOIN s.course c WHERE c.name = :courseName", Student::class.java
)
query.setParameter("courseName", "Mathematics")
val students = query.resultList
(자바 문법)
TypedQuery<Student> query = entityManager.createQuery(
"SELECT s FROM Student s JOIN s.course c WHERE c.name = :courseName", Student.class
);
query.setParameter("courseName", courseName);
return query.getResultList();
2️⃣ @Query 어노테이션을 활용한 JPQL 기본
(코틀린 문법)
interface StudentRepository : JpaRepository<Student, Long> {
@Query("SELECT s FROM Student s JOIN s.course c WHERE c.name = :courseName")
fun findStudentsByCourse(@Param("courseName") courseName: String): List<Student>
}
(자바 문법)
public interface StudentRepository extends JpaRepository<Student, Long> {
@Query("SELECT s FROM Student s JOIN s.course c WHERE c.name = :courseName")
List<Student> findStudentsByCourse(@Param("courseName") String courseName);
}
3️⃣ @Query 어노테이션을 활용한 JPQL 기본(DTO에 담기)
(코틀린 문법)
data class StudentCourseDTO(val studentName: String, val courseName: String)
interface StudentRepository : JpaRepository<Student, Long> {
// 기본 JPA 메서드 (자동 생성되는 쿼리)
fun findByName(name: String): List<Student>
// @Query를 사용한 JPQL
@Query("SELECT new com.example.dto.StudentCourseDTO(s.name, c.name) FROM Student s JOIN s.course c")
fun findStudentCourseInfo(): List<StudentCourseDTO>
}
(자바 문법)
public interface StudentRepository extends JpaRepository<Student, Long> {
// 기본 JPA 메서드 (자동 생성되는 쿼리)
List<Student> findByName(String name);
// @Query를 사용한 JPQL
@Query("SELECT new com.example.dto.StudentCourseDTO(s.name, c.name) FROM Student s JOIN s.course c")
List<StudentCourseDTO> findStudentCourseInfo();
}
4️⃣ JPQL의 FETCH JOIN
(코틀린 문법)
val query = entityManager.createQuery(
"SELECT s FROM Student s JOIN FETCH s.course WHERE s.name = :studentName", Student::class.java
)
query.setParameter("studentName", "John Doe")
val students = query.resultList
(자바 문법)
TypedQuery<Student> query = entityManager.createQuery(
"SELECT s FROM Student s JOIN FETCH s.course WHERE s.name = :studentName", Student.class
);
query.setParameter("studentName", studentName);
return query.getResultList();
N+1 문제란? :
@OneToMany(fetch = FetchType.LAZY) 같은 설정이 되어 있으면,
1개의 SELECT 쿼리 실행 후, 관련 엔티티를 조회할 때 추가적인 N개의 쿼리가 발생하는 문제.
// order가 100개라고 가정할 때
public void getOrderList() {
List<Order> orderList = repository.findAll();
for(Order order : orderList) {
System.out.println(order.getOrderDetails());
// getOrderList를 수행하기 위해서 총 101번의 쿼리 실행
}
}
100개의 행을 조회할때 101개의 쿼리를 실행하게 된다.(findAll() + getOrderDetails() 100개)
—> 해결방법:
- 엔티티에서 연관관계 설정을 FetchType.EAGER로 변경
- JPQL로 직접 패치 조인 쿼리문 작성
- @EntityGraph 어노테이션 사용
3) QueryDSL
: JPA의 JPQL을 타입 안전한 방식으로 작성할 수 있도록 도와주는 DSL (Domain Specific Language)
코드기반으로 SQL과 비슷한 문법을 사용하는 것이 특징
QueryDSL의 장점 :
- 타입 안전성: 문자열 기반 JPQL과 달리, IDE에서 자동 완성과 오류 검출 가능
- 동적 쿼리 작성이 용이: BooleanExpression을 활용하여 조건을 쉽게 조합 가능
- 코드 가독성 향상
// QueryDSL (타입 안전, 컴파일 타임 오류 검출 가능)
val student = QStudent.student
queryFactory.selectFrom(student)
.where(student.name.eq("John Doe"))
.fetch()