Spring

[Spring DB2] 데이터 접근 기술 - JPA

gogi masidda 2024. 3. 19. 21:13

JPA는 ORM 데이터 접근 기술을 제공한다. 

JdbcTemplate이나 MyBatis같은 SQL 매퍼 기술은 개발자가 SQL을 직접 작성해야 하지만, JPA는 SQL도 JPA가 대신 작성해주고, 처리해준다. 


ORM 개념

SQL 중심적인 개발의 문제

무한 반복하는 select, update, insert, ... 코드를 계속해서 작성해야한다.

만약 필드가 추가되면 모든 쿼리들을 수정해야하는 번거로움이 있다.

 

객체를 관계형 데이터베이스에 저장하려면 객체를 SQL로 변환해서 데이터베이스에 조회하거나 넣어야 한다. 그런데 객체를 SQL로 변환하는 것은 개발자가 하는 것이다. 

또, 객체에는 상속이 있지만, 관계형 데이터베이스에는 상속이 없다. 그래서 조회할 때는 두 테이블을 조인해야하고, 넣을 때는 데이터를 분리해서 넣어야 한다. 

 

=> 객체를 자바 컬렉션에 넣듯이 저장할 수 없을 까 => JPA (Java Persistence API)

 

ORM

  • Object-relational mapping (객체 관계 매핑)
  • 객체는 객체대로 설계하고, 관계형 데이터베이스는 관계형 데이터베이스대로 설계한다.
  • ORM 프레임워크가 중간에서 매핑해준다.

JPA를 왜 사용해야 하는가

  • SQL 중심적인 개발에서 객체 중심으로 개발
  • 생산성
    • 저장: jpa.persist(member)
    • 조회: Member member = jpa.find(memberId)
    • 수정: member.setName("name")
    • 삭제: jpa.remove(member)
  • 유지보수
    • Member 클래스에 새로운 필드를 추가해도 JPA가 알아서 처리해준다.
  • 패러다임의 불일치 해결
    • 상속
    • 연관 관계
    • 객체 그래프 탐색
    • 객체 비교 
  • 성능 최적화 기능
    • 1차 캐시와 동일성 보장
      • 같은 트랜잭션 안에서는 같은 엔티티를 반환 
    • 트랜잭션을 지원하는 쓰기 지연
      • 트랜잭션을 커밋할 때까지 INSERT SQL을 모음
      • 커밋하는 순간에 데이터베이스에 INSERT SQL을 모아서 보냄.
    • 지연 로딩 
      • 지연 로딩: 객체가 실제 사용될 때 로딩
      • 즉시 로딩: JOIN SQL로 한번에 연관된 객체까지 미리 조회
  • 데이터 접근 추상화와 벤더 독립성
  • 표준

JPA 적용

//bulid.gradle 추가
//JPA, 스프링 데이터 JPA 추가
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

 

객체 매핑

@Data
@Entity //테이블과 매핑되어 관리되는 객체. 이게 있어야 JPA가 인식할 수 있음. 
//@Table(name = "item") 객체명과 테이블명이 같으면 안적어도 됨.
public class Item {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)//pk임을 알려주기. 값을 넣어주는데 DB에서 넣어주는 전략
    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가 인식할 수 있다. @Entity가 붙은 객체를 엔티티라고 한다.
  • @Id: 테이블의 PK와 해당 필드를 매핑한다.
  • @GeneratedValue(strategy = GenerationType.IDENTITY) : PK 생성 값을 데이터베이스에서 생성하는 'IDENTITY' 전략을 사용한다.
  • @Column : 객체의 필드를 테이블의 컬럼과 매핑한다.
    • 생략할 경우에는 필드의 이름을 컬럼 명으로 사용한다.
    • 스프링 부트와 통합해서 사용하면 필드 이름을 테이블 이름으로 변경할 때 객체 필드의 카멜 케이스를 언더스코어로 자동 변환해준다. 그래서 위의 @Column(name = 'item_name')도 생략해도 된다.
  • JPA는 public 또는 protected의 기본 생성자가 필수이다. 
@Slf4j
@Repository
@Transactional //Jpa의 모든 데이터변경은 트랜잭션 안에서 이루어짐.
public class JpaItemRepository implements ItemRepository {

    private final EntityManager em;

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

    @Override
    public Item save(Item item) {
        em.persist(item); //id값도 알아서 넣어줌.
        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();
    }
}
  • 생성자를 통해 EntityManager를 주입받는다. JPA의 모든 동작은 EntityManager를 통해서 이루어진다. 엔티티 매니저는 내부에 데이터 소스를 가지고 있고, 데이터베이스에 접근할 수 있다. 
  • @Transactional : JPA의 모든 데이터 변경은 트랜잭션 안에서 이루어져야 한다. 조회는 없어도 된다. 
  • save()
    • em.persist(item) 사용
    • 쿼리 실행 이후에 Item 객체의 id 필드에 데이터베이스가 생성한 PK값이 들어가게 된다. 
  • update()
    • em.update() 같은 메서드를 사용하지 않는다.
    • JPA는 트랜잭션이 커밋되는 시점에 변경된 엔티티가 있는지 확인하고, 변경된 엔티티가 있으면 update SQL을 실행한다.
  • findById()
    • 단건을 조회하는 경우에는 em.find(Item.class ,id) 사용
    • Item.class라는 조회 타입을 주어야 한다.
  • findAll()
    • 여러개를 복잡하게 조회하는 경우에는 JPQL을 사용해야 한다.
      • SQL이 테이블을 대상으로 한다면, JPQL은 엔티티 객체를 대상으로 SQL을 실행한다.
      • 엔티티 객체가 대상이라서 from 다음에 엔티티 객체 이름이 들어가고, 엔티티 객체와 속성의 대소문자는 구분해야 한다. 
    • JPA를 사용해도 동적 쿼리 문제가 남아있다. Querydsl이라는 기술을 사용하면 매우 깔끔하게 사용할 수 있다. 

JPA 적용 - 예외 변환

  • EntityManager는 순수한 JPA 관련 기술이고, 스프링과는 관계가 없어서 예외가 발생하면 JPA 관련 예외를 발생시킨다. 
  • JPA는 'PersistenceException'과 그 하위 예외를 발생시킨다. 
  • EntityManager에서 JPA 예외가 발생하면, 그 예외가 서비스까지 올라가 서비스는 JPA 기술에 종속적이게 된다.

@Repository의 기능

  • @Repository가 붙은 클래스는 컴포넌트 스캔의 대상이 된다.
  • @Repository가 붙은 클래스는 예외 변환 AOP의 적용 대상이 된다. 
    • 스프링은 스프링과 JPA를 함께 사용하면 JPA 예외 변환기 'PersistenceExceptionTranslator'를 등록한다.
    • JPA 관련 예외가 발생하면 JPA 예외 변환기를 통해 발생한 예외를 스프링 데이터 접근 예외로 변환하여 서비스 계층이 스프링 예외 추상화에 의존하게 된다. 

그래서 우리는 예외 변환에 대해 고민할 필요가 없다.

 

728x90