관리 메뉴

나구리의 개발공부기록

데이터 접근 기술 - JPA, 시작, ORM 개념(SQL중심적인 개발의 문제점 / JPA 소개), JPA 설정, JPA 적용(개발/리포지토리 분석/예외변환) 본문

인프런 - 스프링 완전정복 코스 로드맵/스프링 DB 2편 - 데이터 접근 활용

데이터 접근 기술 - JPA, 시작, ORM 개념(SQL중심적인 개발의 문제점 / JPA 소개), JPA 설정, JPA 적용(개발/리포지토리 분석/예외변환)

소소한나구리 2024. 9. 21. 17:07

  출처 : 인프런 - 스프링 DB 2편 데이터 접근 핵심 원리 (유료) / 김영한님  
  유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용  

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-2


1. JPA 시작

  • 스프링과 JPA는 자바 엔터프라이즈(기업) 시장의 주력 기술임
  • 스프링은 DI 컨테이너를 포함한 애플리케이션 전반의 다양한 기능을 제공한다면 JPA는 ORM 데이터 접근 기술을 제공함
  • 스프링 + 데이터 접근기술의 조합을 구글 트렌드로 비교했을 때 글로벌에서는 스프링 + JPA조합을 80%이상 사용하고 국내에서도 스프링 + JPA 조합을 50% 정도 사용하고 있으며 2015년부터 점점 추세가 증가하고 있음

  • JPA는 스프링 만큼 방대하고 학습해야할 분량이 많지만 한번 배워두면 데이터 접근 기술에서 매우 큰 생상성 향상을 얻을 수 있음
  • 대표적으로 JdbcTemplate이나 MyBatis같은 SQL 매퍼 기술은 SQL을 개발자가 직접 작성해야 하지만 JPA를 사용하면 SQL도 JPA가 대신 작성하고 처리해줌

  • 실무에서는 JPA를 더욱 편리하게 사용하기 위해서 스프링 데이터 JPA와 Querydsl 이라는 기술을 함께 사용함
  • 중요한것은 JPA이며 스프링 데이터 JPA와 Querydsl은 JPA을 편리하게 사용하도록 도와주는 도구라고 생각하면 됨

** 참고

  • 스프링 DB2편 에서는 모든 내용을 다루기엔 너무 방대하여 JPA, 스프링 데이터 JPA, Querydsl로 이어지는 전체 그림을 보고 각 기술의 장단점, 이 기술들을 왜 사용해야하는지 등을 이해하는데 초점을 맞출 예정
  • 각 기술들의 자세한 강의는 스프링 부트와 JPA로드맵에 있는 강의에서 다룰 예정

2. ORM 개념 1 - SQL 중심적인 개발의 문제점

1) 객체를 관계형 DB의 저장

  • 애플리케이션은 객체지향 언어를 사용하고 관계형 데이터베이스는 SQL을 사용하다보니 데이터베이스에 객체를 저장할 때 SQL 의존적인 개발을 피하기 어려움
  • 자바 객체를 SQL로 SQL을 자바로 변경해야하는 작업을 반복적으로 해야하며 만일 객체에 필드가 하나라도 추가 된다면 수많은 SQL 쿼리문에 추가된 필드를 추가 해줘야하며 이런 과정에서 버그가 발생할 확률이 높아짐
  • 객체를 다양한 저장소(RDB, NoSQL, File, OODB 등)에 영구 보관할 수 있지만 대부분 관계형 데이터베이스에 저장 하는데, 이 과정에서 객체를 SQL로 변환해야하는 과정을 개발자가 직접 SQL 매퍼 역할을 해줘야 함

좌) 필드 추가 전 / 우) 필드 추가

2) 객체와 관계형 데이터베이스의 차이 그리고 문제점

  • 객체 지향 프로그래밍은 추상화, 캡슐화, 정보은닉, 상속, 다형성 등 시스템의 복잡성을 제어할 수 있는 다양한 장치들을 제공함
  • 객체와 관계형 데이터베이스는 상속, 연관관계, 데이터타입, 데이터 식별 방법에서 차이가 남

(1) 상속

  • 객체는 상속이 있어서 카테고리의 종속 개념을 만들 수 있는데, 관계형 DB의 Table은 원칙적으로 상속(객체지향 개념과 동일한)이란 개념이 없고 그나마 슈퍼타입, 서브타입 관계로 객체의 상속과 비슷하게 표현할 수 있지만 객체의 상속과는 다름
  • 가장 큰 차이는 다형성 개념이 적용되지 않는 것

객체의 상속과 Table 슈퍼타입 서브타입 관계

  • 만약에 Album을 저장한다고 하면 INSERT를 ITEM에 한번, ALBUM에 한번 총 2번을 날려야함
  • Album을 조회해야한다고 하면 ALBUM 테이블과 ITEM 테이블을 조인해서 각 객체를 생성 하고 다시 값을 입력하는 등의 복잡한 과정을 거쳐야 하며 조인이 필수가 되어버림
  • 그래서 DB에 저장할 객체에는 상속관계를 잘 안씀
  • 하지만 자바 컬렉션에서 저장하고 조회하면 코드 한줄로 간편하게 조회하고 저장할 수 있음
    • list.add(album)
    • Album album = list.get(albumId);
    • Item item = list.get(albumId); -> 부모 타입으로 조회 후 다형성 활용하는 것도 가능

(2) 연관관계

  • 객체는 참조를 사용: member.getTeam()
  • 테이블은 외래 키를 사용: JOIN ON M.TEAM_ID = T.TEAM_ID

연관관계 비교

  • 그래서 객체를 테이블에 맞추어 모델링을 많이 함

객체를 테이블에 밎추어 모델링하고 테이블에 저장

  • 하지만 원래 객체다운 모델링은 team_Id를 입력하는 것이 아니라 Team을 참조로 연관관계를 맺어야 하는데 team을 조회하는 메서드도 필요하고, 저장을 할때도 member.getTeam().getId() team_id값을 가져와서 테이블에 저장해야함

객체다운 모델링을 하여 테이블에 저장하려면 과정이 추가됨

  • 또 객체 모델링을 조회하려면 조인하는 쿼리문을 입력하고 객체를 생성하고, 조회된 정보들을 입력하고 하는 등등 아래 이미지의 과정을 모두 코드로 입력해야 하는 번잡한 과정이 필요함

객체 모델링을 조회하기위한 코드 입력 방식

  • 하지만 객체 모델링을 자바 컬렉션에 관리하게되면 저장 및 조회가 편리함
  • list.add(member); 한줄로 저장이 됨
  • 조회도 아래처럼 2줄로 할 수 있음
  • member member = list.get(memberId);  
  • Team team = member.getTeam();

(3) 객체 그래프 탐색

  • 객체는 항상 그런건 아니지만 자유롭게 객체 그래프를 탐색할 수 있어야 함
  • 하지만 SQL을 사용하는 순간 처음 SQL에서 정한 탐색 범위 안에서만 조회가 됨
  • 예를 들어 from Team 이라고 탐색 범위가 정해지면 Team에 대해서는 조회가 되지만 Order는 범위에 SQL에 포함하지 않았으므로 조회가 되지 않는 다는 뜻, 결국 SQL을 보고 코드를 판단해야함

좌) 객체 그래프 예시 / 우) 실행하는 SQL에 따라 탐색 범위가 결정 되어버림

  • Member 객체를 Entity라고 가정했을 때 memberDAO에서 조회한 member 객체에 팀의 정보가 있는지, 배송정보가 있는지를 memberDAO에 들어가서 작성된 쿼리문의 범위를 봐야지만 Entity에 값이 null인지 아닌지를 판단할 수 밖에 없는 판단할 수 밖에 없게 되어 조회 코드만 보고 Entity를 신뢰할 수 없는 문제가 발생 됨

좌) Entity를 신뢰할 수 없는 예시 / 우) 이미지처럼 상황에 따라 동일한 메서드를 무한정 만들어낼 수는 없음

  • 만일 모든 객체를 조회 하게 된다면 불필요한 조인이 발생하고, 한번 조인할 때마다 메모리 용량을 많이 잡아 먹게 됨
  • 그렇다고 모든 상황에 따른 조회 메서드를 무한정 만들수도 없는 노릇임
  • memberDAO와 MemberService와 물리적으로는 구분되어있지만, memberDAO에 작성된 SQL의 범위에 따라 Service의 코드를 짤 수 밖에 없는 논리적 계층이 분할이 되어있지 않는 상황이 발생 됨 -> 진정한 의미의 계층 분할이 어려움

(4) 비교하기 문제

  • MemberDAO에서 getMember 메서드를 통해 생성한 결과는 new Member를 반환하기 때문에 두번 getMember 조회한 결과를 == 비교하면 false가 나옴
  • 하지만 컬렉션으로 저장 하고 조회하여 == 비교를 하게 되면 같은 참조를 비교하므로 결과가 true가 나옴

좌) 쿼리 결과마다 new로 Member를 생성 / 우) Member는 한번만 생성

 

이렇게 SQL에 의존한 개발을 하다보니 객체 답게 모델링 할수록 매핑 작업만 늘어나게 되어 객체를 자바 컬렉션에 저장하듯이 DB에 저장할 수는 없을까? 하는 고민을 하게됨


3. JPA 소개

1) JPA

  • Java Persistence API의 줄임말이며 자바 진영의 ORM 기술표준임
  • 자바 진영에 EJB(엔터티 빈)이라는 ORM 기술이 있었지만 너무 복잡해서 개빈 킹이라는 개발자가 하이버네이트라는 오픈소스를 만듦
  • 이후 자바진영에서 하이버네이트를 만든 개빈 킹을 데리고 와서 JPA라는 자바 표준을 만들게 됨
  • JPA는 인터페이스의 모음이며 Hibernate, EclipseLink, DataNucleus 의 구현체가 있으며 대부분 Hibernate가 구현체로 사용됨
  • 초기 JPA 1.0 버전은 기능이 부족했지만 2.0이 들어오면서 대부분의 ORM 기능을 포함하게 되어 실무에서 사용할만하게 되었음
  • JPA 2.1, 2.2 버전을 거치면서 웬만한 기능들이 구현되어 있음

2) ORM

  • Object-relational mapping(객체 관계 매핑)의 앞글자를 따서 ORM이라고 함
  • 객체는 객체대로 설계하고 관계형 데이터베이스는 관계형 데이터베이스대로 설계한뒤 ORM 프레임 워크가 중간에서 매핑함
  • 대중적인 언어에는 대부분 ORM 기술이 존재함

3) JPA 동작

  • JPA 애플리케이션과 JDBC 사이에서 동작하며 패러다임의 불일치를 해결함
  • JPA를 사용함으로 Java의 컬렉션을 사용해서 조회하는 것처럼 데이터를 사용 할 수 있게 됨

4) JPA를 사용해야 하는 이유

  • SQL 중심적인 개발에서 객체 중심으로 개발할 수 있음
  • 생산성이 향상되고 성능부분도 최적화 할 수 있음
  • 유지보수가 편해짐
  • 패러다임의 불일치가 해결됨
  • 데이터 접근 추상화와 벤더 독립성
  • 표준

(1) 생산성 - 간편하게 CRUD를 구현할 수 있음

  • 저장: jpa.persist(member)
  • 조회: Member member = jpa.find(memberId)
  • 수정: member.setName("변경할 이름")
  • 삭제: jpa.remove(member)
  • 이렇게 SQL 작성없이 코드 한줄로 CRUD를 쉽게 할 수 있게됨

 

(2) 유지보수

  • 필드 변경시 객체와 테이블에 필드만 추가하면되고 SQL은 JPA가 대신 작성해줌

좌) 필드 추가 시 SQL을 수정해야하는 상황 / 우) JPA가 SQL을 대신 처리해줌

 

(3) JPA와 패러다임의 불일치 해결 - 상속

  • 객체는 상속 관계, Table은 슈퍼타입 서브타입관계로 설정 되어있을 때 개발자는 자바 코드만 짜면 JPA가 알아서 SQL을 만들어서 처리함
  • jpa.persist(album); 으로 저장을 하면 JPA가 각 ITEM, ALBUM 테이블에 대한 INSERT문을 알아서 생성함
  • Album album = jpa.find(Album.class, albumId) 으로 조회를 하게되면 JPA가 알아서 join 쿼리문을 만들어서 조회함

 

(4) JPA와 패러다임의 불일치 해결 - 연관관계, 객체 그래프 탐색, 신뢰할 수 있는 엔터티

  • member.setTeam(team); 으로 연관관계를 지정하고 persist(member)로 저장을 하면 JPA가 외래키값들을 고려해서 INSERT문을 알아서 생성함
  • find(Member.class, memberId);로 member를 조회하고 member.getTeam()으로 조회하면 team의 값들이 조회가 되는 객체 그래프 탐색이 가능함
  • 자유롭게 객체 그래프 탐색이 가능하다보니 쿼리문에 관계없이 Member Entity를 신뢰할 수 있게되며 Service 계층의 코드를 memberDAO의 SQL 쿼리문의 영향 없이 작성할 수 있어 논리적인 계층 분할을 할 수 있게됨

 

(5) JPA와 패러다임의 불일치 해결 - JPA와 비교하기

  • JPA는 기본 컨셉이 자바 컬렉션과 같다고 이해하면 됨
  • 동일한 트랜잭션에서 조회한 엔터티는 같음을 보장하게 됨

 

(6) 성능 최적화

  • JPA는 애플리케이션이랑 DB사이에 하나의 계층으로 존재하는데, 이렇게 둘 사이에 계층이 있으면 캐시, 버퍼링 쓰기와 같은 기능을 할 수 있음
  • 같은 트랜잭션 안에서는 같은 엔터티를 반환하므로 약간의 조회 성능이 향상됨, 첫번째 조회시에는 SQL이 동작하지만 두번째 같은 식별자로 조회하면 캐시가 적용이 됨
  • 트랜잭션을 커밋할 때 까지 INSERT SQL을 모았다가 JDBC BATCH 기능을 사용해서 네트워크 통신 한번에 SQL을 전송할 수 있음
  • 지연로딩
    • 객체가 실제 사용될때 로딩됨
    • 여러 객체를 사용하는 코드가 있을때, 한번에 조회되는 것이 아니라 실제 객체가 사용될때 조회되므로 쿼리가 여러번 날라감
  • 즉시로딩
    • JOIN SQL로 한번에 연관된 객체까지 미리 조회함
    • 여러 객체를 사용하는 코드가 계속적으로 반복되는 상황이라면 쿼리를 여러번 날리는 것보다 쿼리를 한번에 날려서 같이 가져오도록 하는 것이 나은데 이럴때 사용함
  • 개발할 때는 지연로딩으로 쭉 개발을 하다가 코드 리펙토링 시 최적화가 필요한 시점에 몇가지 설정으로 간편하게 즉시로딩으로 바꿔서 성능 최적화를 할 수 있음

3) JPA를 배우면 RDB는 안배워도 될까?

  • 절대 안됨
  • ORM이라는 것은 객체, 관계형 데이터베이스 두 기술 위에 올라와있는 것이 ORM 기술이므로 객체도 관계형 데이터베이스도 모두 중요함
  • ORM이 어려운 진짜 이유는 객체도 잘 알아야하고 관계형 데이터베이스도 잘 알아야 하기에 ORM을 실무에서 진정하게 사용할 수 있는 것
  • 실무에서 대부분의 장애는 DB에서 나는데 관계형데이터베이스는 어떻게 설계하고 어떻게 개발해야하는지와 SQL문법에 대한부분도 깊이있게 알아야 함
  • 이런부분이 숙련이 되면 JPA이 어떤 SQL을 만드는지 머릿속에서 그려지게 됨

4. JPA 설정

1) build.gradle 수정

  • JPA 의존관계를 추가 하고 새로고침하면 라이브러리 목록에 hibernate와 persistence-api, spring-data-jpa 관련된 라이브러리들이 추가 된 것을 확인할 수 있음
  • spring-boot-starter-data-jpa는 jdbc를 포함(의존)하기 때문에 jdbc 라이브러리 의존관계를 제거해도됨
  • mybatis 라이브러리를 사용할 때도 jdbc를 포함하기 때문에 제거해도 됨
// JPA, 스프링 데이터 JPA 추가
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

// JDBC 의존관계는 주석처리하거나 제거
//implementation 'org.springframework.boot:spring-boot-starter-jdbc'
 

2) application.properties 설정 추가

  • test, main의 application.properties에 로그를 위한 설정을 추가 
  • org.hibernate.SQL=DEBUG: 하이버네이트가 생성하고 실행하는 SQL를 확인할 수 있음
  • org.hibernate.orm.jdbc.bind=TRACE: SQL에 바인딩 되는 파라미터를 확인할 수 있음
  • spring.jpa.show-sql-true라는 설정도 있지만 System.out 콘솔을 통해 출력되므로 권장하지 않음
  • 만약에 둘다 키게 되면 로그가 중복되서 출력됨
#JPA log
logging.level.org.hibernate.SQL=DEBUG

# SQL에 바인딩되는 파라미터 확인 - 스프링 부트 3.0 이상(hibernate 6버전)
logging.level.org.hibernate.orm.jdbc.bind=TRACE

# SQL에 바인딩되는 파라미터 확인 - 스프링 부트 3.0 미만
org.hibernate.type.descriptor.sql.BasicBinder=TRACE

5. JPA 적용1 - 개발

  • JPA에서 가장 중요한 부분은 객체와 테이블을 매핑하는 것 -> JPA가 제공하는 애노테이션을 사용해서 Item 객체와 테이블을 매핑

1) Item - ORM 매핑

(1) @Entity

  • JPA가 사용하는 객체라는 뜻
  • 이 애노테이션이 있어야 JPA가 인식할 수 있으며 @Entity가 붙은 객체를 JPA에서는 엔터티 라고함

(2) @Id

  • 테이블의 PK와 해당 필드를 매핑함

(3) @GeneratedValue(strategy = GenerationType.IDENTITY)

  • PK 생성값을 데이터베이스에서 생성하는 IDENTITY 방식을 사용함
  • ex) MySQL의 auto increment 

(4) @Column

  • 객체의 필드를 테이블의 컬럼과 매핑
  • name = "item_name": 객체는 itemName이지만 테이블의 컬럼은 item_name이므로 매핑,
    • 스프링 부트와 통합해서 사용하면 필드 이름을 테이블 컬럼명으로 변경할 때 객체 필드의 카멜 케이스를 테이블 컬럼의 언더스코어로 자동 변환해주므로 예제에서 @Column(name = "item_name")은 생략해도 됨
  • length = 10 : JPA의 매핑 정보로 DDL(create table)도 생성할 수 있는데 그때 컬럼의 길이 값으로 활용됨(String 타입이므로 varchar 10으로 매핑 됨)
  • @Column을 생략할 경우 필드의 이름을 테이블 컬럼 이름으로 사용함

(5) 기본 생성자

  • JPA는 public 또는 protected의 기본 생성자가 필수이므로 기본 생성자를 꼭 넣어줘야함
  • 롬복의 @NoArgsConstructor를 해도됨
package hello.itemservice.domain;

@Data
@Entity
//@Table(name = "item") // 객체 명과 같으면 테이블명은 생략해도 됨
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;

    // ... 기존 생성자 코드들 JPA는 기본생성자가 필수!이므로 꼭 넣어줘야함
}
  • 해당 설정으로 기본 매핑이 간편하게 끝남

2) JpaItemRepositoryV1

(1) private final EntityManaget em

  • 생성자를 통해 엔터티매니저(EntityManager)를 주입받아서 사용
  • JPA의 모든 동작은 엔터티매니저를 통해서 이루어지며 엔터티 매니저는 내부에 데이터소스도 가질 수 있고, 데이터베이스에 접근도 할 수 있음

(2) @Transactional

  • JPA의 모든 데이터 변경(등록, 수정, 삭제)은 트랜잭션 안에서 이루어져야 하며, 조회는 트랜잭션이 없어도 가능함
  • 변경의 경우 일반적으로 서비스 계층에서 트랜잭션을 시작하기 때문에 문제가 없지만 이번 예제에서는 복잡한 비즈니스 로직이 없어서 서비스 계층에서 트랜잭션을 걸지않고 JPA에서는 변경시 트랜잭션이 필수이기 때문에 리포지토리에서 바로 트랜잭션을 적용하였음
  • 강조 - 일반적으로는 비즈니스 로직을 시작하는 서비스 계층에 트랜잭션을 걸어주는 것이 맞으며, 가끔 하다보면 리포지토리에 트랜잭션을 걸때도 있음
package hello.itemservice.repository.jpa;

@Slf4j
@Repository
@Transactional  // JPA에서 데이터 변경시에는 항상 트랜잭션을 적용해야함
public class JpaItemRepositoryV1 implements ItemRepository {

    // 엔터티 매니저를 의존관계 주입
    private final EntityManager em;

    public JpaItemRepository(EntityManager em) {
        this.em = em;
    }

    @Override
    public Item save(Item item) {
        em.persist(item);   // 저장, persist = 지속하다, 영구히 보존하다
        return item;
    }

    @Override
    public void update(Long itemId, ItemUpdateDto updateParam) {
        // itemId로 찾고
        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) {
        // jpql 문법, 거의 sql와 비슷한데 테이블을 대상으로 하는것이 아닌 엔터티를 대상으로 함
        String jpql = "select i from Item i";

        // JPA도 그냥 쓰면 동적쿼리에 약함 -> 코드가 많아짐
        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();
    }
}

 

** 참고

  • JPA를 설정하려면 EntityManagerFactory, JPA 트랜잭션 매니저(JpaTransactionManager), 데이터 소스 등등 다양한 설정을 해야하는데 스프링부트가 이 과정을 모두 자동화 해줌
  • 스프링 부트의 자동설정은 JpaBaseConfiguration 을 참고
  • main() 메서드부터 시작해서 JPA를 처음부터 어떻게 설정하는지는 JPA 기본편 강의에서 다룸

3) 수동 설정 진행

(1) JpaConfig

  • 기존에 계속 repository, service 의존관계 설정한 것처럼 해주면 됨
  • EntityManager 의존관계 주입
  • Repository를 JpaItemRepository로 변경하고 매개변수에 EntityManager를 입력
package hello.itemservice.config;

@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 JpaItemRepository(em);
    }
}

 

(2) ItemServiceApplication

  • Import를 JpaConfig로 변경
@Import(JpaConfig.class)

4) 테스트 및 애플리케이션 실행

  • 테스트를 실행해보면 JPA를 적용한 테스트가 정상 동작 되며 로그에서 hibernate로 동작하고 SQL 쿼리를 만들어서 동작되는 것을 볼 수 있음
  • H2 데이터베이스를 띄우고 애플리케이션을 실행해보면 모든 동작이 정상 동작됨

6. JPA 적용2 - 리포지토리 코드 분석

1) sava() - 저장

  • 엔터티 매니저가 제공하는 persist() 메서드로 객체를 테이블에 저장
@Override
public Item save(Item item) {
    em.persist(item);   // 저장, persist = 지속하다, 영구히 보존하다
    return item;
}
  • JPA가 만들고 실행하는 쿼리를 보면 id의 값이 null 혹은 default 혹은 완전 빠져있는 경우가 있는데, PK 키 생성 전략을 IDENTITY로 사용했기 때문에 이러한 쿼리를 생성함
  • 쿼리 실행 이후에 Item 객체의 id필드에 데이터베이스가 생성한 PK의 값이 들어가게 됨(JPA가 INSERT SQL 실행 이후에 생성된 ID 결과를 받아서 넣어줌)
# 실제 JPA가 생성한 쿼리
insert into item (item_name,price,quantity,id) values (?,?,?,default)

# 아래의 쿼리로 생성될 수도 있음
insert into item (id, item_name, price, quantity) values (null, ?, ?, ?)
insert into item (item_name, price, quantity) values (?, ?, ?)

2) update() - 수정

  • em.update() 같은게 있어야 할것 같지만 이런 메서드는 없음
  • JPA는 트랜잭션이 커밋되는 시점에, 변경된 엔터티 객체가 있는지 확인하고 특정 엔터티 객체가 변경된 경우에는 UPDATE SQL을 실행함
  • JPA가 처음 조회하는 시점에 내부에서 원본객체를 복사해서 가지고 있는데, 해당 객체와 지금 수정한 findItem이랑 바뀌었는지 커밋 시점에 체크를 해서 바뀌었으면 UPDATE SQL을 만들어서 전송함
  • JPA가 어떻게 변경된 엔터티 객체를 찾는지 명확하게 이해하려면 영속성 컨텍스트라는 JPA 내부 원리를 이해해야 하는데, 이부분은 JPA 기본편에서 자세히 다루므로 지금은 트랜잭션 커밋 시점에 JPA가 변경된 엔터티 객체를 찾아서 UPDATE SQL을 수행한다고 이해하면 됨
  • 테스트의 경우 마지막에 트랜잭션이 롤백되기 때문에 JPA는 UPDATE SQL을 실행하지 않으므로 테스트에서 UPDATE SQL을 확인하려면 @Commit을 붙이면 확인할 수 있음
@Override
public void update(Long itemId, ItemUpdateDto updateParam) {
    // itemId로 찾고
    Item findItem = em.find(Item.class, itemId);
    // 수정 및 저장
    findItem.setItemName(updateParam.getItemName());
    findItem.setPrice(updateParam.getPrice());
    findItem.setQuantity(updateParam.getQuantity());
}
update item set item_name=?,price=?,quantity=? where id=?

3) findById() - 단건 조회

  • find()를 사용해서 조회 타입과, PK 값을 주게 되면 엔티티 객체를 PK 기준으로 조회할 수 있음
  • JPA가 조회 SQL을 만들어서 실행하고 결과를 객체로 바로 변환해줌
@Override
public Optional<Item> findById(Long id) {
    Item item = em.find(Item.class, id);
    return Optional.ofNullable(item);
}
  • JPA가 만든 조회쿼리가 지금은 깔끔하지만 강의를 만들었을 때까지만해도 as 로 별칭이 복잡하게 붙었던 듯함
select
    i1_0.id,
    i1_0.item_name,
    i1_0.price,
    i1_0.quantity
from
    item i1_0
where
    i1_0.id=?

# 과거에 별칭이 붙었던 것을 보면 복잡하게 붙어있음
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_

4) findAll - 목록 조회

  • 단순 PK로 조회하는 것이 아닌 여러 데이터를 복잡한 조건으로 데이터를 조회하려면 JPQL을 사용해야함

(1) JPQL

  • JPA는 JPQL(Java Persistence Query Language)라는 객체지향 쿼리 언어를 제공하는데, 주로 여러 데이터를 복잡한 조건으로 조회할 때 사용함
  • SQL이 테이블을 대상으로 한다면 JPQL은 엔터티 객체를 대상으로 SQL을 실행한다고 생각하면 됨
  • 엔터티 객체를 대상으로 하기 때문에 from 다음에 Item 처럼 엔터티 객체 이름이 들어가며 엔터티 객체와 속성의 대소문자는 구분을 해야함
  • SQL과 문법이 거의 비슷하기 때문에 쉽게 적응이 가능하며 JPQL을 실행하면 그 안에 포함된 엔터티 객체의 매핑 정보를 활용해서 SQL을 만듦
  • where price <= :maxPrice 처럼 파라미터를 입력할 수 있고 query.setParameter("maxPrice", maxPrice) 처럼 파라미터 바인딩도 할 수 있음
@Override
public List<Item> findAll(ItemSearchCond cond) {
    // jpql 문법, 거의 sql와 비슷한데 테이블을 대상으로 하는것이 아닌 엔터티를 대상으로 함
    String jpql = "select i from Item i";

    // JPA도 그냥 쓰면 동적쿼리에 약함 -> 코드가 많아짐
    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();
}
  • JPQL을 통해 실행된 SQL
select
    i1_0.id,
    i1_0.item_name,
    i1_0.price,
    i1_0.quantity
from
    item i1_0
where
    i1_0.item_name like ('%'||?||'%')
escape
    '' and i1_0.price<=?

 

(2) 동적 쿼리 문제

  • JPA를 사용해도 동적 쿼리 문제가 남아있는데, 뒤에서 설명하는 Querydsl이라는 기술을 활용하면 매우 깔끔하게 사용할 수 있음
  • 실무에서는 동적 쿼리 문제 때문에 JPA를 사용할 때 Querydsl도 함께 선택하게 됨 - 거의 필수

7. JPA 적용3 - 예외 변환

  • EntityManager는 순수한 JPA 기술이고 스프링과는 관계가 없으므로 예외가 발생하면 JPA 관련 예외를 발생 시킴
  • JPA는 PersistenceException과 그 하위 예외를 발생 시키며 IllegalStateException, IllegalArgumentException을 발생 시킬 수 있음
  • @Repository를 통해 JPA 예외를 스프링 예외 추상화(DataAccessException)로 변환 시킬 수 있는데, 이렇게 변환하지 않으면 서비스 계층이 JPA 기술에 종속 되어 버림

좌) 예외 변환 전 / 우) 예외 변환 후

 

1) @Respository의 기능

  • 해당 애노테이션이 붙은 클래스는 컴포넌트 스캔의 대상이되며, 예외 변환 AOP의 적용 대상이 됨
  • 스프링과 JPA를 함께 사용하는 경우 스프링은 JPA 예외 변환기를 내부적으로 등록하고, 만들어진 예외 변환 AOP 프록시가 JPA 관련 예외가 발생하면 JPA 예외 변환기를 통해 발생한 예외를 스프링 데이터 접근 예외로 변환함
  • 결과적으로 리포지토리에 @Repository 애노테이션만 있으면 스프링이 예외 변환을 처리하는 AOP를 만들어 줌

** 참고

  • 스프링 부트는 PersistenceExceptiontranslationPostProcessor를 자동으로 등록하는데, 여기에서 @Repository를 AOP 프록시로 만드는 어드바이저가 등록됨
  • 복잡한 과정을 거쳐서 실제 예외를 변환하는데 실제 JPA 예외를 변환하는 코드는 EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible()임

** JPA의 자세한 강의는 JPA 기본편 강의를 참고해야함