보글보글 개발일지
article thumbnail
반응형

섹션5. 상품 도메인 개발

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

- 구현 기능: 상품 등록, 상품 목록 조회, 상품 수정   

- 엔티티 자체가 해결할 수 있는건 엔티티 안에 로직을 넣는 것이 좋다. 데이터 가지고 있는데서 비지니스 로직 나가는게 가장 응집도가 높다. 그게 객체지향적으로 좋다. 따라서 Item.java에 코드를 작성한다.

//Item.java
/**
 * 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;
}

exception 폴더 내부에 예외 파일들을 생성한다.

public class NotEnoughStockException extends  RuntimeException{
    //cmd+n 눌러서 override
    public NotEnoughStockException() {
        super();
    }

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

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

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

}

- 파일 내용은 위와 같은데, command+n 누르고 override를 진행한다.

상품 리포지토리 개발

public void save(Item item){
    if(item.getId() == null){
        em.persist(item);
        //item은 jpa에 저장하기 전까지 id값이 없음
        //새로 생성하는 객체. 신규로 등록.
    } else{
        em.merge(item);
        //이미 DB에 등록된 객체 가져오는 것 
        //쉽게 말해 update의 역할
    }
}

- save할 때 item이 비어있다면 새로 생성하는 객체를 의미한다. Item은 JPA에 저장하기 전까지 id값이 없다.
- 만약 이미 등록된 객체라면 merge를 사용한다. (추후 설명할 예정)

상품 서비스 개발

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

- 회원과 크게 다르지 않다. 위에서 @Transactional(readOnly = true)를 해줬기 때문에 save 할 때는 false 어노테이션을 붙혀줘야 한다.

섹션 6. 주문 도메인 개발

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

- 구현 기능: 상품 주문, 주문 내역 조회, 주문 취소
- 여러개 연관관계 있고 복잡하면 별도의 생성 메서드가 있으면 좋다.

//Order.java
//==생성 메서드==//
//복잡한 생성은 별도의 생성 메소드가 있으면 좋다.
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;
}

- 밖에서 set하는 방식이 아니라 생성할때부터 무조건 createOrder 호출해서 값을 넣어서 생성 메소드에서 주문 생성에 대한 복잡한 비지니스를 완결. 응집성 있게! --> 주문 생성과 관련된건 여기만 고치면 된다.

//Order.java
//==비지니스로직==//
/**
 * 주문 취소
 * 이미 배송된 상품 주문취소못하는 로직이 엔티티 안에 있다.
 * */
public void cancel(){
    if(delivery.getStatus() == DeliveryStatus.COMP){
        throw new IllegalStateException("이미 배송완료된 상품은 취소가 불가능합니다.");
    }
    this.setStatus(OrderStatus.CANCEL);
    for (OrderItem orderItem : orderItems) {
        orderItem.cancel();

    }
}

- 이미 배송완료 되면 오류나도록 설계한다. 이 로직이 엔티티 안에 있다.
- order한 번에 아이템 여러개 있을 수 있어서 상품마다 cancel을 해줘야 한다.

//OrderItem.java
//==비지니스 로직==//
public void cancel() {
    getItem().addStock(count); //재고 수량을 원래대로 돌려준다.
}

- 재고 수량을 원래대로 돌린다.

//Order.java
//==조회로직==//
/**
 * 전체 주문 가격 조회
 * */
public int getTotalPrice(){
    int totalPrice =0;
    for (OrderItem orderItem : orderItems) {
        totalPrice += orderItem.getTotalPrice();
    }
    return totalPrice;
}

- 전체 주문 가격은 order item의 가격 다 더하면 된다.

//OrderItem.java
public int getTotalPrice() {
    return getOrderPrice()*getCount();
}

- 여기서 값을 계산해준다.

//==생성 메서드==//
//여러개 연관관계 있고 복잡하면 별도의 생성 메서드가 있으면 좋다.
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;
}

- 연관관계가 여러개이고, 복잡하므로 별도의 생성 메서드를 만들어준다. 

주문 리포지토리 개발

- 내용은 회원, 상품과 거의 비슷

주문 서비스 개발

 

주문 기능 테스트

 

주문 검색 기능 개발

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

- SOL 1: JPQL로 처리 --> 매우 복잡하다. 실무에서 안쓴다.

- SOL 2: JPA Criteria로 처리

실무에서는 둘다 안쓰고, Querydsl을 위주로 쓴다.

섹션 7. 웹 계층 개발

홈 화면과 레이아웃

//home.html
<head th:replace="fragments/header :: header">
//fragments/header.html
<head th:fragment="header">

- JSP처럼 뭔가 include하는 것. 

- 위처럼 fragment/header 파일 내의 header 부분으로 해당 부분이 대체된다.
- Hierarchical-style layout을 쓰면 중복 제거를 더 하고 심플하게 할 수 있다.
-뷰 템플릿 변경사항을 서버 재시작 없이 즉시 반영하려면?
    1. spring-boot-devtools 추가
    2. html 파일 build-> Recompile

- bootstrap 사이트에서 Compiled CSS and JS을 다운로드한다. 그리고, resource/static 밑에 두 폴더를 넣는다.

회원 등록

//MemberForm.java 
@NotEmpty(message="회원 이름은 필수입니다.") //이름은 필수로 받고, 나머지는 필수 아님.
private String name;
  • @NotEmpty 어노테이션을 쓰면, 필수로 입력을 해야한다.
//MemberController.java
@Controller
@RequiredArgsConstructor
public class MemberController {
    private final MemberService memberService;

    @GetMapping("/members/new")
    public String createForm(Model model){
        model.addAttribute("memberForm", new MemberForm());
        return "members/createMemberForm";
    }
    
    ...
}
  • Model의 경우, 컨트롤러에서 뷰로 넘어갈 때 위 데이터를 실어서 넘긴다.
  • MemberForm이라는 빈 껍데기 객체 가지고 이동하는 이유는 validation 같은 거 해주기 위해서이다!
  • /members/new가 get방식으로 들어오면, 위 컨트롤러를 타고 createMemberForm.html 파일이 렌더링 된다.
  • 이때 model attribute를 넘겼으므로 화면에서 MemberForm() 객체에 접근할 수 있게된다.

- th:object="${memberForm}" => form안에서 이 객체를 계속 쓰겠다는 의미.
- th:field="*{name}" => * 표시가 있으면 위 객체를 참조한다. MemberForm 안의 name을 getter, setter 방식으로 접근을 한다.
- th:field를 쓰면 form에서 id, name을 동일하게 맞춰준다.

//MemberController.java
@PostMapping("/members/new")
public String create(@Valid MemberForm form, BindingResult result){
    //@Valid 쓰면 폼이 @NotEmpty 같은 걸 검사 해준다.
    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:/"; //저장되고 재로딩되면 안좋아서 리다이렉트 쓰는 것이 좋다.
}
  • 매개변수 받을 때 @Valid를 쓰면 폼이 @NotEmpty 같은 걸 검사해준다. 다양한 검사가 다 가능하다.
  • 저장하고 다시 로딩되는 것보다 리다이렉트를 쓰는 것이 좋다.
  • BindingResult result: 오류가 담겨서 실행이 된다. 오류 있으면 다시 폼으로 돌아가게 실행된다.
  • 위와 같이 이름을 입력하지 않고 Submit을 누르면 "회원 이름은 필수입니다."라는 문구와 함께 다시 원래 폼으로 돌아오는 것을 확인할 수 있다.
  • th:class="${#fields.hasErrors('name')}? 'form-control fieldError' : 'form-control' --> name에 에러가 있으면 fieldError 클래스의 css를 통해 빨간색으로 만들어버린다.

   --> 그리고 name필드에 대해 에러메시지를 뽑아서 출력하게 한다.

  • 도시, 거리, 우편번호는 입력한 것이 유지된다. Controller에서 에러 있더라도 form에서 데이터 그대로 가져가므로!
  • 매개변수에 Entity가 아닌 MemberForm을 넣는 이유는 일단 두개가 안 맞는다. Member가 복잡해지고, 컨트롤러에서 화면으로 넘어올 때 validation과 실제 도메인이 원하는 validation이 다를 수 있다. 따라서 form 데이터를 만들고 그걸 매개변수로 받는게 좋다.

회원 목록 조회

 //MemberController.java 
@GetMapping("/members")
public String list(Model model){
    List<Member> members = memberService.findMembers();
    model.addAttribute("members",members);
    return "members/memberList";
}
  • 조회한 회원을 모델 객체에 저장하고 화면에 넘긴다.
  • memberList.html - thymeleaf의 장점: html 태그를 그대로 쓸 수 있다. 모델에 담아서 넘긴 members를 그냥 쭉 찍으면 된다. - ?은 만약 null인 경우 더이상 진행 안하는 것을 의미한다.
  • 폼 객체 vs 엔티티 직접 사용: 화면 요구사항이 복잡해지기 시작하면, 엔티티에 화면을 처리하기 위한 기능이 점점 증가한다. 결과적으로 엔티티는 점점 화면에 종속적으로 변하고, 이렇게 화면 기능 때문에 지저분해진 엔티티는 결국 유지보수하기 어려워진다. 엔티티는 핵심 비즈니스 로직만 가지고 있고, 화면을 위한 로직은 없어야 한다. 엔티티는 최대한 순수하게!
  • API 만들 때는 절대 엔티티를 외부로 반환하면 안된다! API 스펙이 변하기 때문에 문제다.

상품 등록

model.addAttribute("form", new BookForm());

  • 이걸 넣어줘야 html에서 변수 추적할 수 있다. 유지보수하기 간편해진다.

상품 목록

수정 - 회원 목록과 거의 동일하다. 모델에 담긴 items를 출력해주면 된다.

상품 수정 @PostMapping("items/{itemId}/edit")

  • Id를 조작하기 쉬워서, 서비스 계층에서 유저가 이 아이템에 권한이 있나 체크하는 로직이 필요하다.

변경 감지와 병합(merge)

날라갔다ㅠㅠ 2회독때 채울 예정

Merge는 가급적 사용하지 말고, 변경 감지를 이용하자!

상품 주문

//OrderController.java
@GetMapping("/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";
}

- members, items를 조회해서 모델이 넣어준다.

//orderForm.html
<select name="memberId" id="member" class="form-control">
    <option value="">회원선택</option>
    <option th:each="member : ${members}"
            th:value="${member.id}"
            th:text="${member.name}" />
</select>

...

<select name="itemId" id="item" class="form-control">
                <option value="">상품선택</option>
                <option th:each="item : ${items}"
                        th:value="${item.id}"
                        th:text="${item.name}" />
            </select>

- HTML select 문을 활용해서 option을 만든다. thymleaf each 태그를 이용!

//OrderController.java
@PostMapping("/order")
public String order(@RequestParam("memberId") Long memberId,
                    @RequestParam("itemId") Long itemId,
                    @RequestParam("count") int count){
    //requestparam form submit방식으로 오면 선택된 id의 value가 넘어오게 된다.
    orderService.order(memberId, itemId, count);
    return "redirect:/orders"
}

- @RequsetParam은 form submit 방식으로 html에서 보내면, name에 해당하는 값의 value가 넘어오게 된다.

주문 목록 검색, 취소

<form th:object="${orderSearch}" class="form-inline">
    <div class="form-group mb-2">
        <input type="text" th:field="*{memberName}" class="formcontrol"
               placeholder="회원명"/>
    </div>
    <div class="form-group mx-sm-1 mb-2">
        <select th:field="*{orderStatus}" class="form-control">
            <option value="">주문상태</option>
            <option th:each=
                            "status : ${T(jpabook.jpashop.domain.OrderStatus).values()}"
                    th:value="${status}"
                    th:text="${status}">option
            </option>
        </select>
    </div>
    <button type="submit" class="btn btn-primary mb-2">검색</button>
</form>

- 타임리프의 object인 orderSearch를 OrderController.java에서 @ModelAttribute("orderSearch")를 통해 받을 수 있다. orderSearch에 자동으로 담긴다.

 

 

 

반응형
profile

보글보글 개발일지

@보글

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!