[스프링부트 JPA 활용] 주문 도메인 개발
[스프링부트 JPA 활용] 주문 도메인 개발
애플리케이션 구현
목차#
- 회원 도메인 개발
- 회원 리포지토리 개발
- 회원 서비스 개발
- 기능 테스트
- 상품 도메인 개발
- 상품 엔티티개발(비즈니스 로직추가)
- 상품 리포지토리 개발
- 상품 서비스 개발
- 주문 도메인 개발
- 주문, 주문상품 엔티티 개발
- 주문 리포지토리 개발
- 주문 서비스 개발
- 웹 계층 개발
- 홈 화면과 레이아웃
- 회원 등록
- 회원 목록 조회
- 상품 등록
- 상품 목록
- 상품 수정
- 변경 감지와 병함(merge)
- 상품 주문
- 주문 목록 검색, 취소
- API 개발 기본
- 회원 등록 API
- 회원 수정 API
- 회원 조회 API
- API 개발 고급
- 조회용 샘플 데이터 입력
- 지연 로딩과 조회 성능 최적화
- 페이징과 한계 돌파
- OSIV와 성능 최적화
- 다음으로
- 스프링 데이터 JPA 소개
- QueryDSL 소개
- 마무리
주문 도메인 개발#
지금까지 설명했던것 중에 가장 중요한 부분입니다. 비지니스 로직이 서로 얽혀서 돌아가는 것을 JPA와 엔티티를 가지고 어떻게 풀어 내는지 아실 수 있습니다.
주문, 주문 상품 엔티티 개발#
또 트랜잭션 스크립트 패턴과 도메인 모델 패턴 중 도메인 모델 패턴을 많이 접해 보지 못했을 텐데 예제를 통해 접해 볼 수 있습니다.
구현 기능#
- 상품 주문
- 주문 내역 조회
- 주문 취소
상품 주문#
- 주문서 작성
- 사용자를 선택
- 상품종류를 선택
- 상품종류를 선택에 따라 상품 콤보박스 변경
- 상품 선택
- 주문수량 입력 후 저장
- 선택한 상품 입력 주문수량 만큼 감소
- 주문 내역 리스트 이동
주문 내역 조회#
- 주문 내역 추가
- 주문 내역 검색
주문 취소#
- 주문 상태 변경
- 주문 수량 만큼 해당 취소 상품 갯수 추가
구현 순서#
- 주문 엔티티, 주문상품 엔티티 개발
- 주문 리포지토리개발
- 주문 서비스 개발
- 주문 검색 기능 개발
- 주문 기능 테스트
주문 엔티티, 주문상품 엔티티 개발#
Order뿐만 아니라 OrderItem, Delivery를 생성해야 하기 때문에 생성 메서드를 Order 엔티티에 생성합니다.
생성할때 밖에서 set, set, set 으로 각각의 엔티티를 설정하는것이 아니라 static 메서드를 호출해 한번에 생성합니다.
생성 메서드#
Order.java - 생성 메서드
//== 생성 메서드 ==//
public static Order createOrder(Member member, Delivery delivery, OrderItem... orderItems){ // OrderItem... 여러개를 넘길 수 있음
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;
}
해당 코드의 스타일이 중요한 점은 주문을 생성할때, 한곳에 기능을 모아 수정시 돌아다니며 수정할 필요가 없습니다.
OrderItem.java - 생성 메서드 orderPrice를 item의 가격에서 안가져오고, 파라미터로 따로 받는 이유는 구매 당시의 가격을 받기 위함
//==생성 메서드==//
public static OrderItem createOrderItem(Item item, int orderPrice, int count){
OrderItem orderItem = new OrderItem();
orderItem.setItem(item);
orderItem.setOrderPrice(orderPrice);
orderItem.setCount(count);
// 주문 수량만큼 재고를 감소시킴
item.removeStock(count);
return orderItem;
}
비즈니스 로직#
이미 배송완료된 상품은 취소가 불가능하다는 체크로직을 엔티티 안에 넣어 관리합니다.
Order.java - 비지니스 로직 (주문 취소)
//==비즈니스 로직==//
/**
* 주문 취소
*/
public void cancel(){
// 배송이 완료된 주문은 취소가 불가
if (delivery.getStatus() == DeliveryStatus.COMP){
throw new IllegalStateException("이미 배송이 완료된 상품은 취소가 불가능합니다.");
}
this.setStatus(OrderStatus.CANCEL);
for (OrderItem orderItem : this.orderItems){
orderItem.cancel();
}
}
OrderItem.java - 비지니스 로직 (주문 취소)
//==비즈니스 로직==//
/**
* 주문 취소
*/
public void cancel() {
getItem().addStock(this.count);
}
조회 로직#
Order에서 OrderItem에 가서 주문 수량과 주문 가격을 계산해주는 getTotalPrice()를 생성하고 Order에서 orderItems의 각각의 getTotalPrice()의 합계를 반환합니다.
OrderItem.java - 조회 로직 (주문 금액 가져오기)
/**
* 주문상품 전체 가격 조회
*/
public int getTotalPrice() {
return getOrderPrice() * getCount();
}
Order.java - 조회 로직 (전체 주문 가격 조회)
//==조회 로직==//
/**
* 전체 주문 가격 조회
*/
public int getTotalPrice(){
int totalPrice = 0;
for (OrderItem orderItem : this.orderItems) {
totalPrice += orderItem.getTotalPrice();
}
return totalPrice;
}
위의 코드를 좀더 심플하게 변경할 수 있습니다.
sum()으로 바꾸기 (Replace with sum())#
InteliJ 기능을 이용해서 자바 스트림을 이용해 간결하게 변경합니다.
Alt + Enter - 현재 컨텍스트에 대한 액션을 표시#
//==조회 로직==//
/**
* 전체 주문 가격 조회
*/
public int getTotalPrice(){
int totalPrice = this.orderItems.stream().mapToInt(OrderItem::getTotalPrice).sum();
return totalPrice;
}
Ctrl + Alt + N - 인라인화#
인라인화는 return의 변수와 속성 변수의 이름이 같은경우 return에 속성 변수에 세팅한 코드를 세팅해 간결하게 변경합니다.
//==조회 로직==//
/**
* 전체 주문 가격 조회
*/
public int getTotalPrice(){
return this.orderItems.stream()
.mapToInt(OrderItem::getTotalPrice)
.sum();
}
Order.java
package jpabook.jpashop.domain;
import jpabook.jpashop.domain.item.Item;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Entity
@Getter @Setter
@Table(name = "orders")
public class Order {
@Id @GeneratedValue
@Column(name="order_id")
private Long id;
@ManyToOne(fetch = FetchType.LAZY) // ToOne은 fetch = FetchType.LAZY로 꼭 !!! 세팅
@JoinColumn(name = "member_id") // Order의 member가 수정되면 Order의 외래키 값이 변경됩니다.
private Member member;
// mappedBy 연관관계의 주인인 OrderItem의 order로 매핑 되어있다는 뜻
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<OrderItem> orderItems = new ArrayList<>();
// ToOne은 fetch = FetchType.LAZY로 꼭 !!! 세팅
@OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@JoinColumn(name = "delivery_id")
private Delivery delivery;
private LocalDateTime orderDate; //주문시간
@Enumerated(EnumType.STRING) // EnumType.ORDINAL(숫자라 순서바뀌면 큰일)이 기본이지만 무조건 EnumType.STRING(문자 코드)
private OrderStatus status; // 주문상태 [ORDER, CANCEL]
//==연관관계 메서드 (양방향 연관관계시 추가)==//
public void setMember(Member member){
this.member = member;
member.getOrders().add(this);
}
public void addOrderItem(OrderItem orderItem){
this.orderItems.add(orderItem);
orderItem.setOrder(this);
}
public void setDelivery(Delivery delivery){
this.delivery = delivery;
delivery.setOrder(this);
}
//== 생성 메서드==//
public static Order createOrder(Member member, Delivery delivery, OrderItem... orderItems){ // OrderItem... 여러개를 넘길 수 있음
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.getStatus() == DeliveryStatus.COMP){
throw new IllegalStateException("이미 배송이 완료된 상품은 취소가 불가능합니다.");
}
this.setStatus(OrderStatus.CANCEL);
for (OrderItem orderItem : this.orderItems){
orderItem.cancel();
}
}
//==조회 로직==//
/**
* 전체 주문 가격 조회
*/
public int getTotalPrice(){
/*
int totalPrice = 0;
for (OrderItem orderItem : this.orderItems) {
totalPrice += orderItem.getTotalPrice();
}
return totalPrice;
*/
return this.orderItems.stream()
.mapToInt(OrderItem::getTotalPrice)
.sum();
}
}
OrderItem.java
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(fetch = FetchType.LAZY) // ToOne은 fetch = FetchType.LAZY로 꼭 !!! 세팅
@JoinColumn(name = "item_id")
private Item item;
@ManyToOne(fetch = FetchType.LAZY) // ToOne은 fetch = FetchType.LAZY로 꼭 !!! 세팅
@JoinColumn(name = "order_id")
private Order order;
private int orderPrice; //주문 당시의 가격
private int count; //주문 수량
//==생성 메서드==//
public static OrderItem createOrderItem(Item item, int orderPrice, int count){ //orderPrice는 구매 당시의 가격을 받기 위함
OrderItem orderItem = new OrderItem();
orderItem.setItem(item);
orderItem.setOrderPrice(orderPrice);
orderItem.setCount(count);
item.removeStock(count);
return orderItem;
}
//==비즈니스 로직==//
/**
* 주문 취소
*/
public void cancel() {
getItem().addStock(this.count);
}
//==조회 로직==//
/**
* 주문상품 전체 가격 조회
*/
public int getTotalPrice() {
return getOrderPrice() * getCount();
}
}
생성 메서드 사용 시 생성자 사용 제약#
생성 메서드를 사용하게 되면, new를 이용해 엔티티 객체를 생성하는 것을 막아야합니다.
왜냐하면, 다른 코드 스타일로 개발하면 유지보수가 불편할 뿐만 아니라 추가로 컬럼의 변경이 있을때 각각의 코드에서 수정이 필요하기 때문입니다.
Order.java
@Entity
@Getter @Setter
@Table(name = "orders")
public class Order {
protected Order() {} //생성자를 사용 불가로 하고 CteateOrder 사용 유도
OrderItem.java
@Entity
@Getter @Setter
public class OrderItem {
protected OrderItem() {} //생성자를 사용 불가로 하고 CteateOrderItem 사용 유도
해당 생성자를 protected로 적용하는 것을 Lombok을 통해 더 편리하게 사용 할 수 있습니다.
@NoArgsConstructor(access = AccessLevel.PROTECTED)
만일 누군가 생성자를 통해 Order나 OrderItem을 생성하려 하면, 빨간줄로 오류를 뱉어 내기 때문에, 해당 소스에 가서 확인 후 @NoArgsConstructor(access = AccessLevel.PROTECTED)를 보고 다른 생성 방법을 찾아 개발을 할 수 있을 것 입니다.
이전 소스#
설정#
/main/resources/application.properties
application.properties
spring.devtools.restart.enabled=true
spring.devtools.restart.poll-interval=2s
spring.devtools.restart.quiet-period=1s
spring.thymeleaf.cache=false
spring.jpa.properties.hibernate.format_sql=true
main/resources/application.yml
application.yml
spring:
datasource:
url: jdbc:h2:tcp://localhost/~/jpashop; # MVCC=true H2 1.4.200 버전부터 MVCC 옵션이 제거되었습니다.
username: sa
password:
driver-class-name: org.h2.Driver
jpa:
hibernate:
ddl-auto: create-drop # 애플리케이션 동작 시점에 엔티티 재생성
use_sql_comments: true
database: h2
devtools:
livereload:
enabled: true # livereload 사용시 활성화
restart:
enabled: false #운영 에서는 제거.
thymeleaf:
cache: false
logging:
level:
org.hibernate.SQL: debug
org.hibernate.type: trace #파라미터 로깅
org.hibernate.type.descriptor.sql: trace
decorator:
datasource:
p6spy:
enable-logging : true
multiline: true
logging: slf4j
test/resources/application.properties
application.properties
spring.devtools.restart.enabled=true
spring.devtools.restart.poll-interval=2s
spring.devtools.restart.quiet-period=1s
spring.thymeleaf.cache=false
spring.jpa.properties.hibernate.format_sql=true
test/resources/application.yml
application.yml
spring:
# datasource:
# url: jdbc:h2:mem:test
# username: sa
# password:
# driver-class-name: org.h2.Driver
# jpa:
# hibernate:
# ddl-auto: create-drop # 애플리케이션 동작 시점에 엔티티 재생성
# use_sql_comments: true
# database: h2
devtools:
livereload:
enabled: true # livereload 사용시 활성화
restart:
enabled: false #운영 에서는 제거.
thymeleaf:
cache: false
logging:
level:
org.hibernate.SQL: debug
org.hibernate.type: trace #파라미터 로깅
org.hibernate.type.descriptor.sql: trace
decorator:
datasource:
p6spy:
enable-logging : true
multiline: true
logging: slf4j
엔티티#
java/jpabook/jpashop/domain/Address.java
Address.java
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;
}
}
java/jpabook/jpashop/domain/Order.java
Order.java
package jpabook.jpashop.domain;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Entity
@Getter @Setter
@Table(name = "orders")
public class Order {
@Id @GeneratedValue
@Column(name="order_id")
private Long id;
@ManyToOne(fetch = FetchType.LAZY) // ToOne은 fetch = FetchType.LAZY로 꼭 !!! 세팅
@JoinColumn(name = "member_id") // Order의 member가 수정되면 Order의 외래키 값이 변경됩니다.
private Member member;
// mappedBy 연관관계의 주인인 OrderItem의 order로 매핑 되어있다는 뜻
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<OrderItem> orderItems = new ArrayList<>();
// ToOne은 fetch = FetchType.LAZY로 꼭 !!! 세팅
@OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@JoinColumn(name = "delivery_id")
private Delivery delivery;
private LocalDateTime orderDate; //주문시간
@Enumerated(EnumType.STRING) // EnumType.ORDINAL(숫자라 순서바뀌면 큰일)이 기본이지만 무조건 EnumType.STRING(문자 코드)
private OrderStatus status; // 주문상태 [ORDER, CANCEL]
//==연관관계 메서드 (양방향 연관관계시 추가)==//
public void setMember(Member member){
this.member = member;
member.getOrders().add(this);
}
public void addOrderItem(OrderItem orderItem){
this.orderItems.add(orderItem);
orderItem.setOrder(this);
}
public void setDelivery(Delivery delivery){
this.delivery = delivery;
delivery.setOrder(this);
}
}
java/jpabook/jpashop/domain/OrderItem.java
OrderItem.java
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(fetch = FetchType.LAZY) // ToOne은 fetch = FetchType.LAZY로 꼭 !!! 세팅
@JoinColumn(name = "item_id")
private Item item;
@ManyToOne(fetch = FetchType.LAZY) // ToOne은 fetch = FetchType.LAZY로 꼭 !!! 세팅
@JoinColumn(name = "order_id")
private Order order;
private int orderPrice; //주문 당시의 가격
private int count; //주문 수량
}
java/jpabook/jpashop/domain/OrderStatus.java
OrderStatus.java
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(fetch = FetchType.LAZY) // ToOne은 fetch = FetchType.LAZY로 꼭 !!! 세팅
@JoinColumn(name = "item_id")
private Item item;
@ManyToOne(fetch = FetchType.LAZY) // ToOne은 fetch = FetchType.LAZY로 꼭 !!! 세팅
@JoinColumn(name = "order_id")
private Order order;
private int orderPrice; //주문 당시의 가격
private int count; //주문 수량
}
java/jpabook/jpashop/domain/Delivery.java
Delivery.java
package jpabook.jpashop.domain;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
@Entity
@Getter @Setter
public class Delivery {
public Delivery() {
}
@Id @GeneratedValue
@Column(name = "delivery_id")
private Long id;
@OneToOne(fetch = FetchType.LAZY // ToOne은 fetch = FetchType.LAZY로 꼭 !!! 세팅
, mappedBy = "delivery")
private Order order;
@Embedded
private Address address;
@Enumerated(EnumType.STRING)
private DeliveryStatus status; //READY, COMP
}
java/jpabook/jpashop/domain/item/Item.java
Item.java
package jpabook.jpashop.domain.item;
import jpabook.jpashop.exception.NotEnoughStockException;
import jpabook.jpashop.domain.Category;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;
@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<>();
//==비즈니스 로직==//
/**
* 재고 증가
* @param quantity
*/
public void addStock(int quantity){
this.stockQuantity += quantity;
}
/**
* 재고 감소
* @param quantity
*/
public void removeStock(int quantity){
int restStock = this.stockQuantity - quantity;
if (restStock < 0) {
throw new NotEnoughStockException("need more stock");
}
this.stockQuantity = restStock;
}
}
java/jpabook/jpashop/domain/item/Album.java
Album.java
package jpabook.jpashop.domain.item;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.DiscriminatorValue;
import javax.persistence.Entity;
@Entity
@DiscriminatorValue("A") //구분값 A
@Getter @Setter
public class Album extends Item{
private String artist;
private String etc;
}
java/jpabook/jpashop/domain/item/Book.java
Book.java
package jpabook.jpashop.domain.item;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.DiscriminatorValue;
import javax.persistence.Entity;
@Entity
@DiscriminatorValue("B") //구분값 B
@Getter @Setter
public class Book extends Item{
private String author;
private String isbn;
}
java/jpabook/jpashop/domain/item/Movie.java
Movie.java
package jpabook.jpashop.domain.item;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.DiscriminatorValue;
import javax.persistence.Entity;
@Entity
@DiscriminatorValue("M") //구분값 M
@Getter @Setter
public class Movie extends Item{
private String director;
private String actor;
}
java/jpabook/jpashop/domain/Category.java
Category.java
package jpabook.jpashop.domain;
import jpabook.jpashop.domain.item.Item;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;
@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) // ToOne은 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);
}
}
도메인#
java/jpabook/jpashop/repository/MemberRepository.java
MemberRepository.java
package jpabook.jpashop.repository;
import jpabook.jpashop.domain.Member;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.TypedQuery;
import java.util.List;
@Repository
@RequiredArgsConstructor
public class MemberRepository {
/*
//최초 소스이며 아래 소스로 대체
@PersistenceContext // EntityManager는 @PersistenceContext라는 표준 어노테이션을 통해서만 가능 (@AutoWired 불가)
private EntityManager em;
*/
/*
//2번째 버전의 소스이며, @RequiredArgsConstructor로 대체
@Autowired //스프링 DATA JPA 에서 지원
private EntityManager em;
public MemberRepository(EntityManager em){
this.em = em;
}
*/
private final 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();
}
}
java/jpabook/jpashop/service/MemberService.java
MemberService.java
package jpabook.jpashop.service;
import jpabook.jpashop.domain.Member;
import jpabook.jpashop.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor // 생성자 주입
public class MemberService {
/*
// 최초 코드 이며, Setter Injection로 대체
@Autowired
private MemberRepository memberRepository;
*/
/*
//Constructor Injection로 대체
private MemberRepository memberRepository;
public void setMemberService(MemberRepository memberRepository) { //Setter Injection
this.memberRepository = memberRepository;
}
*/
/*
// @RequiredArgsConstructor로 대체
private final MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository) { //Constructor Injection
this.memberRepository = memberRepository;
}
*/
private final MemberRepository memberRepository;
/**
* 회원 가입
*/
@Transactional(readOnly = false)
public Long join(Member member){
validateDuplicateMember(member); //중복 회원 검증
memberRepository.save(member);
return member.getId(); //save()를 통해 em.persist()를 수행하므로 Member 엔티티의 키 생성을 보장함
}
private void validateDuplicateMember(Member member) {
List<Member> findMembers = memberRepository.findByName(member.getName());
if(findMembers.size() != 0){
throw new IllegalStateException("이미 존재하는 회원입니다.");
}
}
/**
* 회원 전체 조회
*/
//@Transactional(readOnly = true)
public List<Member> findMembers(){
return memberRepository.findAll();
}
/**
* 회원 조회
*/
//@Transactional(readOnly = true)
public Member findOne(Long memberId){
return memberRepository.findOne(memberId);
}
}
java/jpabook/jpashop/repository/ItemRepository.java
ItemRepository.java
package jpabook.jpashop.repository;
import jpabook.jpashop.domain.item.Item;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
import javax.persistence.EntityManager;
import java.util.List;
@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();
}
}
java/jpabook/jpashop/service/ItemService.java
ItemService.java
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.List;
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor // 생성자 주입
public class ItemService {
private final ItemRepository itemRepository;
@Transactional
public Item saveItem(Item item){
itemRepository.save(item);
return item; //등록한 엔티티 정보 리턴, api response 리턴 및 test code 검증용
}
public List<Item> findItems(){
return itemRepository.findAll();
}
public Item findItem(Long item_id){
return itemRepository.findOne(item_id);
}
}
Exception#
java/jpabook/jpashop/exception/NotEnoughStockException.java
NotEnoughStockException.java
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);
}
protected NotEnoughStockException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
테스트#
test/java/jpabook/jpashop/service/MemberServiceTest.java
MemberServiceTest.java
package jpabook.jpashop.service;
import static org.junit.jupiter.api.Assertions.*;
import jpabook.jpashop.domain.Member;
import jpabook.jpashop.repository.MemberRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;
import javax.persistence.EntityManager;
@SpringBootTest
@Transactional
class MemberServiceTest {
// 테스트 케이스에서는 다른곳에서 참조할 곳이 없으므로 @Autowired로 사용
@Autowired MemberRepository memberRepository;
@Autowired MemberService memberService;
@Autowired EntityManager em;
@Test
//@Rollback(value = false)
public void 회원가입() throws Exception{
//given //given : 이렇게 주어졌을때
Member member = new Member();
member.setName("userA");
//when //when : 이렇게 하면
Long savedId = memberService.join(member);
//then //then : 이렇게 된다.
// JPA안에서 하나의 트랜잭션에서 같은 엔티티에서 PK 키가 같으면 같은 영속성 컨텍스트 1차 캐시로 같은 객체로 관리
em.flush();
assertEquals(member, memberRepository.findOne(savedId));
}
@Test
public void 중복_회원_예외() throws Exception{
//given
String username = "user";
Member member1 = new Member();
member1.setName(username);
Member member2 = new Member();
member2.setName(username);
//when
memberService.join(member1);
//then
IllegalStateException thrown = assertThrows(IllegalStateException.class, () -> memberService.join(member2));
}
}
test/java/jpabook/jpashop/service/ItemServiceTest.java
ItemServiceTest.java
package jpabook.jpashop.service;
import static org.junit.jupiter.api.Assertions.*;
import jpabook.jpashop.domain.item.Album;
import jpabook.jpashop.domain.item.Book;
import jpabook.jpashop.domain.item.Item;
import jpabook.jpashop.domain.item.Movie;
import jpabook.jpashop.repository.ItemRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;
import javax.persistence.EntityManager;
@SpringBootTest
@Transactional
class ItemServiceTest {
// 테스트 케이스에서는 다른곳에서 참조할 곳이 없으므로 @Autowired로 사용
@Autowired ItemRepository itemRepository;
@Autowired ItemService itemService;
@Autowired EntityManager em;
@Test
public void 음반_상품등록() throws Exception{
//given
Item item = new Album();
item.setName("멜론 TOP 100");
((Album) item).setArtist("Various Artists");
((Album) item).setEtc("방탄소년단 외 다수");
item.setPrice(20000);
item.addStock(50);
//when
Item savedItem = itemService.saveItem(item);
//then
em.flush();
assertEquals(item, itemRepository.findOne(savedItem.getId()));
}
@Test
public void 책_상품등록() throws Exception{
//given
Item item = new Book();
item.setName("JPA BOOK");
((Book) item).setAuthor("김영한");
((Book) item).setIsbn("11111");
item.setPrice(15000);
item.addStock(100);
//when
Item savedItem = itemService.saveItem(item);
//then
em.flush();
assertEquals(item, itemRepository.findOne(savedItem.getId()));
}
@Test
public void 영화_상품등록() throws Exception{
//given
Item item = new Movie();
item.setName("쥬라기월드: 도미니언");
((Movie) item).setDirector("콜린 트레보로우");
((Movie) item).setActor("크리스 프랫");
item.setPrice(15000);
item.addStock(1000);
//when
Item savedItem = itemService.saveItem(item);
//then
em.flush();
assertEquals(item, itemRepository.findOne(savedItem.getId()));
}
}