구현 기능

  • 상품 주문
  • 주문 내역 조회
  • 주문 취소

 

순서

  1. 주문 엔티티, 주문 상품 엔티티 개발
  2. 주문 리포지토리 개발
  3. 주문 서비스 개발
  4. 주문 검색 기능 개발
  5. 주문 기능 테스트

 

 

주문, 주문상품 엔티티 개발


Order Entity

package jpabook.jpashop.domain;

import lombok.Getter;
import lombok.Setter;

import java.time.LocalDateTime;
import java.util.*;

import javax.persistence.*;

@Entity
@Table(name = "Orders")
@Getter
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)      // 기본 생성자를 protected로 선언하여 다른 패키지의 클래스에서 빈생성자를 생성을 막음
public class Order {
    ...

    //==생성 메서드==//
    public static Order createOrder(Member member, Delivery delivery, OrderItem... orderItems){
        Order order = new Order();
        order.setMember(member);
        order.setDelivery(delivery);
        for(OrderItem orderItem : orderItems){
            order.addOrderItem(orderItem);
        }
        order.setStatus(OrderStatus.ORDER);
        order.setOrderDate(LocalDateTime.now());
        return order;
    }

    //==비즈니스 로직==//
    /**
     * 주문 취소
     */
    public void cancel(){
        if(delivery.getDeliveryStatus() == DeliveryStatus.COMP){
            throw new IllegalStateException("이미 배송완료한 상품은 취소가 불가능합니다.");
        }

        this.setStatus(OrderStatus.CANCLE);
        for(OrderItem orderItem : orderItems){
            orderItem.cancel();
        }
    }

    //==조회 로직==//
    /**
     * 전체 주문 가격 조회
     */
    public int getTotalPrice() {
        return orderItems.stream()
                .mapToInt(OrderItem::getTotalPrice)
                .sum();
        /*
        위와 같은 코드
        int totalPrice = 0;
        for (OrderItem orderItem : orderItems) {
            totalPrice += orderItem.getTotalPrice();
        }
        return totalPrice;*/
    }
}

 

OrderItem Entity

package jpabook.jpashop.domain;

import jpabook.jpashop.domain.Item.Item;
import lombok.Getter;
import lombok.Setter;

import javax.persistence.*;

@Entity
@Getter
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)      // 기본 생성자를 protected로 선언하여 다른 패키지의 클래스에서 빈생성자를 생성을 막음
public class OrderItem {
    ...
    
    //==생성 메서드==//
    public static OrderItem createOrderItem(Item item, int orderPirce, int count){
        OrderItem orderItem = new OrderItem();
        orderItem.setItem(item);
        orderItem.setOrderPirce(orderPirce);
        orderItem.setCount(count);
        item.removeStock(count);
        return orderItem;
    }

    //==비즈니스 로직==//
    public void cancel() {
        getItem().addStock(count);
    }

    public int getTotalPrice(){
        return getOrderPirce()*getCount();
    }
}

 

 

 

 

주문 리포지토리 개발


OrderRepository

package jpabook.jpashop.repository;

import jpabook.jpashop.domain.Order;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
import java.util.*;

import javax.persistence.EntityManager;

@Repository
@RequiredArgsConstructor
public class OrderRepository {

    private final EntityManager em;

    public void save(Order order){
        em.persist(order);
    }

    public Order findOne(Long orderId){
        return em.find(Order.class, orderId);
    }

    //후에 개발
    //public List<Order> findAll(OrderSearch orderSearch) {}
}

 

 

 

 

주문 서비스 개발


OrderService

package jpabook.jpashop.service;

import jpabook.jpashop.domain.Delivery;
import jpabook.jpashop.domain.Item.Item;
import jpabook.jpashop.domain.Member;
import jpabook.jpashop.domain.Order;
import jpabook.jpashop.domain.OrderItem;
import jpabook.jpashop.repository.ItemRepository;
import jpabook.jpashop.repository.MemberRepository;
import jpabook.jpashop.repository.OrderRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class OrderService {

    private final OrderRepository orderRepository;
    private final MemberRepository memberRepository;
    private final ItemRepository itemRepository;

    /**
     * 주문
     */
    @Transactional
    public Long order(Long memberId, Long itemId, int count){

        // 엔티티 조회
        Member member = memberRepository.findOne(memberId);
        Item item = itemRepository.findOne(itemId);

        // 배송 정보 생성
        Delivery delivery = new Delivery();
        delivery.setAddress(member.getAddress());

        // 주문 상품 생성
        OrderItem orderItem = OrderItem.createOrderItem(item, item.getPrice(), count);

        // 주문 생성
        Order order = Order.createOrder(member, delivery, orderItem);

        // 저장
        orderRepository.save(order);

        return order.getId();
    }

    /**
     * 취소
     */
    @Transactional
    public void cancelOrder(Long orderId){
        // 주문 엔티티 조회
        Order order = orderRepository.findOne(orderId);

        // 주문 취소 (비즈니스 로직을 만들어 놓았기 때문에, cancel 메소드만 접근하면 된다.)
        order.cancel();

        // jpa가 아니라면 수정된 데이터들을 update 쿼리문을 날려야하지만,
        // jpa는 더티 체킹을 통해 변경된 내역을 알아서 update 쿼리문을 날린다.
        // 정말 편리
    }


    /**
     * 검색
     */
    //public List<Order> findOrders(OrderSearch orderSearch){
    //    return orderRepository.findAll(orderSearch);
    //}
}

jpa가 아니라면 order.cancel() 아래 코드에서 변경된 데이터에 대한 업데이트 쿼리문을 작성하여 DB에 날려야 하지만, jpa의 경우 더티체킹을 통해 기존의 데이터와 바뀐 데이터가 있다면 알아서 찾아내어 update 쿼리문을 날리기때문에 따로 update 쿼리문을 작성하여 날리지 않아도 됩니다!!! (정말 편리하네요..)

 

참고

엔티티에 핵심 비즈니스 로직을 적는 패턴을 "도메인 모델 패턴"이라고 부릅니다. "도메인 모델 패턴"은 서비스 계층을 단순히 엔티티에 필요한 요청을 위임하는 역할을 합니다. 

반대로, 엔티티에는 비즈니스 로직이 거의 없고, 서비스 계층에서 대부분의 비즈니스 로직을 처리하는 것을 "트랜잭션 스크립트 패턴"이라고 합니다.

두 가지 방법이 있는 것은 어느 패턴이 더 뛰어나기 때문이 아닌 상황에 따라서, 유지 보수에 좋은 상황이 있기 때문입니다.

결론 : 상황에 따라서 유지 보수하기 편리한 패턴을 사용하자!

 

 

 

 

주문 기능 테스트


OrderServiceTest

package jpabook.jpashop.service;

import jpabook.jpashop.domain.Address;
import jpabook.jpashop.domain.Item.Book;
import jpabook.jpashop.domain.Item.Item;
import jpabook.jpashop.domain.Member;
import jpabook.jpashop.domain.Order;
import jpabook.jpashop.domain.OrderStatus;
import jpabook.jpashop.exception.NotEnoughStockException;
import jpabook.jpashop.repository.OrderRepository;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.transaction.annotation.Transactional;

import javax.persistence.EntityManager;

import static org.junit.Assert.*;

@RunWith(SpringRunner.class)
@SpringBootTest
@Transactional
public class OrderServiceTest {
    @Autowired
    private EntityManager em;
    @Autowired OrderService orderService;
    @Autowired
    OrderRepository orderRepository;

    @Test
    public void 상품주문() throws Exception {
        //given
        Member member = createMember();
        Item book = createBook("영진 JPA", 10000, 10);

        int orderCount = 2;

        //when
        Long orderId = orderService.order(member.getId(), book.getId(), orderCount);

        //then
        Order getOrder = orderRepository.findOne(orderId);
        assertEquals("상품 주문시 상태는 ORDER", OrderStatus.ORDER,getOrder.getStatus());
        assertEquals("주문한 상품 종류 수가 정확해야 한다.", 1, getOrder.getOrderItems().size());
        assertEquals("주문 가격은 가격*수량이다.", 10000*orderCount, getOrder.getTotalPrice());
        assertEquals("주문 수량만큼 재고가 줄어야 한다.", 10-orderCount, book.getStockQuantity());
    }

    @Test
    public void 주문취소() throws Exception {
        //given
        Member member = createMember();
        Item book = createBook("영진 JPA", 10000, 10);
        int orderCount = 2;

        Long orderId = orderService.order(member.getId(), book.getId(), orderCount);
        //when
        orderService.cancelOrder(orderId);

        //then
        Order getOrder = orderRepository.findOne(orderId);
        assertEquals("상품 주문시 상태는 CANCEL", OrderStatus.CANCEL,getOrder.getStatus());
        assertEquals("주문이 취소된 상품은 재고가 증가되야 한다.", 10, book.getStockQuantity());
    }


    @Test(expected = NotEnoughStockException.class)
    public void 재고수량초과() throws Exception {
        //given
        Member member = createMember();
        Item book = createBook("영진 JPA", 10000, 10);
        int orderCount = 11;

        //when
        Long orderId = orderService.order(member.getId(), book.getId(), orderCount);    // 여기서 예외가 터져야함.

        //then
        fail("재고 수량 부족 예외가 발생해야 한다.");
    }

    // 만들어 놓은 로직을 ctrl+alt+M 하면 메소드화 가능
    private Item createBook(String name, int price, int stockQuantity) {
        Item book = new Book();
        book.setName(name);
        book.setPrice(price);
        book.setStockQuantity(stockQuantity);
        em.persist(book);
        return book;
    }

    private Member createMember() {
        Member member = new Member();
        member.setName("회원1");
        member.setAddress(new Address("서울", "경기", "123-123"));
        em.persist(member);
        return member;
    }
}

 

위의 코드는 DB와 연동해서 테스트를 하는 예제입니다.

 

참고

도메인 모델 패턴은 도메인 엔티티에 비즈니스 로직이 있기때문에 도메인으로만 로직이 정상적으로 작동하는지 테스트하는 것이 장점입니다. 이를 잘 활용하는 것이 도메인 모델 패턴을 보다 잘 살려내는 방법이라고 생각합니다. 이유는 DB와 연동하지 않기때문에 테스트가 빠르기 때문입니다.

 

 

 

주문 검색 기능 개발


JPA에서 동적 쿼리를 어떻게 해결해야 하는가?

  • JPA Criteria
    • 유지보수성이 너무 떨어진다. 직접 쿼리를 작성해보면 무슨 쿼리가 작성되는지 머리로 그려지지 않기 때문입니다.
  • Querydsl
    • 동적 쿼리를 정말 강력하게 해결할 수 있다.
    • 강력하게라 함은 자바코드이기 때문에 컴파일 시점에서 오타를 다 잡아줍니다.
    • 또한, 코드가 간결하고 유지보수성이 뛰어납니다.

 

복잡한 동적 쿼리는 Querydsl로 해결하는 것을 권장합니다.

Querydsl에 대한 설명은 후에 함.

 

 

 

 

출처

 

실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발 - 인프런 | 강의

실무에 가까운 예제로, 스프링 부트와 JPA를 활용해서 웹 애플리케이션을 설계하고 개발합니다. 이 과정을 통해 스프링 부트와 JPA를 실무에서 어떻게 활용해야 하는지 이해할 수 있습니다., - 강

www.inflearn.com

 

구현 기능

  • 상품 등록
  • 상품 목록 조회
  • 상품 수정

 

순서

  • 상품 엔티티 개발 (비즈니스 로직 추가)
  • 상품 리포지토리 개발
  • 상품 서비스 개발, 상품 기능 테스트

 

 

상품 엔티티 개발 (비즈니스 로직 추가)


Item Entity

package jpabook.jpashop.domain.Item;

import jpabook.jpashop.domain.Category;
import jpabook.jpashop.exception.NotEnoughStockException;
import lombok.Getter;
import lombok.Setter;
import java.util.*;

import javax.persistence.*;

@Entity
@Getter
@Setter
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "dtype")
public abstract class Item {
	...
    
    //==비즈니스 로직==//

    /**
     * stock 증가
     */
    public void addStock(int quantity){
        this.stockQuantity += quantity;
    }

    /**
     * stock 감소
     */
    public void removeStock(int quantity){
        int restStock = this.stockQuantity - quantity;
        if(restStock < 0){
            throw new NotEnoughStockException("need more stock");
        }
        this.stockQuantity = restStock;
    }

}

비즈니스 로직 추가

  • 아이템 수량 추가
  • 아이템 수량 제거
    • 수량이 0개 이하가 되면 에러 

 

NotEnoughStockException 예외 클래스

package jpabook.jpashop.exception;

public class NotEnoughStockException extends RuntimeException {

    public NotEnoughStockException() {
        super();
    }

    public NotEnoughStockException(String message) {
        super(message);
    }

    public NotEnoughStockException(String message, Throwable cause) {
        super(message, cause);
    }

    public NotEnoughStockException(Throwable cause) {
        super(cause);
    }
}

예외처리 클래스는 처음 만들어 보았는데 RuntimeException을 상속받고 Override를 추가해주었다. 

 

 

 

 

상품 리포지토리 개발


ItemRepository

package jpabook.jpashop.repository;

import jpabook.jpashop.domain.Item.Item;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
import java.util.*;

import javax.persistence.EntityManager;

@Repository
@RequiredArgsConstructor
public class ItemRepository {

    private final EntityManager em;

    public void save(Item item){
        if(item.getId() == null){
            em.persist(item);
        } else {
            em.merge(item);     //merge에 대한 설명은 후에 한다고 합니다.
        }
    }

    public Item findByOne(Long id){
        return em.find(Item.class, id);
    }

    public List<Item> findAll(){
        return em.createQuery("select i from Item i", Item.class)
                .getResultList();
    }
}

 

 

 

 

상품 서비스 개발


ItemService

package jpabook.jpashop.service;

import jpabook.jpashop.domain.Item.Item;
import jpabook.jpashop.repository.ItemRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class ItemService {

    private final ItemRepository itemRepository;

    @Transactional
    public void saveItem(Item item){
        itemRepository.save(item);
    }

    public List<Item> findItems(){
        return itemRepository.findAll();
    }

    public Item findOne(Long id){
        return itemRepository.findOne(id);
    }
}
위와 같이 Service가 굳이 필요한 가에 대한 궁금증이 생길 수 있습니다.

이와같은 로직(Service가 위임만 한다.)에서는 Controller에서 바로 Repository에 접근해도 문제가 될 건 없다고 합니다.

 

테스트 코드 생성은 앞선 도메인 테스트와 다를게 없기때문에 하지 않았습니다.

 

 

 

 

 

 

 

출처

 

실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발 - 인프런 | 강의

실무에 가까운 예제로, 스프링 부트와 JPA를 활용해서 웹 애플리케이션을 설계하고 개발합니다. 이 과정을 통해 스프링 부트와 JPA를 실무에서 어떻게 활용해야 하는지 이해할 수 있습니다., - 강

www.inflearn.com

 

구현 기능

  • 회원 등록
  • 회원 목록 조회

 

순서

  1. 회원 엔티티 코드 다시 보기
  2. 회원 리포지토리 개발
  3. 회원 서비스 개발
  4. 회원 기능 테스트

 

 

회원 리포지토리 개발


MemberRepository  클래스

package repository;

import jpabook.jpashop.domain.Member;
import org.springframework.stereotype.Repository;
import java.util.*;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

@Repository     // componentscan에 의하여 스프링 빈으로 관리됨
public class MemberRepository {

    // PersistenceContext는 스프링이 생성한 EntityManager를 자동으로 주입시켜줌
    @PersistenceContext
    private EntityManager em;

    public void save(Member member){
        em.persist(member);
    }

    public Member findOne(Long id){
        return em.find(Member.class, id);
    }

    public List<Member> findAll() {
        return em.createQuery("select m from Member m", Member.class)
                .getResultList();
    }

    public List<Member> findByName(String name){
        return em.createQuery("select m from Member m where m.name = :name", Member.class)
                .setParameter("name", name)
                .getResultList();
    }
}
  • @Repository는 @Component 스캔의 대상이기 때문에 자동으로 스프링 빈에서 관리합니다.

  • @PersistenceContext는 스프링이 생선한 EntityManager를 자동으로 주입시켜 줍니다.
참고로 EntityManagerFactory를 쓰고 싶다면 @PersistenceUnit을 사용하시면 됩니다.

 

 

 

회원 서비스 개발


MemberService

package jpabook.jpashop.service;

import jpabook.jpashop.domain.Member;
import lombok.AllArgsConstructor;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import repository.MemberRepository;

import java.util.List;

@Service
@RequiredArgsConstructor            // final로 이루어진 필드로만 생성자를 만들어 줌
public class MemberService {

    private final MemberRepository memberRepository;        // 한번 초기화하면 바꿀 일이 없으니 final 선언

    /**
     * 회원 가입
     */
    @Transactional
    public Long join(Member member){
        validateDuplicateMember(member);
        memberRepository.save(member);
        return member.getId();
    }

    /**
     * 같은 이름이 있는 회원은 등록 안되도록
     */
    private void validateDuplicateMember(Member member) {
        //exception
        // 이러한 경우 멀티스레드 환경에서 문제가 될 수 있다.
        // 방지하기 위해서 member의 name을 unique 제약조건을 거는 것을 권장한다.
        List<Member> findMembers = memberRepository.findByName(member.getName());

        if(!findMembers.isEmpty()){
            throw new IllegalStateException("이미 존재하는 회원입니다.");
        }
    }

    /**
     * 회원 전체 조회
     */
    @Transactional(readOnly = true)     // 조회 성능 최적화
    public List<Member> findMembers() {
        return memberRepository.findAll();
    }

    /**
     * 멤버 1명 조회
     */
    @Transactional(readOnly = true)     // 조회 성능 최적화
    public Member findOne(Long memberId){
        return memberRepository.findOne(memberId);
    }
}

코드는 다르지만 같은 동작을 합니다. @RequiredArgsConstructor은 왼쪽의 코드 기능을 제공합니다.

 

Spring Data JPA를 사용하지 않는 경우 @PersistenceContext를 사용해야 bean에서 injection시켜주지만, Spring Data JPA를 사용하는 경우 @PersistenceContext를 사용하지 않고 @Autowired만 사용해도 injection 시켜줍니다. 

따라서, MemberRepository를 아래와 같이 변경할 수 있습니다.

 

 

 

 

회원 기능 테스트


MemberServiceTest

package jpabook.jpashop.service;

import jpabook.jpashop.domain.Member;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.Rollback;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.transaction.annotation.Transactional;
import jpabook.jpashop.repository.MemberRepository;

import static org.junit.Assert.*;

@RunWith(SpringRunner.class)    // "junit을 실행할 때, 스프링과 엮어서 실행하겠다"를 의미합니다.
@SpringBootTest         // SpringBoot를 띄운 상태에서 Test를 할 때 추가해야되는 어노테이션 (추가하지 않으면 @Autowired기능에서 에러가 발생합니다.)
@Transactional  // 기본적으로 rollback
public class MemberServiceTest {

    @Autowired MemberService memberService;
    @Autowired MemberRepository memberRepository;

    @Test
    // @Rollback(false)  insert query 문을 보고싶다면 해보자
    // 또는 EntityManager를 만들어서 flush() 해주면된다.
    public void 회원가입() throws Exception {
        //given
        Member member = new Member();
        member.setName("kim");

        //when
        Long saveId = memberService.join(member);

        //then
        assertEquals(member, memberRepository.findOne(saveId));
    }
    
    @Test(expected = IllegalStateException.class)
    public void 중복_회원_예외() throws Exception {
        //given
        Member member1 = new Member();
        member1.setName("kim");

        Member member2 = new Member();
        member2.setName("kim");
        
        //when
        memberService.join(member1);
        memberService.join(member2);        // 예외 발생해야 함.

        //then
        fail("예외가 발생해야 한다.");
    }
}

 

 

given, when, then 이란?

"given을 주고 when 상황이 있을 때, then 이러한 결과가 나올 것이다."의 flow로 이어지는 테스트 작성 방법입니다.

 

예외 테스트하는 방법

예외가 터져야하는 테스트라면 @Test에 옵션을 추가하여 코드를 간결화 할 수 있습니다.

아래와 같이 try, catch 문을 쓰지 않아도 됩니다.

좌, 우 같은 코드

 

In-Memory로 테스트 하는 방법

 

H2 Database Engine

Using H2 Documentation Reference: SQL grammar, functions, data types, tools, API Features: fulltext search, encryption, read-only (zip/jar), CSV, auto-reconnect, triggers, user functions Embedded jdbc:h2:~/test 'test' in the user home directory jdbc:h2:/da

www.h2database.com

h2 Database 홈페이지에서 cheat sheet를 보시면,

In-Memory 설정하는 url이 있습니다. test를 할 땐 In-Memory로 하기위해 

프로젝트 test 폴더에 resources 파일을 만들어 main에 있는 application.yml을 복사해서 저장해줍니다. 그리고 application.yml 코드 중 datasource url을 jdbc:h2:mem:test로 수정해주시면 In-Memory로 테스트를 할 수 있습니다.

 

참고로 Spring boot에서 database에 대한 별다른 설정이 없다면 memory DB로 돌리기 때문에 Database 설정에 대한 모든 코드를 지워도 memory 모드로 사용가능 합니다.

 

 

 

 

 

출처

 

실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발 - 인프런 | 강의

실무에 가까운 예제로, 스프링 부트와 JPA를 활용해서 웹 애플리케이션을 설계하고 개발합니다. 이 과정을 통해 스프링 부트와 JPA를 실무에서 어떻게 활용해야 하는지 이해할 수 있습니다., - 강

www.inflearn.com

 

구현 요구사항


  • 회원기능
    • 회원 등록
    • 회원 조회
  • 상품 기능
    • 상품 등록
    • 상품 수정
    • 상품 조회
  • 주문 기능
    • 상품 주문
    • 주문 내역 조회
    • 주문 취소

 

예제를 단순화 하기 위해 다음 기능은 구현X

  • 로그인과 권한 관리X
  • 파라미터 검증과 예외 처리X
  • 상품은 도서만 사용
  • 카테고리는 사용X
  • 배송 정보는 사용X

 

 

 

애플리케이션 아키텍쳐


계층형 구조 사용

  • controller, web : 웹 계층
  • service : 비즈니스 로직, 트랜잭션 처리
  • repository : JPA를 직접 사용하는 계층, 엔티티 매니저 사용
  • domain : 엔티티가 모여 있는 계층, 모든 계층에서 사용

 

패키지 구조

  • jpabook.jpashop
    • domain
    • exception
    • repository
    • service
    • web

 

개발 순서

  1. service
  2. repository
  3. testcase
  4. web 적용

 

 

 

 

 

출처

 

실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발 - 인프런 | 강의

실무에 가까운 예제로, 스프링 부트와 JPA를 활용해서 웹 애플리케이션을 설계하고 개발합니다. 이 과정을 통해 스프링 부트와 JPA를 실무에서 어떻게 활용해야 하는지 이해할 수 있습니다., - 강

www.inflearn.com

 

 요구사항 분석


 

실제 동작하는 화면을 먼저 확인합니다.

 

기능 목록

  • 회원 기능
    • 회원 등록
    • 회원 조회
  • 상품 기능
    • 상품 등록
    • 상품 수정
    • 상품 조회
  • 주문 기능
    • 상품 주문
    • 주문 내역 조회
    • 주문 취소
  • 기타 요구사항
    • 상품은 제고 관리가 필요하다.
    • 상품의 종류는 도서, 음반 영화가 있다.
    • 상품을 카테고리로 구분할 수 있다.
    • 상품 주문시 배송 정보를 입력할 수 있다.

 

 

도메인 모델과 테이블 설계


회원, 주문, 상품의 관계

회원은 여러 상품을 주문할 수 있다. 그리고 한 번 주문할 때 여러 상품을 선택할 수 있으므로 주문과 상품은 다대다 관계다. 하지만 이런 다대다 관계는 관계형 데이터베이스는 물론이고 엔티티에서도 거의 사용하지 않는다. 따라서 그림처럼 주문상품이라는 엔티티를 추가해서 다대다 관계를 일대다, 다대일 관계로 풀어냈다.

 

상품 분류

상품은 도서, 음반, 영화로 구분되는데 상품이라는 공통 속성을 사용하므로 상속 구조로 표현했다.

 

 

회원 엔티티 분석

회원(Member)

이름과 임베디드 타입인 주소 (Address), 주문(orders) 리스트 

 

주문(Order)

한 번 주문시 여러 상품을 주문할 수 있으므로 주문과 주문상품(OrderItem)은 일대다 관계, 주문은 상품을 주문한 회원과 배송 정보, 주문 날짜, 주문 상태(Status)를 가지고 있습니다. 주문 상태는 열거형으로 사용했는데 주문(Order), 취소(Cancel)를 표현할 수 있습니다.

 

주문 상품(OrderItem)

주문한 상품 정보와 주문 금액(orderPrice), 주문 수량(count)

 

상품(Item)

이름, 가격, 재고수량(stockQuantity)

 

배송(Delivery)

주문 시 하나의 배송 정보를 생성합니다. 주문과 배송은 일대일 관계입니다.

 

카테고리(Category)

상품과 다대다 관계를 맺습니다. parent, child로 부모, 자식 카테고리를 연결합니다.

 

 

 

회원 테이블 분석

MEMBER

회원 엔티티의 Address 임베디드 타입 정보가 회원 테이블에 그대로 들어갔다. DELIVERY 테이블도 마찬가지.

 

ITEM

앨범, 도서, 영화 타입을 통합해서 하나의 테이블로 만들었다. DTYPE 컬럼으로 타입을 구분

 

참고 : 테이블명이 ORDER가 아니라 ORDERS인 것은 데이터베이스가 order by때문에 예약어로 잡고 있는 경우가 많기 때문입니다.
참고 : 실제 코드에서는 DB에 소문자 + _(언터스코어) 스타일을 사용
데이터베이스 테이블명, 컬럼명에 대한 관례는 회사마다 다르다.

 

 

연관관계 매핑 분석

회원과 주문

일대다, 다대일의 양방향 관계다. 따라서 연관관계의 주인을 정해야 하는데, 외래 키가 있는 주문을 연관관계의 주인으로 정하는 것이 좋다. 그러므로 Order.member를 ORDERS.MEMBER_ID 외래키와 매핑한다.

 

주문상품과 주문

다대일 양방향 관계다. 외래 키가 주문상품에 있으므로 주문상품이 연관관계의 주인이다. 그러므로 OrderItem.order를 ORDER_ITEM.ORDER_ID 외래 키와 매핑한다.

 

주문상품과 상품

다대일 단방향 관계다. OrderItem.item을 ORDER_ITEM.ITEM_ID 외래 키와 매핑한다.

 

주문과 배송

일대일 양방향 관계다. Order.delibery를 ORDERS.DELIVERY_ID 외래 키와 매핑한다.

 

카테고리와 상품

@ManyToMany를 사용해서 매핑한다. (실무에서 @ManyToMany는 사용하지 말자. 여기서는 다대다 관계를 예제로 보여주기 위해 추가했을 뿐이다.)

 

참고 : 외래 키가 있는 곳을 연관관계의 주인으로 정해라.
연관관계의 주인은 단순히 외래 키를 누가 관리하냐의 문제이지 비즈니스상 우위에 있다고 주인으로 정하면 안된다.. 예를 들어서 자동차와 바퀴가 있으면, 일대다 관계에서 항상 다쪽에 외래 키가 있으므로 외래 키가 있는 바퀴를 연관관계의 주인으로 정하면 된다. 물론 자동차를 연관관계의 주인으로 정하는 것이 불가능 한 것은 아니지만, 자동차를 연관관계의 주인으로 정하면 자동차가 관리하지 않는 바퀴 테이블의 외래 키 값이 업데이트 되므로 관리와 유지보수가 어렵고, 추가적으로 별도의 업데이트 쿼리가 발생하는 성능 문제도 있다. 자세한 내용은 JPA 기본편을 참고하자.

 

 

 

엔티티 클래스 개발


Member 엔티티

package jpabook.jpashop.domain;

import lombok.Getter;
import lombok.Setter;

import javax.persistence.*;
import java.util.*;

@Entity
@Getter
@Setter
public class Member {

    @Id				// pk
    @GeneratedValue
    @Column(name = "member_id")
    private Long id;

    private String name;

    @Embedded
    private Address address;		// Address에 Embeddable을 추가하거나 Address사용할 때 Embedded 어노테이션 추가

    @OneToMany(mappedBy = "member")     // 연관관계 주인이 아님을 명시
    private List<Order> orders = new ArrayList<>();
}

 

Address 값 타입

package jpabook.jpashop.domain;

import lombok.Getter;

import javax.persistence.Embeddable;

@Embeddable
@Getter
public class Address {
    private String city;
    private String street;
    private String zipcode;

    protected Address(){}

    public Address(String city, String street, String zipcode) {
        this.city = city;
        this.street = street;
        this.zipcode = zipcode;
    }
}

 

Order 엔티티

package jpabook.jpashop.domain;

import lombok.Getter;
import lombok.Setter;

import java.time.LocalDateTime;
import java.util.*;

import javax.persistence.*;

@Entity
@Table(name = "Orders")
@Getter
@Setter
public class Order {
    @Id
    @GeneratedValue
    @Column(name = "order_id")
    private Long id;

    @ManyToOne
    @JoinColumn(name = "member_id")			// Member에 pk값과 join
    private Member member;

	// 연관관계 주인이 아님을 명시 (OrderItem 엔티티에 order 컬럼명이 연관관계의 주인이다.)
    // cascade는 Order를 저장할 때 orderItems에 저장한 것도 같이 저장된다.
    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
    private List<OrderItem> orderItems = new ArrayList<>();

    @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    @JoinColumn(name = "delivery_id")
    private Delivery delivery;

    private LocalDateTime orderDate;		// java 8이상부터 hibernate에서 지원

    @Enumerated(EnumType.STRING)			// EnumType에는 ORDINAL과 STRING이 있다. (디폴트는 ORDINAL인데, 수정할 때 데이터가 원하는대로 들어가지 않아 힘들어질 수 있다. STRING 사용을 권장)
    private OrderStatus status;         // enum (ORDER, CANCLE) 두 개의 상태를 표시
    
    //==연관관계 편의 메서드==//
    public void changeMember(Member member){
        this.member = member;
        member.getOrders().add(this);
    }

    public void addOrderItem(OrderItem orderItem){
        orderItems.add(orderItem);
        orderItem.setOrder(this);
    }

    public void setDelivery(Delivery delivery){
        this.delivery = delivery;
        delivery.setOrder(this);
    }
}

OrderStatus Enum

package jpabook.jpashop.domain;

public enum OrderStatus {
    ORDER, CANCLE
}

 

 

Delivery 엔티티

package jpabook.jpashop.domain;

import lombok.Getter;
import lombok.Setter;

import javax.persistence.*;

@Entity
@Getter
@Setter
public class Delivery {
    @Id
    @GeneratedValue
    @Column(name = "delivery_id")
    private long id;

    @OneToOne(mappedBy = "delivery")		// 연관관계 주인이 Order 엔티티의 delivery 컬럼명임을 명시
    private Order order;

    @Embedded
    private Address address;

    @Enumerated(EnumType.STRING)
    private DeliveryStatus deliveryStatus;
}

DeliveryStatus Enum

package jpabook.jpashop.domain;

public enum DeliveryStatus {
    READY,COMP
}

 

OrderItem 엔티티

package jpabook.jpashop.domain;

import jpabook.jpashop.domain.Item.Item;
import lombok.Getter;
import lombok.Setter;

import javax.persistence.*;

@Entity
@Getter
@Setter
public class OrderItem {
    @Id
    @GeneratedValue
    @Column(name = "order_item_id")
    private Long id;

    @ManyToOne
    @JoinColumn(name = "item_id")
    private Item item;

    @ManyToOne
    @JoinColumn(name = "order_id")
    private Order order;

    private int orderPirce;    // 주문 가격
    private int count;    // 주문 수량
}

 

Item 엔티티

package jpabook.jpashop.domain.Item;

import jpabook.jpashop.domain.Category;
import lombok.Getter;
import lombok.Setter;
import java.util.*;

import javax.persistence.*;

@Entity
@Getter
@Setter
// SINGLE_TABLE, TABLE_PER_CLASS, JOINED 전략이 있다.
// SINGLE_TABLE은 상속받은 엔티티의 테이블을 만드는 것이 아닌 DTYPE을 통해 상속받은 엔티티 중 어떤 엔티티를
// 사용하는지 파악한다.
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)	
@DiscriminatorColumn(name = "dtype")
public abstract class Item {
    @Id
    @GeneratedValue
    @Column(name = "item_id")
    private Long id;

    private String name;
    private int price;
    private int stockQuantity;

    @ManyToMany(mappedBy = "items")
    private List<Category> categories = new ArrayList<>();
}

Album 엔티티

package jpabook.jpashop.domain.Item;

import lombok.Getter;
import lombok.Setter;

import javax.persistence.DiscriminatorValue;
import javax.persistence.Entity;

@Entity
@Getter
@Setter
@DiscriminatorValue("A")		// Item 중 Dtype이 'A'인 것은 Alumn 엔티티를 가르킨다.
public class Album extends Item {
    private String artist;
    private String etc;
}

Book 엔티티

package jpabook.jpashop.domain.Item;

import lombok.Getter;
import lombok.Setter;

import javax.persistence.DiscriminatorValue;
import javax.persistence.Entity;

@Entity
@Getter
@Setter
@DiscriminatorValue("B")			// Album 엔티티의 주석과 같은 느낌이다.
public class Book extends Item {
    private String author;
    private String isbn;
}

Movie 엔티티

package jpabook.jpashop.domain.Item;

import lombok.Getter;
import lombok.Setter;

import javax.persistence.DiscriminatorValue;
import javax.persistence.Entity;

@Entity
@Getter
@Setter
@DiscriminatorValue("M")		// Album 엔티티의 주석과 같은 느낌이다.
public class Movie extends Item {
    private String director;
    private String actor;
}

 

Category 엔티티

package jpabook.jpashop.domain;

import jpabook.jpashop.domain.Item.Item;
import lombok.Getter;
import lombok.Setter;

import javax.persistence.*;
import java.util.*;

@Entity
@Getter
@Setter
public class Category {
    @Id
    @GeneratedValue
    @Column(name = "category_id")
    private Long id;

    private String name;

    @ManyToMany
    @JoinTable(name = "category_item",
        joinColumns = @JoinColumn(name = "category_id"),
            inverseJoinColumns = @JoinColumn(name = "item_id")
    ) // 다대다의 경우 조인 테이블이 필요하다 (다대다를 일대다, 다대일로 풀어낼 테이블이 필요)
    private List<Item> items = new ArrayList<>();

    @ManyToOne
    @JoinColumn(name = "parent_id")			// 부모
    private Category parent;

    @OneToMany(mappedBy = "parent")				// 자식
    private List<Category> child = new ArrayList<>();
    
    //==연관관계 메서드==//
    public void addChildCategory(Category child){
        this.child.add(child);
        child.setParent(this);
    }
}

 

위의 코드를 추가한 뒤 실행하고 H2 콘솔에서 테이블을 확인해보면 위의 설계대로 테이블이 생성된 것을 확인할 수 있습니다.

생성된 테이블

참고 : 이론적으로 Getter, Setter 모두 제공하지 않고 꼭 필요한 별도의 메서드를 제공하는게 가장 이상적입니다.

하지만, 실무에서 엔티티의 데이터는 조회할 일이 너무 많으므로 Getter의 경우 모두 열어두는 것이 편리합니다. Getter은 아무리 호출해도 어떤 일이 발생하지 않기 때문입니다. 하지만, Setter의 경우는 문제가 다릅니다. Setter를 호출하면 데이터가 변합니다. Setter를 막 열어두면 가까운 미래에 엔티티가 도대체 왜 변경되는지 추적하기 점점 힘들어집니다. 그래서 엔티티를 변경할 때는 Setter 대신에 변경지점이 명확하도록 변경을 위한 비즈니스 메서드를 별도로 제공해야 합니다. 그래야 유지보수성이 올라갑니다. 

결론 : Getter 어노테이션은 추가하되, Setter는 지양

 

참고 : 실무에서는 @ManyToMany를 지양 (지양보다는 절대 사용하지 마라.)

@ManyToMany는 편리한 것 같지만, 중간 테이블(CATEGORY_ITEM)에 컬럼을 추가할 수 없고, 세밀하기 쿼리를 실행하기 어렵기 때문에 실무에서 사용하기에는 한계가 있다. 중간 엔티티 (CategoryItem을 만들고 @ManyToOne, @OneToMany로 매핑해서 사용하자)

결론 : 다대다 매핑 사용 X 필요 시  (다대다 => 일대다 + 다대일) 관계로 풀어내자

 

참고 : 위에서 사용한 Address와 같은 값 타입은 변경 불가능하게 설계해야 합니다.

@Setter를 제거하고, 생성자에서 값을 모두 초기화해서 변경 불가능한 클래스를 만들자. JPA 스펙상 엔티티나 임베디드 타입(@Embeddable)은 자바 기본 생성자(default constructor)를 pulbic 또는 protected로 설정해야 한다. public 보다는 protected로 설정하는 것이 더 안전하다.
>JPA가 이런 제약을 두는 이유는 JPA 구현 라이브러리가 객체를 생성할 때 리플렉션 같은 기술을 사용할 수 있도록 지원해야 하기 때문이다.   

결론 : 값 타입의 경우 기본 생성자로만 변경이 가능하도록 설계하자.

 

 

 

엔티티 설계시 주의점


엔티티에는 가급적 Setter를 사용하지 말자.

  • Setter가 모두 열려있다면, 변경 포인트가 너무 많아 유지보수가 어렵다.
  • 모든 연관관계는 지연로딩으로 설정
    • 즉시로딩은 예측이 어렵고, 어떤 SQL이 실행될지 추적하기 어렵다. 특히 JPQL을 실행할 때 N+1 문제가 자주 발생한다.
    • 실무에서 모든 연관관계는 지연로딩으로 설정해야 한다.
    • 연관된 엔티티를 함께 DB에서 조회해야 하면, fetch join 또는 엔티티 그래프 기능을 사용한다.
    • @XToOne(OneToOne, ManyToOne) 관계는 기본이 즉시로딩이므로 직접 지연로딩으로 설정해야 한다.
  • 컬렉션은 필드에서 초기화 하자
    • 컬렉션은 필드에서 바로 초기화 하는 것이 안전하다.
    • null 문제에서 안전하다. (바로 초기화 시키기 때문)
    • 하이버네이트는 엔티티를 영속화할 때, 컬렉션을 감싸서 하이버네이트가 제공하는 내장 컬렉션으로 변경한다. 만약 getOrders() 처럼 임의의 메서드에서 컬렉션을 잘못 생성하면 하이버네이트 내부 메커니즘에서 문제가 발생할 수 있다.
    • 따라서, 필드레벨에서 생성하는 것이 안전하고, 코드도 간결하다.
Member member = new Member();
System.out.println(member.getOrders().getClass());
em.persist(member);
System.out.pritnln(member.getOrders().getClass());

// 출력 결과
class java.util.ArrayList
class org.hibernate.collection.internal.PersistentBag
hibernate collection으로 감싸는 이유는 변경 감지(더티체킹)를 위해서 감싸야합니다. 한번 초기화만 해주는 이유는 hibernate에서 감쌋는데, 다시 초기화를 해버리면 hibernate에서 원하는대로 동작할 수 없기 때문입니다.  그렇기 때문에 되도록이면 바꾸지 않는 것을 권장합니다.

 

 

 

 

 

출처

 

실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발 - 인프런 | 강의

실무에 가까운 예제로, 스프링 부트와 JPA를 활용해서 웹 애플리케이션을 설계하고 개발합니다. 이 과정을 통해 스프링 부트와 JPA를 실무에서 어떻게 활용해야 하는지 이해할 수 있습니다., - 강

www.inflearn.com

 

프로젝트 환경설정

  • 프로젝트 생성
  • 라이브러리 살펴보기
  • View 환경 설정
  • H2 데이터베이스 설치
  • JPA와 DB 설정, 동작확인

 

 

프로젝트 생성

  • 스프링 부트 스타터(https://start.spring.io/)
  • 사용 기능 : web, thymeleaf, jpa, h2, lonbok, validation
    • groupId : jpabook
    • artifactId : jpashop
스프링 부트 스타터 설정 필독!
스프링 부트 버전은 2.4.x 버전을 선택해주세요.
자바 버전은 11을 선택해주세요
Validation (JSR-303 validation with Hibernate validator) 모듈을 꼭! 추가해주세요.

 

사용 기능

  • web
    • RESTful api, Spring MVC, Apache Tomcat 내장하고 있기 때문에 웹 애플리케이션을 만들 땐 꼭 추가해야되는 Dependency 입니다.
  • thymeleaf
    • view template입니다.
    • jsp를 사용하지 않는 이유는 성능면에서 좋지 않고, 스프링 부트에서도 권장하지 않기 때문입니다.
  • jpa
    • Java Persistence API의 약자로 Spring Data와 Hibernate를 사용하는 ORM 표준 기술입니다.
  • h2
    • MySQL과 같은 좋은 데이터베이스가 많지만, h2를 사용하는 이유는 MySQL은 설치하거나 하는 부분에서 까다롭고 강의의 목적이 아니기 때문에 강의에 좀 더 집중하기 위해 사용합니다.
    • h2는 간단하게 웹 애플리케이션 실행할 때 데이터베이스를 내장해서 실행할 수 있고, 간단하게 교육용으로 장점이 많다고 합니다.
  • lombok
    • 지루하게 반복하는 코드를 줄여줍니다.
    • 예를들면, getter, setter 같은 경우 직접 코드를 작성하지 않고 @getter, @setter과 같은 어노테이션만 추가하면 기능을 사용할 수 있게 해줍니다. (사용해봤는데 정말 편리한 라이브러리인 것 같습니다.)

 

 

Lombok 설정

lombok을 사용하기 위해선 아래와 같은 설정을 해주어야합니다.

  1. preference -> plugins -> lombok 검색 후 설치
  2. preference -> annotation processors -> enable annotation processing 체크

 

스프링 부트 라이브러리 살펴보기

  • spring-boot-starter-web
    • spring-boot-starter-tomcat : 톰캣 (웹서버)
    • spring-webmvc : 스프링 웹 MVC 
  • spring-boot-starter-thymeleaf : 타임리프 템플릿 엔진(View)
  • spring-boot-starter-data-jpa
    • spring-boot-starter-aop
    • spring-boot-starter-jdbc
      • HikariCP 커넥션 풀 (부트 2.0 기본)
    • hibernate + JPA : 하이버네이트 + JPA
    • spring-data-jpa : 스프링 데이터 JPA
  • spring-boot-starter : 스프링 부트 + 스프링 코어 + 로깅
    • spring-boot
      • spring-core
    • spring-boot-starter-logging
      • logback, slf4j

 

 

테스트 라이브러리

  • spring-boot-starter-test
    • junit : 테스트 프레임워크 (자바 테스트)
    • mockito : 목 라이브러리 
    • assertj : 테스트 코드를 좀 더 편하게 작성하게 도와주는 라이브러리 (보통 원하는 값과 결과 값이 일치하는지 테스트할 때 많이 써봄)
    • spring-test : 스프링 통합 테스트 지원

 

  • 핵심 라이브러리
    • 스프링 MVC
    • 스프링 ORM
    • JPA, Hibernate
    • 스프링 데이터 JPA
  • 기타 라이브러리
    • h2 데이터베이스 클라이언트
    • 커넥션 풀 : 부트 기본은 HikariCP
    • WEB(thymeleaf)
    • 로깅 SLF4J & LogBack
    • 테스트

참고 : 스프링 데이터 JPA는 스프링과 JPA를 먼저 이해하고 사용해야 하는 응용기술

 

 

 

View 환경 설정

thymeleaf 템플릿 엔진

 

  • 스프링 부트 thymeleaf viewName 매핑
    • resources : templeates/ + {ViewName} + .html
@Controller
public class HelloController {

    @GetMapping("hello")
    public String hello(Model model){
        model.addAttribute("data", "hello!!!!");
        return "hello";
    }
}

 

thymeleaf 템플릿 엔진 동작 확인 (hello.html)

<!DOCTYPE HTML>
  <html xmlns:th="http://www.thymeleaf.org">
  <head>
      <title>Hello</title>
      <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
  </head>
<body>
<p th:text="'안녕하세요. ' + ${data}" >안녕하세요. 손님</p>
  </body>
  </html>

위치 : resources/templates/hello.html

참고 : spring-boot-devtools 라이브러리를 추가하면, html 파일을 컴파일만 해주면 서버 재시작 없이 View 파일 변경이 가능합니다.
인텔리제이 컴파일 방법 : 메뉴 Build -> Recompile

implementation 'org.springframework.boot:spring-boot-devtools'  << 를 build.gradle에 추가해주면 됩니다.

 

 

H2 데이터베이스 설치

  • https://www.h2database.com
  • 다운로드 및 설치
  • 데이터베이스 파일 생성 방법
    • jdbc:h2:~/jpashop (최소 한번)
    • ~/jpashop.mv.db 파일 생성 확인
    • 이후 부터는 jdbc:h2:tcp://localhost/~/jpashop 이렇게 접속
참고!
H2 데이터베이스는 꼭 1.4.199 버전 사용
실행할 때, permission denied된다면 "chmod 755 h2.sh" 친 다음 ./h2.sh로 실행

 

 

 

JPA와 DB 설정, 동작확인

김영한님은 application.properties 대신 yaml인 application.yaml을 사용하신다. 개인적으로 더 편리하다고 합니다. application.properties를 삭제하고 application.yml을 생성한 뒤 아래 코드를 추가해줍니다.

spring:
  datasource:
    url: jdbc:h2:tcp://localhost/~/jpashop
    username: sa
    password:
    driver-class-name: org.h2.Driver
jpa:
  hibernate:
    ddl-auto: create
  properties:
    hibernate:
      #show_sql: true
      format_sql: true
logging.level:
  org.hibernate.SQL: debug
  • spring.jpa.hibernate.ddl-auto : create
    • 애플리케이션 실행 시점에 테이블을 drop하고, 다시 생성한다는 의미입니다.
참고: 모든 로그 출력은 가급적 로거를 통해 남겨야 합니다.
show_sql 옵션은 System.out에 하이버네이트 실행 SQL을 남기는 것이고,
org.hibernate.SQL 옵션은 logger를 통해 하이버네이트 실행 SQL을 남기는 것 입니다.
그렇기 때문에 show_sql은 주석처리해주었고, org.hibernate.SQL을 통해 로거로 로그를 확인할 수 있도록 했습니다.

 

yml 코드 작성법 등을 보고싶으면 아래 링크를 통해 레퍼런스 자료를 찾아보시는 걸 추천드립니다.

 

Spring Boot

Commercial support Business support from Spring experts during the OSS timeline, plus extended support after OSS End-Of-Life. Publicly available releases for critical bugfixes and security issues when requested by customers.

spring.io

 


실제 동작하는지 확인하기

 

회원 엔티티

@Entity
@Getter
@Setter
public class Member {

    @Id
    @GeneratedValue
    private Long id;
    private String username;
}

회원 리포지토리

@Repository
public class MemberRepository {

    @PersistenceContext
    private EntityManager em;

    public Long save(Member member){
        em.persist(member);
        return member.getId();
    }

    public Member find(Long id){
        return em.find(Member.class, id);
    }
}

테스트

@RunWith(SpringRunner.class)
@SpringBootTest
public class MemberRepositoryTest {

    @Autowired
    MemberRepository memberRepository;

    @Test
    @Transactional
    @Rollback(false)
    public void testMember() throws Exception {
        //given
        Member member = new Member();
        member.setUsername("memberA");

        //when
        Long saveId = memberRepository.save(member);
        Member findMember = memberRepository.find(saveId);
        
        //then
        Assertions.assertThat(findMember.getId()).isEqualTo(member.getId());
        Assertions.assertThat(findMember.getUsername()).isEqualTo(member.getUsername());
    }
}
  • @Transactional
    • Entity 변경 작업은 모두 트랜잭션 내에서만 이루어져야합니다. Member는 Entity이기 때문에 Transactional을 추가하지 않으면 에러가 뜨게됩니다.
    • 해당 어노테이션이 테스트에있으면 테스트가 끝난다음 바로 롤백을 하기 때문에 h2에 데이터가 남아있지 않습니다.
  • @Rollback(false)
    • 위의 트랜잭셔널 어노테이션이 테스트에 있으면 바로 롤백이되어 h2-console에서 입력한 데이터를 볼 수 없습니다.
    • 이러한 경우 위의 @Rollback(false) 어노테이션을 추가해주면 h2-console에서 입력한 데이터를 볼 수 있습니다.
주의! @Test는 JUnit4를 사용하시면 org.junit.Test를 사용해야 합니다. 만약 JUnit5 버전을 사용하면 그것에 맞게 사용하면 됩니다.

 

  • Entity, Repository 동작확인
  • jar 빌드해서 동작 확인

 

 

 

쿼리 파라미터 로그 남기기

  • 김영한님 피셜로 어마어마한 꿀팁이라고 합니다. 개발할 때 엄청 편리하다고 합니다. jpa로 쓰면 답답한게 sql 나가는거랑 데이터베이스 커넥션 가져오는게 어느 타이밍에 되는지 궁금할 때가 많은데 이러한 궁금증을 해결해준다고합니다.
  • 로그에 다음을 추가하기 org.hibernate.type : SQL 실행 파라미터를 로그로 남긴다.
  • 외부 라이브러리 사용

스프링 부트를 사용하면 아래 라이브러리만 추가하면 됩니다.

implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.5.6'
참고 : 쿼리 파라미터를 로그로 남기는 외부 라이브러리는 시스템 자원을 사용하므로, 개발 단계에서는 편하게 사용해도 됩니다. 하지만 운영 시스템에 적용하려면 꼭 성능테스트를 하고 사용하는 것이 좋습니다.

 

 

 

 

출처

 

실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발 - 인프런 | 강의

실무에 가까운 예제로, 스프링 부트와 JPA를 활용해서 웹 애플리케이션을 설계하고 개발합니다. 이 과정을 통해 스프링 부트와 JPA를 실무에서 어떻게 활용해야 하는지 이해할 수 있습니다., - 강

www.inflearn.com

 

객체지향 쿼리 언어 소개


JPA는 다양한 쿼리 방법을 지원

  • JPQL
  • JPA Criteria
  • QueryDSL
  • 네이티브 SQL
  • JDBC API 직접 사용, MyBatis, SpringJdbcTemplate 함께 사용

 

JPQL 소개

  • 가장 단순한 조회 방법
    • EntityManager.find()
    • 객체 그래프 탐색(a.getB().getC())
  • 나이가 18살 이상인 회원을 모두 검색하고 싶다면?

 

JPQL

  • JPA를 사용하면 엔티티 객체를 중심으로 개발
  • 문제는 검색 쿼리
  • 검색을 할 때도 테이블이 아닌 엔티티 객체를 대상으로 검색
  • 모든 DB 데이터를 객체로 변환해서 검색하는 것은 불가능
  • 애플리케이션이 필요한 데이터만 DB에서 불러오려면 결국 검색 조건이 포함된 SQL이 필요
  • JPA는 SQL을 추상화한 JPQL이라는 객체 지향 쿼리 언어 제공
  • SQL과 문법 유사, SELECT, FROM ,WHERE, GROUP BY, HAVING, JOIN 지원
  • JPQL은 엔티티 객체를 대상으로 쿼리
  • SQL은 데이터베이스 테이블을 대상으로 쿼리
  • SQL을 추상화해서 특정 데이터베이스 SQL에 의존 X

JPQL 쿼리문 검색  /  실제 실행된 SQL

JPQL의 쿼리문은 단순한 String이기 때문에 동적 쿼리를 만들기가 어렵다. 그렇기때문에 동적 쿼리를 작성할 대안이 필요한데 Criteria가 해결해준다.

 

 

Criteria 소개

// Criteria 사용 준비
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Member> query = cb.createQuery(Member.class);

// 루트 클래스 (조회를 시작할 클래스)
Root<Member> m = query.from(Member.class);

// 쿼리 생성
CriteriaQuery<Member> cq = query.select(m).where(cb.equal(m.get("username"), "kim"));
List<Member> reulstList = em.createQuery(cq).getResultList();

Criteria를 통해 실행된 쿼리문

  • 장점
    • 문자가 아닌 자바코드로 JPQL을 작성할 수 있음
    • JPQL 빌더 역할
    • JPA 공식 기능
  • 단점
    • SQL스럽지 않다.
    • 너무 복잡하고 실용성이 없다. (보기 어려워 유지보수하기 힘듦)
  • 결론
    • Criteria 대신에 QueryDSL 사용 권장

 

QueryDSL 소개

// JPQL
// select m from Member m where m.age > 18
JPAFactoryQuery query = new JPAQueryFactory(em);
QMember m = QMember.member;

List<Member> list = query.selectFrom(m)
                                             .where(m.age.at(18))
                                                  .orderBy(m.name.desc())
                                             .fetch();
  • 문자가 아닌 자바코드로 JPQL을 작성할 수 있음
  • JPQL 빌더 역할
  • 컴파일 시점에 문법 오류를 찾을 수 있음
  • 동적쿼리 작성 편리함
  • 단순하고 쉬움
  • 실무 사용 권장

자세히 공부하고 싶으면 아래 레퍼런스가서 공부하자

 

Querydsl Reference Guide

The Java 6 APT annotation processing functionality is used in Querydsl for code generation in the JPA, JDO and Mongodb modules. This section describes various configuration options for the code generation and an alternative to APT usage. 3.3.1. Path initi

querydsl.com

 

 

 

네이티브 SQL 소개

// 네이티브 쿼리
String sql =
         “SELECT ID, AGE, TEAM_ID, NAME FROM MEMBER WHERE NAME = ‘kim’";

List<Member> resultList =
         em.createNativeQuery(sql, Member.class).getResultList();
  • JPA가 제공하는 SQL을 직접 사용하는 기능
  • JPQL로 해결할 수 없는 특정 데이터베이스에 의존적인 기능
  • 예) 오라클 CONNECT BY, 특정 DB만 사용하는 SQL 힌트

 

JDBC 직접 사용, SpringJdbcTemplate 등

  • JPA를 사용하면서 JDBC 커넥션을 직접 사용하거나, 스프링 JdbcTemplate, MyBatis 등을 함께 사용 가능
  • 단 영속성 컨텍스트를 적절한 시점에 강제로 플러시 필요
  • 예) JPA를 우회해서 SQ을 실행하기 직전에 영속성 컨텍스트 수동 플러스

 

 

기본 문법과 쿼리 API 


 

JPQL 문법

select_문 :: =
    select_절
    from_절
    [where_절]
    [groupby_절]
    [having_절]
    [orderby_절]

update_문 :: = update_절 [where_절]
delete_문 :: = delete_절 [where_절]
  • select m from Member as m where m.age > 18
  • 엔티티와 속성은 대소문자 구분O (Member, age)
  • JPQL 키워드는 대소문자 구분X (SELECT, FROM, where)
  • 엔티티 이름 사용, 테이블 이름이 아님(Member)
  • 별칭은 필수(m) (as는 생략가능)

 

집합과 정렬

select
     COUNT(m), //회원수
     SUM(m.age), //나이 합
     AVG(m.age), //평균 나이
     MAX(m.age), //최대 나이
     MIN(m.age) //최소 나이
from Member m
  • GROUP BY, HAVING
  • ORDER BY

 

TypeQuery, Query

  • TypeQuery : 반환 타입이 명확할 때 사용
  • Query : 반환 타입이 명확하지 않을 때 사용

 

결과 조회 API

  • query.getResultList() : 결과가 하나 이상일 때, 리스트 반환
    • 결과가 없으면 빈 리스트 반환
  • qeury.getSingleResult() : 결과가 정확히 하나, 단일 객체 반환
    • 결과가 없으면 : javax.persistence.NoResultException
    • 둘 이상이면 : javax

 

파라미터 바인딩 - 이름 기준, 위치 기준

 

 

 

프로젝션


프로젝션

  • SELECT 절에 조회할 대상을 지정하는 것 
  • 프로젝션 대상 : 엔티티, 임베디드 타입, 스칼라 타입 (숫자, 문자등 기본 데이터 타입)
  • SELECT m FROM Member m -> 엔티티 프로젝션
  • SELECT m.team FROM Member m -> 엔티티 프로젝션
  • SELECT m.address FROM Member m -> 임베디드 타입 프로젝션
  • SELECT m.username, m.age FROM Member m -> 스칼라 타입 프로젝션
  • DISTINCT로 중복 제거
  • SELECT m.username, m.age FROM Member m
  • 1. Query 타입으로 조회
  • 2. OBject[] 타입으로 조회
  • 3. new 명령어로 조회
    • 단순 값을 DTO로 바로 조회
      SELECT new jpabook.jpql.UserDTO(m.username, m.age) FROM Member m
    • 패키지 명을 포함한 전체 클래스 명 입력
    • 순서와 타입이 일치하는 생성자 필요

 

 

페이징 API


페이징 API

  • JPA는 페이징을 다음 두 API로 추상화
  • setFirstResult(int startPosition) : 조회 시작 위치 (0부터 시작)
  • setMaxResults(int maxResult) : 조회할 데이터 수

JPA 페이징 쿼리
Oracle 페이징 쿼리

실제 Oracle 페이징 쿼리를 보면 3번의 뎁스를 통해 쿼리문을 작성해야 하지만, JPA는 setFirstResult, setMaxResults로 편하게 페이징 작업을 할 수 있는 것을 볼 수 있습니다.

 

 

 

조인


조인

  • 내부조인
    • SELECT m FROM Member m [INNER] JOIN m.team t
  • 외부 조인
    • SELECT m FROM Member m LEFT [OUTER] JOIN m.team t
    • 참고로 외부 조인할 때 SQL log를 oracle로 하면 에러 납니다.
  • 세타 조인
    • SELECT count(m) from Member m, Team t where m.username = t.name

 

조인 - ON 절

  • ON절을 활용한 조인 (JPA 2.1부터 지원)
    1. 조인 대상 필터링
    2. 연관관계 없는 엔티티 외부 조인 (하이버네이트 5.1 부터)

 

 

1. 조인 대상 필터링

예) 회원과 팀을 조인하면서, 팀 이름이 A인 팀만 조인

JPQL
SELECT m, t FROM Member m LEFT JOIN m.team t on t.name = 'A'

SQL
SELECT m.*, t.* FROM Member m LEFT JOIN Team t ON m.TEAM_ID = t.id and t.name = 'A'

 

 

2. 연관관계 없는 엔티티 외부 조인

예) 회원의 이름과 팀의 이름이 같은 대상 외부 조인

JPQL
SELECT m, t FROM Member m LEFT JOIN Team t on m.username = t.name

SQL
SELECT m.*, t.* FROM Member m LEFT JOIN Team t ON m.username = t.name

 

 

 

서브 쿼리


서브 쿼리

메인 쿼리랑 서브 쿼리랑 테이블 관계가 없다.
  • 나이가 평균보다 많은 회원
    select m from Member m
    where m.age > (select avg(m2.age) from Member m2)
  • 한 건이라도 주문한 고객
    select m from Member m
    where (select count(o) from Order o where m = o.member) > 0

 

서브 쿼리 지원 함수

  • [NOT] EXISTS (subquery) : 서브쿼리에 결과가 존재하면 참
    • {ALL | ANY | SOME} (subquery)
    • ALL 모두 만족하면 참
    • ANY, SOME : 같은 의미, 조건을 하나라도 만족하면 참
  • [NOT] IN (subquery) : 서브쿼리의 결과 중 하나라도 같은 것이 있으면 참

 

서브 쿼리 - 예제

  • 팀A 소속인 회원
    select m from Member m
    where exists (select t from m.team t where t.name = '팀A')
  • 전체 상품 각각의 재고보다 주문량이 많은 주문들
    select o from Order o
    where o.orderAmount > ALL (select p.stockAmount from Product p)
  • 어떤 팀이든 팀에 소속된 회원
    select m from Member m
    where m.team = ANY (select t from Team t)

 

JPA 서브 쿼리 한계

  • JPA는 WHERE, HAVING 절에서만 서브 쿼리 사용 가능
  • SELECT 절도 가능 (하이버네이트에서 지원)
  • FROM 절의 서브 쿼리는 현재 JPQL에서 불가능
    • 조인으로 풀 수 있으면 풀어서 해결

 

 

 

JPQL 타입 표현


JPQL 타입 표현

  • 문자 : 'HELLO', 'She''s'
  • 숫자 : 10L(Long), 10D(Double), 10F(Float)
  • Boolean : TRUE, FALSE
  • ENUM : jpabook.MemberType.Admin (패키지명 포함)
  • 엔티티 타입 : TYPE(m) = Member (상속 관계에서 사용)

 

JPQL 기타

  • SQL과 문법이 같은 식
  • EXISTS, IN
  • AND, OR, NOT
  • =, >, >=, <, <=, <>
  • BETWEEN, LIKE, IS NULL

 

 

 

조건식 - CASE 식


  • 기본 CASE 식

  • 단순 CASE 식

  • COALESCE : 하나씩 조회해서 null이 아니면 반환

  • NULLIF : 두 값이 같으면 null 반환, 다르면 첫번째 값 반환

 

 

 

JPQL 함수


JPQL 기본 함수

  • CONCAT
  • SUBSTRING
  • TRIM
  • LOWER, UPPER
  • LENGTH
  • LOCATE
  • ABS, SQRT, MOD
  • SIZE, INDEX (JPA 용도)

 

사용자 정의 함수 호출

  • 하이버네이트는 사용전 방언에 추가해야 한다.
    • 사용하는 DB 방언을 상속받고, 사용자 정의 함수를 등록한다.
select function('group_concat', i.name) from Item i

 

 

 

 

경로 표현식


경로 표현식

.(점)을 찍어 객체 그래프를 탐색하는 것
select m.username -> 상태 필드
from Member m
join m.team t      -> 단일 값 연관 필드
join m.order o       -> 컬렉션 값 연관 필드
where t.name = '팀A'

 

 

경로 표현식 용어 정리

  • 상태 필드 : 단순히 값을 저장하기 위한 필드 (ex : m.username)
  • 연관 필드 : 연관관계를 위한 필드
    • 단일 값 연관 필드
      • @ManyToOne, @OneToOne, 대상이 엔티티 (ex : m.team)
    • 컬렉션 값 연관 필드
      • @OneToMany, @ManyToMany, 대상이 컬렉션 (ex : m.orders)

 

경로 표현식 특징

  • 상태 필드 : 경로 탐색의 끝, 탐색 X
  • 단일 값 연관 경로 : 묵시적 내부 조인(inner join) 발생, 탐색 O
    • 탐색이 된다는 것은 "select m.team.name From Member m" 에서  m.team에서 .name으로 탐색이 가능하다는 것을 의미합니다.
  • 컬렉션 값 연관 경로 : 묵시적 내부 조인 발생, 탐색 X
    • FROM 절에서 명시적 조인을 통해 별칭을 얻으면 별칭을 통해 탐색 가능
묵시적 내부 조인이란?

"select m.team From Member m" 의 JPQL 쿼리문을 날리면 Member와 Team 테이블간의 inner join이 발생하게 됩니다.
이렇듯 inner join을 사용하지 않았지만 실제 쿼리문에서 inner join이 사용되는 것을 말합니다.
(참고로 묵시적 내부 조인이 발생하도록 짜면 안됩니다.) => 성능 튜닝에 어려움이 있다고 합니다.
==>> 묵시적 조인보다 명시적 조인을 사용하는 것을 권장합니다.

 

 

상태 필드 경로 탐색

  • JPQL
    • select m.username, m.age from Member m
  • SQL
    • select m.username, m.age from Member m

 

단일 값 연관 경로 탐색

  • JPQL
    • select o.member from Order o
  • SQL
    • select m.*
      from Orders o
      inner join Member m on o.member_id=m.id
묵시적 조인이 발생한다!

 

 

명시적 조인, 묵시적 조인

  • 명시적 조인 : join 키워드 직접 사용
    • select m from Member m join m.team t
  • 묵시적 조인 : 경로 표현식에 의해 묵시적으로 SQL 조인 발생 (내부 조인만 가능)
    • select m.team from Member m

 

 

경로 탐색을 사용한 묵시적 조인 시 주의사항

  • 항상 내부 조인
  • 컬렉션은 경로 탐색의 끝, 명시적 조인을 통해 별칭을 얻어야 탐색이 가능함
  • 경로 탐색은 주로 SELECT, WHERE 절에서 사용하지만 묵시적 조인으로 인해 SQL의 FROM (JOIN) 절에 영향을 줌

 

실무 조언

  • 가급적 묵시적 조인 대신에 명시적 조인 사용
  • 조인은 SQL 튜닝에 중요 포인트
  • 묵시적 조인은 조인이 일어나는 상황을 한눈에 파악하기 어려움

 

 

 

JPQL - 페치 조인 (fetch join)


실무에서 정말정말 중요함

 

페치 조인 (fetch join)

  • SQL 조인 종류 X
  • JPQL에서 성능 최적화를 위해 제공하는 기능
  • 연관된 엔티티나 컬렉션을 SQL 한 번에 함께 조회하는 기능
  • join fetch 명령어 사용
  • 페치 조인 ::= [LEFT [OUTER] | INNER] JOIN FETCH 조인 경로

 

엔티티 패치 조인

  • 회원을 조회하면서 연관된 팀도 함께 조회(SQL 한 번에)
  • SQL을 보면 회원 뿐만 아니라 팀(T.*)도 함께 SELECT
  • [JPQL]
    select m from Member m join fetch m.team
  • [SQL]
    select M.*, T.* from Member M
    INNER JOIN Team T ON M.TEAM_ID=T.ID

 

페치 조인 사용 코드

String jqpl = "select m from Member m join fetch m.team";
List<Member> members = em.createQuery(jpql, Member.class).getResultList();

for(Member member : members){
    //페치 조인으로 회원과 팀을 함께 조회해서 지연 로딩 X
    System.out.println("username = " + member.getUsername() + ", " +
                                      "teamNmae = " + member.getTeam().name());
}

생성된 쿼리문 (inner join)
출력문

 

컬렉션 페치 조인

  • 일대다 관계, 컬렉션 페치 조인
  • [JPQL]
    select t
    from Team t join fetch t.members
    where t.name = '팀A'
  • [SQL]
    SELECT T.*, M.*
    INNER JOIN MEMBER M ON T.ID=M.TEAM_ID
    WHERE T.NAME='팀A'
주의해야할 건
조회한 건 팀A 한개이지만, 팀A와 연관된 member가 몇명인지 모르기 때문에
row의 수가 1개인지 100개인지 10000개인지 알 수 없습니다.
아래는 팀A를 조회하고 팀A에 속한 member가 2명인 경우 조회했을 때의 결과입니다.

 

 

컬렉션 페치 조인 사용 코드

String jqpl = "select t from Team t join fetch t.members where t.name = '팀A'";
List<Team> teams = em.createQuery(jqpl, Team.class).getResultList();

for(Team teamA : teams){ 
    System.out.println("team = " + teamA.getName() + ", team = " + teamA);
    for(Member memberA : team.getMembers()){
        System.out.println("-> username = " + memberA.getName() + ",member = " + memberA);
    }
}

생성된 쿼리문 (inner join)
출력문

주의!
team은 1개 member은 2개이지만
같은 팀 2개 같은 member가 2개가 만들어진 걸 볼 수 있습니다.
이렇게 중복된 부분을 제거하기 위해 sql의 distinct가 필요합니다.

 

 

페치 조인과 DISTINCT

  • SQL의 DISTINCT는 중복된 결과를 제거하는 명령
  • JPQL의 DISTINCT 2가지 기능 제공
    1. SQL에 DISTINCT를 추가
    2. 애플리케이션에서 엔티티 중복 제거
  • select distinct t
    from Team t join fetch t.members
    where t.name = '팀A'
  • SQL에 DISTINCT를 추가하지만 데이터가 다르므로 SQL 결과에서 중복제거 실패

  • DISTINCT가 추가로 애플리케이션에서 중복 제거시도
  • 같은 식별자를 가진 Team 엔티티 제거

이전 쿼리문에 distinct를 추가하면 위처럼 됩니다.

실제 distinct 쓰기 전 출력문과 쓰고난 후의 출력문은 아래와 같습니다.

distinct 쓰기 전
distinct를 쓴 후

 

 

페치 조인과 일반 조인의 차이

  • 일반 조인 실행 시 연관된 엔티티를 함께 조회하지 않음 (그렇기 때문에 지연 로딩이 발생한다.)
  • [JQPL]
    select t
    from Team t join t.members m
    where t.name = '팀A'
  • [SQL]
    SELECT T.*
    FROM TEAM T
    INNER JOIN MEMBER M ON T.ID = M.TEAM_ID
    WHERE T.NAME = '팀A'
  • JPQL은 결과를 반환할 때 연관관계를 고려 X
  • 단지 SELECT 절에 지정한 엔티티만 조회할 뿐
  • 여기서는 팀 엔티티만 조회하고, 회원 엔티티는 조회 X
  • 페치 조인을 사용할 때만 연관된 엔티티도 함께 조회(즉시 로딩)
  • 페치 조인은 객체 그래프를 SQL 한번에 조회하는 개념

 

 

페치 조인의 특징과 한계

  • 페치 조인 대상에는 별칭을 줄 수 없다.
    • 페치 조인 대상이 5명이 있는데, 1명만 조회해서 갖고오면 4명이 누락되기 때문에 문제가 생길 수 있다. (아직 현업에서 사용해보지 않아서 이것때문에 문제가 생길지 감이 오지 않지만 그렇다고 합니다.)
    • 하이버네이트는 가능, 가급적 사용 X
    • 유일하게 사용할 때는 fetch join을 여러번 이어서 사용해야할 때 사용한다고합니다.
  • 둘 이상의 컬렉션은 페치 조인 할 수 없다.
    • 팀에 members와 orders 두 개의 컬렉션이 있는 경우 페치조인 할 수 없다고 합니다.
    • 가끔 되기도 하지만, 문제가 생길 수 있다고 합니다.
    • 일대다도 데이터가 뻥튀기가 되는데, 일대 다대다 이기 때문에 데이터가 예상하지 못하게 NxN이 되어 엄청난 데이터가 나올 수 있습니다. (어떻게 되기도 하는데 데이터가 제대로 맞지 않는다고 합니다.)
  • 컬렉션을 페치 조인하면 페이징 API(setFirstResult, setMaxResult)를 사용할 수 없다.
    • 일대일, 다대일 같은 단일 값 연관 필드들은 페치 조인해도 페이징 가능 (데이터 뻥튀기가 되지 않기 때문)
    • 하이버네이트는 경고 로그를 남기고 메모리에서 페이징 (매우 위험) (매모리에서 페이징이란 모든 데이터를 다 가져온 뒤, 페이징을 하기 때문에 필요하지 않은 모든 데이터를 갖고 온다.)
    • 해결책
      • 방향을 뒤집어서 해결할 수 있습니다. (컬렉션 대상으로 시작하는 쿼리문으로 작성하면 됩니다.)
      • "select t From Team t"를 해준 뒤, 배치사이즈를 통해 N+1 문제를 최소화 해줍니다.

컬렉션 페치 조인 시 페이징을 사용할 때 뜨는 경고문

  • 연관된 엔티티들을 SQL 한 번으로 조회 - 성능 최적화
  • 엔티티에 직접 적용하는 글로벌 로딩 전략보다 우선함
    • @OneToMany(fetch = FetchType.LAZY) // 글로벌 로딩 전략
  • 실무에서 글로벌 로딩 전략은 모두 지연 로딩
  • 최적화가 필요한 곳은 페치 조인 적용

 

 

페치 조인 - 정리

  • 모든 것을 페치 조인으로 해결할 수는 없다.
  • 페치 조인은 객체 그래프를 유지할 때 사용하면 효과적
    • m.team.getname() 이런식으로 객체 그래프를 유지할 때
  • 여러 테이블을 조인해서 엔티티가 가진 모양이 아닌 전혀 다른 결과를 내야 하면, 페치 조인 보다는 일반 조인을 사용하고 필요한 데이터들만 조회해서 DTO로 반환하는 것이 효과적
김영한님은 현업에서 복잡한 테이블 7~8개를 조인해서 풀어내야하는 문제에 직면했을 때, 결과가 빨리 나오지 않아 성능이 좋지 않았다.
왜이리 성능이 안좋은가 봤더니 쿼리가 너무 많아 발생했다.
batch, fetch join으로 짧은 시간안에 결과를 해결해냈다는 일화가...

그만큼 fetch join가 중요하기 때문에 잘 이해하고 있어야 한다는 점을 기억하면 될 것 같다!

 

 

 

JPQL - 다형성 쿼리


TYPE

  • 조회 대상을 특정 자식으로 한정
  • 예) Item 중에 Book, Movie를 조회해라
  • [JPQL]
    select i from Item i
    where type(i) IN (Book, Movie)
  • [SQL]
    select i from i
    where i.DTYPE in ('B', 'M')

 

TREAT (JPA 2.1)

  • 자바의 타입 캐스팅과 유사
  • 상속 구조에서 부모 타입을 특정 자식 타입으로 다룰 때 사용
  • FROM, WHERE, SELECT (하이버네이트 지원) 사용
  • 예) 부모인 Item과 자식 Book이 있다.
  • [JPQL]
    select i from Item i
    where treat(i as Book).auther = 'kim'
  • [SQL]
    select i.* from Item i
    where i.DTYPE = 'B' and i.auther = 'kim'

 

 

 

JPQL - 엔티티 직접 사용


엔티티 직접 사용 - 기본 키 값

  • JPQL에서 엔티티를 직접 사용하면 SQL에서 해당 엔티티의 기본 키 값을 사용
  • [JQPL]
    select count(m.id) from Member m    // 엔티티의 아이디를 사용
    select count(m) from Member m    // 엔티티를 직접 사용
  • [SQL] (JPQL 둘 다 같은 다음 SQL 실행)
    select count(m.id) as cnt from Member m
  • 엔티티를 파라미터로 전달
String jpql = "select m from Member m where m = :member";
List result = em.createQuery(jqpl)
                           .getParameter("member", member);
                           .getReulstList();
  • 식별자를 직접 전달
String jpql = "select m from Member m where m.id = :memberId";
List result = em.createQuery(jqpl)
                           .setParameter("memberId", memberId);
                           .getReulstList();
  • 실행된 SQL
select m.* from Member m where m.id=?

 

엔티티 직접 사용 - 외래 키 값

Team team = em.find(Team.class, 1L);

String qlString = "select m from Member m where m.team = :team";
List<Member> resultList = em.createQuery(qlString, Member.class)
                                 .setParameter("team", team)
                                 .getResultList();
String qlString = "select m from Member m where m.team.id = :teamId";
List<Member> resultList = em.createQuery(qlString, Member.class)                                 
                                 .setParameter("teamId", teamId)
                                 .getResultList();

실행된 SQL

select m.* from member m where m.team_id = ?

 

 

 

JPQL - Named 쿼리


Named 쿼리 - 정적 쿼리

  • 미리 정의해서 이름을 부여해두고 사용하는 JPQL
  • 정적 쿼리
  • 어노테이션, XML에 정의
  • 애플리케이션 로딩 시점에 초기화 후 재사용
  • 애플리케이션 로딩 시점에 쿼리를 검증

NamedQuery에 오타를 낸 경우

NamedQuery에서 오타를 낸 경우 프로그램이 정상 작동하는 것이 아닌 컴파일 시점에 에러를 찾아 프로그램이 중단되게 만들어줍니다. (해당 쿼리를 컴파일 시점에 캐시하며 검증을 한다고 합니다.)

 

 

 

Named 쿼리 - 어노테이션

엔티티에 namedQuery 생성
NamedQuery 직접 사용

위처럼 엔티티위에 쿼리를 작성하게 되면 엔티티를 보기 힘들어지고 엔티티 클래스의 코드가 더러워집니다. 이를해결하기 위한 방법은 아래 방법을 사용하면 됩니다.

 

Spring Data JPA reference document를 보면 

 

Spring Data JPA - Reference Documentation

Example 109. Using @Transactional at query methods @Transactional(readOnly = true) interface UserRepository extends JpaRepository { List findByLastname(String lastname); @Modifying @Transactional @Query("delete from User u where u.active = false") void del

docs.spring.io

 

아래와 같이 interface 메서드 위에 NamedQuery를 선언이 가능합니다. 아래와 같이 작성하면 컴파일 시점에 에러를 확인해주며 캐시에 저장합니다.

 

 

 

JPQL - 벌크 연산


벌크 연산

  • 재고가 10개 미만인 모든 상품의 가격을 10% 상승하려면?
  • JPA 변경 감지 기능으로 실행하려면 너무 많은 SQL 실행
    1. 재고가 10개 미만인 상품을 리스트로 조회한다.
    2. 상품의 엔티티의 가격을 10% 증가한다.
    3. 트랜잭션 커밋 시점에 변경감지가 동작한다.
  • 변경된 데이터가 100건이라면 더티체킹을 통해 100번의 UPDATE SQL 쿼리문이 나간다.

 

 

벌크 연산 예제

  • 쿼리 한 번으로 여러 테이블 로우 변경(엔티티)
  • executeUpdate()의 결과는 영향받은 엔티티 수 반환
  • UPDATE, DLEETE 지원
  • INSERT(insert into .. select, 하이버네이트 지원)
String qlString = "update Product p " + 
                              "set p.price = p.price * 1.1 " + 
                              "where p.stockAmount < :stockAmount";

int resultCount = em.createQuery(qlString)
                                      .setParameter("stockAmount", 10)
                                   .executeUpdate();

 

 

벌크 연산 주의

  • 벌크 연산은 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리
    • 벌크 연산을 먼저 실행
    • (벌크 연산할 때 자동으로 flush가 됌) 벌크 연산 수행 후 영속성 컨텍스트 초기화 (em.clear() 필수)

 

 

 

 

 

 

출처

 

자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런 | 강의

JPA를 처음 접하거나, 실무에서 JPA를 사용하지만 기본 이론이 부족하신 분들이 JPA의 기본 이론을 탄탄하게 학습해서 초보자도 실무에서 자신있게 JPA를 사용할 수 있습니다., - 강의 소개 | 인프런

www.inflearn.com

 

 

목차

  • 기본값 타입
  • 임베디드 타입(복합 값 타입)
  • 값 타입과 불변 객체
  • 값 타입의 비교
  • 값 타입 컬렉션
  • 실전 예제 - 6. 값 타입 매핑

 

 

기본값 타입


JPA의 데이터 타입 분류

  • 엔티티 타입
    • @Entity로 정의하는 객체
    • 데이터가 변해도 식별자로 지속해서 추적 가능
    • 예) 회원 엔티티의 키나 나이 값을 변경해도 식별자로 인식 가능
  • 값 타입
    • int, Integer, String처럼 단순히 값으로 사용하는 자바 기본 타입이나 객체
    • 식별자가 없고 값만 있으므로 변경시 추적 불가
    • 예) 숫자 100을 200으로 변경하면 완전히 다른 값으로 대체

 

값 타입 분류

  • 기본값 타입
    • 자바 기본 타입 (int, double)
    • 래퍼 클래스 (Integer, Long)
    • String
  • 임베디드 타입(embedded type, 복합 값 타입)
    • x,y 좌표 같은 경우
  • 컬렉션 값 타입(collection value type)

 

기본값 타입

  • 예) int age, String name
  • 생명 주기를 엔티티의 의존
    • 예) 회원을 삭제하면 이름, 나이 필드도 함께 삭제
  • 값 타입은 공유하면 X
    • 예) 회원 이름 변경시 다른 회원의 이름도 함꼐 변경되면 안됨

 

참고 : 자바의 기본 타입은 절대 공유 X

  • int, double 같은 기본 타입(primitive type)은 절대 공유 X
  • 기본 타입은 항상 값을 복사함
  • Integer같은 래퍼 클래스나 String 같은 특수한 클래스는 공유 가능한 객체이지만 변경 X

 

 

임베디드 타입 (복합 값 타입)


임베디드 타입

  • 새로운 값 타입을 직접 정의할 수 있음
  • JPA는 임베디드 타입(embedded type)이라 함
  • 주로 기본 값 타입을 모아서 만들어서 복합 값 타입이라고도 함
  • int, String과 같은 값 타입

 

임베디드 타입

  • 회원 엔티티는 이름, 근무 시작일, 근무 종료일, 주소 도시, 주소 번지, 주소 우편번호를 가진다.

  • 회원 엔티티는 이름, 근무 기간, 주소를 가진다.

 

임베디드 타입 사용법

  • @Embeddeable : 값 타입을 정의하는 곳에 표시
  • @Embedded : 값 타입을 사용하는 곳에 표시
  • 기본 생성자 필수

 

임베디드 타입의 장점

  • 재사용
  • 높은 응집도
  • Perioid.isWork() 처럼 해당 값 타입만 사용하는 의미있는 메소드를 만들 수 있음
  • 임베디드 타입을 포함한 모든 값 타입은 값 타입을 소유한 엔티티에 생명주기를 의존함

 

임베디드 타입과 테이블 매핑

 

임베디드 타입과 테이블 매핑

  • 임베디드 타입은 엔티티의 값일 뿐이다.
  • 임베디드 타입을 사용하기 전과 후에 매핑하는 테이블은 같다.
  • 객체와 테이블을 아주 세밀하게(find-grained) 매핑하는 것이 가능
  • 잘 설계한 ORM 애플리케이션은 매핑한 테이블의 수 보다 클래스의 수가 더 많음

 

임베디드 타입과 연관관계

 

@AttributeOverride : 속성 재정의

  • 한 엔티티에서 같은 값 타입을 사용하면?
  • 컬럼 명이 중복 됨
  • @AttributeOverrides, @AttributeOverride를 사용해서 컬럼 명 속성을 재정의

@AttributeOverrides, @AttributeOverride 예시

 

 

 

값 타입과 불변 객체


값 타입은 복잡한 객체 세상을 조금이라도 단순화하려고 만든 개념이다. 따라서 값 타입은 단순하고 안전하게 다룰 수 있어야 한다.

 

값 타입 공유 참조

  • 임베디드 타입 같은 값 타입을 여러 엔티티에서 공유하면 위험함
  • 부작용 (side effect) 발생

  • 예시

member1의 city만 바꾸고싶었지만, member2도 변경되기 때문에 update쿼리문이 두번 나가는 것을 볼 수 있다.

  • 해결 방법
    • 이렇게 값을 공유하고 싶을 때는 임베디드가 아닌 엔티티로 해주어야 합니다. 

 

값 타입 복사

  • 값 타입의 실제 인스턴스인 값을 공유하는 것은 위험
  • 대신 값(인스턴스)를 복사해서 사용

값을 복사해서 사용하는 경우, update 쿼리문이 한번만 나가 의도치 않은 업데이트 쿼리문이 나가지 않습니다.

 

객체 타입의 한계

  • 항상 값을 복사해서 사용하면 공유 참조로 인해 발생하는 부작용을 피할 수 있다.
  • 문제는 임베디드 타입처럼 직접 정의한 값 타입은 자바의 기본 타입이 아니라 객체 타입이다.
  • 자바 기본 타입에 값을 대입하면 값을 복사한.
  • 객체 타입은 참조 값을 직접 대입하는 것을 막을 방법이 없다.
  • 객체의 공유 참조는 피할 수 없다.

기본 타입은 타입의 값을 복사해서 넘기지만, 객체 타입은 참조 값을 전달하기 때문에 값 변경시 a와 b모두 변경이 됩니다. 

 

 

불변 객체

  • 객체 타입을 수정할 수 없게 만들면 부작용을 원천 차단
  • 값 타입은 불변 객체(immutable object)로 설계해야한다.
  • 불변 객체 : 생성 시점 이후 절대 값을 변경할 수 없는 객체
  • 생성자로만 값을 설정하고 수정자(Setter)를 만들지 않으면 됨 (또는 setter를 public이 아닌 private로 선언)
  • 참고 : Integer, String은 자바가 제공하는 대표적인 불변 객체 

불변이라는 작은 제약으로 부작용이라는 큰 재앙을 막을 수 있다.

 

 

 

값 타입의 비교


값 타입의 비교

  • 값 타입 : 인스턴스가 달라도 그 안에 값이 같으면 같은 것으로 봐야 함

 

값 타입의 비교

  • 동일성(identity) 비교 : 인스턴스의 참조 값을 비교, == 사용
  • 동등성(equivalence) 비교 : 인스턴스의 값을 비교, equals()
  • 값 타입은 a.equals(b)를 사용해서 동등성 비교를 해야 함
  • 값 타입의 equals() 메소드를 적절하게 재정의(주로 모든 필드 사용)

 

 

값 타입 컬렉션


값 타입 컬렉션

위의 Member 엔티티의 경우 1: N의 개념이기 때문에 별도의 테이블을 만들어주어야 관리가 가능합니다.

 

값 타입 사용 법

 

값 타입 컬렉션

  • 타입을 하나 이상 저장할 때 사용
  • @ElementCollection, @CollectionTable 사용
  • 데이터베이스는 컬렉션을 같은 테이블에 저장할 수 없다. (정석적으로 불가능)
  • 컬렉션을 저장하기 위한 별도의 테이블이 필요함

 

값 타입 컬렉션 사용

  • 값 타입 조회 예제
    • 값 타입 컬렉션도 지연 로딩 전략 사용 (해당 컬럼에 접근할 때 select 쿼리문이 나간다.)
  • 참고 : 값 타입 컬렉션은 영속성 전에(Cascade) + 고아 객체 제거 기능을 필수로 가진다고 볼 수 있다.

 

값 타입 컬렉션의 제약사항

  • 값 타입은 엔티티와 다르게 식별자 개념이 없다.
  • 값은 변경하면 추적이 어렵다.
  • 값 타입 컬렉션에 변경 사항이 발생하면, 주인 엔티티와 연관된 모든 데이터를 삭제하고, 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장한다. (매우 비효율적인데...) - 역시나 쓰면 안된다고 한다.
  • 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본키를 구성해야 함 : Null 입력 X, 중복 저장 X

 

값 타입 컬렉션 대안

  • 실무에서는 상황에 따라 값 타입 컬렉션 대신에 일대다 관계를 고려
  • 일대다 관계를 위한 엔티티를 만들고, 여기에서 값 타입을 사용
  • 영속성 전이(Cascade) + 고아 객체 제거를 사용해서 값 타입 컬렉션 처럼 사용
  • EX) AddressEntity

 

 

 

 

실전 예제 6 - 값 타입 매핑


 

Address 임베디드를 추가해서 변경

 

 

 

 

 

 

출처

 

자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런 | 강의

JPA를 처음 접하거나, 실무에서 JPA를 사용하지만 기본 이론이 부족하신 분들이 JPA의 기본 이론을 탄탄하게 학습해서 초보자도 실무에서 자신있게 JPA를 사용할 수 있습니다., - 강의 소개 | 인프런

www.inflearn.com

 

 

+ Recent posts