JPA 시작
스프링과 JPA는 자바 엔터프라이즈(기업) 시장의 주력 기술이다.
스프링이 DI 컨테이너를 포함한 애플리케이션 전반의 다양한 기능을 제공한다면,
JPA는 ORM 데이터 접근 기술을 제공한다
JPA는 스프링 만큼이나 방대하고, 학습해야할 분량도 많다.
하지만 한번 배워두면 데이터 접근 기술에서 매우 큰 생산성 향상을 얻을 수 있다.
대표적으로 JdbcTemplate이나 MyBatis 같은 SQL 매퍼 기술은 SQL을 개발자가 직접 작성해야 하지만,
JPA를 사용하면 SQL도 JPA가 대신 작성하고 처리해준다
실무에서는 JPA를 더욱 편리하게 사용하기 위해 스프링 데이터 JPA와 Querydsl이라는 기술을 함께 사용한다.
중요한 것은 JPA이다. 스프링 데이터 JPA, Querydsl은 JPA를 편리하게 사용하도록 도와주는 도구라 생각하면 된다.
JPA 설정
spring-boot-starter-data-jpa
라이브러리를 사용하면 JPA와 스프링 데이터 JPA를 스프링 부트와 통합하고, 설정도 아주 간단히 할 수 있다.
spring-boot-starter-data-jpa 라이브러리를 사용해서 간단히 설정하는 방법을 알아보자.
build.gradle 에 다음 의존 관계를 추가한다.
//JPA, 스프링 데이터 JPA 추가
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
[주석처리]
//JdbcTemplate 추가
// implementation 'org.springframework.boot:spring-boot-starter-jdbc'
// //MyBatis 추가
// implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.2.0'
spring-boot-starter-data-jpa 는 spring-boot-starter-jdbc 도 함께 포함(의존)한다.
따라서 해당 라이브러리 의존관계를 제거해도 된다.
참고로 mybatis-spring-boot-starter 도 spring-bootstarter-jdbc 를 포함하기 때문에 제거해도 된다
(결국 jdbc를 편리하게 사용하기 위해 만들어진것들이니까)
application.properties 에 다음 설정을 추가하자.
main,test 둘다
#JPA log
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE
org.hibernate.SQL=DEBUG
하이버네이트가 생성하고 실행하는 SQL을 확인할 수 있다. (하이버네이트 == jpa 구현체)
org.hibernate.type.descriptor.sql.BasicBinder=TRACE
SQL에 바인딩 되는 파라미터를 확인할 수 있다.
spring.jpa.show-sql=true
참고로 이런 설정도 있다.
이전 설정은 logger 를 통해서 SQL이 출력된다.
이 설정은 System.out 콘솔을 통해서 SQL이 출력된다. 따라서 이 설정은 권장하지는 않는다.
(둘다 켜면 logger , System.out 둘다 로그가 출력되어서 같은 로그가 중복해서 출력된다.)
JPA 적용1 - 개발
JPA에서 가장 중요한 부분은 "객체와 테이블을 매핑하는 것"이다.
JPA가 제공하는 애노테이션을 사용해서 Item 객체와 테이블을 매핑해보자
Item - ORM 매핑
package hello.itemservice.domain;
import javax.persistence.Column; //persistence 라이브러리를 사용한다.
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import lombok.Data;
@Data
@Entity // jpa에서 관리하는 객체라는것을 알리기 위함. [ 테이블이랑 같이 매핑이 되서 관리가 되는 객체]
//@Table(name = "item") 이렇게 테이블명도 지정할수있지만 객체의 이름과 테이블명이 같으면 생략가능하다.
public class Item {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) //pk를 알려줘야한다,어떤 방식으로 id값이 생성되는지 알려준다.
//identity 전략은 db에서 id값을 넣어주는 전략 autoincrement처럼
private Long id;
@Column(name = "item_name", length = 10) //어떤 컬럼과 매핑이 되는지 알려주기 위해 컬럼명을 적어준다, length는 해당 컬럼의 길이 설정
private String itemName;
//컬럼명과 필드명이 같으면 @Column을 따로설정할 필요없다.
private Integer price;
private Integer quantity;
//jpa는 public 또는 protected의 접근제어자를 가진 기본생성자가 필수이다.
public Item() {
}
public Item(String itemName, Integer price, Integer quantity) {
this.itemName = itemName;
this.price = price;
this.quantity = quantity;
}
}
@Entity
JPA가 사용하는 객체라는 뜻이다.
이 에노테이션이 있어야 JPA가 인식할 수 있다.
이렇게 @Entity 가 붙은 객체를 JPA에서는 엔티티라 한다.
@Id :
테이블의 PK와 해당 필드를 매핑한다.
@GeneratedValue(strategy = GenerationType.IDENTITY) :
PK 생성 값을 데이터베이스에서 생성하는 IDENTITY 방식을 사용한다. 예) MySQL auto increment
@Column(name = "item_name", length = 10)
@Column :
객체의 필드를 테이블의 컬럼과 매핑한다.
name = "item_name" :
객체는 itemName 이지만 테이블의 컬럼은 item_name 이므로 이렇게 매핑했다
length = 10 :
JPA의 매핑 정보로 DDL( create table )도 생성할 수 있는데, 그때 컬럼의 길이 값으로 활용된다. ( varchar 10 )
@Column 을 생략할 경우 필드의 이름을 테이블 컬럼 이름으로 사용한다.
참고로 지금처럼 스프링 부트와 통합해서 사용하면 필드 이름을 테이블 컬럼 명으로 변경할 때
객체 필드의 카멜 케이스를 테이블 컬럼의 언더스코어로 자동으로 변환해준다
예시) itemName -> item_name
따라서 위 예제의 @Column(name = "item_name") 를 생략해도 된다
//jpa는 public 또는 protected의 접근제어자를 가진 기본생성자가 필수이다.
public Item() {
}
jpa spec에 이걸 가지고 프록시 기술을 사용하기 때문에 필요하다고 한다.
어려운 부분이라 그냥 꼭 필요하구나 하고 넘어가면된다. [ jpa강의에서 설명]
이렇게 하면 기본 매핑은 모두 끝난다. 이제 JPA를 실제 사용하는 코드를 작성해보자.
우선 코드를 작성하고 실행하면서 하나씩 알아보자
JpaItemRepositoryV1
@Slf4j
@Repository
@Transactional
/*
jpa의 모든 데이터변경은 트랜잭션안에서 이루어진다. 그래서 항상 필요하다. 이 어노테이션은 클래스레벨 또는 메서드레벨에 붙일수있다.
클래스레벨에 붙이지 않을거라면 save, update 메소드에 붙이면 된다. 조회는 데이터변경이 아니니까.
* */
public class JpaItemRepositoryV1 implements ItemRepository {
//jpa에는 EntityManager라는 의존관계주입을 받아야한다, 스프링이 알아서 만들어서 빈에 넣어놨음
private final EntityManager em;
//생성자가 하나이므로 @Autowired를 생략할 수 있다, 생성자를 통해 의존관계 주입
public JpaItemRepositoryV1(EntityManager em) {
this.em = em;
}
@Override
public Item save(Item item) {
//persist -> 영구희 보존하다. ( 지속하다)
em.persist(item); // persist()를 사용하면 Item 필드를 가지고 sql를 만들어서 insert해준다. 그리고 id값도 가져와서 전달인자에 넣어준다.
return item;
}
@Override
public void update(Long itemId, ItemUpdateDto updateParam) {
Item findItem = em.find(Item.class, itemId);
findItem.setItemName(updateParam.getItemName()); // 객체의 값을 바꾸듯이 set을 이용해서 값을 바꿔주면 알아서 데이터베이스에 적용된다.
findItem.setPrice(updateParam.getPrice());
findItem.setQuantity(updateParam.getQuantity());
//이렇게만 해줘도 나중에 commit될때 jpa가 sql를 만들어서 db에 날려준다. [commit을 이 메서드(update)가 끝날때 진행된다.]
//마치 자바 collection을 이용해서 값을 변경하는것처럼 데이터를 변경할 수 있다.
}
@Override
public Optional<Item> findById(Long id) {
Item item = em.find(Item.class, id); // 찾으려는 엔티티 클래스,primary key를 넣어준다.
return Optional.ofNullable(item); //혹시 null일수도 있으니까 Optional.ofNullable() 사용
}
@Override
public List<Item> findAll(ItemSearchCond cond) {
/*
위의처럼 기본키를 넣는게 아닌 , 조건이 여러가지인 경우 jpa는 sql이 아닌 jpql이라는것을 사용한다.
jpql은 객체 쿼리 언어라고 한다. jpql은 sql과 거의 비슷한데 테이블을 대상으로 하는것이 아닌 엔티티를 대상으로 한다.
*/
String jpql = "select i from Item i";
/*
여기서 Item은 엔티티객체인 Item.class를 말하는것이고 i는 sql의 as(alias==별칭) 처럼 Item의 별칭설정이다.
하지만 jpql도 동적쿼리에 약하다.
아래는 jppql을 이용해서 동적쿼리를 만들때 코드
*/
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";
}
//예전 jdbcTemplate을 이용해서 동적쿼리 짜는것과 비슷하다. 다른건 이름기반(필드이름)의 파라미터를 넣을 수 있다.
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();
}
}
private final EntityManager em
생성자를 보면 스프링을 통해 엔티티 매니저( EntityManager ) 라는 것을 주입받은 것을 확인할 수 있다.
JPA의 모든 동작은 엔티티 매니저를 통해서 이루어진다.
엔티티 매니저는 내부에 데이터소스를 가지고 있고, 데이터베이스에 접근할 수 있다.
@Transactional
JPA의 모든 데이터 변경(등록, 수정, 삭제)은 트랜잭션 안에서 이루어져야 한다.
조회는 트랜잭션이 없어도 가능하다.
변경의 경우 일반적으로 서비스 계층에서 트랜잭션을 시작하기 때문에 문제가 없다.
하지만 이번 예제에서는 복잡한 비즈니스 로직이 없어서 서비스 계층에서 트랜잭션을 걸지 않았다.
JPA에서는 데이터 변경시 트랜잭션이 필수다.
따라서 리포지토리에 트랜잭션을 걸어주었다. [예시를 위해]
다시한번 강조하지만 일반적으로는 비즈니스 로직을 시작하는 서비스 계층에 트랜잭션을 걸어주는 것이 맞다
[하다보면 repository에 걸어주는경우도있다 ( 서비스가 없는경우)]
참고
JPA를 설정하려면 EntityManagerFactory , JPA 트랜잭션 매니저( JpaTransactionManager ), 데이터소스 등등 다양한 설정을 해야 한다. 스프링 부트는 이 과정을 모두 자동화 해준다.
main() 메서드 부터 시작해서 JPA를 처음부터 어떻게 설정하는지는 JPA 기본편을 참고하자.
그리고 스프링 부트의 자동 설정은 JpaBaseConfiguration 를 참고하자.
JpaConfig
@Configuration
@RequiredArgsConstructor
public class JpaConfig {
private final EntityManager em;
@Bean
public ItemService itemService() {
return new ItemServiceV1(itemRepository());
}
@Bean
public ItemRepository itemRepository() {
return new JpaItemRepositoryV1(em);
}
}
테스트를 실행하자 -> 성공
[현재 test의 application.properties에는 db설정이 주석처리 되어있다. -> 임베디드 db를 이용해서 테스트를 진행중
https://keeeeeepgoing.tistory.com/237]
JPA 적용2 - JpaItemRepositoryV1 분석
save() - 저장
public Item save(Item item) {
//persist -> 영구희 보존하다. ( 지속하다)
em.persist(item); // persist()를 사용하면 Item 필드를 가지고 sql를 만들어서 insert해준다. 그리고 id값도 가져와서 전달인자에 넣어준다.
return item;
}
JPA에서 객체를 테이블에 저장할 때는 엔티티 매니저가 제공하는 persist() 메서드를 사용하면 된다.
JPA가 만들어서 실행한 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 (?, ?, ?)
이런식으로 sql을 만들어준다.
JPA가 만들어서 실행한 SQL을 보면 id 에 값이 빠져있는 것을 확인할 수 있다.
PK 키 생성 전략을 IDENTITY 로 사용했기 때문에 JPA가 이런 쿼리를 만들어서 실행한 것이다.
물론 쿼리 실행 이후에 Item 객체의 id 필드에 데이터베이스가 생성한 PK값이 들어가게 된다.
(JPA가 INSERT SQL 실행 이후에 생성된 ID 결과를 받아서 넣어준다)
PK매핑참고
@Entity
public class Item {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
item 엔티티에 pk매핑을 한 코드
update() - 수정
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());
}
JPA가 만들어서 실행한 SQL
update item set item_name=?, price=?, quantity=? where id=?
em.update() 같은 메서드를 전혀 호출하지 않았다.
그런데 어떻게 UPDATE SQL이 실행되는 것일까?
JPA는 트랜잭션이 커밋되는 시점에, 변경된 엔티티 객체가 있는지 확인한다.
특정 엔티티 객체가 변경된 경우에는 UPDATE SQL을 실행한다.
JPA가 어떻게 변경된 엔티티 객체를 찾는지 명확하게 이해하려면 영속성 컨텍스트라는 JPA 내부 원리를 이해해야 한다. 이 부분은 JPA 기본편에서 자세히 다룬다.
지금은 트랜잭션 커밋 시점에 JPA가 변경된 엔티티 객체를 찾아서 UPDATE SQL을 수행한다고 이해하면 된다
테스트의 경우 마지막에 트랜잭션이 롤백되기 때문에 JPA는 UPDATE SQL을 실행하지 않는다.
[@Transactional 이 붙어있어서]
테스트에서 UPDATE SQL을 확인하려면 @Commit 을 붙이면 확인할 수 있다.
findById() - 단건 조회
public Optional<Item> findById(Long id) {
Item item = em.find(Item.class, id);
return Optional.ofNullable(item);
}
JPA에서 엔티티 객체를 PK를 기준으로 조회할 때는 find() 를 사용하고 조회 타입과, PK 값을 주면 된다.
그러면 JPA가 다음과 같은 조회 SQL을 만들어서 실행하고, 결과를 객체로 바로 변환해준다.
JPA가 만들어서 실행한 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=?
JPA(하이버네이트)가 만들어서 실행한 SQL은 별칭이 조금 복잡하다.
조인이 발생하거나 복잡한 조건에서도 문제 없도록 기계적으로 만들다 보니 이런 결과가 나온 듯 하다.
JPA에서 단순히 PK를 기준으로 조회하는 것이 아닌,
여러 데이터를 복잡한 조건으로 데이터를 조회하려면 어떻게 하면 될까?
findAll - 목록 조회
public List<Item> findAll(ItemSearchCond cond) {
String jpql = "select i from Item i";
//동적 쿼리 생략
TypedQuery<Item> query = em.createQuery(jpql, Item.class);
return query.getResultList();
}
JPQL
JPA는 JPQL(Java Persistence Query Language)이라는 객체지향 쿼리 언어를 제공한다.
주로 여러 데이터를 복잡한 조건으로 조회할 때 사용한다.
SQL이 테이블을 대상으로 한다면, JPQL은 엔티티 객체를 대상으로 SQL을 실행한다 생각하면 된다.
엔티티 객체를 대상으로 하기 때문에 from 다음에 Item 엔티티 객체 이름이 들어간다.
엔티티 객체와 속성의 대소문자는 구분해야 한다.
JPQL은 SQL과 문법이 거의 비슷하기 때문에 개발자들이 쉽게 적응할 수 있다.
결과적으로 JPQL을 실행하면 그 안에 포함된 엔티티 객체의 매핑 정보를 활용해서 SQL을 만들게 된다.
실행된 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에서 파라미터는 다음과 같이 입력한다
where price <= :maxPrice
파라미터 바인딩은 다음과 같이 사용한다.
query.setParameter("maxPrice", maxPrice)
동적 쿼리 문제
JPA를 사용해도 동적 쿼리 문제가 남아있다.
동적 쿼리는 뒤에서 설명하는 Querydsl이라는 기술을 활용하면 매우 깔끔하게 사용할 수 있다.
실무에서는 동적 쿼리 문제 때문에, JPA 사용할 때 Querydsl도 함께 선택하게 된다
JPA 적용3 - 예외 변환
JPA의 경우 예외가 발생하면 JPA 예외가 발생하게 된다.
@Repository
@Transactional
public class JpaItemRepositoryV1 implements ItemRepository {
private final EntityManager em;
@Override
public Item save(Item item) {
em.persist(item);
return item;
}
}
EntityManager 는 순수한 JPA 기술이고, 스프링과는 관계가 없다.
따라서 엔티티 매니저는 예외가 발생하면 JPA 관련 예외를 발생시킨다
JPA는 PersistenceException 과 그 하위 예외를 발생시킨다.
추가로 JPA는 IllegalStateException , IllegalArgumentException 을 발생시킬 수 있다.
그렇다면 JPA 예외를 스프링 예외 추상화( DataAccessException )로 어떻게 변환할 수 있을까?
비밀은 바로 @Repository 에 있다
예외 변환 전
@Repository의 기능
@Repository 가 붙은 클래스는 컴포넌트 스캔의 대상이 된다.
@Repository 가 붙은 클래스는 예외 변환 AOP의 적용 대상이 된다.
- 스프링과 JPA를 함께 사용하는 경우 스프링은 JPA 예외 변환기 ( PersistenceExceptionTranslator )를 등록한다.
예외 변환 AOP 프록시는 JPA 관련 예외가 발생하면 JPA 예외 변환기를 통해
발생한 예외를 스프링 데이터 접근 예외로 변환한다.
예외 변환 후
결과적으로 리포지토리에 @Repository 애노테이션만 있으면 스프링이 예외 변환을 처리하는 AOP를 만들어준다
리포지토리에서 jpa예외가 밖으로 나갈때 예외변환 AOP프록시가 받아서 예외를 변환해서 던져준다.
어떤 itemrepository가 주입되는지 로그를 찍어보면
참고
스프링 부트는 PersistenceExceptionTranslationPostProcessor 를 자동으로 등록하는데,
여기에서 @Repository 를 AOP 프록시로 만드는 어드바이저가 등록된다.
복잡한 과정을 거쳐서 실제 예외를 변환하는데, 실제 JPA 예외를 변환하는 코드는 EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible() 이다
'인프런 > 스프링 DB 2편' 카테고리의 다른 글
7) 데이터 접근 기술 - 스프링 데이터 JPA (0) | 2024.02.15 |
---|---|
8) 데이터 접근 기술 - Querydsl (0) | 2024.02.15 |
5)데이터 접근 기술 - MyBatis (0) | 2023.03.08 |
4) 데이터 접근 기술 - 테스트 (0) | 2023.03.07 |
3) 데이터 접근 기술 - 스프링 JdbcTemplate (2) (0) | 2023.03.07 |
댓글