스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 27

Spring 정리 2022. 5. 29. 14:29

인프런 강의 38일차.

 - 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 1 (김영한 강사님)

 - 서블릿, JSP, MVC 패턴

 - 서블릿으로 1차 구현

   -> 서블릿으로 구현했을 때 불편한 점 개선을 위해 JSP로 2차 구현

   -> JSP로도 불편한 점을 개선하기 위해 MVC 패턴으로 3차 구현

 - 스프링 프로젝트 시작

   -> 스프링 기본기능 이해

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

 

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

 5.1 프로젝트 생성 (스프링 부트 스타터 사이트로 이동해서 스프링 프로젝트 생성)

  > 스프링 프로젝트 생성 (https://start.spring.io)

   - 프로젝트 선택 Project: Gradle Project

   - Language: Java

   - Spring Boot: 2.7.0

   - Group: hello

   - Artifact: item-service

   - Name: item-service

   - Package name: hello.itemservice

   - Packaging: Jar

   - Java: 11

   - Dependencies: Spring Web, Thymeleaf, Lombok

  > 파일 오픈 시 build.gradle을 선택하자

파일 오픈 후 lombok 사용을 위해 annotation enable이 필요하다
build 실행을 gradle -> intelliJ로 변경하자.

 5.2 요구사항 분석

  > 상품을 관리할 수 있는 화면을 만들어보자!

  * 상품 도메인 모델

   - 상품 ID

   - 상품명

   - 가격

   - 수량

 

  * 상품 관리 기능

   - 상품 목록

   - 상품 상세

   - 상품 등록

   - 상품 수정

 

검은색박스 : Controller, 하얀색박스 : View

  > 요구사항이 정리되고 디자이너, 웹 퍼블리셔, 백엔드 개발자가 업무를 나누어 진행한다.

  > 디자이너 : 요구사항에 맞도록 디자인하고, 디자인 결과물을 웹 퍼블리셔에게 넘겨준다.

  > 웹 퍼블리셔 : 다자이너에서 받은 디자인을 기반으로 HTML, CSS를 만들어 개발자에게 제공한다.

  > 백엔드 개발자 : 디자이너, 웹 퍼블리셔를 통해서 HTML 화면이 나오기 전까지 시스템을 설계하고, 핵심 비즈니스 모델을 개발한다. 이후 HTML이 나오면 이 HTML을 뷰 템플릿으로 변환해서 동적으로 화면을 그리고, 또 웹 화면의 흐름을 제어한다

 

* 참고

  > React, Vue.js 같은 웹 클라이언트 기술을 사용하고, 웹 프론트엔드 개발자가 별도로 있으면, 웹 프론트엔드 개발자가 웹 퍼블리셔 역할까지 포함해서 하는 경우도 있다.

  > 웹 클라이언트 기술을 사용하면, 웹 프론트엔드 개발자가 HTML을 동적으로 만드는 역할과 웹 화면의 흐름을 담당한다. 이 경우 백엔드 개발자는 HTML 뷰 템플릿을 직접 만지는 대신에, HTTP API를 통해 웹 클라이언트가 필요로 하는 데이터와 기능을 제공하면 된다.

 

 5.3 상품 도메인 개발

package hello.itemservice.domain.item;
import lombok.Data;

/**
 * @Data는 도메인 상황에서 예측하지 못한 동작을 수행할 수 있다.
 * 그러므로 필요에 따라 @Getter, @Setter를 사용하는 것을 추천한다.
 * 일반적으로 데이터만 왔다갔다 하는 객체(DTO) 대상으로는 @Data를 써도 무방하다.
 * Item 클래스에서는 예제이므로 @Data를 사용.
 */

@Data
public class Item {

    private Long id;
    private String itemName;
    private Integer price;              //price가 null일 수도 있으니 Integer를 사용
    private Integer quantity;           //quantity가 null일 수도 있으니 Integer를 사용

    public Item() {
    }

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

  - domain.item.Item.java

  - Item 객체 선언

 

package hello.itemservice.domain.item;
import org.springframework.stereotype.Repository;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

//@Repository를 사용했기 때문에 Component Scan 대상이 됨
@Repository
public class ItemRepository {

    //일반적으로 싱글톤이기 때문에 static을 사용하지 않아도 되나, new ItemRepository() 할 경우를 대비하기 위해 static으로 선언함.
    //실무에서는 HashMap을 사용할 시 멀티쓰레드 환경에서 동시에 접근이 불가능하다.. 만약 HashMap을 사용하고 싶다면 ConcurrentHashMap을 사용해야한다.
    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 객체의 멤버 중 id가 사용되지 않기 때문에 따로 ItemPramDto 등으로 선언해서 기존 객체와 구분해서 사용해주어야 한다.(귀찮아도 명확성을 위해 선언해주는 것이 설계상 좋다)
        //안그러면 개발자가 setId, getId 등은 사용할 수 있는데 왜 안했지?라고 헷갈리거나 데이터 변조가 가능함.
        Item findItem = findById(itemId);
        findItem.setItemName(updateParam.getItemName());
        findItem.setPrice(updateParam.getPrice());
        findItem.setQuantity(updateParam.getQuantity());
    }

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

  - domain.item.ItemRepository.java

  - ItemRepository 사용

 

package hello.itemservice.domain.item;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;

class ItemRepositoryTest {      //public class ItemRepositoryTest 이지만 JUnit5이상에서 public 생략이 가능하게 됨.

    ItemRepository itemRepository = new ItemRepository();

    @AfterEach
    void afterEach() {
        itemRepository.clearStore();
    }

    @Test
    void save() {
        //given
        Item item = new Item("itemA", 10000, 10);
        //when
        Item savedItem = itemRepository.save(item);
        //then
        Item findItem = itemRepository.findById(item.getId());
        assertThat(findItem).isEqualTo(savedItem);
    }

    @Test
    void findById() {
    }

    @Test
    void findAll() {
        //given
        Item item1 = new Item("item1", 10000, 10);
        Item item2 = new Item("item2", 20000, 20);
        itemRepository.save(item1);
        itemRepository.save(item2);
        //when
        List<Item> result = itemRepository.findAll();
        //then
        assertThat(result.size()).isEqualTo(2);
        assertThat(result).contains(item1, item2);
    }

    @Test
    void updateItem() {
        //given
        Item item = new Item("item1", 10000, 10);
        Item savedItem = itemRepository.save(item);
        Long itemId = savedItem.getId();
        //when
        Item updateParam = new Item("item2", 20000, 30);
        itemRepository.update(itemId, updateParam);
        Item findItem = itemRepository.findById(itemId);
        //then

        assertThat(findItem.getItemName()).isEqualTo(updateParam.getItemName());
        assertThat(findItem.getPrice()).isEqualTo(updateParam.getPrice());

        assertThat(findItem.getQuantity()).isEqualTo(updateParam.getQuantity());
    }

    @Test
    void clearStore() {
    }
}

  - domain.item.ItemRepositoryTest.java

  - ItemRepository를 테스트하기위한 Test클래스

 

 * HTML을 편리하게 개발하기 위해 부트스트랩 사용

  - 부트스트랩(Bootstrap)은 웹사이트를 쉽게 만들 수 있게 도와주는 HTML, CSS, JS 프레임워크이다. 하나의 CSS로 휴대폰, 태블릿, 데스크탑까지 다양한 기기에서 작동한다. 다양한 기능을 제공하여 사용자가 쉽게 웹사이트를 제작, 유지, 보수할 수 있도록 도와준다.

  - 부트스트랩 공식 사이트 : https://getbootstrap.com

  - 부트스트랩을 다운로드 받고 압축을 풀자 : https://getbootstrap.com/docs/5.0/getting-started/download/

  - Compiled CSS and JS 항목을 다운로드하자.

  - 압축을 출고 bootstrap.min.css 를 복사해서 resources/static/css/bootstrap.min.css 폴더에 추가하자 

  - /resources/static/css/bootstrap.min.css (부트스트랩 다운로드)

<!DOCTYPE HTML>
<html>
<head>
    <meta charset="utf-8">
    <link 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'" 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>
                <td><a href="item.html">1</a></td>
                <td><a href="item.html">테스트 상품1</a></td>
                <td>10000</td>
                <td>10</td>
            </tr>
            <tr>
                <td><a href="item.html">2</a></td>
                <td><a href="item.html">테스트 상품2</a></td>
                <td>20000</td>
                <td>20</td>
            </tr>
            </tbody>
        </table>
    </div>
</div> <!-- /container -->
</body>
</html>

  - /resources/static/html/items.html

 

<!DOCTYPE HTML>
<html>
<head>
    <meta charset="utf-8">
    <link 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>
        <label for="itemId">상품 ID</label>
        <input type="text" id="itemId" name="itemId" class="form-control"
               value="1" readonly>
    </div>
    <div>
        <label for="itemName">상품명</label>
        <input type="text" id="itemName" name="itemName" class="form-control"
               value="상품A" readonly>
    </div>
    <div>
        <label for="price">가격</label>
        <input type="text" id="price" name="price" class="form-control"
               value="10000" readonly>
    </div>
    <div>
        <label for="quantity">수량</label>
        <input type="text" id="quantity" name="quantity" class="form-control"
               value="10" 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'" type="button">상품 수정</button>
        </div>
        <div class="col">
            <button class="w-100 btn btn-secondary btn-lg"
                    onclick="location.href='items.html'" type="button">목록으로</button>
        </div>
    </div>
</div> <!-- /container -->
</body>
</html>

  - /resources/static/html/item.html

 

<!DOCTYPE HTML>
<html>
<head>
    <meta charset="utf-8">
    <link 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>
    <h4 class="mb-3">상품 입력</h4>
    <form action="item.html" method="post">
        <div>
            <label for="itemName">상품명</label>
            <input type="text" id="itemName" name="itemName" class="formcontrol" placeholder="이름을 입력하세요">
        </div>
        <div>
            <label for="price">가격</label>
            <input type="text" id="price" name="price" class="form-control"
                   placeholder="가격을 입력하세요">
        </div>
        <div>
            <label for="quantity">수량</label>
            <input type="text" id="quantity" name="quantity" class="formcontrol" placeholder="수량을 입력하세요">
        </div>
        <hr class="my-4">

        <div class="row">
            <div class="col">
                <button class="w-100 btn btn-primary btn-lg" type="submit">상품
                    등록</button>
            </div>
            <div class="col">
                <button class="w-100 btn btn-secondary btn-lg"
                        onclick="location.href='items.html'" type="button">취소</button>
            </div>
        </div>
    </form>
</div> <!-- /container -->
</body>
</html>

  - /resources/static/html/addForm.html

 

<html>
<head>
    <meta charset="utf-8">
    <link 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>
    <form action="item.html" method="post">
        <div>
            <label for="id">상품 ID</label>
            <input type="text" id="id" name="id" class="form-control" value="1"
                   readonly>
        </div>
        <div>
            <label for="itemName">상품명</label>
            <input type="text" id="itemName" name="itemName" class="formcontrol" value="상품A">
        </div>
        <div>
            <label for="price">가격</label>
            <input type="text" id="price" name="price" class="form-control"
                   value="10000">
        </div>
        <div>
            <label for="quantity">수량</label>
            <input type="text" id="quantity" name="quantity" class="formcontrol" value="10">
        </div>
        <hr class="my-4">
        <div class="row">
            <div class="col">
                <button class="w-100 btn btn-primary btn-lg" type="submit">저장
                </button>
            </div>
            <div class="col">
                <button class="w-100 btn btn-secondary btn-lg"
                        onclick="location.href='item.html'" type="button">취소</button>
            </div>
        </div>
    </form>
</div> <!-- /container -->
</body>
</html>

  - /resources/static/html/editForm.html

 

 * 참고

  > /resources/static 에 넣어두었기 때문에 스프링 부트가 정적 리소스를 제공한다. (http://localhost:8080/html/items.html)

  > 그런데 정적 리소스여서 해당 파일을 탐색기를 통해 직접 열어도 동작하는 것을 확인할 수 있다.

  > 정적 리소스가 공개되는 /resources/static 폴더에 HTML을 넣어두면, 실제 서비스에서도 공개된다. 서비스를 운영한다면 지금처럼 공개할 필요없는 HTML을 두는 것은 주의하자

 

 

 

스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 23

Spring 정리 2022. 5. 22. 16:11

인프런 강의 37일차.

 - 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 1 (김영한 강사님)

 - 서블릿, JSP, MVC 패턴

 - 서블릿으로 1차 구현

   -> 서블릿으로 구현했을 때 불편한 점 개선을 위해 JSP로 2차 구현

   -> JSP로도 불편한 점을 개선하기 위해 MVC 패턴으로 3차 구현

 - 스프링 프로젝트 시작

   -> 스프링 기본기능 이해

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

 

4. 스프링 MVC - 기본 기능

 4.5 HTTP 요청 파라미터 - 쿼리 파라미터, HTML Form

  > HTTP 요청 데이터 조회 - 개요

   - 서블릿에서 학습했던 HTTP 요청 데이터를 조회 하는 방법을 다시 떠올려보자.

   - 그리고 서블릿으로 학습했던 내용을 스프링이 얼마나 깔끔하고 효율적으로 바꾸어주는지 알아보자.

   - HTTP 요청 메시지를 통해 클라이언트에서 서버로 데이터를 전달하는 방법을 알아보자.

 

  > 클라이언트에서 서버로 요청 데이터를 전달할 때는 주로 다음 3가지 방법을 사용한다

   - GET - 쿼리 파라미터

      /url?username=hello&age=20

      메시지 바디 없이, URL의 쿼리 파라미터에 데이터를 포함해서 전달

      예) 검색, 필터, 페이징등에서 많이 사용하는 방식

   - POST - HTML Form

      content-type: application/x-www-form-urlencoded

      메시지 바디에 쿼리 파리미터 형식으로 전달 username=hello&age=20

      예) 회원 가입, 상품 주문, HTML Form 사용

   - HTTP message body에 데이터를 직접 담아서 요청

      HTTP API에서 주로 사용, JSON, XML, TEXT

      데이터 형식은 주로 JSON 사용

      POST, PUT, PATCH

 

  > 스프링으로 요청 파라미터를 조회하는 방법

package hello.springmvc.basic.request;

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

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Slf4j
@Controller
public class RequestParamController {

    @RequestMapping("/request-param-v1")
    public void requestParamV1(HttpServletRequest request, HttpServletResponse response) throws IOException {
        String username = request.getParameter("username");
        int age = Integer.parseInt(request.getParameter("age"));
        log.info("username={}, age={}", username, age);

        response.getWriter().write("ok");
    }
}

  - hello.springmvc.basic.request.RequestParamController.java

  - HTTP get/post 호출 처리

 

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<form action="/request-param-v1" method="post">
    username: <input type="text" name="username" />
    age: <input type="text" name="age" />
    <button type="submit">전송</button>
</form>
</body>
</html>

  - resources/static/basic/hello-form.html

  - HTTP post 호출을 하기 위한 샘플 html

  - Jar 를 사용하면 webapp 경로를 사용할 수 없다. 이제부터 정적 리소스도 클래스 경로에 함께 포함해야 한다.

 

 4.6 HTTP 요청 파라미터 - @RequestParam

  > 스프링이 제공하는 @RequestParam 을 사용하면 요청 파라미터를 매우 편리하게 사용할 수 있다. 

/**
 * @RequestParam 사용
 * - 파라미터 이름으로 바인딩
 * @ResponseBody 추가
 * - View 조회를 무시하고, HTTP message body에 직접 해당 내용 입력
 */
@ResponseBody
@RequestMapping("/request-param-v2")
public String requestParamV2(
        @RequestParam("username") String memberName,
        @RequestParam("age") int memberAge) {
    log.info("username={}, age={}", memberName, memberAge);
    return "ok";            //String 반환일 경우 ViewResolver를 호출하는 것이지만 ResponseBody Annotation을 추가함으로써 REST API 호출로 변경할 수 있다.
}

  - requestParamV2

  - @RequestParam : 파라미터 이름으로 바인딩

  - @ResponseBody : View 조회를 무시하고, HTTP message body에 직접 해당 내용 입력

  - @RequestParam의 name(value) 속성이 파라미터 이름으로 사용된다.

  - @RequestParam("username") String memberName -> request.getParameter("username")

/**
 * @RequestParam 사용
 * HTTP 파라미터 이름이 변수 이름과 같으면 @RequestParam(name="xx") 생략 가능
 */
@ResponseBody
@RequestMapping("/request-param-v3")
public String requestParamV3(
        @RequestParam String username,
        @RequestParam int age) {
    log.info("username={}, age={}", username, age);
    return "ok";
}

  - requestParamV3

  - HTTP 파라미터 이름이 변수 이름과 같으면 @RequestParam(name="xx") 생략 가능

/**
 * @RequestParam 사용
 * String, int 등의 단순 타입이면 @RequestParam 도 생략 가능
 */
@ResponseBody
@RequestMapping("/request-param-v4")
public String requestParamV4(String username, int age) {
    log.info("username={}, age={}", username, age);
    return "ok";
}

  - requestParamV4

  - String , int , Integer 등의 단순 타입이면 @RequestParam 도 생략 가능

  * @RequestParam 애노테이션을 생략하면 스프링 MVC는 내부에서 required=false 를 적용한다.

  - @RequestParam을 생략하지 않음으로써 명확하게 요청 파리미터에서 데이터를 읽는 다는 것을 표현하기도 한다.

/**
 * @RequestParam.required
 * /request-param -> username이 없으므로 예외
 *
 * 주의!
 * /request-param?username= -> 빈문자로 통과
 *
 * 주의!
 * /request-param
 * int age -> null을 int에 입력하는 것은 불가능, 따라서 Integer 변경해야 함(또는 다음에 나오는
defaultValue 사용)
 */
@ResponseBody
@RequestMapping("/request-param-required")
public String requestParamRequired(
        @RequestParam(required = true) String username,
        @RequestParam(required = false) Integer age) {
    log.info("username={}, age={}", username, age);
    return "ok";
}

  - requestParamRequired (파라미터 필수 여부)

  - @RequestParam.required : 파라미터 필수 여부. 기본값이 파라미터 필수( true )이다.

  - /request-param 요청 : username 이 없으므로 400 예외가 발생한다.

  - 파라미터 이름만 사용 : /request-param?username= 파라미터 이름만 있고 값이 없는 경우 빈문자로 통과

  - 기본형(primitive)에 null 입력 : /request-param 요청 @RequestParam(required = false) int age

  - null 을 int 에 입력하는 것은 불가능(500 예외 발생). null 을 받을 수 있는 Integer 로 변경 혹은 defaultValue 사용

/**
 * @RequestParam
 * - defaultValue 사용
 *
 * 참고: defaultValue는 빈 문자의 경우에도 적용
 * /request-param?username=
 */
@ResponseBody
@RequestMapping("/request-param-default")
public String requestParamDefault(
        @RequestParam(required = true, defaultValue = "guest") String username,
        @RequestParam(required = false, defaultValue = "-1") int age) {
    log.info("username={}, age={}", username, age);
    return "ok";
}

  - requestParamDefault (기본 값 적용)

  - 파라미터에 값이 없는 경우 defaultValue 를 사용하면 기본 값을 적용할 수 있다. 기본 값이 있기 때문에 required 는 의미가 없다

  - defaultValue 는 빈 문자의 경우에도 설정한 기본 값이 적용된다. (e.g. /request-param?username=)

/**
 * @RequestParam Map, MultiValueMap
 * Map(key=value)
 * MultiValueMap(key=[value1, value2, ...] ex) (key=userIds, value=[id1, id2])
 */
@ResponseBody
@RequestMapping("/request-param-map")
public String requestParamMap(@RequestParam Map<String, Object> paramMap) {
    log.info("username={}, age={}", paramMap.get("username"),
            paramMap.get("age"));
    return "ok";
}

  - requestParamMap (파라미터를 Map으로 조회하기)

  - 파라미터를 Map, MultiValueMap으로 조회할 수 있다

  - @RequestParam Map : Map(key=value)

  - @RequestParam MultiValueMap : MultiValueMap(key=[value1, value2, ...] ex) (key=userIds, value=[id1, id2])

  - 파라미터의 값이 1개가 확실하다면 Map 을 사용해도 되지만, 그렇지 않다면 MultiValueMap 을 사용하자.

 

 4.7 HTTP 요청 파라미터 - @ModelAttribute

  > 실제 개발을 하면 아래와 같이 요청 파라미터를 받아서 필요한 객체를 만들고 그 객체에 값을 넣어주어야 한다.

@RequestParam String username;
@RequestParam int age;
HelloData data = new HelloData();
data.setUsername(username);
data.setAge(age);

  - 스프링은 이 과정을 완전히 자동화해주는 @ModelAttribute 기능을 제공한다

package hello.springmvc.basic;
import lombok.Data;

@Data
public class HelloData {
    private String username;
    private int age;
}

  - @Data Annotation의 역할 : 롬복에서 제공해주는 @Data의 경우 @Getter , @Setter , @ToString , @EqualsAndHashCode , @RequiredArgsConstructor 를 자동으로 적용해준다.

/**
 * @ModelAttribute 사용
 * 참고: model.addAttribute(helloData) 코드도 함께 자동 적용됨, 뒤에 model을 설명할 때
자세히 설명
 */
@ResponseBody
@RequestMapping("/model-attribute-v1")
public String modelAttributeV1(@RequestParam String username, @RequestParam int age) {
    HelloData helloData = new HelloData();
    helloData.setUsername(username);
    helloData.setAge(age);
    log.info("username={}, age={}", helloData.getUsername(),
            helloData.getAge());
    log.info("hellodata={}", helloData);        //객체를 log에 찍을 경우 @Data에서 ToString으로 변환해준다.
    return "ok";
}

@ResponseBody
@RequestMapping("/model-attribute-v1")
public String modelAttributeV1(@ModelAttribute HelloData helloData) {
    //@ModelAttribute HelloData helloData의 경우 @RequestParam String username, @RequestParam int age와 동일한 역할을 한다.
    log.info("username={}, age={}", helloData.getUsername(),
            helloData.getAge());
    return "ok";
}

  - @ModelAttribute 적용

  - HelloData 객체가 생성되고, 요청 파라미터의 값도 모두 들어가 있다

  - 스프링MVC는 @ModelAttribute 가 있으면 다음을 실행한다.

  - HelloData 객체를 생성한다 -> 요청 파라미터의 이름으로 HelloData 객체의 프로퍼티를 찾는다 -> 해당 프로퍼티의 setter를 호출해서 파라미터의 값을 입력(바인딩) 한다.

  - 예) 파라미터 이름이 username 이면 setUsername() 메서드를 찾아서 호출하면서 값을 입력한다

 

  > 프로퍼티
  - 객체에 getUsername() , setUsername() 메서드가 있으면, 이 객체는 username 이라는 프로퍼티를 가지고 있다.

  - username 프로퍼티의 값을 변경하면 setUsername() 이 호출되고, 조회하면 getUsername() 이 호출된다

class HelloData {
 getUsername();
 setUsername();
}

  > 바인딩 오류

  - age=abc 처럼 숫자가 들어가야 할 곳에 문자를 넣으면 BindException 이 발생한다. 

 

/**
 * @ModelAttribute 생략 가능
 * String, int 같은 단순 타입 = @RequestParam
 * argument resolver 로 지정해둔 타입 외 = @ModelAttribute
 */
@ResponseBody
@RequestMapping("/model-attribute-v2")
public String modelAttributeV2(HelloData helloData) {
    log.info("username={}, age={}", helloData.getUsername(),
            helloData.getAge());
    return "ok";
}

  - @ModelAttribute 생략

  - @ModelAttribute 는 생략할 수 있다. 그런데 @RequestParam 도 생략할 수 있으니 혼란이 발생할 수 있다

  - 스프링은 해당 생략시 다음과 같은 규칙을 적용한다.

  - String , int , Integer 같은 단순 타입 = @RequestParam

  - 나머지 = @ModelAttribute (argument resolver 로 지정해둔 타입 외)