Spring DB - JPA

Kwon Yongho·2023년 5월 22일
0

Spring-DB

목록 보기
11/16
post-thumbnail

JPA

  1. JPA 시작
  2. ORM 개념1 - SQL 중심적인 개발의 문제점
  3. ORM 개념2 - JPA 소개
  4. JPA 설정
  5. JPA 적용1 - 개발
  6. JPA 적용2 - 예외 변환

1. JPA 시작

  • 글로벌에서는 스프링+JPA 조합을 80%이상 사용한다.
  • 국내에서도 스프링 + JPA 조합을 50% 정도 사용하고, 2015년 부터 점점 그 추세가 증가하고 있다.
  • 실무에서는 JPA를 더욱 편리하게 사용하기 위해 스프링 데이터 JPAQuerydsl이라는 기술을 함께 사용한다.
  • 중요한 것은 JPA이다.

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

  • 무한 반복, 지루한 코드
    • CRUD
    • 객체에 필드 추가 시 모든 CRUD에 필드를 추가해야한다.
  • 패러다임의 불일치
    • 객체 vs 관계형 데이터베이스

객체와 관계형 데이터베이스의 차이
1. 상속

Album 저장할라면?
1. 객체 분해
2. INSERT INTO ITEM ...
3. INSERT INTO ALBUM ...

Album 조회?
1. 각각의 테이블에 따른 조인 SQL 작성...
2. 각각의 객체 생성...
3. 상상만 해도 복잡
4. 더 이상의 설명은 생략한다.
5. 그래서 DB에 저장할 객체에는 상속 관계 안쓴다.

자바 컬렉션에 저장하면?

  • list.add(album);
    자바 컬렉션에서 조회하면?
  • Album album = list.get(albumId)

2. 연관관계

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

객체를 테이블에 맞추어 모델링

객체 모델링 저장

객체 모델링 조회

객체 모델링, 자바 컬렉션에 관리하면?

list.add(member);
Member member - list.get(memberId);
Team tem = member.getTeam();

상황에 따라 동일한 회원 조회 메서드를 여러벌 생성

memberDAO.getMember(); // Member만 조회
memberDAO.getMemberWithTeam(); // Member와 Team 조회
memberDAO.getMemberWithOrderWithDelivery(); // Member, Order, Delivery 조회

-> 끝이 없다.
--> 진정한 의미의 계층 분할이 어렵다.

3. 데이터 타입
4. 데이터 식별 방법

객체를 자바 컬렉션에 저장 하듯이 DB에 저장할 수는 없을까? --> JPA

3. ORM 개념2 - JPA 소개

JPA(Java Persistence API)

  • 자바 진영의 ORM 기술 표준

ORM?

  • Object-relational mapping(객체 관계 매핑)
  • 객체는 객체대로 설계
  • 관계형 데이터베이스는 관계형 데이터베이스대로 설계
  • ORM 프레임워크가 중간에서 매핑
  • 대중적인 언어에는 대부분 ORM 기술이 존재

JPA 동작 - 저장

JPA 동작 - 조회

JPA 왜 사용하는가?

  • SQL 중심적인 개발에서 객체 중심으로 개발
  • 생산성
  • 유지보수
  • 패러다임의 불일치 해결
  • 성능
  • 데이터 접근 추상화와 벤더 독립성
  • 표준

생산성 - JPA와 CRUD

  • 저장: jpa.persist(member)
  • 조회: Member member = jpa.find(memberId)
  • 수정: member.setName(“변경할 이름”)
  • 삭제: jpa.remove(member)

객체에 필드 추가 시?

  • JPA: 필드만 추가하면 됨, SQL은 JPA가 처리

JPA와 패러다임의 불일치 해결
1.JPA와 상속
저장

  • jpa.persist(album);
  • 나머지는 JPA가 처리

조회

  • Album album = jpa.find(Album.class, albumId);
  • 나머지는 JPA가 처리

2.JPA와 연관관계

member.setTeam(team);
jpa.persist(member);

3.JPA와 객체 그래프 탐색

Member member = jpa.find(Member.class, memberId);
Team team = member.getTeam();

4.JPA와 비교하기

String memberId = "100";
Member member1 = jpa.find(Member.class, memberId);
Member member2 = jpa.find(Member.class, memberId);
  • member1 == member2 같음
  • 동일한 트랜잭션에서 조회한 엔티티는 같음을 보장

4. JPA 설정

spring-boot-starter-data-jpa 라이브러리를 사용하면 JPA와 스프링 데이터 JPA를 스프링 부트와
통합하고, 설정도 아주 간단히 할 수 있다.

build.gradle 추가

implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

build.gradle 제거

//implementation 'org.springframework.boot:spring-boot-starter-jdbc'

main/test application.properties 추가

logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE
  • org.hibernate.SQL=DEBUG: 하이버네이트가 생성하고 실행하는 SQL을 확인할 수 있다.
  • org.hibernate.type.descriptor.sql.BasicBinder=TRACE: SQL에 바인딩 되는 파라미터를 확인할 수 있다.
  • spring.jpa.show-sql=true: 참고로 이런 설정도 있다. 이전 설정은 logger를 통해서 SQL이 출력된다. 이 설정은 System.out콘솔을 통해서 SQL이 출력된다. 따라서 이 설정은 권장하지는 않는다. (둘다 켜면 logger,System.out둘다 로그가 출력되어서 같은 로그가 중복해서 출력된다.)

5. JPA 적용1 - 개발

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

Item

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가 사용하는 객체라는 뜻이다. 이 에노테이션이 있어야 JPA가 인식할 수 있다. 이렇게 @Entity가 붙은 객체를 JPA에서는 엔티티라 한다.
  • @Id : 테이블의 PK와 해당 필드를 매핑한다.
  • @GeneratedValue(strategy = GenerationType.IDENTITY): PK 생성 값을 데이터베이스에서 생성하는 IDENTITY 방식을 사용한다. 예) MySQL auto increment
  • @Column: 객체의 필드를 테이블의 컬럼과 매핑한다.
    • name = "item_name": 객체는 itemName이지만 테이블의 컬럼은 item_name이므로 이렇게 매핑했다.
  • JPA는 public 또는 protected의 기본 생성자가 필수이다. 기본 생성자를 꼭 넣어주자.

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.extern.slf4j.Slf4j;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

import javax.persistence.EntityManager;
import java.util.List;
import java.util.Optional;

@Slf4j
@Repository
@Transactional
public class JpaItemRepositoryV1 implements ItemRepository {

    private 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.empty();
    }

    @Override
    public List<Item> findAll(ItemSearchCond cond) {
        String jpql = "select i from Item i"; // item Entity 객체를 말함

        List<Item> result = em.createQuery(jpql, Item.class).getResultList();
        return result;
    }

}
  • findAll()에 동적쿼리 부분은 일단 빼고 올렸다. 엄청 간단해졌다.
  • private final EntityManager em: 생성자를 보면 스프링을 통해 엔티티 매니저(EntityManager)라는 것을 주입받은 것을 확인할 수 있다. JPA의 모든 동작은 엔티티 매니저를 통해서 이루어진다. 엔티티 매니저는 내부에 데이터소스를 가지고 있고, 데이터베이스에 접근할 수 있다.
  • @Transactional: JPA의 모든 데이터 변경(등록, 수정, 삭제)은 트랜잭션 안에서 이루어져야 한다. 조회는 트랜잭션이 없어도 가능하다. 변경의 경우 일반적으로 서비스 계층에서 트랜잭션을 시작하기 때문에 문제가 없다. 하지만 이번 예제에서는 복잡한 비즈니스 로직이 없어서 서비스 계층에서 트랜잭션을 걸지 않았다. JPA에서는 데이터 변경시 트랜잭션이 필수다. 따라서 리포지토리에 트랜잭션을 걸어주었다. 다시한번 강조하지만 일반적으로는 비즈니스 로직을 시작하는 서비스 계층에 트랜잭션을 걸어주는 것이 맞다.

JpaItemRepositoryV1 코드를 분석

save()

  • em.persist(item): JPA에서 객체를 테이블에 저장할 때는 엔티티 매니저가 제공하는 persist()
    메서드를 사용하면 된다.

update() - 수정

  • em.update() 같은 메서드를 전혀 호출하지 않았다. 그런데 어떻게 UPDATE SQL이 실행되는 것일까?
  • JPA는 트랜잭션이 커밋되는 시점에, 변경된 엔티티 객체가 있는지 확인한다. 특정 엔티티 객체가 변경된
    경우에는 UPDATE SQL을 실행한다.
  • JPA가 어떻게 변경된 엔티티 객체를 찾는지 명확하게 이해하려면 영속성 컨텍스트라는 JPA 내부 원리를
    이해해야 한다.

findById() - 단건 조회

  • JPA에서 엔티티 객체를 PK를 기준으로 조회할 때는 find()를 사용하고 조회 타입과, PK 값을 주면 된다.

findAll - 목록 조회
JPQL

  • JPA는 JPQL(Java Persistence Query Language)이라는 객체지향 쿼리 언어를 제공한다. 주로 여러 데이터를 복잡한 조건으로 조회할 때 사용한다.

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

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 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);
    }
}

ItemServiceApplication - 변경

테스트 결과

오류가 발생했다.

itemRepository.findById 부분에서 value가 없다고 나오고 있다 확인해보자

JpaItemRepositoryV1에서 find로 찾은 item를 return 해줘야 되는데 코드 오류였다. 다시 테스트 해보자


--> 성공했다.

애플리케이션 테스트


--> 잘 실행된다.

6. JPA 적용2 - 예외 변환

JPA의 경우 예외가 발생하면 JPA 예외가 발생하게 된다.

  • EntityManager는 순수한 JPA 기술이고, 스프링과는 관계가 없다. 따라서 엔티티 매니저는 예외가 발생하면 JPA 관련 예외를 발생시킨다.
  • JPA는 PersistenceException과 그 하위 예외를 발생시킨다.
    • 추가로 JPA는 IllegalStateException, IllegalArgumentException을 발생시킬 수 있다.
  • 그렇다면 JPA 예외를 스프링 예외 추상화(DataAccessException)로 어떻게 변환할 수 있을까?
  • 비밀은 바로 @Repository에 있다.

@Repository의 기능

  • @Repository가 붙은 클래스는 컴포넌트 스캔의 대상이 된다.
  • @Repository가 붙은 클래스는 예외 변환 AOP의 적용 대상이 된다.
    • 스프링과 JPA를 함께 사용하는 경우 스프링은 JPA 예외 변환기(PersistenceExceptionTranslator)를 등록한다.
    • 예외 변환 AOP 프록시는 JPA 관련 예외가 발생하면 JPA 예외 변환기를 통해 발생한 예외를 스프링 데이터 접근 예외로 변환한다.

예외 변환 후

  • 결과적으로 리포지토리에 @Repository애노테이션만 있으면 스프링이 예외 변환을 처리하는 AOP를 만들어준다.

참고
김영한: 스프링 DB 2편 - 데이터 접근 활용 기술(인프런)
Github - https://github.com/b2b2004/Spring_DB

0개의 댓글