모든 연관관계는 지연로딩으로 설정
- @XToOne(OneToOne, ManyToOne) 관계는 기본이 즉시로딩이므로 직접 지연로딩으로 설정한다
페이징과 한계 돌파
- 먼저 ToOne(OneToOne, ManyToOne) 관계를 모두 페치조인 한다. ToOne 관계는 row수를 증가시키지 않으므로 페이징 쿼리에 영향을 주지 않는다.
- 컬렉션은 지연 로딩으로 조회한다. ex) OneToMany
- 지연 로딩 성능 최적화를 위해 hibernate.default_batch_fetch_size , @BatchSize 를 적용한다.
- hibernate.default_batch_fetch_size: 글로벌 설정 //application.yml
- @BatchSize: 개별 최적화
H2 Database
- h2 다운로드
h2/bin/h2.bat
실행JDBC URL
에jdbc:h2:~/{db명}
입력하고 연결내 컴퓨터 -> Users -> 사용자 이름 -> {db명}.mv.db
확인- 연결 끊고
JDBC URL
에jdbc:h2:tcp://localhost/~/{db명}
입력하고 연결
//console.verdependencies { runtimeOnly 'com.h2database:h2' }
application.yml 설정
spring: datasource: url: jdbc:h2:tcp://localhost/~/{DB명} username: password: driver-class-name: org.h2.Driver #h2 database 사용 jpa: hibernate: ddl-auto: create properties: hibernate: # show_sql: true format_sql: true logging: level: org.hibernate.SQL: debug org.hibernate.orm.jdbc.bing: trace //console ver. spring: h2: console: enabled: true path: /h2-console datasource: driver-class-name: org.h2.Driver url: jdbc:h2:~/{DB명} username: sa password: jpa: hibernate: ddl-auto: create properties: hibernate: format_sql: true logging: level: org.hibernate.SQL: debug org.hibernate.orm.jdbc.bing: trace
요구사항
기능 목록
- 회원 기능
- 회원 등록
- 회원 조회
- 상품 기능
- 상품 등록
- 상품 수정
- 상품 조회
- 주문 기능
- 상품 주문
- 주문 내역 조회
- 주문 취소
- 기타 요구사항
- 상품은 재고 관리가 필요하다.
- 상품의 종류는 도서, 음반, 영화가 있다.
- 상품을 카테고리로 구분할 수 있다.
도메인 모델 설계
엔티티 설계
테이블 설계
구현
- Application
//JpashopApplication.java @SpringBootApplication public class JpashopApplication { public static void main(String[] args) { SpringApplication.run(JpashopApplication.class, args); } }
- Controller
//MemberController.java @Controller @RequiredArgsConstructor public class MemberController { private final MemberService memberService; @GetMapping("/members/new") public String createFrom(Model model) { model.addAttribute("memberForm", new MemberForm()); return "members/createMemberForm"; } @PostMapping("/members/new") public String create(@Valid MemberForm form, BindingResult result) { if (result.hasErrors()) { return "members/createMemberForm"; } Address address = new Address(form.getCity(), form.getStreet(), form.getZipcode()); Member member = new Member(); member.setName(form.getName()); member.setAddress(address); memberService.join(member); return "redirect:/"; } @GetMapping("/members") public String list(Model model) { List<Member> members = memberService.findMembers(); model.addAttribute("members", members); return "members/memberList"; } }
- Domain
//Order.java @Entity @Table(name = "orders") @Getter @Setter @NoArgsConstructor(access = AccessLevel.PROTECTED) 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(fetch = FetchType.LAZY, cascade = CascadeType.ALL) @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 setOrderItem(OrderItem orderItem) { 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) { Order order = new Order(); order.setMember(member); order.setDelivery(delivery); for (OrderItem orderItem : orderItems) { order.setOrderItem(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 : orderItems) { orderItem.cancel(); } } //==조회 로직==// /** * 전체 주문 가격 조회 */ public int getTotalPrice() { int totalPrice = 0; for (OrderItem orderItem : orderItems) { totalPrice += orderItem.getTotalPrice(); } return totalPrice; } }
- Repository
//MemberRepository.java @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); } 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(); } }
- Service
//OrderService.java @Service @Transactional(readOnly = true) @RequiredArgsConstructor 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.findAllByString(orderSearch); } }
- Test
//OrderServiceTest.java @RunWith(SpringRunner.class) @SpringBootTest @Transactional public class OrderServiceTest { @Autowired EntityManager em; @Autowired OrderService orderService; @Autowired OrderRepository orderRepository; @Test public void 상품주문() throws Exception { //given Member member = createMember("회원1", new Address("서울", "강가", "123-123")); Book 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("주문 수량만큼 재고가 줄어야 한다.", 8, book.getStockQuantity()); } @Test public void 주문취소() throws Exception { //given Member member = createMember("회원1", new Address("서울", "강가", "123-123")); Item item = createBook("시골 JPA", 10000, 10); int orderCount = 2; Long orderId = orderService.order(member.getId(), item.getId(), orderCount); //when orderService.cancelOrder(orderId); //then Order getOrder = orderRepository.findOne(orderId); assertEquals("주문 취소 시 상태는 CANCEL 이다.", OrderStatus.CANCEL, getOrder.getStatus()); assertEquals("주문이 취소된 상품은 그만큼 재고가 증가해야 한다.", 10, item.getStockQuantity()); } @Test(expected = NotEnoughStockException.class) public void 상품주문_재고수량초과() throws Exception { //given Member member = createMember("회원1", new Address("서울", "강가", "123-123")); Item item = createBook("시골 JPA", 10000, 10); int orderCount = 11; //when orderService.order(member.getId(), item.getId(), orderCount); //then fail("재고 수량 부족 예외가 발생해야 한다."); } private Book createBook(String name, int price, int stockQuantity) { Book book = new Book(); book.setName(name); book.setPrice(price); book.setStockQuantity(stockQuantity); em.persist(book); return book; } private Member createMember(String name, Address address) { Member member = new Member(); member.setName(name); member.setAddress(address); em.persist(member); return member; } }
- Api
//MemberApiController.java @RestController @RequiredArgsConstructor public class MemberApiController { private final MemberService memberService; //회원 조회 @GetMapping("/api/v2/members") public Result membersV2() { List<Member> findMembers = memberService.findMembers(); List<MemberDto> collect = findMembers.stream() .map(m -> new MemberDto(m.getName())) .collect(Collectors.toList()); return new Result(collect); } //회원 등록 @PostMapping("/api/v2/members") public CreateMemberResponse saveMemberV2(@RequestBody @Valid CreateMemberRequest request) { Member member = new Member(); member.setName(request.getName()); Long id = memberService.join(member); return new CreateMemberResponse(id); } //회원 수정 @PostMapping("/api/v2/members/{id}") public UpdateMemberResponse updateMemberV2(@PathVariable("id") Long id, @RequestBody @Valid UpdateMemberRequest request) { memberService.update(id, request.getName()); Member findMember = memberService.findOne(id); return new UpdateMemberResponse(findMember.getId(), findMember.getName()); } @Data @AllArgsConstructor static class Result<T> { private T data; } @Data @AllArgsConstructor static class MemberDto { private String name; } @Data static class CreateMemberRequest { private String name; } @Data static class CreateMemberResponse { private Long id; public CreateMemberResponse(Long id) { this.id = id; } } @Data static class UpdateMemberRequest { private String name; } @Data @AllArgsConstructor static class UpdateMemberResponse { private Long id; private String name; } }
//OrderRepository.java public List<Order> findAllWithMemberDelivery(int offset, int limit) { return em.createQuery( "select o from Order o" + " join fetch o.member m" + " join fetch o.delivery d", Order.class) .setFirstResult(offset) .setMaxResults(limit) .getResultList(); }
//OrderApiController @RestController @RequiredArgsConstructor public class OrderApiController { private final OrderRepository orderRepository; private final OrderQueryRepository orderQueryRepository; @GetMapping("/api/v3.1/orders") public List<OrderDto> ordersV3_page( @RequestParam(value = "offset", defaultValue = "0") int offset, @RequestParam(value = "limit", defaultValue = "100") int limit) { List<Order> orders = orderRepository.findAllWithMemberDelivery(offset, limit); List<OrderDto> result = orders.stream() .map(o -> new OrderDto(o)) .collect(Collectors.toList()); return result; } @Data static class OrderDto { private Long orderId; private String name; private LocalDateTime orderDate; private OrderStatus orderStatus; private Address address; private List<OrderItemDto> orderItems; public OrderDto(Order order) { orderId = order.getId(); name = order.getMember().getName(); orderDate = order.getOrderDate(); orderStatus = order.getStatus(); address = order.getDelivery().getAddress(); orderItems = order.getOrderItems().stream() .map(orderItem -> new OrderItemDto(orderItem)) .collect(Collectors.toList()); } } @Data static class OrderItemDto { private String itemName; private int orderPrice; private int count; public OrderItemDto(OrderItem orderItem) { itemName = orderItem.getItem().getName(); orderPrice = orderItem.getOrderPrice(); count = orderItem.getCount(); } } }