Spring MVC-TIL (11)

YulHee Kim·2021년 9월 17일
0

Spring MVC

목록 보기
11/11

[참고 강의] 김영한님의 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술

💡 스프링 MVC - 기본기능

✏️ 요청 매핑 핸들러 어뎁터 구조

그전 시간에 정리해본 HTTP 메시지 컨버터는 스프링 MVC 어디쯤에 사용되는 것일까?

Spring MVC 구조


@RequestMapping을 처리하는 핸들러 어댑터인 RequestMappingHandlerAdapter(요청 매핑 핸들러 어뎁터)에 있다.

RequestMappingHandlerAdapter 동작 방식

ArgumentResolver

애노테이션 기반의 컨트롤러는 매우 다양한 파라미터를 사용할 수 있다.
HttpServletRequest, Model과 @RequestParam, @ModelAttribute 같은 애노테이션 그리고 @RequestBody, HttpEntity 같은 HTTP메시지를 처리하는 부분까지 매우 큰 유연함을 보여주었다.
이렇게 파라미터를 유연하게 처리할 수 있는 이유가 ArgumentResolver 덕분이다.

애노테이션 기반 컨트롤러를 처리하는 RequestMappingHandlerAdaptor는 바로 이 ArgumentResolver를 호출해서 컨트롤러가 필요로 하는 다양한 파라미터의 값(객체)을 생성한다. 이렇게 파라미터의 값이 모두 준비되면 컨트롤러를 호출하면서 값을 넘겨준다.

ReturnValueHandler

ArgumentResolver 와 비슷한데, 이것은 응답 값을 변환하고 처리한다.

컨트롤러에서 String으로 뷰 이름을 반환해도, 동작하는 이유가 바로 ReturnValueHandler 덕분이다.

✏️ HTTP 메시지 컨버터

HTTP 메시지 컨버터 위치

요청의 경우
@RequestBody 를 처리하는 ArgumentResolver 가 있고, HttpEntity 를 처리하는 ArgumentResolver 가 있다. 이 ArgumentResolver 들이 HTTP 메시지 컨버터를 사용해서 필요한 객체를 생성하는 것이다.

응답의 경우
@ResponseBody 와 HttpEntity 를 처리하는 ReturnValueHandler 가 있다. 그리고 여기에서 HTTP 메시지 컨버터를 호출해서 응답 결과를 만든다.


💡 스프링 MVC - 웹 페이지 만들기

요구사항 분석

상품 도메인 모델

  • 상품 ID
  • 상품명
  • 가격
  • 수량

상품 관리 기능

  • 상품 목록
  • 상품 상세
  • 상품 등록
  • 상품 수정

✏️ 상품 도메인 개발

Item - 상품 객체

@Getter @Setter
public class Item {
    
    private Long id;
    private String itemName;
    private Integer price;
    private Integer quantity;

    public Item() {
    }

    public Item(String itemName, Integer price, Integer quantity) {
        this.itemName = itemName;
        this.price = price;
        this.quantity = quantity;
    }
}

ItemRepository - 상품 저장소

@Repository
public class ItemRepository {

    private static final Map<Long, Item> store = new HashMap<>(); //static
    private static long sequence = 0L; //static

    public Item save(Item item) {
        item.setId(++sequence);
        store.put(item.getId(), item);
        return item;
    }

    public Item findById(Long id) {
        return store.get(id);
    }

    public List<Item> findAll() {
        return new ArrayList<>(store.values());
    }

    public void update(Long itemId, Item updateParam) {
        Item findItem = findById(itemId);
        findItem.setItemName(updateParam.getItemName());
        findItem.setPrice(updateParam.getPrice());
        findItem.setQuantity(updateParam.getQuantity());
    }

    public void clearStore() {
        store.clear();
    }

}

✏️ 상품 목록 - 타임리프

BasicItemController

@Controller
@RequestMapping("/basic/items")
@RequiredArgsConstructor
public class BasicItemController {

    private final ItemRepository itemRepository;

    @GetMapping
    public String items(Model model) {
        List<Item> items = itemRepository.findAll();
        model.addAttribute("items", items);
        return "basic/items";
    }
    
    //테스용 데이터 추가
    @PostConstruct
    public void init() {
        itemRepository.save(new Item("itemA", 10000, 10));
        itemRepository.save(new Item("itemB", 20000, 20));
    }
}

@RequiredArgsConstructor
final이 붙은 멤버변수만 사용해서 생성자를 자동으로 만들어준다.

public BasicItemController(ItemRepository itemRepository) {
    this.itemRepository = itemRepository;
}
  • 이렇게 생성자가 딱 1개만 있으면 스프링이 해당 생성자에 @Autowired로 의존관계를 주입해준다.
  • 따라서 final 키워드를 빼면 안된다. 의존관계 주입이 안되기 때문!

items.html

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="utf-8">
    <link  th:href="@{/css/bootstrap.min.css}"
            href="../css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container" style="max-width: 600px">
    <div class="py-5 text-center">
        <h2>상품 목록</h2> </div>
    <div class="row">
        <div class="col">
            <button class="btn btn-primary float-end"
                    onclick="location.href='addForm.html'"
                    th:onclick="|location.href='@{/basic/items/add}'|"
                    type="button">상품 등록</button>
        </div>
    </div>
    <hr class="my-4">
    <div>
        <table class="table">
            <thead>
            <tr>
                <th>ID</th>
                <th>상품명</th>
                <th>가격</th>
                <th>수량</th>
            </tr>
            </thead>
            <tbody>
            <tr th:each="item : ${items}">
                <td><a href="item.html" th:href="@{/basic/items/{itemId}(itemId=${item.id})}"
                       th:text="${item.id}">회원id</a></td>
                <td><a href="item.html" th:href="@{|/basic/items/${item.id}|}"
                       th:text="${item.itemName}">상품명</a></td>
                <td th:text="${item.price}">10000</td>
                <td th:text="${item.quantity}">10</td>
            </tr>
            </tbody>
        </table>
    </div>
</div> <!-- /container -->
</body>
</html>

타임리프 간단히 알아보기

속성 변경 - th:href

th:href="@{/css/bootstrap.min.css}

  • href="value1"을 th:href="value2"의 값으로 변경한다.
  • 타임리프 뷰 템플릿을 거치게 되면 원래 값을 th:xxx 값으로 변경한다. 만약 값이 없다면 새로 생성한다.
  • HTML을 그대로 볼 때는 href 속성이 사용되고, 뷰 템플릿을 거치면 th:href 의 값이 href 로 대체되면서 동적으로 변경할 수 있다.

타임리프 핵심

핵심은 th:xxx 가 붙은 부분은 서버사이드에서 렌더링 되고, 기존 것을 대체한다. th:xxx 이 없으면 기존 html의 xxx 속성이 그대로 사용된다.

URL 링크 표현식 - @{...}

th:href="@{/css/bootstrap.min.css}"

  • 타임리프는 URL 링크를 사용하는 경우 @{...} 를 사용한다. 이것을 URL 링크 표현식이라 한다.
  • URL 링크 표현식을 사용하면 서블릿 컨텍스트를 자동으로 포함한다.

상품 등록 폼으로 이동

속성 변경 - th:onclick

onclick="location.href='addForm.html'"
th:onclick="|location.href='@{/basic/items/add}'|"

리터럴 대체 - |...|

  • 타임리프에서 문자와 표현식 등은 분리되어 있기 때문에 더해서 사용해야 한다.
    <span th:text="'Welcome to our application, ' + ${user.name} + '!'">
  • 다음과 같이 리터럴 대체 문법을 사용하면, 더하기 없이 편리하게 사용할 수 있다.
    <span th:text="|Welcome to our application, ${user.name}!|">

반복 출력 - th:each

<tr th:each="item : ${items}">

  • 반복은 th:each 를 사용한다. 이렇게 하면 모델에 포함된 items 컬렉션 데이터가 item 변수에 하나씩 포함되고, 반복문 안에서 item 변수를 사용할 수 있다.
  • 컬렉션의 수 만큼 .. 이 하위 테그를 포함해서 생성된다.

변수 표현식 - ${...}

<td th:text="${item.price}">10000</td>

  • 모델에 포함된 값이나, 타임리프 변수로 선언한 값을 조회할 수 있다.
  • 프로퍼티 접근법을 사용한다. ( item.getPrice() )

내용 변경 - th:text

<td th:text="${item.price}">10000</td>

  • 내용의 값을 th:text 의 값으로 변경한다.
  • 여기서는 10000을 ${item.price} 의 값으로 변경한다.

URL 링크 표현식2 - @{...}

th:href="@{/basic/items/{itemId}(itemId=${item.id})}"

  • 다음과 동일함. th:href="@{|/basic/items/${item.id}|}"
  • 경로변수({itemId}) 뿐만 아니라 쿼리 파라미터도 생성
    th:href="@{/basic/items/{itemId}(itemId=${item.id}, query='test')}"
    생성 링크:http://localhost:8080/basic/items/1?query=test

✏️ 상품 상세

BasicItemController

    @GetMapping("/{itemId}")
    public String item(@PathVariable long itemId, Model model) {
        Item item = itemRepository.findById(itemId);
        model.addAttribute("item", item);
        return "basic/item";
    }

PathVariable 로 넘어온 상품ID로 상품을 조회하고, 모델에 담아둔다. 그리고 뷰 템플릿을 호출한다.

item.html

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="utf-8">
    <link  th:href="@{/css/bootstrap.min.css}"
            href="../css/bootstrap.min.css" rel="stylesheet">
    <style>
                 .container {
            max-width: 560px;
} </style>
</head>
<body>
<div class="container">
    <div class="py-5 text-center">
        <h2>상품 상세</h2> </div>
    <div>

        <h2 th:if="${param.status}" th:text="'저장 완료'"></h2>

        <label for="itemId">상품 ID</label>
        <input type="text" id="itemId" name="itemId" class="form-control"
               value="1" th:value="${item.id}" readonly>
    </div> <div>
    <label for="itemName">상품명</label>
    <input type="text" id="itemName" name="itemName" class="form-control"
           value="상품A" th:value="${item.itemName}" readonly> </div>
    <div>
        <label for="price">가격</label>
        <input type="text" id="price" name="price" class="form-control"
               value="10000" th:value="${item.price}" readonly>
    </div> <div>
    <label for="quantity">수량</label>
    <input type="text" id="quantity" name="quantity" class="form-control"
           value="10" th:value="${item.quantity}" readonly>
</div>
    <hr class="my-4">
    <div class="row">
        <div class="col">
            <button class="w-100 btn btn-primary btn-lg"
                    onclick="location.href='editForm.html'"
                    th:onclick="|location.href='@{/basic/items/{itemId}/edit(itemId=${item.id})}'|"
                    type="button">상품 수정</button> </div>
        <div class="col">
            <button class="w-100 btn btn-secondary btn-lg"
                    onclick="location.href='items.html'"
                    th:onclick="|location.href='@{/basic/items}'|"
                    type="button">목록으로</button> </div>
    </div>
</div> <!-- /container -->
</body>
</html>

속성 변경 - th:value

th:value="${item.id}"

  • 모델에 있는 item 정보를 획득하고 프로퍼티 접근법으로 출력한다. ( item.getId() )
  • value 속성을 th:value 속성으로 변경한다.

상품 수정 링크

th:onclick="|location.href='@{/basic/items/{itemId}/edit(itemId=${item.id})}'|"

목록으로 링크

th:onclick="|location.href='@{/basic/items}'|"

길어져서 나눠서 정리합니당

profile
백엔드 개발자

0개의 댓글