티스토리 뷰
| Web Application (Spring + JPA) 2.
--
1. 프로젝트 환경설정
2. 도메일 모델과 테이블 설게
3. 애플리케이션 기능 구현
|| 프로젝트 환경 설정
--
prior post : [Spring + JPA] Make Web Application (1)
|| 도메인 모델과 테이블 설계
--
prior post : [Spring + JPA] Make Web Application (1)
|| 애플리케이션 기능 구현
--
||| 개발 방법
--
* Controller : MVC Controller가 모여 있는 곳
Controller는 Service 계층을 호출하고 결과를 뷰(JSP)에 전달
* Service : Service 계층에는 비즈니스 로직이 있고 트랜잭션을 시작
Service 계층은 데이터 접근 계층인 repository 호출
* Repository : JPA를 직접 사용하는 계층
엔티티 매니저를 사용해서 엔티티를 저장하고 조회
* Domain : 엔티티가 모여 있는 계층, 모든 계층에서 사용
> 개발 순서
0. Domain
1. Service (Business Logic) & Repository
2. Controller
3. JSP
||| 회원 기능
--
회원 등록
회원 목록 조회
> Member Repository
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | import jpabook.jpashop.domain.Member; import org.springframework.stereotype.Repository; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import java.util.List; @Repository public class MemberRepository { @PersistenceContext 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(); } // JPQL을 사용하여 이름으로 회원 엔티티 조회 public List<Member> findByName(String name) { return em.createQuery("select m from Member m where m.name = :name", Member.class) .setParameter("name", name) .getResultList(); } } | cs |
@Repository
- <context:component-scan>에 의해 Spring Bean으로 자동 등록
- JPA 전용 예외 발생 시 스피링이 추상화한 예외로 변환
@PersistenceContext
- 컨테이너가 관리하는 엔티티 매니저 주입
- spring에서는 컨테이너가 엔티티 매니저를 관리 및 제공
(엔티티 매니저 팩토리에서 엔티티 매니저를 직접 생성하지 않고 컨테이너가 제공하는 엔티티 매니저 사용)
@PersistenceUnit
- 엔티티 매니저 팩토리 주입 (직접 사용할 경우)
> Member Service
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | import jpabook.jpashop.domain.Member; import jpabook.jpashop.repository.MemberRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; @Service @Transactional public class MemberService { @Autowired MemberRepository memberRepository; /** * 회원 가입 */ public Long join(Member member) { validateDuplicateMember(member); //중복 회원 검증 memberRepository.save(member); return member.getId(); } private void validateDuplicateMember(Member member) { List<Member> findMembers = memberRepository.findByName(member.getName()); if (!findMembers.isEmpty()) { throw new IllegalStateException("이미 존재하는 회원입니다."); } } /** * 전체 회원 조회 */ public List<Member> findMembers() { return memberRepository.findAll(); } public Member findOne(Long memberId) { return memberRepository.findOne(memberId); } } | cs |
@Service
- <context:component-scan>에 의해 Spring Bean으로 자동 등록
@Transactional
- 트랜잭션을 적용
- 외부에서 이 클래스의 메소드를 호출할 때 트랜잭션을 시작하고, 메소드를 종료할 때 트랜잭션을 커밋
- 예외 발생 시 트랜잭션 롤백
+ 체크 예외가 발생해도 롤백 설정 @Transactional(rollbackFor = Exception.class)
@Autowired
- spring container가 적절한 spring bean을 주입
> Member Service Test
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | package jpabook.jpashop.service; import jpabook.jpashop.domain.Member; import jpabook.jpashop.repository.MemberRepository; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.ContextConfiguration; import org.springframework.transaction.annotation.Transactional; import static org.junit.Assert.*; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = "classpath:appConfig.xml") @Transactional public class MemberServiceTest { @Autowired MemberService memberService; @Autowired MemberRepository memberRepository; @Test 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("예외가 발생해야 한다."); } } | cs |
@RunWith(SpringJUnit4ClassRunner.class)
- Test Case를 spring framework와 통합
- 테스트가 스프링 컨테이너에서 실행 (@Autowired 같은 기능 사용 가능)
@ContextConfiguration(locations = "classpath:appConfig.xml")
- 테스트 케이스를 실행할 때 사용할 스프링 설정 정보 지정
@Transactional
- 테스트에서 사용 시 각각의 테스트를 실행할 때마다 트랜잭션을 시작하고 테스트가 끝나면 트랜잭션을 강제로 롤백
- 반복해서 테스트를 진행
@Test(expected = IllegalStateException.class)
- expected 속성에 예외 클래스를 지정하면 테스트 결과로 지정한 예외가 발생해야 테스트 성공
||| 상품 기능
--
상품 등록
상품 목록 조회
상품 수정
> Item Repository
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | import jpabook.jpashop.domain.item.Item; import org.springframework.stereotype.Repository; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import java.util.List; @Repository public class ItemRepository { @PersistenceContext 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(); } } | cs |
> Item Service
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | import jpabook.jpashop.domain.item.Item; import jpabook.jpashop.repository.ItemRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; @Service @Transactional public class ItemService { @Autowired ItemRepository itemRepository; public void saveItem(Item item) { itemRepository.save(item); } public List<Item> findItems() { return itemRepository.findAll(); } public Item findOne(Long itemId) { return itemRepository.findOne(itemId); } } | cs |
||| 주문 기능
--
상품 주문
주문 내역 조회
주문 취소
> Order Repository
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | import jpabook.jpashop.domain.Member; import jpabook.jpashop.domain.Order; import jpabook.jpashop.domain.OrderSearch; import org.springframework.stereotype.Repository; import org.springframework.util.StringUtils; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import javax.persistence.TypedQuery; import javax.persistence.criteria.*; import java.util.ArrayList; import java.util.List; @Repository public class OrderRepository { @PersistenceContext 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 orderSearch) { CriteriaBuilder cb = em.getCriteriaBuilder(); CriteriaQuery<Order> cq = cb.createQuery(Order.class); Root<Order> o = cq.from(Order.class); List<Predicate> criteria = new ArrayList<Predicate>(); //주문 상태 검색 if (orderSearch.getOrderStatus() != null) { Predicate status = cb.equal(o.get("status"), orderSearch.getOrderStatus()); criteria.add(status); } //회원 이름 검색 if (StringUtils.hasText(orderSearch.getMemberName())) { Join<Order, Member> m = o.join("member", JoinType.INNER); //회원과 조인 Predicate name = cb.like(m.<String>get("name"), "%" + orderSearch.getMemberName() + "%"); criteria.add(name); } cq.where(cb.and(criteria.toArray(new Predicate[criteria.size()]))); TypedQuery<Order> query = em.createQuery(cq).setMaxResults(1000); //최대 검색 1000 건으로 제한 return query.getResultList(); } } | cs |
> Order Service
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | import jpabook.jpashop.domain.Delivery; import jpabook.jpashop.domain.Member; import jpabook.jpashop.domain.Order; import jpabook.jpashop.domain.OrderItem; import jpabook.jpashop.domain.item.Item; import jpabook.jpashop.domain.OrderSearch; import jpabook.jpashop.repository.MemberRepository; import jpabook.jpashop.repository.OrderRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; @Service @Transactional public class OrderService { @Autowired MemberRepository memberRepository; @Autowired OrderRepository orderRepository; @Autowired ItemService itemService; /** 주문 */ public Long order(Long memberId, Long itemId, int count) { //엔티티 조회 Member member = memberRepository.findOne(memberId); Item item = itemService.findOne(itemId); //배송정보 생성 Delivery delivery = new Delivery(member.getAddress()); //주문상품 생성 OrderItem orderItem = OrderItem.createOrderItem(item, item.getPrice(), count); //주문 생성 Order order = Order.createOrder(member, delivery, orderItem); //주문 저장 orderRepository.save(order); return order.getId(); } /** 주문 취소 */ public void cancelOrder(Long orderId) { //주문 엔티티 조회 Order order = orderRepository.findOne(orderId); //주문 취소 order.cancel(); } /** 주문 검색 */ public List<Order> findOrders(OrderSearch orderSearch) { return orderRepository.findAll(orderSearch); } } | cs |
line 24~40) 주문하는 회원 식별자, 상품 식별자, 주문 수량 정보로 주문 엔티티 생성 후 저장
line 44~51) 주문 식별자로 주문 엔티티를 조회한 후 주문 엔티티에 주문 최소 요청
line 54~56) 검색 조건을 가진 OrderSearch 객체로 주문 엔티티 검색
> OrderSearch (검색 조건을 가진 객체)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | public class OrderSearch { private String memberName; //회원 이름 private OrderStatus orderStatus; //주문 상태(ORDER, CANCEL) //Getter, Setter public String getMemberName() { return memberName; } public void setMemberName(String memberName) { this.memberName = memberName; } public OrderStatus getOrderStatus() { return orderStatus; } public void setOrderStatus(OrderStatus orderStatus) { this.orderStatus = orderStatus; } } | cs |
> Order Service Test
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 | import jpabook.jpashop.domain.Address; import jpabook.jpashop.domain.Member; import jpabook.jpashop.domain.Order; import jpabook.jpashop.domain.OrderStatus; import jpabook.jpashop.domain.item.Book; import jpabook.jpashop.domain.item.Item; 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.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.transaction.annotation.Transactional; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = "classpath:appConfig.xml") @Transactional //@TransactionConfiguration(defaultRollback = false) public class OrderServiceTest { @PersistenceContext EntityManager em; @Autowired OrderService orderService; @Autowired OrderRepository orderRepository; @Test public void 상품주문() throws Exception { //Given Member member = createMember(); Item item = createBook("시골 JPA", 10000, 10); //이름, 가격, 재고 int orderCount = 2; //When Long orderId = orderService.order(member.getId(), item.getId(), orderCount); //Then Order getOrder = orderRepository.findOne(orderId); assertEquals("상품 주문시 상태는 주문(ORDER)이다.", OrderStatus.ORDER, getOrder.getStatus()); assertEquals("주문한 상품 종류 수가 정확해야 한다.", 1, getOrder.getOrderItems().size()); assertEquals("주문 가격은 가격 * 수량이다.", 10000 * 2, getOrder.getTotalPrice()); assertEquals("주문 수량만큼 재고가 줄어야 한다.", 8, item.getStockQuantity()); } @Test(expected = NotEnoughStockException.class) public void 상품주문_재고수량초과() throws Exception { //Given Member member = createMember(); Item item = createBook("시골 JPA", 10000, 10); //이름, 가격, 재고 int orderCount = 11; //재고 보다 많은 수량 //When orderService.order(member.getId(), item.getId(), orderCount); //Then fail("재고 수량 부족 예외가 발생해야 한다."); } @Test public void 주문취소() { //Given Member member = createMember(); 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()); } private Member createMember() { Member member = new Member(); member.setName("회원1"); member.setAddress(new Address("서울", "강가", "123-123")); em.persist(member); return member; } private Book createBook(String name, int price, int stockQuantity) { Book book = new Book(); book.setName(name); book.setStockQuantity(stockQuantity); book.setPrice(price); em.persist(book); return book; } } | cs |
line 35~52) 상품 주문 검증
주문 상태, 주문 상품, 주문 가격, 주문 후 재고 수량 검증
line 55~68) 재고 수량 초과 검증
재고 수량 초과 시 NotEnoughStockException 예외 발생
line 72~89) 주문 취소 검증
주문 취소 상태, 주문 취소 후 재고 수량 검증
||| 웹 계층 구현
--
> Item Controller
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | import jpabook.jpashop.domain.item.Book; import jpabook.jpashop.domain.item.Item; import jpabook.jpashop.service.ItemService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import java.util.List; @Controller public class ItemController { @Autowired ItemService itemService; @RequestMapping(value = "/items/new", method = RequestMethod.GET) public String createForm() { return "items/createItemForm"; } @RequestMapping(value = "/items/new", method = RequestMethod.POST) public String create(Book item) { itemService.saveItem(item); return "redirect:/items"; } /** * 상품 수정 폼 */ @RequestMapping(value = "/items/{itemId}/edit", method = RequestMethod.GET) public String updateItemForm(@PathVariable("itemId") Long itemId, Model model) { Item item = itemService.findOne(itemId); model.addAttribute("item", item); return "items/updateItemForm"; } /** * 상품 수정 */ @RequestMapping(value = "/items/{itemId}/edit", method = RequestMethod.POST) public String updateItem(@ModelAttribute("item") Book item) { itemService.saveItem(item); return "redirect:/items"; } /** * 상품 목록 */ @RequestMapping(value = "/items", method = RequestMethod.GET) public String list(Model model) { List<Item> items = itemService.findItems(); model.addAttribute("items", items); return "items/itemList"; } } | cs |
ㅇ 상품 등록
line 19~22) 상품 등록을 누르면 "/items/new" URL을 HTTP GET 방식으로 요청하고
"items/createItemForm.jsp" 로 전달
line 24~29) createItemForm.jps 에서 정보를 입력
<form role="form" action="/items/new" method="post"> 를 통해
파라미터로 전달한 Item에는 화면에서 입력한 데이터가 모두 바인딩
(HttpServletRequest 의 파라미터와 객체의 프로퍼티 이름을 비교해서 같으면 스프링 프레임워크가 값을 바인딩)
Item 저장(saveItem) 요청 후 "/items" URL로 Redirect
ㅇ 상품 목록
line 55~61) 상품 목록을 선택하면 "/items" URL 로 이동
전체 Item을 조회하고 Model 객체에 담은 후 "item/itemList.jsp" 로 전달
ㅇ 상품 수정
line 34~40) 수정 버튼을 선택하면 "/items/{itemId}/edit" URL을 HTTP GET 방식으로 요청
Model 객체에 수정할 item 정보를 담고 "items/updateItemForm.jsp" 로 전달
line 45~50) 상품 수정 후 Submit 버튼을 선택하면 "/items/{itemId}/edit" URL을 HTTP POST 방식으로 요청
컨트롤러의 파라미터로 넘어온 item 엔티티 인스턴스는 준영속 상태이므로 변경 감지 기능이 동작하지 않음
> Order Controller
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | import jpabook.jpashop.domain.Member; import jpabook.jpashop.domain.Order; import jpabook.jpashop.domain.item.Item; import jpabook.jpashop.domain.OrderSearch; import jpabook.jpashop.service.ItemService; import jpabook.jpashop.service.MemberService; import jpabook.jpashop.service.OrderService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.*; import java.util.List; @Controller public class OrderController { @Autowired OrderService orderService; @Autowired MemberService memberService; @Autowired ItemService itemService; @RequestMapping(value = "/order", method = RequestMethod.GET) 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"; } @RequestMapping(value = "/order", method = RequestMethod.POST) public String order(@RequestParam("memberId") Long memberId, @RequestParam("itemId") Long itemId, @RequestParam("count") int count) { orderService.order(memberId, itemId, count); return "redirect:/orders"; } @RequestMapping(value = "/orders", method = RequestMethod.GET) public String orderList(@ModelAttribute("orderSearch") OrderSearch orderSearch, Model model) { List<Order> orders = orderService.findOrders(orderSearch); model.addAttribute("orders", orders); return "order/orderList"; } @RequestMapping(value = "/orders/{orderId}/cancel") public String processCancelBuy(@PathVariable("orderId") Long orderId) { orderService.cancelOrder(orderId); return "redirect:/orders"; } } | cs |
ㅇ 상품 주문
line 22~32) 상품 주문 선택 시 "/order" URL을 HTTP GET 방식으로 요청하고
고객 정보와 상품 정보를 모델 객체에 담아서 "orderForm.jsp"로 전달
line 34~39) 상품 주문 정보를 입력한 후 Submit 버튼을 선택하면
<form role="form" action="/order" method="post"> 를 통해 주문 엔티티 생성
주문이 끝나면 "/orders" URL로 Redirect
ㅇ 주문 내역
line 41~48) 주문 내역을 선택하면
<form class="navbar-form navbar-left" role="search"> 를 통해
orderSearch 객체를 통해 주문 내역을 담고 "order/orderList.jsp"로 전달
ㅇ 주문 취소
line 50~56) 주문 취소 버튼을 선택할 시 해당 주문 ID에 해당하는 주문을 취소한 후
"/orders" URL로 Redirect
||| 변경 감지와 병합
--
ㅇ 파라미터로 넘어온 준영속 상태의 엔티티를 수정하는 방법
> 변경 감지 기능 사용
- 영속성 컨텍스트에서 엔티티를 다시 조회한 후 데이터를 수정하는 방법
- 준영속 엔티티의 식별자로 엔티티를 다시 조회하면 영속 상태의 엔티티를 얻을 수 있음
이렇게 영속 상태인 엔티티의 값을 파라미터로 넘어온 준영속 상태의 엔티티 값으로 변경
- 이후 트랜잭션이 커밋될 때 변경 감지 기능이 동작해서 데이터베이스에 수정사항이 반영
> 병합 사용
- 준영속 엔티티 식별자 값으로 영속 엔티티를 조회
- 영속 엔티티의 값을 준영속 엔티티의 값으로 채워 넣기
* 변경 감지 기능을 사용하면 원하는 속성만 선택해서 변경할 수 있지만,
병합을 사용하면 모든 속성이 변경
|| Spring Data JPA 적용
--
[Spring Data JPA] Make Web Application (3)
출처 : 자바 ORM 표준 JPA 프로그래밍
https://github.com/holyeye/jpabook/tree/master/ch11-jpa-shop
'Books' 카테고리의 다른 글
[Spring + JPA] Spring Data JPA 란? (2) (0) | 2020.12.28 |
---|---|
[Spring + JPA] Spring Data JPA 란? (1) (2) | 2020.12.24 |
[Spring + JPA] Make Web Application (1) (0) | 2020.12.23 |
[JPA] 벌크 연산이란? (3) | 2020.12.23 |
[JPA] 네이티브 SQL 정리 (0) | 2020.12.23 |