JPA 활용1 코드 정리

HwangJerry·2023년 5월 10일
0
post-custom-banner

이 포스팅은 (인프런) 김영한 - 실전! 스프링 부트와 JPA 활용1 강의를 정리한 내용입니다.

개발 환경 설정


  • 개발 기술

    • spring boot 3.0.6
    • jdk 17.0.7
  • dependencies

    • web
    • devtools
    • lombok
    • H2
    • dataJPA
  • check

    • annotation processor
    • build/run/test tool : gradle -> intelliJ

라이브러리 설명


  • 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, 하이버네이트
  • 스프링 데이터 JPA
    // 기타 라이브러리
  • H2 데이터베이스 클라이언트
  • 커넥션 풀: 부트 기본은 HikariCP
  • WEB(thymeleaf)
  • 로깅 SLF4J & LogBack
  • 테스트

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 # 쿼리 파라미터 로그 남기기
  #org.hibernate.orm.jdbc.bind: trace

구현 준비


DB ERD

  • simple
  • detailed

DB Schema

Architecture

controller : request handler, reponse sender
service : transaction 처리, 비즈니스 로직 수행
repository : Data Access object, JPA 사용시 엔티티 매니저 활용(영속성 컨텍스트;컨테이너 로 관리)
domain : 엔티티 모델, DB 테이블 구조와 함께 엔티티 필드와 직접 관련된 메소드 포함. 주로 repository에서 사용. (service 또는 controller에서는 dto 사용 권장)

회원 API 개발


Member Entity

@Entity
@Getter @Setter
@NoArgsConstructor
public class Member {
    @Id @GeneratedValue
    @Column(name = "member_id")
    private Long id;

    private String name;

MemberForm

@Getter @Setter
public class MemberForm {
    @NotEmpty(message = "회원 이름은 필수 입니다.")
    private String name;
    private String city;
    private String street;
    private String zipcode;
}

Member Repository

@Repository
@RequiredArgsConstructor
public class MemberRepository {

    private final EntityManager em;

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

    public Member findOne(Long id) {
        return em.find(Member.class, id); // .find(<조회 타입>, <PK>)
    }
}

MemberRepositoryTest

@RunWith(SpringRunner.class)
@SpringBootTest
@Transactional
class MemberServiceTest {

    @Autowired MemberService memberService;
    @Autowired MemberRepository memberRepository;
//    @PersistenceContext EntityManager em;

    @Test
    public void join() throws Exception {
        //given
        Member member = new Member();
        member.setName("kim");

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

        //then
//        em.flush(); // DB에 강제로 쿼리를 날리는 메소드
        assertEquals(member, memberRepository.findOne(savedId));
    }

    @Test
    public void check_dupl_member() throws Exception {
        //given
        Member member1 = new Member();
        member1.setName("kim");
        Member member2 = new Member();
        member2.setName("kim");

        //when
        memberService.join(member1);

        //then
        assertThrows(IllegalStateException.class, () -> {
            memberService.join(member2); // 에외 발생 의도 지점
        });
    }

}

스프링부트는 기본적으로 Datasource 설정이 없다면 메모리 모드로 실행을 수행합니다. 따라서 test환경을 설정할 때에 application 파일을 분리할텐데, 이 때 test 환경에서는 메모리 모드 활용을 하는 것도 좋다.

상품 API 개발


DB는 상속을 지원하지 않습니다. 따라서 자바 코드에서 구현되어있는 상속 관계를 "슈퍼타입", "서브타입" 논리 모델로 가져가면서, DB에 이를 물리적으로 구현하는 방안이 필요합니다.

JPA는 이를 위해 @Inheritance, @DiscriminatorColumn, @DiscriminatorValue 어노테이션을 지원합니다. 이를 이용하여 DB에 해당 논리 모델을 물리적으로 구현하는 방법은 3가지 정도로 추릴 수 있습니다.

  • @Inheritance(strategy=InheritanceType.XXX)

    1. SINGLE_TABLE(default)
    2. JOINED
    3. TABLE_PER_CLASS
  • @DiscriminatorColumn(name="DTYPE")

    • 부모 클래스에 선언
    • 하위 클래스를 구분하는 column을 선언하는 것
    • default name은 DTYPE (일반적인 관례)
  • @DiscriminatorValue("XXX")

    • 하위 클래스에 선언
    • 엔티티를 저장할 때 슈퍼타입의 구분 컬럼에 저장할 값을 지정
    • 선언하지 않으면 클래스 이름이 입력됨

Item Entity

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "dtype")
@Getter @Setter
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<Category>();

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

Book Entity

package jpabook.jpashop.domain.item;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.DiscriminatorValue;
import javax.persistence.Entity;

@Entity
@DiscriminatorValue("B")
@Getter @Setter
public class Book extends Item {
	 private String author;
	 private String isbn;
}

BookForm

@Getter @Setter
public class BookForm {
    private Long id;

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

    private String author;
    private String isbn;
}

Album Entity

@Entity
@DiscriminatorValue("A")
@Getter @Setter
public class Album extends Item {
	 private String artist;
	 private String etc;
}

Movie Entity

@Entity
@DiscriminatorValue("M")
@Getter @Setter
public class Movie extends Item {
	 private String director;
	 private String actor;
}

Category Entity

@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(fetch = FetchType.LAZY)
	 @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);
	 }
}

ItemRepository

@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);
          }
}
      public Item findOne(Long id) {
          return em.find(Item.class, id);
}
      public List<Item> findAll() {
          return em.createQuery("select i from Item
  i",Item.class).getResultList();
      }
}

ItemService

  @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 itemId) {
          return itemRepository.findOne(itemId);
	} 
}

주문 API 개발


주문은 이번 프로젝트의 핵심이라고 할 수 있습니다. 주문 기능을 구현하기 위해 엔티티 간 연관관계가 발생하며, 주문 서비스에서는 여러 레포지토리를 참조해야 합니다.

Order Entity

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

	 @ManyToOne(fetch = FetchType.LAZY)
	 @JoinColumn(name = "member_id")
	 private Member member; 

	 @OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
	 private List<OrderItem> orderItems = new ArrayList<>();
	 
	 @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
	 @JoinColumn(name = "delivery_id")
	 private Delivery delivery; 

	 private LocalDateTime orderDate; 

	 @Enumerated(EnumType.STRING)
	 private OrderStatus status; [ORDER, CANCEL]
	 
	 public void setMember(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);
	 }
}

OrderItem Entity

@Entity
@Table(name = "order_item")
@Getter @Setter
public class OrderItem {

	 @Id @GeneratedValue
	 @Column(name = "order_item_id")
	 private Long id;

	 @ManyToOne(fetch = FetchType.LAZY)
	 @JoinColumn(name = "item_id")
	 private Item item; 

	 @ManyToOne(fetch = FetchType.LAZY)
	 @JoinColumn(name = "order_id")
	 private Order order; 

	 private int orderPrice; 
	 private int count; 
}

주문 상태 Enum

public enum OrderStatus {
	 ORDER, CANCEL
}

Delivery Entity

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

	 @OneToOne(mappedBy = "delivery", fetch = FetchType.LAZY)
	 private Order order;

	 @Embedded
	 private Address address;

	 @Enumerated(EnumType.STRING)
	 private DeliveryStatus status; //ENUM [READY(준비), COMP(배송)]
}

배송 상태 Enum

public enum DeliveryStatus {
	 READY, COMP
}

OrderSearch

@Entity
@Getter @Setter
public class OrderSearch {
    @Id @GeneratedValue
    private Long id;
    private String memberName; //회원 이름
    private OrderStatus orderStatus;//주문 상태[ORDER, CANCEL]
}

OrderRepository

@Repository
@RequiredArgsConstructor
public class OrderRepository {
    private final EntityManager em;

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

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

    /**
     * 검색기능
     */
    public List<Order> findAll(OrderSearch orderSerach) {
        return em.createQuery("select o from Order o", Order.class).getResultList();
    }
}

OrderService

@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);
        // 주문 취소
        order.cancel();
    }



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

}

OrderController

@Controller
@RequiredArgsConstructor
public class OrderController {
    private final OrderService orderService;
    private final MemberService memberService;
    private final ItemService itemService;
    @GetMapping(value = "/order")
    public String createForm(Model model) {
        List<Member> members = memberService.findMembers();
        List<Item> items = itemService.findItems();
        model.addAttribute("members", members);
        model.addAttribute("items", items);
        return "order/orderForm";
    }
    @PostMapping(value = "/order")
    public String order(@RequestParam("memberId") Long memberId,
                        @RequestParam("itemId") Long itemId, @RequestParam("count") int count) {
        orderService.order(memberId, itemId, count);
        return "redirect:/orders";
    }
    @GetMapping(value = "/orders")
    public String orderList(@ModelAttribute("orderSearch") OrderSearch orderSearch, Model model) {
        List<Order> orders = orderService.findOrders(orderSearch);
        model.addAttribute("orders", orders);
        return "order/orderList";
    }

    @PostMapping(value = "/orders/{orderId}/cancel")
    public String cancelOrder(@PathVariable("orderId") Long orderId) {
        orderService.cancelOrder(orderId);
        return "redirect:/orders";
    }
}
profile
알고리즘 풀이 아카이브
post-custom-banner

0개의 댓글