| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | ||||||
| 2 | 3 | 4 | 5 | 6 | 7 | 8 |
| 9 | 10 | 11 | 12 | 13 | 14 | 15 |
| 16 | 17 | 18 | 19 | 20 | 21 | 22 |
| 23 | 24 | 25 | 26 | 27 | 28 | 29 |
| 30 | 31 |
- Spring
- java
- DL
- Studying
- DB
- Android
- OS
- Algorithm
- GCP
- frontend
- architecture
- Network
- docker
- 배포
- Design
- cloud
- VUE
- AI
- Kotlin
- http
- springboot
- TypeScript
- inflearn
- Database
- Python
- CS
- spring boot
- SQL
- react
- blockchain
- Today
- Total
소소한 지식 저장소
[스프링 DB 2편 - 데이터 접근 활용 기술] 5. 데이터 접근 기술 - JPA 본문
1. JPA 시작
스프링이 DI 컨테이너를 비롯한 애플리케이션 전반의 기능을 제공한다면, JPA는 ORM 기반 데이터 접근 기술을 제공한다. JdbcTemplate·MyBatis는 개발자가 SQL을 직접 작성하지만, JPA는 객체와 테이블의 매핑 정보를 바탕으로 SQL 생성·실행·결과 매핑을 처리한다.
실무에서는 JPA를 더 편리하게 쓰기 위해 스프링 데이터 JPA와 Querydsl을 함께 사용한다. 이 강의에서는 JPA의 기본 적용 흐름을 확인하고, 이후 스프링 데이터 JPA·Querydsl로 확장할 수 있는 기반을 만든다.
- JPA: 자바 ORM 표준
- 스프링 데이터 JPA: JPA 사용을 편리하게 하는 리포지토리 추상화
- Querydsl: 타입 안전한 동적 쿼리 작성 도구
2. JPA 설정
spring-boot-starter-data-jpa는 JPA, 스프링 데이터 JPA, JPA 구현체인 Hibernate를 스프링 부트와 통합한다. 이 starter가 spring-boot-starter-jdbc도 포함하므로 기존 JDBC starter는 별도로 둘 필요가 없다.
build.gradle - JPA 의존성 추가
//JPA, 스프링 데이터 JPA 추가
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
//JdbcTemplate 추가
//implementation 'org.springframework.boot:spring-boot-starter-jdbc'
build.gradle - 의존관계 전체
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
//JdbcTemplate 추가
//implementation 'org.springframework.boot:spring-boot-starter-jdbc'
//MyBatis 추가
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.2.0'
//JPA, 스프링 데이터 JPA 추가
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
//H2 데이터베이스 추가
runtimeOnly 'com.h2database:h2'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
//테스트에서 lombok 사용
testCompileOnly 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'
}
추가되는 핵심 라이브러리는 hibernate-core(JPA 구현체), jakarta.persistence-api(JPA 인터페이스), spring-data-jpa다.
main - application.properties
#JPA log
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE
test - application.properties
#JPA log
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE
org.hibernate.SQL=DEBUG는 Hibernate가 생성·실행한 SQL을, BasicBinder=TRACE는 SQL에 실제 바인딩하는 값을 출력한다. spring.jpa.show-sql=true도 가능하지만 System.out에 출력하므로 logger 기반 설정을 사용한다.
스프링 부트 3.x(Hibernate 6)에서는 바인딩 로그 설정이 바뀐다.
#JPA log
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.orm.jdbc.bind=TRACE
3. JPA 적용 1 - 엔티티 ORM 매핑
JPA에서 가장 중요한 시작점은 객체와 테이블의 매핑이다. @Entity가 붙은 객체를 엔티티라고 하며, JPA는 이 매핑 정보를 이용해 SQL을 생성한다.
Item - ORM 매핑
package hello.itemservice.domain;
import lombok.Data;
import javax.persistence.*;
@Data
@Entity
public class Item {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "item_name", length = 10)
private String itemName;
private Integer price;
private Integer quantity;
public Item() {
}
public Item(String itemName, Integer price, Integer quantity) {
this.itemName = itemName;
this.price = price;
this.quantity = quantity;
}
}
- @Entity: JPA가 관리할 객체임을 표시한다.
- @Id: 테이블 PK와 필드를 매핑한다.
- @GeneratedValue(strategy = GenerationType.IDENTITY): DB가 PK를 생성하는 IDENTITY 전략을 사용한다.
- @Column(name = "item_name", length = 10): itemName 필드를 item_name 컬럼과 연결하며, DDL 생성 시 길이 정보로도 사용한다.
- JPA 엔티티는 public 또는 protected 기본 생성자가 필수다.
스프링 부트 통합 환경은 카멜 케이스 필드명을 언더스코어 컬럼명으로 자동 변환하므로, itemName과 item_name의 단순 차이는 @Column(name = "item_name") 없이도 매핑할 수 있다.
4. JPA 적용 2 - JpaItemRepositoryV1
JPA의 모든 동작은 EntityManager를 통해 실행한다. 등록·수정·삭제는 트랜잭션 안에서 수행되어야 하므로 이 예제는 리포지토리에 @Transactional을 선언한다. 실제 서비스에서는 일반적으로 비즈니스 로직이 시작되는 서비스 계층에 트랜잭션을 둔다.
JpaItemRepositoryV1
package hello.itemservice.repository.jpa;
import hello.itemservice.domain.Item;
import hello.itemservice.repository.ItemRepository;
import hello.itemservice.repository.ItemSearchCond;
import hello.itemservice.repository.ItemUpdateDto;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;
import java.util.List;
import java.util.Optional;
@Slf4j
@Repository
@Transactional
public class JpaItemRepositoryV1 implements ItemRepository {
private final EntityManager em;
public JpaItemRepositoryV1(EntityManager em) {
this.em = em;
}
@Override
public Item save(Item item) {
em.persist(item);
return item;
}
@Override
public void update(Long itemId, ItemUpdateDto updateParam) {
Item findItem = em.find(Item.class, itemId);
findItem.setItemName(updateParam.getItemName());
findItem.setPrice(updateParam.getPrice());
findItem.setQuantity(updateParam.getQuantity());
}
@Override
public Optional<Item> findById(Long id) {
Item item = em.find(Item.class, id);
return Optional.ofNullable(item);
}
@Override
public List<Item> findAll(ItemSearchCond cond) {
String jpql = "select i from Item i";
Integer maxPrice = cond.getMaxPrice();
String itemName = cond.getItemName();
if (StringUtils.hasText(itemName) || maxPrice != null) {
jpql += " where";
}
boolean andFlag = false;
if (StringUtils.hasText(itemName)) {
jpql += " i.itemName like concat('%',:itemName,'%')";
andFlag = true;
}
if (maxPrice != null) {
if (andFlag) {
jpql += " and";
}
jpql += " i.price <= :maxPrice";
}
log.info("jpql={}", jpql);
TypedQuery<Item> query = em.createQuery(jpql, Item.class);
if (StringUtils.hasText(itemName)) {
query.setParameter("itemName", itemName);
}
if (maxPrice != null) {
query.setParameter("maxPrice", maxPrice);
}
return query.getResultList();
}
}
5. JPA 적용 3 - 구성 교체
JpaConfig
package hello.itemservice.config;
import hello.itemservice.repository.ItemRepository;
import hello.itemservice.repository.jpa.JpaItemRepositoryV1;
import hello.itemservice.service.ItemService;
import hello.itemservice.service.ItemServiceV1;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.persistence.EntityManager;
@Configuration
public class JpaConfig {
private final EntityManager em;
public JpaConfig(EntityManager em) {
this.em = em;
}
@Bean
public ItemService itemService() {
return new ItemServiceV1(itemRepository());
}
@Bean
public ItemRepository itemRepository() {
return new JpaItemRepositoryV1(em);
}
}
ItemServiceV1은 ItemRepository 인터페이스에만 의존한다. 따라서 설정에서 구현체를 JpaItemRepositoryV1으로 교체해도 서비스와 컨트롤러는 수정하지 않는다. 애플리케이션 시작 설정에서는 JpaConfig를 사용하도록 바꾸고 ItemRepositoryTest와 웹 애플리케이션을 실행해 검증한다.
스프링 부트는 EntityManagerFactory, JpaTransactionManager, DataSource 등 JPA 실행에 필요한 설정을 자동 구성한다.
6. 리포지토리 분석 - 저장·수정·단건 조회
저장: persist()
em.persist(item)은 엔티티를 영속성 컨텍스트에 저장하고, IDENTITY 전략에서는 INSERT 후 DB가 만든 PK를 item.id에 반영한다. Hibernate는 매핑 정보를 이용해 다음과 같은 INSERT SQL을 생성한다.
insert into item (id, item_name, price, quantity) values (null, ?, ?, ?)
또는
insert into item (id, item_name, price, quantity) values (default, ?, ?, ?)
또는
insert into item (item_name, price, quantity) values (?, ?, ?)
수정: 변경 감지(Dirty Checking)
update()에는 em.update()가 없다. 먼저 em.find()로 영속 상태의 엔티티를 조회한 뒤 필드만 바꾼다. JPA는 트랜잭션 커밋 시 변경된 엔티티를 감지하여 UPDATE SQL을 실행한다.
update item set item_name=?, price=?, quantity=? where id=?
테스트는 마지막에 트랜잭션을 롤백하므로 UPDATE SQL을 확인하려면 @Commit을 사용한다.
단건 조회: find()
em.find(Item.class, id)는 엔티티 타입과 PK로 대상을 조회한다. JPA는 결과를 Item 객체로 직접 변환한다. Hibernate가 생성한 SQL은 조인·복잡한 조건에도 안전하게 동작하도록 기계적인 별칭을 포함할 수 있다.
select
item0_.id as id1_0_0_,
item0_.item_name as item_nam2_0_0_,
item0_.price as price3_0_0_,
item0_.quantity as quantity4_0_0_
from item item0_
where item0_.id=?
7. 리포지토리 분석 - JPQL과 목록 조회
PK 기반 조회가 아닌 복잡한 조건 조회에는 JPQL(Java Persistence Query Language)을 사용한다. SQL이 테이블을 대상으로 한다면 JPQL은 엔티티 객체를 대상으로 한다. 따라서 from Item i의 Item은 테이블명이 아니라 엔티티 이름이며 대소문자를 구분한다.
실행된 JPQL
select i from Item i
where i.itemName like concat('%',:itemName,'%')
and i.price <= :maxPrice
JPQL을 통해 실행된 SQL
select
item0_.id as id1_0_,
item0_.item_name as item_nam2_0_,
item0_.price as price3_0_,
item0_.quantity as quantity4_0_
from item item0_
where (item0_.item_name like ('%'||?||'%'))
and item0_.price<=?
JPQL 파라미터는 :maxPrice처럼 이름으로 선언하고 query.setParameter("maxPrice", maxPrice)로 바인딩한다. JPQL 역시 문자열을 조립해야 하므로 동적 쿼리 문제는 남는다. 실무에서는 이 문제를 Querydsl로 해결하기 위해 JPA와 함께 사용하는 경우가 많다.
8. JPA 적용 4 - 예외 변환
EntityManager는 순수 JPA 기술이므로 예외가 발생하면 PersistenceException과 하위 예외, 또는 IllegalStateException, IllegalArgumentException 등을 던진다.
예외 변환 전

예외 변환 AOP가 없으면 EntityManager의 PersistenceException이 JpaItemRepositoryV1을 거쳐 서비스 계층까지 그대로 전파된다. 서비스 계층이 JPA 기술의 구체 예외에 의존하게 되는 상태다.
@Repository 기반 예외 변환 후
리포지토리에 @Repository를 붙이면 스프링은 예외 변환 AOP를 적용하고 JPA 예외를 DataAccessException 계층으로 변환한다.

EntityManager에서 발생한 PersistenceException이 리포지토리의 예외 변환 AOP 프록시를 거쳐 서비스 계층에는 DataAccessException으로 전달되는 관계만 보여 준다.
- EntityManager에서 JPA 예외가 발생한다.
- JpaItemRepositoryV1 호출 경로의 예외 변환 AOP 프록시가 예외를 가로챈다.
- PersistenceExceptionTranslator가 JPA 예외를 분석한다.
- 스프링 데이터 접근 예외 추상화인 DataAccessException으로 변환한다.
- 서비스 계층은 구현 기술의 세부 예외 대신 스프링의 일관된 예외 계층을 받는다.
스프링 부트는 PersistenceExceptionTranslationPostProcessor를 자동 등록하며, 실제 JPA 예외 변환에는 EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible()이 사용된다.
9. 최종 요약 정리
| 주제 | 핵심 내용 |
| JPA | 객체와 테이블의 매핑 정보를 기반으로 SQL 생성·실행·결과 매핑을 처리하는 자바 ORM 표준이다. |
| 엔티티 | @Entity, @Id, @GeneratedValue, @Column으로 객체와 테이블을 매핑한다. |
| EntityManager | persist, find, JPQL 실행 등 JPA의 핵심 동작을 수행한다. |
| 변경 감지 | 영속 엔티티의 필드를 바꾸면 커밋 시점에 JPA가 UPDATE SQL을 실행한다. |
| JPQL | 테이블이 아닌 엔티티 객체를 대상으로 작성하는 객체지향 쿼리 언어다. |
| 동적 쿼리 | JPQL에서도 문자열 조립 문제가 남으며, 실무에서는 Querydsl을 함께 사용한다. |
| 예외 변환 | @Repository와 예외 변환 AOP가 JPA 예외를 DataAccessException으로 변환한다. |
'INFLEARN' 카테고리의 다른 글
| [스프링 DB 2편 - 데이터 접근 활용 기술] 7. 데이터 접근 기술 - Querydsl (0) | 2026.07.26 |
|---|---|
| [스프링 DB 2편 - 데이터 접근 활용 기술] 6. 데이터 접근 기술 - 스프링 데이터 JPA (0) | 2026.07.19 |
| [스프링 DB 2편 - 데이터 접근 활용 기술] 3. 데이터 접근 기술 - 테스트 (0) | 2026.07.17 |
| [스프링 DB 2편 - 데이터 접근 활용 기술] 2. 데이터 접근 기술 - 스프링 JdbcTemplate (0) | 2026.07.13 |
| [스프링 DB 2편 - 데이터 접근 활용 기술] 1. 데이터 접근 기술 - 시작 (0) | 2026.07.12 |
