웹 계층 개발

woom·2023년 5월 1일

Spring Boot

목록 보기
6/6
post-thumbnail

김영한 강사님 [실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발] 강의 참조


🌼 홈 화면과 레이아웃


📕 컨트롤러 관련 개념

  • @Controller : 요청 처리 클래스를 Spring Bean으로 등록하기 위한 어노테이션
    • @RequestMapping 어노테이션으로 다수의 요청 처리 메소드를 작성하여 클라이언트의 요청에 대한 처리 가능
  • 로그 구현체 사용
      1. Logger log = LoggerFactory.getLogger(getClass());(slf4j)
      1. 롬북을 이용해서 @Slf4j 으로도 사용 가능
    • log.info("a") : a라는 메시지를 info 수준으로 로그에 기록해라

📙 HomeController

package jpabook.jpashop.controller;

import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@Slf4j
public class HomeController {

    @RequestMapping("/")
    public String home(){
        log.info("home controller");
        return "home";
    }
}

📌 Thymeleaf

  • '템플릿 엔진'의 일종. html 태그에 속성을 추가해 페이지에 동적으로 값을 추가하거나 처리

  • <html xmlns:th="http://www.thymeleaf.org">

    • Thymeleaf의 네임스페이스를 선언하는 것으로, 이 문서에서는 Thymeleaf 태그 및 속성을 사용
  • <head th:replace="fragments/header :: header">

    • th:replace는 Thymeleaf에서 제공하는 속성 중 하나로, 해당 요소를 다른 템플릿 파일의 내용으로 대체합니다. 여기서는 "fragments/header"라는 템플릿 파일의 "header" 블록을 이 요소로 대체하라는 뜻
    • ::는 Thymeleaf에서 템플릿 파일과 블록을 구분하기 위해 사용되는 구분자
  • 템플릿 엔진을 사용하여 코드를 모듈화하고, 코드의 재사용성을 높일 수 있습니다.

  • Thymeleaf 스타일

    • Include-style layouts : include 스타일로 매번 해줘야 하지만 단순
    • Hierarchical-style layouts : 중복 없이 사용 가능하나 setting해야함 (실무에서 권장)
  • 문법
    • th:object="${memberForm}" : memberform 객체
    • th:field="*{city}" => id="city" name="city" : 객체 안에 있는 city라는 필드
  • 문법은 Thymeleaf 홈페이지 docs에 상세히 나와있으므로 참조
  • a 태그 사용 : th:href="@{}"
<!-- 특정 url로 이동 -->
<a th:href="@{https://www.a.com}"></a>

<!-- 현재 서버 내에서 이동 -->
<a th:href="@{/board/list}">

<!-- 파라미터를 넘길 시 -->
<a th:href="@{/board/view(id = ${board.id})}">

<!-- 파라미터를 여러 개 넘길 시 -->
<a th:href="@{/board/view(id = ${board.id}, writer = ${board.writer}})}">

<!-- PathVariable 사용 시 -->
<a th:href="@{/board/view/{id}(id = ${board.id})}">

📒 home.html

  • Shift + Ctrl + F9 : Recompile (spring-boot-devtools에서 뷰 템플릿 변경사항을 서버 재시작 없이 즉시 반영)
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="fragments/header :: header">
    <title>Hello</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<div class="container">
    <div th:replace="fragments/bodyHeader :: bodyHeader" />
    <div class="jumbotron">
        <h1 style="font-weight: bold; color:navy;">🐳 Welcome to JPA SHOP 🐳</h1>
        <br>
        <p class="lead">회원 기능</p>
        <p>
            <a class="btn btn-lg btn-secondary" href="/members/new">회원 가입</a>
            <a class="btn btn-lg btn-secondary" href="/members">회원 목록</a>
        </p>
        <p class="lead">상품 기능</p>
        <p>
            <a class="btn btn-lg btn-dark" href="/items/new">상품 등록</a>
            <a class="btn btn-lg btn-dark" href="/items">상품 목록</a>
        </p>
        <p class="lead">주문 기능</p>
        <p>
            <a class="btn btn-lg btn-info" href="/order">상품 주문</a>
            <a class="btn btn-lg btn-info" href="/orders">주문 내역</a>
        </p>
    </div>
    <div th:replace="fragments/footer :: footer" />
</div> <!-- /container -->
</body>
</html>

📌 bootstrap

  • https://getbootstrap.com 홈페이지에서 Compiled CSS and JS Download해서 static 디렉토리에 붙여넣기 하면 디자인 적용 가능
    • static 디렉토리에 있으면 정적파일은 자동 실행


🌼 회원 기능


📗 회원등록

  • 검증

    • @Valid : 객체에 들어가는 값을 검증. (validation annotation을 쓰겠다.)
    • BingingResult : 오류가 담겨서 실행
    • bindingResult.hasErrors() 함수로 값이 알맞은지 체크할 수 있다.
  • return "redirect:/"; : 재로딩하지 않고 이동

  • th:class="${#fields.hasErrors('name')}? 'form-control fieldError' : 'form-control'"

    • membercontroller에서 전달받은 name에 에러가 있으면 form-control class는 fieldError(테두리 레드로 설정됨)
  • <p th:if="${#fields.hasErrors('name')}" th:errors="*{name}">

    • name에 에러가 있으면 name필드에 관련된 에러메시지를 뽑아 출력해라
@Controller
@RequiredArgsConstructor
public class MemberController {

    private final MemberService memberService;

    //회원가입 form
    @GetMapping("/members/new")
    public String createForm(Model model){
        model.addAttribute("memberForm", new MemberForm());//validation 해줌
        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:/";
    }
}

📘 회원 목록 조회

  • ${member.address?.city} : ?를 사용하면 null 을 무시한다.
    //회원 전체 조회
    @GetMapping("/members")
    public String list(Model model) {
        List<Member> members = memberService.findMembers();
        model.addAttribute("members", members);
        return "members/memberList";
    }

📌 @NotNull vs @NotEmpty vs @NotBlank

  • @NotNull : null 값만 허용하지 않음. "", " " 가 입력되었을 경우는 허용

  • @NotEmpty : null과 추가로 "" 입력도 허용하지 않음. 다만 " " 의 입력은 허용

  • @NotBlank : null, "", " " 모두 허용하지 않음


🌼 상품 기능


📕 상품등록

@Controller
@RequiredArgsConstructor
public class ItemController {

    private final ItemService itemService;

    //상품등록 form
    @GetMapping("/items/new")
    public String createForm(Model model){
        model.addAttribute("form", new BookForm());
        return "items/createItemForm";
    }

    //상품등록
    @PostMapping("/items/new")
    public String create(BookForm form){
        Book book = new Book();
        book.setName(form.getName());
        book.setPrice(form.getPrice());
        book.setStockQuantity(form.getStockQuantity());
        book.setAuthor(form.getAuthor());
        book.setIsbn(form.getIsbn());

        itemService.saveItem(book);
        return "redirect:/";
    }
}

📙 상품 목록 조회

  • Dtype : @DiscriminatorColumn(name = "dtype") & @DiscriminatorValue("B")

    //상품 전체 조회
    @GetMapping("/items")
    public String list(Model model){
        List<Item> items = itemService.findItems();
        model.addAttribute("items", items);
        return "items/itemList";
    }

📒 상품 수정

  • @RequestMapping 어노테이션 값으로 {템플릿변수}사용
    • @PathVariable 어노테이션을 이용해서 {템플릿 변수} 와 동일한 이름을 갖는 파라미터를 추가
    • 이들 변수는 @PathVariable 어노테이션이 적용된 동일한 이름을 갖는 파라미터에 매핑
//상품 수정 form
    @GetMapping("items/{itemId}/edit")
    public String updateItemForm(@PathVariable("itemId") Long itemId, Model model){
        Book item = (Book) itemService.findOne(itemId);

        BookForm form = new BookForm();
        form.setId(item.getId());
        form.setName(item.getName());
        form.setPrice(item.getPrice());
        form.setStockQuantity(item.getStockQuantity());
        form.setAuthor(item.getAuthor());
        form.setIsbn(item.getIsbn());

        model.addAttribute("form", form);
        return "items/updateItemForm";
    }

    //상품수정 처리
    @PostMapping("items/{itemId}/edit")
    public String updateItem(@PathVariable String itemId, @ModelAttribute("form") BookForm form){//updateform의 변경값 가져오기

        Book book = new Book();
        book.setId(form.getId());
        book.setName(form.getName());
        book.setPrice(form.getPrice());
        book.setStockQuantity(form.getStockQuantity());
        book.setAuthor(form.getAuthor());
        book.setIsbn(form.getIsbn());
        itemService.saveItem(book);
        return "redirect:/items";
    }

⭐ 변경 감지와 병합(merge)

  • 준영속 엔티티 : 영속성 컨텍스트가 더는 관리하지 않는 엔티티

    • ex. itemService.saveItem(book) 에서 수정을 시도하는 Book 객체다. Book 객체는 이미 DB에 한번 저장되어서 식별자가 존재한다. 이렇게 임의로 만들어낸 엔티티도 기존 식별자를 가지고 있으면 준영속 엔티티로 볼 수 있다.
  • 준영속 엔티티를 수정하는 2가지 방법

  1. 변경 감지(dirty checking) 기능 사용 (권장!!)
@Transactional
void update(Item itemParam) { //itemParam: 파리미터로 넘어온 준영속 상태의 엔티티
 Item findItem = em.find(Item.class, itemParam.getId()); //같은 엔티티
조회
 findItem.setPrice(itemParam.getPrice()); //데이터를 수정한다.
}
  • 영속성 컨텍스트에서 엔티티를 다시 조회한 후에 데이터를 수정
    • 트랜잭션 안에서 엔티티를 다시 조회, 변경할 값 선택 트랜잭션 커밋 시점에 변경 감지(Dirty Checking)이 동작해서 데이터베이스에 UPDATE SQL 실행
  1. 병합(merge) 사용
@Transactional
void update(Item itemParam) { //itemParam: 파리미터로 넘어온 준영속 상태의 엔티티
 Item mergeItem = em.merge(itemParam);
}
  • 준영속 상태의 엔티티를 영속 상태로 변경할 때 사용
  • 병합시 동작 방식
      1. 준영속 엔티티의 식별자 값으로 영속 엔티티를 조회한다.
      1. 영속 엔티티의 값을 준영속 엔티티의 값으로 모두 교체한다.(병합한다.)
      1. 트랜잭션 커밋 시점에 변경 감지 기능이 동작해서 데이터베이스에 UPDATE SQL이 실행
  • 주의: 변경 감지 기능을 사용하면 원하는 속성만 선택해서 변경할 수 있지만, 병합을 사용하면 모든 속성이 변경된다. 병합시 값이 없으면 null 로 업데이트 할 위험 (병합은 모든 필드를 교체한다.)

🌼 주문 기능

  • @RequestParam("가져올 데이터이름")

    • 클라이언트가 요청한 URL의 쿼리 파라미터에 대한 값 가져오기
    • form-submit 형식에서 select문의 value값 가져오기
    • input실행 시 name값 가져오기
  • @ModelAttribute (param) : 매개변수에 저장된 값을 속성값으로 저장

    • @ModelAttribute 어노테이션이 붙은 객체를 자동으로 생성(orderSearch)
    • 생성된 오브젝트에(orderSearch) HTTP로 넘어 온 값들을 자동으로 바인딩
    • @ModelAttribute 어노테이션이 붙은 객체가(OrderSearch) 자동으로 Model 객체에 추가됨

📗 상품주문

    //주문생성 form
    @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";
    }

    //주문 생성
    @PostMapping("/order")
    public String order(@RequestParam("memberId") Long memberId,
                        @RequestParam("itemId") Long itemId, @RequestParam("count") int count){
        orderService.order(memberId, itemId, count);
        return "redirect:/orders";
    }

📘 주문조회(검색)

  • Thymeleaf를 이용한 반복문 해석
<option th:each="status : ${T(jpabook.jpashop.domain.OrderStatus).values()}"
                                th:value="${status}"
                                th:text="${status}">option
</option>
  1. jpabook.jpashop.domain.OrderStatus 클래스의 모든 열거형 상수들을 배열 형태로 가져옴
  2. th:each 속성에서 status라는 이름의 변수를 선언하고, 열거형 상수들 값의 배열 요소들을 순회하면서 <option> 태그를 생성
  3. 각 <option> 태그에는 th:value 속성에 status 변수의 값을, th:text 속성에는 status 변수의 문자열 표현을 넣어줌
  • 이렇게 생성된 <option> 태그들은 <select> 태그 내부에서 사용되며, 해당 열거형 상수를 선택 가능
    //주문 목록 조회
    @GetMapping("/orders")
    public String orderList(@ModelAttribute("orderSearch") OrderSearch orderSearch, Model model){
        List<Order> orders = orderService.findOrders(orderSearch);
        model.addAttribute("orders", orders);

        return "order/orderList";
    }

📔 주문취소

  • javascript
    • document.body.appendChild(폼 변수명); : 부모태그변수명.appendChild(자식태그변수명)으로 Body태그에 생성한 html을 붙여 submit을 할 수 있게 만들어준다.
 function cancel(id) {
 var form = document.createElement("form");
 form.setAttribute("method", "post");
 form.setAttribute("action", "/orders/" + id + "/cancel");
 document.body.appendChild(form);
 form.submit();
 }

    //주문 취소
    @PostMapping("/orders/{orderId}/cancel")
    public String cancelOrder(@PathVariable("orderId") Long orderId){
        orderService.cancelOrder(orderId);
        return "redirect:/orders";
    }

profile
Study Log 📂

0개의 댓글