๐ŸŒฑ ์Šคํ”„๋ง MVC 2

yeopยท2023๋…„ 5์›” 31์ผ

โœ”๏ธ ํƒ€์ž„๋ฆฌํ”„ ๊ธฐ๋ณธ๊ธฐ๋Šฅ

์ปจํ…์ธ ์•ˆ์— ๋‚ด์šฉ ๋ฐ”๋กœ ์‚ฝ์ž…ํ•˜๊ธฐ - [[โ€ฆ]]

<ul>
  <li>th:text ์‚ฌ์šฉ <span th:text="${data}"></span></li>
  <li>์ปจํ…์ธ  ์•ˆ์—์„œ ์ง์ ‘ ์ถœ๋ ฅํ•˜๊ธฐ = [[${data}]]</li>
</ul>

์ด์Šค์ผ€์ดํ”„ - th:utext / [(โ€ฆ)]

๋ฌธ์ž๋ฅผ ํƒœ๊ทธ๋กœ ๋ณ€ํ™˜ํ•˜์ง€ ์•Š๊ณ  ๋ฌธ์ž ๊ทธ๋Œ€๋กœ ์ถœ๋ ฅ

  • th: text : ์ด์Šค์ผ€์ดํ”„ VS th: utext: ์–ธ์ด์Šค์ผ€์ดํ”„
  • [[โ€ฆ]] : ์ด์Šค์ผ€์ดํ”„ VS [(โ€ฆ)] : ์–ธ์ด์Šค์ผ€์ดํ”„

โ€ป ์ด์Šค์ผ€์ดํ”„๋ฅผ ๊ธฐ๋ณธ์œผ๋กœ ํ•˜๊ณ  ๊ผญ ํ•„์š”ํ•  ๋•Œ๋งŒ ์–ธ์ด์Šค์ผ€์ดํ”„ ์‚ฌ์šฉ

์ง€์—ญ๋ณ€์ˆ˜ - th:with

<div th:with="first=${users[0]}">
  <p>์ฒ˜์Œ ์‚ฌ๋žŒ์˜ ์ด๋ฆ„์€ <span th:text="${first.username}"></span></p>
</div>

Param, Session, Spring Bean

  • Thymeleaf ์—์„œ ์ง€์›ํ•˜๋Š” ์ด๋ฆ„์œผ๋กœ ๋ฐ”๋กœ ๊บผ๋‚ด์“ธ ์ˆ˜ ์žˆ๋‹ค.
<li>Request Parameter = <span th:text="${param.paramData}"></span></li>
<li>session = <span th:text="${session.sessionData}"></span></li>
<li>spring bean = <span th:text="${@helloBean.hello('Spring!')}"></span></li>

URL

<li><a th:href="@{/hello}">basic url</a></li>
<li><a th:href="@{/hello(param1=${param1}, param2=${param2})}">hello query param</a></li>
<li><a th:href="@{/hello/{param1}/{param2}(param1=${param1}, param2=${param2})}">path variable</a></li>
<li><a th:href="@{/hello/{param1}(param1=${param1}, param2=${param2})}">path variable + query parameter</a></li>

๋ฆฌํ„ฐ๋Ÿด - ์ž‘์€ ๋”ฐ์˜ดํ‘œ

  • ์†Œ์Šค์ฝ”๋“œ ์ƒ์˜ ๊ณ ์ •๋œ ๊ฐ’์„ ๋งํ•˜๋Š” ์šฉ์–ด
  • ํƒ€์ž„๋ฆฌํ”„์—์„œ ๋ฆฌํ„ฐ๋Ÿด์€ ์ž‘์€ ๋”ฐ์˜ดํ‘œ๋กœ ๊ฐ์‹ธ์•ผํ•œ๋‹ค.
<li>'hello' + ' world!' = <span th:text="'hello' + ' world!'"></span></li>
 <li>'hello world!' = <span th:text="'hello world!'"></span></li>
 <li>'hello ' + ${data} = <span th:text="'hello ' + ${data}"></span></li>
 <li>๋ฆฌํ„ฐ๋Ÿด ๋Œ€์ฒด |hello ${data}| = <span th:text="|hello ${data}|"></span></li>

์—ฐ์‚ฐ - No-Operation

  • ์ฝ”๋“œ
    <body>
    <ul>
      <li>์‚ฐ์ˆ  ์—ฐ์‚ฐ
        <ul>
          <li>10 + 2 = <span th:text="10 + 2"></span></li>
          <li>10 % 2 == 0 = <span th:text="10 % 2 == 0"></span></li>
        </ul>
      </li>
      <li>๋น„๊ต ์—ฐ์‚ฐ
        <ul>
          <li>1 > 10 = <span th:text="1 &gt; 10"></span></li>
          <li>1 gt 10 = <span th:text="1 gt 10"></span></li>
          <li>1 >= 10 = <span th:text="1 >= 10"></span></li>
          <li>1 ge 10 = <span th:text="1 ge 10"></span></li>
          <li>1 == 10 = <span th:text="1 == 10"></span></li>
          <li>1 != 10 = <span th:text="1 != 10"></span></li>
        </ul>
      </li>
      <li>์กฐ๊ฑด์‹
        <ul>
          <li>(10 % 2 == 0)? '์ง์ˆ˜':'ํ™€์ˆ˜' = <span th:text="(10 % 2 == 0)?
    '์ง์ˆ˜':'ํ™€์ˆ˜'"></span></li>
        </ul>
      </li>
      <li>Elvis ์—ฐ์‚ฐ์ž
        <ul>
          <li>${data}?: '๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.' = <span th:text="${data}?: '๋ฐ์ดํ„ฐ๊ฐ€์—†์Šต๋‹ˆ๋‹ค.'"></span></li>
          <li>${nullData}?: '๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.' = <span th:text="${nullData}?: '๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.'"></span></li>
        </ul>
      </li>
      <li>No-Operation
        <ul>
          <li>${data}?: _ = <span th:text="${data}?: _">๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.</span></li>
          <li>${nullData}?: _ = <span th:text="${nullData}?: _">๋ฐ์ดํ„ฐ๊ฐ€์—†์Šต๋‹ˆ๋‹ค.</span></li>
        </ul>
      </li>
    </ul>
    </body>
  • No-Operation
    • _ ์ธ ๊ฒฝ์šฐ ๋งˆ์น˜ ํƒ€์ž„๋ฆฌํ”„๊ฐ€ ์‹คํ–‰๋˜์ง€ ์•Š๋Š” ๊ฒƒ ์ฒ˜๋Ÿผ ๋™์ž‘ํ•œ๋‹ค. ์ด๊ฒƒ์„ ์ž˜ ์‚ฌ์šฉํ•˜๋ฉด HTML
      ์˜ ๋‚ด์šฉ ๊ทธ๋Œ€๋กœ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค. ๋งˆ์ง€๋ง‰ ์˜ˆ๋ฅผ ๋ณด๋ฉด ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ๋ถ€๋ถ„์ด ๊ทธ๋Œ€๋กœ ์ถœ๋ ฅ๋œ๋‹ค

์†์„ฑ ๊ฐ’ ์„ค์ • - ์ถ”๊ฐ€, Checked

  • ์†์„ฑ ์ถ”๊ฐ€
    - th:attrappend = <input type="text" class="text" th:attrappend="class='large'" /><br/>
    - th:attrprepend = <input type="text" class="text" th:attrprepend="class='large '" /><br/>
    - th:classappend = <input type="text" class="text" th:classappend="large" /><br/>
  • Checked ์†์„ฑ
    • ๊ธฐ์กด์˜ checked๋Š” false๋กœ ์„ค์ •ํ•ด๋„ ๋ฌด์กฐ๊ฑด ์ฒดํฌ๊ฐ€ ๋˜์–ด์ ธ์„œ ๋ Œ๋”๋ง๋œ๋‹ค. ํ•˜์ง€๋งŒ th:ckecked๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด false๋กœ ์„ค์ •ํ•  ๊ฒฝ์šฐ checked ์„ค์ • ์ž์ฒด๋ฅผ ์—†์• ์ค€๋‹ค.

    • checked o

    • checked x

๋ฐ˜๋ณต - th:each , ___Stat

  • ___Stat
    • index : 0๋ถ€ํ„ฐ ์‹œ์ž‘ํ•˜๋Š” ๊ฐ’

    • count : 1๋ถ€ํ„ฐ ์‹œ์ž‘ํ•˜๋Š” ๊ฐ’

    • size : ์ „์ฒด ์‚ฌ์ด์ฆˆ

    • even , odd : ํ™€์ˆ˜, ์ง์ˆ˜ ์—ฌ๋ถ€( boolean )

    • first , last :์ฒ˜์Œ, ๋งˆ์ง€๋ง‰ ์—ฌ๋ถ€( boolean )

    • current : ํ˜„์žฌ ๊ฐ์ฒด

      <tr th:each="user, userStat : ${users}">
       <td th:text="${userStat.count}">username</td>
       <td th:text="${user.username}">username</td>
       <td th:text="${user.age}">0</td>
       <td>
       index = <span th:text="${userStat.index}"></span>
       count = <span th:text="${userStat.count}"></span>
       size = <span th:text="${userStat.size}"></span>
       even? = <span th:text="${userStat.even}"></span>
       odd? = <span th:text="${userStat.odd}"></span>
       first? = <span th:text="${userStat.first}"></span>
       last? = <span th:text="${userStat.last}"></span>
       current = <span th:text="${userStat.current}"></span>
       </td>
       </tr>

์กฐ๊ฑด๋ถ€ - if, unless, switch/case

  • ์กฐ๊ฑด์„ ๋งŒ์กฑํ•˜์ง€ ๋ชปํ•  ์‹œ ํ•ด๋‹น ํƒœ๊ทธ ์ „์ฒด๊ฐ€ ์‚ญ์ œ๋œ๋‹ค.
    <span th:text="'๋ฏธ์„ฑ๋…„์ž'" th:if="${user.age lt 20}"></span>
    <span th:text="'๋ฏธ์„ฑ๋…„์ž'" th:unless="${user.age ge 20}"></span>
    
    <td th:switch="${user.age}">
    	 <span th:case="10">10์‚ด</span>
    	 <span th:case="20">20์‚ด</span>
    	 <span th:case="*">๊ธฐํƒ€</span>
     </td>

์ฃผ์„

  • HTML ์ฃผ์„
    
    <!--
    <span th:text="${data}">html data</span>
    -->
  • ํƒ€์ž„๋ฆฌํ”„ ํŒŒ์„œ ์ฃผ์„
    <!--/* [[${data}]] */-->
    <!--/*-->
    <span th:text="${data}">html data</span>
    <!--*/-->
  • ํƒ€์ž„๋ฆฌํ”„ ํ”„๋กœํ† ํƒ€์ž… ์ฃผ์„ - ํŒŒ์ผ ์ž์ฒด๋ฅผ ์—ด๋•Œ๋Š” ์ฃผ์„์ฒ˜๋ฆฌ, ํƒ€์ž„๋ฆฌํ”„๋ฅผ ์‚ฌ์šฉํ•ด์„œ ๋ Œ๋”๋ง๋˜๋Š” ๊ฒฝ์šฐ์—๋Š” ์ฃผ์„ ์ฒ˜๋ฆฌ X
    <!--/*/
    <span th:text="${data}">html data</span>
    /*/-->

๋ธ”๋ก - ํƒ€์ž„๋ฆฌํ”„ ์ž์ฒด ํƒœ๊ทธ <th:block>

์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ์ธ๋ผ์ธ

  • ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ์ธ๋ผ์ธ ์‚ฌ์šฉ์ „
    • <script>~~~</script>
  • ์‚ฌ์šฉ
    • <script th:inline=โ€javascriptโ€>~~~</script>

ํ…œํ”Œ๋ฆฟ ์กฐ๊ฐ (์ปดํฌ๋„ŒํŠธ)

  • ์ปดํฌ๋„ŒํŠธ ์„ ์–ธ ์ฝ”๋“œ
    <footer th:fragment="copyParam (param1, param2)">
     <p>ํŒŒ๋ผ๋ฏธํ„ฐ ์ž๋ฆฌ ์ž…๋‹ˆ๋‹ค.</p>
     <p th:text="${param1}"></p>
     <p th:text="${param2}"></p>
    </footer>
  • ์ปดํฌ๋„ŒํŠธ ์‚ฌ์šฉ ์ฝ”๋“œ
    • replace ๋Œ€์‹  insert ์‚ฌ์šฉ ์‹œ div ํƒœ๊ทธ ๋‚ด๋ถ€๋กœ ์‚ฝ์ž…

      <h1>ํŒŒ๋ผ๋ฏธํ„ฐ ์‚ฌ์šฉ</h1>
      <div th:replace="~{template/fragment/footer :: copyParam ('๋ฐ์ดํ„ฐ1', '๋ฐ์ดํ„ฐ2')}"></div>

ํ…œํ”Œ๋ฆฟ ๋ ˆ์ด์•„์›ƒ

  • ๋ ˆ์ด์•„์›ƒ ์„ ์–ธ ์ฝ”๋“œ
    <html xmlns:th="http://www.thymeleaf.org">
    <head th:fragment="common_header(title,links)">
     <title th:replace="${title}">๋ ˆ์ด์•„์›ƒ ํƒ€์ดํ‹€</title>
     <!-- ๊ณตํ†ต -->
     <link rel="stylesheet" type="text/css" media="all" th:href="@{/css/
    awesomeapp.css}">
     <link rel="shortcut icon" th:href="@{/images/favicon.ico}">
     <script type="text/javascript" th:src="@{/sh/scripts/codebase.js}"></
    script>
     <!-- ์ถ”๊ฐ€ -->
     <th:block th:replace="${links}" />
    </head>
  • ๋ ˆ์ด์•„์›ƒ ์‚ฌ์šฉ ์ฝ”๋“œ
    <!DOCTYPE html>
    <html xmlns:th="http://www.thymeleaf.org">
    <head th:replace="template/layout/base :: common_header(~{::title},~{::link})">
     <title>๋ฉ”์ธ ํƒ€์ดํ‹€</title>
     <link rel="stylesheet" th:href="@{/css/bootstrap.min.css}">
     <link rel="stylesheet" th:href="@{/themes/smoothness/jquery-ui.css}">
    </head>
    <body>
    ๋ฉ”์ธ ์ปจํ…์ธ 
    </body>
    </html>
  • common_header(~{::title},~{::link}) ์ด ๋ถ€๋ถ„์ด ํ•ต์‹ฌ์ด๋‹ค.
    • ::title ์€ ํ˜„์žฌ ํŽ˜์ด์ง€์˜ title ํƒœ๊ทธ๋“ค์„ ์ „๋‹ฌํ•œ๋‹ค.
    • ::link ๋Š” ํ˜„์žฌ ํŽ˜์ด์ง€์˜ link ํƒœ๊ทธ๋“ค์„ ์ „๋‹ฌํ•œ๋‹ค.

โœ”๏ธ ํƒ€์ž„๋ฆฌํ”„ - ์Šคํ”„๋ง ํ†ตํ•ฉ๊ณผ ํผ

์ž…๋ ฅ ํผ ์ฒ˜๋ฆฌ

th:object

  • ์ปค๋งจ๋“œ ๊ฐ์ฒด๋ฅผ ์ง€์ •ํ•œ๋‹ค
  • *{...} : ์„ ํƒ ๋ณ€์ˆ˜์‹์ด๋ผ๊ณ  ํ•˜๋ฉฐ th:object ์—์„œ ์„ ํƒํ•œ ๊ฐ์ฒด์— ์ ‘๊ทผํ•œ๋‹ค.
    • *{itemName} == ${item.itemName}

th:field

  • HTML ํƒœ๊ทธ์˜ Id, name, value ์†์„ฑ์„ ์ž๋™์œผ๋กœ ์ฒ˜๋ฆฌํ•ด์ค€๋‹ค.

์ฒดํฌ๋ฐ•์Šค

๋‹จ์ผ1 - ํžˆ๋“  ํ•„๋“œ ์ถ”๊ฐ€

  • ํƒ€์ž„๋ฆฌํ”„๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ  HTML ํƒœ๊ทธ๋งŒ์„ ์‚ฌ์šฉํ•ด์„œ ์ฒดํฌ๋ฐ•์Šค๋ฅผ ๊ตฌํ˜„ํ•˜๋ฉด ์ฒดํฌ๋ฅผ ํ–ˆ์„ ๋•Œ๋Š” True๊ฐ’์ด ์ •์ƒ์ ์œผ๋กœ ๋„˜์–ด์˜ค์ง€๋งŒ ์ฒดํฌ๋ฅผ ํ•˜์ง€ ์•Š์•˜์„ ๊ฒฝ์šฐ์—๋Š” ์•„๋ฌด๊ฒƒ๋„ ๋„˜์–ด์˜ค์ง€ ์•Š์•„ Null๊ฐ’์œผ๋กœ ์ฒ˜๋ฆฌ๋œ๋‹ค.
  • ํ•ด๊ฒฐ๋ฐฉ๋ฒ•
    • ํžˆ๋“  ํ•„๋“œ ์ถ”๊ฐ€
      • <input type="hidden" name="_open" value="on">
      • ์œ„์™€ ๊ฐ™์ด ํžˆ๋“  ํ•„๋“œ๋ฅผ ์ถ”๊ฐ€ํ•˜๋ฉด ์ฒดํฌ๋ฅผ ํ•˜์ง€ ์•Š์•˜์„ ๊ฒฝ์šฐ False ๊ฐ’์ด ๋„˜์–ด์˜ค๊ฒŒ ๋œ๋‹ค.

๋‹จ์ผ2 - th:field

  • th:field๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์ฒดํฌ๋ฐ•์Šค์˜ ํžˆ๋“ ํ•„๋“œ ๋ถ€๋ถ„์„ ์ž๋™์œผ๋กœ ์ƒ์„ฑํ•ด์„œ ์ฒ˜๋ฆฌํ•ด์ค€๋‹ค.
  • ์ฒดํฌ๋ฐ•์Šค์—์„œ ์ฒดํฌ๋ฅผ ํ•ด์„œ ์ €์žฅํ•œ ํ›„ ๊ฐ’์„ ๋ถˆ๋Ÿฌ์™€์„œ ์‚ฌ์šฉํ•  ๋•Œ๋Š” ๊ฐ’์ด True์ด๋ฉด Checked ์†์„ฑ์„ ์‚ฝ์ž…ํ•ด์ฃผ๋Š” ๊ฒƒ์„ ๊ฐœ๋ฐœ์ž๊ฐ€ ์ง์ ‘ ์ฒ˜๋ฆฌํ•ด์•ผํ•œ๋‹ค.
    • ํ•˜์ง€๋งŒ ํƒ€์ž„๋ฆฌํ”„์˜ th:field๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๊ฐ’์ด True์ธ ๊ฒฝ์šฐ ์ฒดํฌ๋ฅผ ์ž๋™์œผ๋กœ ์ฒ˜๋ฆฌํ•ด์ค€๋‹ค.

๋ฉ€ํ‹ฐ

<!-- multi checkbox -->
        <div>
            <div>๋“ฑ๋ก ์ง€์—ญ</div>
            <div th:each="region : ${regions}" class="form-check form-check-inline">
                <input type="checkbox" th:field="*{regions}" th:value="${region.key}"
                       class="form-check-input">
                <label th:for="${#ids.prev('regions')}"
                       th:text="${region.value}" class="form-check-label">์„œ์šธ</label>
            </div>
        </div>
  • th:for="${#ids.prev('regions')}"
    • each ๋ฃจํ”„ ์•ˆ์—์„œ id๊ฐ’ ์ถ”์ ํ•˜๋Š” ๋ฐฉ๋ฒ•

    • each ๋ฃจํ”„ ์•ˆ์—์„œ ํƒ€์ž„๋ฆฌํ”„๋Š” field๊ฐ’์œผ๋กœ ๋„ฃ์€ ๊ฐ’ ๋’ค์— ์ˆซ์ž๋ฅผ ๋ถ™์—ฌ ์ž„์˜๋กœ id๋ฅผ ์„ ์–ธํ•œ๋‹ค.

      @ModelAttribute์˜ ํŠน๋ณ„ํ•œ ์‚ฌ์šฉ๋ฒ•

    • ๋“ฑ๋กํผ, ์ƒ์„ธํ™”๋ฉด, ์ˆ˜์ •ํผ์—์„œ ๋ชจ๋‘ ์ฒดํฌ๋ฐ•์Šค๋ฅผ ๋ณด์—ฌ์ฃผ๊ธฐ ์œ„ํ•ด์„œ๋Š” ๊ฐ๊ฐ์˜ ์ปจํŠธ๋กค๋Ÿฌ์—์„œ model.addAttribute(...) ๋ฅผ ์‚ฌ์šฉํ•ด์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ˜๋ณตํ•ด์„œ ๋„ฃ์–ด์ฃผ์–ด์•ผํ•œ๋‹ค.

      		@ModelAttribute("regions")
          public Map<String, String> regions(){
              Map<String, String> regions = new LinkedHashMap<>();
              regions.put("SEOUL", "์„œ์šธ");
              regions.put("BUSAN", "๋ถ€์‚ฐ");
              regions.put("JEJU", "์ œ์ฃผ");
              return regions;
          }
      • ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ์ปจํŠธ๋กค๋Ÿฌ๋ฅผ ์š”์ฒญํ•  ๋•Œ regions์—์„œ ๋ฐ˜ํ™˜ํ•œ ๊ฐ’์ด ์ž๋™์œผ๋กœ model์— ๋‹ด๊ธฐ๊ฒŒ ๋œ๋‹ค.

๋ผ๋””์˜ค ๋ฒ„ํŠผ

<!-- radio button -->
        <div>
            <div>์ƒํ’ˆ ์ข…๋ฅ˜</div>
            <div th:each="type : ${itemTypes}" class="form-check form-check-inline">
                <input type="radio" th:field="*{itemType}" th:value="${type.name()}"
                       class="form-check-input">
                <label th:for="${#ids.prev('itemType')}" th:text="${type.description}"
                       class="form-check-label">
                    BOOK
                </label>
            </div>
        </div>

ํƒ€์ž„๋ฆฌํ”„์—์„œ ENUM ์ง์ ‘ ์ ‘๊ทผ

  • ํ˜„์žฌ ์ƒํƒœ: @ModelAttribute๋ฅผ ํ†ตํ•ด์„œ ๋ชจ๋ธ์— ENUM์„ ๋‹ด์•„ ์ „๋‹ฌ
  • ์ง์ ‘ ์ ‘๊ทผ
    • <div th:each="type: ${T(hello.itemservice.domain.item.ItemType).values()}>

์…€๋ ‰ํŠธ ๋ฐ•์Šค

<!-- SELECT -->
    <div>
        <div>๋ฐฐ์†ก ๋ฐฉ์‹</div>
        <select th:field="${item.deliveryCode}" class="form-select">
            <option value="">==๋ฐฐ์†ก ๋ฐฉ์‹ ์„ ํƒ==</option>
            <option th:each="deliveryCode : ${deliveryCodes}" th:value="${deliveryCode.code}" disabled
                    th:text="${deliveryCode.displayName}">FAST</option>
        </select>
    </div>

โœ”๏ธ ๋ฉ”์‹œ์ง€, ๊ตญ์ œํ™”

๋ฉ”์‹œ์ง€

  • ๋ชจ๋“  ํŒŒ์ผ์—์„œ โ€˜์ƒํ’ˆ๋ช…โ€™์ด๋ผ๋Š” ๋‹จ์–ด๋ฅผ ๋ชจ๋‘ โ€˜์ƒํ’ˆ์ด๋ฆ„โ€™์œผ๋กœ ๊ณ ์ณ์•ผ ํ•  ๊ฒจ์šฐ ๋ชจ๋“ ํ™”๋ฉด์„ ์ฐพ์•„๊ฐ€๋ฉด์„œ ๋ณ€๊ฒฝํ•˜๊ธฐ์—๋Š” ๋„ˆ๋ฌด ์˜ค๋žœ ์‹œ๊ฐ„์ด ๊ฑธ๋ฆฐ๋‹ค.
  • ์ด๋Ÿฐ ๋‹ค์–‘ํ•œ ๋ฉ”์‹œ์ง€๋ฅผ ํ•œ ๊ณณ(๋ณ„๋„์˜ ํŒŒ์ผ)์—์„œ ๊ด€๋ฆฌํ•˜๋„๋ก ํ•˜๋Š” ๊ธฐ๋Šฅ์„ ๋ฉ”์‹œ์ง€ ๊ธฐ๋Šฅ์ด๋ผ๊ณ  ํ•œ๋‹ค.

๊ตญ์ œํ™”

  • ์˜์–ด๊ถŒ ๊ตญ๊ฐ€์—์„œ ๋“ค์–ด์˜ค๋Š” ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ๋Š” ์˜์–ด, ํ•œ๊ตญ์—์„œ ๋“ค์–ด์˜ค๋ฉด ํ•œ๊ตญ์–ด๋ฅผ ์ œ๊ณต
  • ๋ฉ”์‹œ์ง€ ํŒŒ์ผ์„ ์–ธ์–ด๋ณ„๋กœ ์ƒ์„ฑํ•ด์„œ ๊ตญ์ œํ™”ํ•˜๋‹ค.
  • ์ธ์‹ ๋ฐฉ๋ฒ•
    • HTTP accpt-language ํ—ค๋” ๊ฐ’์„ ์‚ฌ์šฉ
    • ์‚ฌ์šฉ์ž๊ฐ€ ์ง์ ‘ ์–ธ์–ด๋ฅผ ์„ ํƒ โ†’ ์ฟ ํ‚ค ์ฒ˜๋ฆฌ

์Šคํ”„๋ง์ด ์ œ๊ณตํ•˜๋Š” ๋ฉ”์‹œ์ง€, ๊ตญ์ œํ™”

ํ…Œ์ŠคํŠธ ์‚ฌ์šฉ

  • resources ์•ˆ์— message.properties โ†’ ๋””ํดํŠธ ํŒŒ์ผ message_en.properties โ†’ Locale.ENGLISH
  • ์ฝ”๋“œ
    @SpringBootTest
    public class MessageSourceTest {
    
        @Autowired
        MessageSource ms;
    
        @Test
        void helloMessage(){
            String result = ms.getMessage("hello", null, null);
    assertThat(result).isEqualTo("์•ˆ๋…•");
        }
    
        @Test
        void notFoundMessageCode(){
    assertThatThrownBy(() -> ms.getMessage("no_code", null, null))
                    .isInstanceOf(NoSuchMessageException.class);
            System.out.println(Locale.getDefault());
        }
    
        @Test
        void notFoundMessageCodeDefaultMessage(){
            String result = ms.getMessage("no_code", null, "๊ธฐ๋ณธ ๋ฉ”์‹œ์ง€", null);
    assertThat(result).isEqualTo("๊ธฐ๋ณธ ๋ฉ”์‹œ์ง€");
        }
    
        @Test
        void argumentMessage(){
            String result = ms.getMessage("hello.name", new Object[]{"Spring"}, null);
    assertThat(result).isEqualTo("์•ˆ๋…• Spring");
        }
    
        @Test
        void defaultLang(){
    assertThat(ms.getMessage("hello", null, null)).isEqualTo("์•ˆ๋…•");
    assertThat(ms.getMessage("hello", null, Locale.KOREA)).isEqualTo("์•ˆ๋…•");
        }
    
        @Test
        void enLang(){
    assertThat(ms.getMessage("hello", null, Locale.ENGLISH)).isEqualTo("hello");
        }
    
    }

์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ ์šฉ

  • ๋ฉ”์‹œ์ง€
    • <div th:text="#{label.item}"></h2>
    • ์ธ์ž: <p th:text="#{hello.name(${item.itemName})}"></p>
  • ๊ตญ์ œํ™”
    • ๊ฐ locale์— ๋งž๋Š” properties ํŒŒ์ผ์ด ์žˆ๋‹ค๋ฉด ์Šคํ”„๋ง์ด Accept-language ํ—ค๋”๊ฐ’์„ ์‚ฌ์šฉํ•ด์„œ ์ž๋™์œผ๋กœ ์ ์šฉ์‹œ์ผœ์ค€๋‹ค.
    • LocaleResolver
      • ๋งŒ์•ฝ Locale ์„ ํƒ ๋ฐฉ์‹์„ ๋ณ€๊ฒฝํ•˜๋ ค๋ฉด LocaleResolver์˜ ๊ตฌํ˜„์ฒด๋ฅผ ๋ณ€๊ฒฝํ•ด์„œ ์ฟ ํ‚ค๋‚˜ ์„ธ์…˜ ๊ธฐ๋ฐ˜์˜ Locale ์„ ํƒ ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋”ฐ.

โœ”๏ธ Validation

V1 - Error Map

  • ์ปจํŠธ๋กค๋Ÿฌ ์ฝ”๋“œ
    • ModelAttribute Annotation์œผ๋กœ ์ธํ•ด item์ด ์ž๋™์œผ๋กœ model์— ๋“ฑ๋ก๋˜๊ธฐ ๋•Œ๋ฌธ์— return์œผ๋กœ ๋ฆฌ๋ Œ๋”๋ง ๋˜์–ด๋„ ๊ธฐ์กด์— ์ž…๋ ฅํ–ˆ๋˜ ๊ฐ’์ด ๊ทธ๋Œ€๋กœ ์œ ์ง€๋œ๋‹ค.

      @PostMapping("/add")
          public String addItem(@ModelAttribute Item item, RedirectAttributes redirectAttributes, Model model) {
      
              Map<String, String> errors = new HashMap<>();
      
              if (!StringUtils.hasText(item.getItemName())){
                  errors.put("itemName", "์ƒํ’ˆ ์ด๋ฆ„์€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.");
              }
              if (item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1000000){
                  errors.put("price", "๊ฐ€๊ฒฉ์€ 1,000 ~ 1,000,000 ๊นŒ์ง€ ํ—ˆ์šฉํ•ฉ๋‹ˆ๋‹ค");
              }
              if(item.getQuantity() == null || item.getQuantity() > 9999){
                  errors.put("quantity", "์ˆ˜๋Ÿ‰์€ 9,999๊ฐœ๊นŒ์ง€ ํ—ˆ์šฉ๋ฉ๋‹ˆ๋‹ค.");
              }
              if(item.getPrice() != null && item.getQuantity() != null){
                  int resultPrice = item.getPrice() * item.getQuantity();
                  if (resultPrice < 10000) {
                      errors.put("globalError", "๊ฐ€๊ฒฉ * ์ˆ˜๋Ÿ‰์˜ ํ•ฉ์€ 10,000์› ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ํ˜„์žฌ ๊ฐ’ = " + resultPrice);
                  }
              }
      
              if(!errors.isEmpty()){
                  model.addAttribute("errors", errors);
                  return "validation/v1/addForm";
              }
      
              Item savedItem = itemRepository.save(item);
              redirectAttributes.addAttribute("itemId", savedItem.getId());
              redirectAttributes.addAttribute("status", true);
              return "redirect:/validation/v1/items/{itemId}";
          }
  • HTML ์ฝ”๋“œ
    • errors ๋‹ค์Œ์— ์žˆ๋Š” ? ์—ฐ์‚ฐ์ž
      - ?๋ฅผ ๋ถ™์ด์ง€ ์•Š๊ณ  ์‹คํ–‰ํ•  ๋•Œ errors๊ฐ€ null๊ฐ’์ด๋ผ๋ฉด null๊ฐ’์˜ ์š”์†Œ๋ฅผ ์ฐพ๋Š” ๊ฒƒ์ด๊ธฐ ๋•Œ๋ฌธ์— NullpointException ์˜ค๋ฅ˜๊ฐ€ ๋‚œ๋‹ค.
      - ํ•˜์ง€๋งŒ ?๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด errors๊ฐ€ null๊ฐ’์ด๋ผ๋ฉด ๋ฐ”๋กœ null์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.

      <form action="item.html" th:action th:object="${item}" method="post">
      
              <div th:if="${errors?.containsKey('globalError')}">
                  <p th:class="field-error" th:text="${errors['globalError']}">์ „์ฒด ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€</p>
              </div>
      
              <div>
                  <label for="itemName" th:text="#{label.item.itemName}">์ƒํ’ˆ๋ช…</label>
                  <input type="text" id="itemName" th:field="*{itemName}" class="form-control" th:classappend="${errors?.containsKey('itemName')} ? 'field-error' : _" placeholder="์ด๋ฆ„์„ ์ž…๋ ฅํ•˜์„ธ์š”">
                  <div th:class="field-error" th:if="${errors?.containsKey('itemName')}" th:text="${errors['itemName']}">์ƒํ’ˆ๋ช… ์˜ค๋ฅ˜</div>
              </div>
  • ๋‚จ์€ ๋ฌธ์ œ์ 
    • ๋ทฐ์— ์ค‘๋ณต๋˜๋Š” ์ฝ”๋“œ๊ฐ€ ๋งŽ๋‹ค
    • ํƒ€์ž… ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ๊ฐ€ ์•ˆ๋œ๋‹ค.

V2 - BindingResult

// FieldError
bindingResult.addError(new FieldError("item", "price", item.getPrice(), false, null, null, "๊ฐ€๊ฒฉ์€ 1,000 ~ 1,000,000 ๊นŒ์ง€ ํ—ˆ์šฉํ•ฉ๋‹ˆ๋‹ค"));
// ObjectError
bindingResult.addError(new ObjectError("item", null, null,"๊ฐ€๊ฒฉ * ์ˆ˜๋Ÿ‰์˜ ํ•ฉ์€ 10,000์› ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ํ˜„์žฌ ๊ฐ’ = " + resultPrice));

BindingResult

  • ์ธ์ž์— BindingResult bindingResult ์ถ”๊ฐ€
    • @ModelAttribute ๋‹ค์Œ์— ์œ„์น˜ํ•ด์•ผํ•œ๋‹ค.
    • Model์— ์ž๋™์œผ๋กœ ํฌํ•จ๋œ๋‹ค.

FieldError

bindingResult.addError(new FieldError(objectName(ModelAttribute ์ธ์Šคํ„ด์Šค ์ด๋ฆ„)
, field, rejectedValue, bindingFailure(ํƒ€์ž… ์˜ค๋ฅ˜๊ฐ™์€ ๋ฐ”์ธ๋”ฉ ์‹คํŒจ์ธ์ง€, ๊ฒ€์ฆ ์‹คํŒจ์ธ์ง€ ๊ตฌ๋ถ„ ๊ฐ’),
 codes(๋ฉ”์‹œ์ง€ ์ฝ”๋“œ), arguments(๋ฉ”์‹œ์ง€์— ์‚ฌ์šฉํ•˜๋Š” ์ธ์ž) defaultMessage)
  • FieldError๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ ์ž˜๋ชป๋œ ๊ฐ’์„ ์ž…๋ ฅํ•˜๋ฉด ์ž…๋ ฅ๊ฐ’์ด ์œ ์ง€๋˜์ง€ ์•Š๋Š” ์ด์œ 
    • ์‚ฌ์šฉ์ž์˜ ๋ฐ์ดํ„ฐ๊ฐ€ ์ปจํŠธ๋กค๋Ÿฌ์˜ @ModelAttribute์— ๋ฐ”์ธ๋”ฉ๋˜๋Š” ์‹œ์ ์— ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด ๋ชจ๋ธ ๊ฐ์ฒด์— ์‚ฌ์šฉ์ž ์ž…๋ ฅ ๊ฐ’์„ ์œ ์ง€ํ•˜๊ธฐ ์–ด๋ ต๋‹ค.
    • ์˜ˆ๋ฅผ ๋“ค์–ด์„œ ๊ฐ€๊ฒฉ์— ์ˆซ์ž๊ฐ€ ์•„๋‹Œ ๋ฌธ์ž๊ฐ€ ์ž…๋ ฅ๋œ๋‹ค๋ฉด ๊ฐ€๊ฒฉ์€ Integer ํƒ€์ž…์ด๋ฏ€๋กœ ๋ฌธ์ž๋ฅผ ๋ณด๊ด€ํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐฉ๋ฒ•์ด ์—†๋‹ค.
    • ๊ทธ๋ž˜์„œ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•œ ๊ฒฝ์šฐ ์‚ฌ์šฉ์ž ์ž…๋ ฅ ๊ฐ’์„ ๋ณด๊ด€ํ•˜๋Š” ๋ณ„๋„์˜ ๋ฐฉ๋ฒ•์ด ํ•„์š”ํ•œ๋‹ค.
    • FieldError๊ฐ€ rejectedValue ์†์„ฑ์„ ์‚ฌ์šฉํ•ด ํ•ด๋‹น ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•œ๋‹ค.
      • ์ด ๋•Œ ํƒ€์ž„๋ฆฌํ”„์˜ th:field๋Š” ์ •์ƒ ์ƒํ™ฉ์—์„œ๋Š” ๋ชจ๋ธ ๊ฐ์ฒด์˜ ๊ฐ’์„ ์‚ฌ์šฉํ•˜๊ณ , ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด FieldError์—์„œ ๋ณด๊ด€ํ•œ ๊ฐ’์„ ์‚ฌ์šฉํ•ด์„œ ๊ฐ’์„ ์ถœ๋ ฅํ•จ์œผ๋กœ์จ ์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•œ๋‹ค.
  • GlobalError
    • bindingResult.addError(new ObjectError(objectName, defaultMessage)

ํƒ€์ž„๋ฆฌํ”„

  • #fields
    • #fields.hasGlobalError()
    • BindingResult๊ฐ€ ์ œ๊ณตํ•˜๋Š” ๊ฒ€์ฆ ์˜ค๋ฅ˜๊ฐ€ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋‹ค.
  • th:errors
    • ํ•ด๋‹น ํ•„๋“œ์— ์˜ค๋ฅ˜๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ ํƒœ๊ทธ ์ถœ๋ ฅ
  • th:errorclass
    • th:field์—์„œ ์ง€์ •ํ•œ ํ•„๋“œ์— ์˜ค๋ฅ˜๊ฐ€ ์žˆ์œผ๋ฉด class์ •๋ณด๋ฅผ ์ถ”๊ฐ€ํ•œ๋‹ค.

โ€ป @ModelAttribute์— ๋ฐ”์ธ๋”ฉ ์‹œ ํƒ€์ž… ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด?

  • ์˜ค๋ฅ˜์ •๋ณด(FieldError)๋ฅผ BindingResult์— ๋‹ด์•„์„œ ์ปจํŠธ๋กค๋Ÿฌ๋ฅผ ์ •์ƒ ํ˜ธ์ถœํ•œ๋‹ค.

V3 - errors.properties

  • errors.properties ํŒŒ์ผ์„ ์ƒ์„ฑํ•˜๊ณ  application.properties ํŒŒ์ผ์— spring.messages.basename=messages,errors ๋ฅผ ์ถ”๊ฐ€ํ•œ๋‹ค.
  • errors.properties ํŒŒ์ผ์— ์žˆ๋Š” ๊ฐ’์— ๋”ฐ๋ผ ๊ฐ๊ฐ code, argument์— ๊ฐ’์„ ๋Œ€์ž…ํ•œ๋‹ค.
// FieldError
bindingResult.addError(new FieldError("item", "itemName", item.getItemName(), false, new String[]{"required.item.itemName"}, null, null));
// ObjectError
bindingResult.addError(new ObjectError("item", new String[]{"totalPriceMin"}, new Object[]{10000, resultPrice}, null));

V4 - reject, rejectValue

// FieldError , rejectValue
bindingResult.rejectValue("price", "range", new Object[]{1000, 1000000}, null);
// ObjectError, reject
bindingResult.reject("totalPriceMin", new Object[]{10000, resultPrice}, null);
  • FieldError, ObjectError๋Š” ๋‹ค๋ฃจ๊ธฐ๊ฐ€ ๋ฒˆ๊ฑฐ๋กญ๋‹ค.
  • BindingResult๋Š” ์ด๋ฏธ ๋ณธ์ธ์ด ๊ฒ€์ฆํ•ด์•ผ ํ•  ๊ฐ์ฒด์ธ target์„ ์•Œ๊ณ  ์žˆ๋‹ค. โ†’ ์ด๋ฅผ ํ™œ์šฉํ•ด rejectValue์™€, reject๊ฐ€ FieldError, ObjectError๋ฅผ ๋Œ€์ฒดํ•œ๋‹ค.
  • ์˜ค๋ฅ˜ ์ฝ”๋“œ๋ฅผ ์ถ•์•ฝํ•ด์„œ ์ž…๋ ฅํ•ด๋„ ์ž˜ ์ฐพ์•„์„œ ์ถœ๋ ฅํ•œ๋‹ค.
    • MessageCodesResolver๋ฅผ ์ด์šฉํ•˜๊ธฐ ๋•Œ๋ฌธ
      MessageCodesResolver codesResolver = new DefaultMessageCodesResolver();
      String [] messageCodes = codesResolver.resolveMessageCodes("required", "item");
      • ๊ธฐ๋ณธ ๋ฉ”์‹œ์ง€ (์ฝ”๋“œ) ์ƒ์„ฑ ๊ทœ์น™

        ๊ฐ์ฒด ์˜ค๋ฅ˜
        ๊ฐ์ฒด ์˜ค๋ฅ˜์˜ ๊ฒฝ์šฐ ๋‹ค์Œ ์ˆœ์„œ๋กœ 2๊ฐ€์ง€ ์ƒ์„ฑ
        1.: code + "." + object name
        2.: code
        ์˜ˆ) ์˜ค๋ฅ˜ ์ฝ”๋“œ: required, object name: item

        1. : required.item
        2. : required

        ํ•„๋“œ ์˜ค๋ฅ˜
        ํ•„๋“œ ์˜ค๋ฅ˜์˜ ๊ฒฝ์šฐ ๋‹ค์Œ ์ˆœ์„œ๋กœ 4๊ฐ€์ง€ ๋ฉ”์‹œ์ง€ ์ฝ”๋“œ ์ƒ์„ฑ
        1.: code + "." + object name + "." + field
        2.: code + "." + field
        3.: code + "." + field type
        4.: code

      • ๋™์ž‘๋ฐฉ์‹
        • rejectValue(), reject() ๋Š” ๋‚ด๋ถ€์—์„œ MessageCodesResolver๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.
        • FieldError - rejectValue(โ€itemNameโ€, โ€œrequiredโ€) required.item.itemName
          required.itemName
          required.java.lang.String
          required
        • ObjectError - reject(โ€totalPriceMinโ€) totalPriceMin.item
          totalPriceMin

ValidationUtils

if (!StringUtils.hasText(item.getItemName())){
    bindingResult.rejectValue("itemName", "required");
   }   // ==
ValidationUtils.rejectIfEmptyOrWhitespace(bindingResult, "itemName", "required");

์Šคํ”„๋ง์ด ์ง์ ‘ ๋งŒ๋“  ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ

๊ฒ€์ฆ ์˜ค๋ฅ˜ ์ฝ”๋“œ ์ข…๋ฅ˜

  • ๊ฐœ๋ฐœ์ž๊ฐ€ ์ง์ ‘ ์„ค์ •ํ•œ ์˜ค๋ฅ˜ ์ฝ”๋“œ โ†’ rejectValue()๋ฅผ ์ง์ ‘ ํ˜ธ์ถœ
  • ์Šคํ”„๋ง์ด ์ง์ ‘ ๊ฒ€์ฆ ์˜ค๋ฅ˜์— ์ถ”๊ฐ€ํ•œ ๊ฒฝ์šฐ
    • โ†’ ๋กœ๊ทธ ํ™•์ธ โ†’ ๋ฉ”์‹œ์ง€ ์ฝ”๋“œ ํ™•์ธ typeMismatch.item.price
      typeMismatch.price
      typeMismatch.java.lang.Integer
      typeMismatch ๋‹ค์Œ๊ณผ ๊ฐ™์ด 4๊ฐ€์ง€ ๋ฉ”์‹œ์ง€ ์ฝ”๋“œ๊ฐ€ ์ž…๋ ฅ๋˜์–ด ์žˆ๋‹ค. โ†’ ์Šคํ”„๋ง์€ ํƒ€์ž… ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด typeMismatch ๋ผ๋Š” ์˜ค๋ฅ˜ ์ฝ”๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๋Š”๋ฐ ์ด ์ฝ”๋“œ๊ฐ€ MessageCodesResolver๋ฅผ ํ†ตํ•˜๋ฉด์„œ 4๊ฐ€์ง€ ๋ฉ”์‹œ์ง€ ์ฝ”๋“œ๊ฐ€ ์ƒ์„ฑ๋œ ๊ฒƒ์ด๋‹ค.

V5 - Validator ๋ถ„๋ฆฌ

  • ์ปจํŠธ๋กค๋Ÿฌ ์ฝ”๋“œ
    		@PostMapping("/add")
        public String addItemV5(@ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {
    
            itemValidator.validate(item, bindingResult);
    
            if(bindingResult.hasErrors()){
                log.info("error={}", bindingResult);
                return "validation/v2/addForm";
            }
    
            Item savedItem = itemRepository.save(item);
            redirectAttributes.addAttribute("itemId", savedItem.getId());
            redirectAttributes.addAttribute("status", true);
            return "redirect:/validation/v2/items/{itemId}";
        }

์ปจํŠธ๋กค๋Ÿฌ ๋กœ์ง์—์„œ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒƒ์ด ๋„ˆ๋ฌด ๋งŽ๋‹ค

  • validator ํด๋ž˜์Šค ์ƒ์„ฑ
    @Component
    public class ItemValidator implements Validator {
        @Override
        public boolean supports(Class<?> clazz) {
            return Item.class.isAssignableFrom(clazz);
        }
    
        @Override
        public void validate(Object target, Errors errors) {
            Item item = (Item) target;
    
            ValidationUtils.rejectIfEmptyOrWhitespace(errors, "itemName", "required");
    
            if (item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1000000){
                errors.rejectValue("price", "range", new Object[]{1000, 1000000}, null);
            }
            if(item.getQuantity() == null || item.getQuantity() > 9999){
                errors.rejectValue("quantity", "max", new Object[]{9999}, null);
            }
            if(item.getPrice() != null && item.getQuantity() != null){
                int resultPrice = item.getPrice() * item.getQuantity();
                if (resultPrice < 10000) {
                    errors.reject("totalPriceMin", new Object[]{10000, resultPrice}, null);
                }
            }
        }
    }
  • validator๋ฅผ ์ปดํฌ๋„ŒํŠธ ์Šค์บ”ํ–ˆ๊ธฐ ๋•Œ๋ฌธ์— ์ปจํŠธ๋กค๋Ÿฌ์—์„œ๋Š” validator๋ฅผ ์ฃผ์ž…ํ•˜๊ณ  ์‚ฌ์šฉํ•˜๋ฉด ๋œ๋‹ค.

V6 - @Validated, WebDataBinder

  • Validator ํด๋ž˜์Šค์— Validator ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ implementํ•œ ์ด์œ ๋Š” ์ฒด๊ณ„์ ์œผ๋กœ ๊ฒ€์ฆ ๊ธฐ๋Šฅ์„ ๋„์ž…ํ•˜๊ธฐ ์œ„ํ•ด์„œ์ด๋‹ค.
@InitBinder
    public void init(WebDataBinder dataBinder){
        dataBinder.addValidators(itemValidator);
    } 
  • ์œ„์™€ ๊ฐ™์ด ์„ ์–ธํ•œ ํ›„์— ๊ฒ€์ฆ์„ ํ•˜๊ณ ์ž ํ•˜๋Š” ์ปจํŠธ๋กค๋Ÿฌ์˜ Argument๋กœ @Validated Annotation์„ ์ถ”๊ฐ€ํ•˜๋ฉด ์•ž์„œ WebDataBinder์— ๋“ฑ๋กํ•œ ๊ฒ€์ฆ๊ธฐ๋ฅผ ์ฐพ์•„์„œ ์‹คํ–‰ํ•œ๋‹ค.
  • ์ด ๋•Œ ์—ฌ๋Ÿฌ ๊ฒ€์ฆ๊ธฐ๋ฅผ ๋“ฑ๋กํ•œ๋‹ค๋ฉด ๊ทธ ์ค‘ ์–ด๋–ค ๊ฒ€์ฆ๊ธฐ๊ฐ€ ์‹คํ–‰๋˜์–ด์•ผ ํ•  ์ง€ ๊ตฌ๋ถ„ํ•˜๊ธฐ ์œ„ํ•ด supports ๋ฉ”์„œ๋“œ๊ฐ€ ์‚ฌ์šฉ๋œ๋‹ค.

โ€ป ๊ธ€๋กœ๋ฒŒ ์„ค์ •

@SpringBootApplication
public class ItemServiceApplication implements WebMvcConfigurer {
 public static void main(String[] args) {
	 SpringApplication.run(ItemServiceApplication.class, args);
	 }
	 @Override
	 public Validator getValidator() {
	 return new ItemValidator();
	 }
}

โœ”๏ธ Bean Validation

  • build.gradle์— ์˜์กด๊ด€๊ณ„ ์ถ”๊ฐ€
    • implementation 'org.springframework.boot:spring-boot-starter-validation

@NotBlank : ๋นˆ๊ฐ’ + ๊ณต๋ฐฑ๋งŒ ์žˆ๋Š” ๊ฒฝ์šฐ๋ฅผ ํ—ˆ์šฉํ•˜์ง€ ์•Š๋Š”๋‹ค.
@NotNull : null ์„ ํ—ˆ์šฉํ•˜์ง€ ์•Š๋Š”๋‹ค.
@Range(min = 1000, max = 1000000) : ๋ฒ”์œ„ ์•ˆ์˜ ๊ฐ’์ด์–ด์•ผ ํ•œ๋‹ค.
@Max(9999) : ์ตœ๋Œ€ 9999๊นŒ์ง€๋งŒ ํ—ˆ์šฉํ•œ๋‹ค.

๊ฒ€์ฆ ์ˆœ์„œ

  1. @ModelAttribute ๊ฐ๊ฐ์˜ ํ•„๋“œ์— ํƒ€์ž… ๋ณ€ํ™˜ ์‹œ๋„
    1. ์„ฑ๊ณตํ•˜๋ฉด ๋‹ค์Œ์œผ๋กœ

    2. ์‹คํŒจํ•˜๋ฉด typeMismatch ๋กœ FieldError ์ถ”๊ฐ€

      โ†’ errors.properties์— ์„ ์–ธํ•œ ์—๋Ÿฌ๋ฉ”์‹œ์ง€ ์ถœ๋ ฅ

  2. Validator ์ ์šฉ

๋ฐ”์ธ๋”ฉ์— ์„ฑ๊ณตํ•œ ํ•„๋“œ๋งŒ Bean Validation ์ ์šฉ
BeanValidator๋Š” ๋ฐ”์ธ๋”ฉ์— ์‹คํŒจํ•œ ํ•„๋“œ๋Š” BeanValidation์„ ์ ์šฉํ•˜์ง€ ์•Š๋Š”๋‹ค.
ํƒ€์ž… ๋ณ€ํ™˜์— ์„ฑ๊ณตํ•ด์„œ ๋ฐ”์ธ๋”ฉ์— ์„ฑ๊ณตํ•œ ํ•„๋“œ์—ฌ์•ผ BeanValidation ์ ์šฉ์ด ์˜๋ฏธ ์žˆ๋‹ค

์—๋Ÿฌ ๋ฉ”์‹œ์ง€

//errors.properties
#Bean Validation ์ถ”๊ฐ€

NotBlank={0} ๊ณต๋ฐฑX
Range={0}, {2} ~ {1} ํ—ˆ์šฉ
Max={0}, ์ตœ๋Œ€ {1}
  • {0} ์€ ํ•„๋“œ๋ช…์ด๊ณ , {1} , {2} ...์€ ๊ฐ ์• ๋…ธํ…Œ์ด์…˜ ๋งˆ๋‹ค ๋‹ค๋ฅด๋‹ค.
  • BeanValidation ๋ฉ”์‹œ์ง€ ์ฐพ๋Š” ์ˆœ์„œ
    1. ์ƒ์„ฑ๋œ ๋ฉ”์‹œ์ง€ ์ฝ”๋“œ ์ˆœ์„œ๋Œ€๋กœ messageSource(errors.properties) ์—์„œ ๋ฉ”์‹œ์ง€ ์ฐพ๊ธฐ
    2. ์• ๋…ธํ…Œ์ด์…˜์˜ message ์†์„ฑ ์‚ฌ์šฉ โ†’ @NotBlank(message = "๊ณต๋ฐฑ! {0}")
    3. ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ์ œ๊ณตํ•˜๋Š” ๊ธฐ๋ณธ ๊ฐ’ ์‚ฌ์šฉ

์˜ค๋ธŒ์ ํŠธ ์˜ค๋ฅ˜

  • FieldError๊ฐ€ ์•„๋‹Œ ObjectError๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฐฉ๋ฒ•
  • ์‹ค๋ฌด์—์„œ๋Š” ๋‘๋ฒˆ์งธ ๋ฐฉ๋ฒ• ๊ถŒ์žฅ
    • @ScriptAssert
      @Data
      @ScriptAssert(lang = "javascript", script = "_this.price * _this.quantity >= 
      10000")
      public class Item {
       //...
      }
    • ๊ธ€๋กœ๋ฒŒ ์˜ค๋ฅ˜ ์ง์ ‘ ์ถ”๊ฐ€
      @PostMapping("/add")
      public String addItem(@Validated @ModelAttribute Item item, BindingResult 
      bindingResult, RedirectAttributes redirectAttributes) {
       //ํŠน์ • ํ•„๋“œ ์˜ˆ์™ธ๊ฐ€ ์•„๋‹Œ ์ „์ฒด ์˜ˆ์™ธ
       if (item.getPrice() != null && item.getQuantity() != null) {
       int resultPrice = item.getPrice() * item.getQuantity();
       if (resultPrice < 10000) {
       bindingResult.reject("totalPriceMin", new Object[]{10000,
      resultPrice}, null);
       }
       }
       if (bindingResult.hasErrors()) {
       log.info("errors={}", bindingResult);
       return "validation/v3/addForm";
       }
       //์„ฑ๊ณต ๋กœ์ง
       Item savedItem = itemRepository.save(item);
       redirectAttributes.addAttribute("itemId", savedItem.getId());
       redirectAttributes.addAttribute("status", true);
       return "redirect:/validation/v3/items/{itemId}";
      }

ํ•œ๊ณ„์™€ ํ•ด๊ฒฐ

  • ์ƒํ™ฉ
    • ์ˆ˜์ •ํ•  ๋•Œ๋Š” id๋ฅผ ํ•„์ˆ˜๊ฐ’์œผ๋กœ ํ•˜๊ณ  ์ˆ˜๋Ÿ‰์— ์ œํ•œ์„ ๋‘์ง€ ์•Š์ง€๋งŒ ๋“ฑ๋กํ•  ๋•Œ๋Š” id๊ฐ€ ์—†์–ด๋„ ๋˜๊ณ  ์ˆ˜๋Ÿ‰์— ์ œํ•œ์„ ๋‘”๋‹ค โ†’ ์ˆ˜์ •๊ณผ ๋“ฑ๋ก์˜ Validation์„ ๋‹ค๋ฅด๊ฒŒ ์„ค์ •ํ•˜๋Š” ๊ฒฝ์šฐ

2๊ฐ€์ง€ ๋ฐฉ๋ฒ•

  1. BeanValidation์˜ Group ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉ
    1. ๊ฐ ๊ทธ๋ฃน๋ณ„ ์ธํ„ฐํŽ˜์ด์Šค ํŒŒ์ผ ์ƒ์„ฑ
    2. ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค์˜ ๊ฐ Validation Annotation์— groups Argument ์ถ”๊ฐ€
    3. ์ปจํŠธ๋กค๋Ÿฌ์˜ Validated Annotation์— Argument๋กœ ์ธํ„ฐํŽ˜์ด์Šค ํด๋ž˜์Šค ์„ ์–ธ
  2. Item์„ ์ง์ ‘ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ , ItemSaveForm, ItemUpdateForm ๊ฐ™์€ ํผ ์ „์†ก์„ ์œ„ํ•œ ๋ณ„๋„์˜ ๋ชจ๋ธ ๊ฐ์ฒด๋ฅผ ๋งŒ๋“ค์–ด ์‚ฌ์šฉ - DTO
    • ์ฃผ์˜์‚ฌํ•ญ
      • @ModelAttribute์˜ Argument ๊ณ ๋ ค - ์ƒ๋žตํ•  ์‹œ ๋ชจ๋ธ ๊ฐ์ฒด ์ด๋ฆ„์œผ๋กœ ๋“ฑ๋ก
      • ๋ ˆํฌ์ง€ํ† ๋ฆฌ ๋ฉ”์„œ๋“œ์˜ ์ธ์ž๊ฐ’์œผ๋กœ๋Š” item ์ธ์Šคํ„ด์Šค๊ฐ€ ๋“ค์–ด๊ฐ€์•ผํ•˜๊ธฐ ๋•Œ๋ฌธ์— item ์ƒ์„ฑ ํ›„ ์‚ฝ์ž…

Bean Validation - HTTP ๋ฉ”์‹œ์ง€ ์ปจ๋ฒ„ํ„ฐ(Body)

@ModelAttribute๋Š” HTTP์š”์ฒญ ํŒŒ๋ผ๋ฏธํ„ฐ(URL ์ฟผ๋ฆฌ์ŠคํŠธ๋ง, Post Form)๋ฅผ ๋‹ค๋ฃฐ ๋•Œ ์‚ฌ์šฉํ•œ๋‹ค.

@RequestBody๋Š” HTTP Body์˜ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜ํ•  ๋•Œ ์‚ฌ์šฉํ•œ๋‹ค. (API JSON ์š”์ฒญ)

API - 3๊ฐ€์ง€ ๊ฒฝ์šฐ

  • ์„ฑ๊ณต ์š”์ฒญ: ์„ฑ๊ณต
  • ์‹คํŒจ ์š”์ฒญ: JSON ๊ฐ์ฒด๋กœ ์ƒ์„ฑํ•˜๋Š” ๊ฒƒ ์ž์ฒด๊ฐ€ ์‹คํŒจ
  • ๊ฒ€์ฆ ์˜ค๋ฅ˜ ์š”์ฒญ: JSON์„ ๊ฐ์ฒด๋กœ ์ƒ์„ฑํ•˜๋Š” ๊ฒƒ์€ ์„ฑ๊ณตํ–ˆ๊ณ , ๊ฒ€์ฆ์—์„œ ์‹คํŒจ

@ModelAttribute VS @RequestBody

  • @ModelAttribute๋Š” ๊ฐ๊ฐ์˜ ํ•„๋“œ ๋‹จ์œ„๋กœ ์ ์šฉ๋œ๋‹ค. ๊ทธ๋ž˜์„œ ํŠน์ • ํ•„๋“œ์— ํƒ€์ž…์ด ๋งž์ง€ ์•Š๋Š” ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•ด๋„ ๋‚˜๋จธ์ง€ ํ•„๋“œ๋Š” ์ •์ƒ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค. โ†’ ํŠน์ • ํ•„๋“œ๊ฐ€ ๋ฐ”์ธ๋”ฉ๋˜์ง€ ์•Š์•„๋„ ๋‚˜๋จธ์ง€ ํ•„๋“œ๋Š” ์ •์ƒ ๋ฐ”์ธ๋”ฉ, Validator ๊ฒ€์ฆ ๊ฐ€๋Šฅ
  • HttpMessageConverter๋Š” ์ „์ฒด ๊ฐ์ฒด ๋‹จ์œ„๋กœ ์ ์šฉ๋œ๋‹ค.
    • ๋”ฐ๋ผ์„œ ๋ฉ”์‹œ์ง€ ์ปจ๋ฒ„ํ„ฐ์˜ ์ž‘๋™์ด ์„ฑ๊ณตํ•ด์„œ ItemSaveForm ๊ฐ์ฒด๋ฅผ ๋งŒ๋“ค์–ด์•ผ @Validated๊ฐ€ ์ ์šฉ๋œ๋‹ค.

      โ†’ **RequestBody๋Š” ๋ฉ”์‹œ์ง€ ์ปจ๋ฒ„ํ„ฐ ๋‹จ๊ณ„์—์„œ JSON ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ์ฒด๋กœ ๋ณ€๊ฒฝํ•˜์ง€ ๋ชปํ•˜๋ฉด Validator ๊ฒ€์ฆ ๋ถˆ๊ฐ€๋Šฅ**

      โ†’ HttpMessageConverter ๋‹จ๊ณ„์—์„œ ์‹คํŒจํ•œ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๋ฐฉ๋ฒ•์€ ๋’ค์˜ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ๋ถ€๋ถ„์—์„œ ๋‹ค๋ฃธ.

โœ”๏ธ ๋กœ๊ทธ์ธ ์ฒ˜๋ฆฌ - ์ฟ ํ‚ค, ์„ธ์…˜

๋กœ๊ทธ์ธ, ํšŒ์›๊ฐ€์ž… ํผ, ๊ธฐ๋Šฅ ๊ตฌํ˜„ (Base)

  1. ํ™ˆ ํ™”๋ฉด HTML
  2. ํšŒ์›๊ฐ€์ž…
    1. Member Data Class

    2. MemberRepository

      // ์ž๋ฐ” ๋ฌธ๋ฒ• ์ฐธ๊ณ 
      public Optional<Member> findByLoginId(String loginId) {
       return findAll().stream()
       .filter(m -> m.getLoginId().equals(loginId))
       .findFirst();
       }
    3. MemberController

    4. HTML

  3. ๋กœ๊ทธ์ธ
    1. LoginService (๋„๋ฉ”์ธ ํŒจํ‚ค์ง€์— ์ƒ์„ฑ)
    2. login DTO (LoginForm)
    3. Login Controller
      • ๊ธ€๋กœ๋ฒŒ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ
        @PostMapping("/login")
        public String login(@Valid @ModelAttribute LoginForm form, BindingResult 
        bindingResult) {
        	 if (bindingResult.hasErrors()) {
        	 return "login/loginForm";
        	 }
        	 Member loginMember = loginService.login(form.getLoginId(),form.getPassword());
        	 <-- ๊ธ€๋กœ๋ฒŒ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ -->
        	 if (loginMember == null) {
        		 bindingResult.reject("loginFail", "์•„์ด๋”” ๋˜๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ๋งž์ง€ ์•Š์Šต๋‹ˆ๋‹ค.");
        		 return "login/loginForm";
        	 }
        	 <--     -->
        	 //๋กœ๊ทธ์ธ ์„ฑ๊ณต ์ฒ˜๋ฆฌ TODO
        	 return "redirect:/";
         }

์ฟ ํ‚ค

  • ์ฟ ํ‚ค ์ƒ์„ฑ ๋กœ์ง
    Cookie cookie = new Cookie("memberId", String.valueOf(loginMember.getId()));
    response.addCookie(cookie);
  • ๋กœ๊ทธ์•„์›ƒ ๋กœ์ง
    • ์ƒˆ๋กœ์šด ์ฟ ํ‚ค๋ฅผ ๋งŒ๋“ค๊ณ  setMaxAge๋กœ ์œ ํšจ์‹œ๊ฐ„์„ 0์ดˆ๋กœ ๋งŒ๋“ ๋‹ค.

      		@PostMapping("/logout")
          public String logout(HttpServletResponse response){
              expiredCookie(response, "memberId");
              return "redirect:/";
          }
      
          private static void expiredCookie(HttpServletResponse response, String cookieName) {
              Cookie cookie = new Cookie(cookieName, null);
              cookie.setMaxAge(0);
              response.addCookie(cookie);
          }

์ฟ ํ‚ค ๋ณด์•ˆ ๋ฌธ์ œ

๋ฌธ์ œ์ 

  • ์ฟ ํ‚ค ๊ฐ’์€ ์ž„์˜๋กœ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ๋‹ค.
  • ์ฟ ํ‚ค์— ๋ณด๊ด€๋œ ์ •๋ณด๋Š” ํ›”์ณ๊ฐˆ ์ˆ˜ ์žˆ๋‹ค.
  • ํ•ด์ปค๊ฐ€ ์ฟ ํ‚ค๋ฅผ ํ•œ๋ฒˆ ํ›”์ณ๊ฐ€๋ฉด ํ‰์ƒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

๋Œ€์•ˆ

  • ์ฟ ํ‚ค์— ์ค‘์š”ํ•œ ๊ฐ’์„ ๋…ธ์ถœํ•˜์ง€ ์•Š๊ณ , ์‚ฌ์šฉ์ž ๋ณ„๋กœ ์˜ˆ์ธก ๋ถˆ๊ฐ€๋Šฅํ•œ ์ž„์˜์˜ ํ† ํฐ์„ ๋…ธ์ถœ, ์„œ๋ฒ„์—์„œ ํ† ํฐ๊ณผ ์‚ฌ์šฉ๊ณผ id๋ฅผ ๋งคํ•‘ํ•ด์„œ ์ธ์‹, ์„œ๋ฒ„์—์„œ ํ† ํฐ ๊ด€๋ฆฌ
  • ํ† ํฐ์ด ํ•ดํ‚น๋‹นํ•ด๋„ ์‹œ๊ฐ„์ด ์ง€๋‚˜๋ฉด ์‚ฌ์šฉํ•  ์ˆ˜ ์—†๋„๋ก ์„œ๋ฒ„์—์„œ ํ•ด๋‹น ํ† ํฐ์˜ ๋งŒ๋ฃŒ์‹œ๊ฐ„์„ ์งง๊ฒŒ ์œ ์ง€ํ•œ๋‹ค.

์„ธ์…˜

๋™์ž‘ ๋ฐฉ์‹

  • ์ฟ ํ‚ค์˜ ๋ณด์•ˆ ์ด์Šˆ๋ฅผ ํ•ด๊ฒฐํ•˜๋ ค๋ฉด ์ค‘์š”ํ•œ ์ •๋ณด๋Š” ๋ชจ๋‘ ์„œ๋ฒ„์— ์ €์žฅํ•ด์•ผ ํ•œ๋‹ค. โ†’ ์ด๋ ‡๊ฒŒ ์„œ๋ฒ„์— ์ค‘์š”ํ•œ ์ •๋ณด๋ฅผ ๋ณด๊ด€ํ•˜๊ณ  ์—ฐ๊ฒฐ์„ ์œ ์ง€ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์„ธ์…˜์ด๋ผ๊ณ  ํ•œ๋‹ค.

์„ธ์…˜ ์ง์ ‘ ๋งŒ๋“ค๊ธฐ

์„ธ์…˜ ๋งค๋‹ˆ์ € ํด๋ž˜์Šค

@Component
public class SessionManager {

    private Map<String, Object> sessionStore = new ConcurrentHashMap<>();
    public static final String SESSION_COOKIE_NAME = "mySessionId";
    /**
     * ์„ธ์…˜ ์ƒ์„ฑ
     */
    public void createSession(Object value, HttpServletResponse response){
        String sessionId = UUID.randomUUID().toString();
        sessionStore.put(sessionId, value);

        Cookie mySessionCookie = new Cookie(SESSION_COOKIE_NAME, sessionId);
        response.addCookie(mySessionCookie);
    }
    /**
     * ์„ธ์…˜ ์กฐํšŒ
     */
    public Object getSession(HttpServletRequest request){
        Cookie sessionCookie = findCookie(request, SESSION_COOKIE_NAME);
        if (sessionCookie == null){
            return null;
        }
        return sessionStore.get(sessionCookie.getValue());
    }

    /**
     * ์„ธ์…˜ ๋งŒ๋ฃŒ
     */
    public void expire(HttpServletRequest request){
        Cookie sessionCookie = findCookie(request, SESSION_COOKIE_NAME);
        if (sessionCookie != null){
            sessionStore.remove(sessionCookie.getValue());
        }
    }

    private static Cookie findCookie(HttpServletRequest request, String cookieName) {
        if (request.getCookies() == null){
            return null;
        }
        return Arrays.stream(request.getCookies())
                .filter(c -> c.getName().equals(cookieName))
                .findAny().orElse(null);
    }
}
  • ์„ธ์…˜ ํ…Œ์ŠคํŠธ
    @Test
        void sessionTest(){
            //์„ธ์…˜ ์ƒ์„ฑ
            MockHttpServletResponse response = new MockHttpServletResponse();
            Member member = new Member();
            sessionManager.createSession(member, response);
    
            //์ฟ ํ‚ค ์กฐ์ž‘
            MockHttpServletRequest request = new MockHttpServletRequest();
            request.setCookies(response.getCookies());
    
            //์„ธ์…˜ ์กฐํšŒ
            Object session = sessionManager.getSession(request);
            assertThat(session).isEqualTo(member);
    
            //์„ธ์…˜ ๋งŒ๋ฃŒ
            sessionManager.expire(request);
            Object expired = sessionManager.getSession(request);
            assertThat(expired).isNull();
        }

์„œ๋ธ”๋ฆฟ HTTP ์„ธ์…˜

  • HttpSession์˜ ์ฟ ํ‚ค ์ด๋ฆ„์€ JSESSIONID ์ด๊ณ  ๊ฐ’์€ ์ถ”์ • ๋ถˆ๊ฐ€๋Šฅํ•œ ๋žœ๋ค ใ„ฑ๋ฐง

์„ธ์…˜ ์ƒ์„ฑ๊ณผ ์กฐํšŒ

  • ์ฝ”๋“œ
    // ๋กœ๊ทธ์ธ ์„ฑ๊ณต ์ฒ˜๋ฆฌ TODO
    //sessionManager.createSession(loginMember, response);
    HttpSession session = request.getSession();
    session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);
    
    //sessionManager.expire(request);
    HttpSession session = request.getSession(false);
    if (session != null){
        session.invalidate();
    }
  • ์„ธ์…˜ ์ƒ์„ฑ ๋ฐ ์กฐํšŒ
    • request.getSession(True)
      • True์ผ ๊ฒฝ์šฐ - ์ƒ์„ฑ - Default๊ฐ’
        • ์„ธ์…˜์ด ์žˆ์œผ๋ฉด ๊ธฐ์กด ์„ธ์…˜ ๋ฐ˜ํ™˜
        • ์—†์œผ๋ฉด ์ƒˆ๋กœ์šด ์„ธ์…˜ ์ƒ์„ฑ ํ›„ ๋ฐ˜ํ™˜
      • False์ผ ๊ฒฝ์šฐ - ์กฐํšŒ
        • ์„ธ์…˜์ด ์žˆ์œผ๋ฉด ๊ธฐ์กด ์„ธ์…˜ ๋ฐ˜ํ™˜
        • ์—†์œผ๋ฉด null ๋ฐ˜ํ™˜
  • ์„ธ์…˜ ์‚ญ์ œ
    • session.invalidate()

@SessionAttribute

  • ์„ธ์…˜์„ ํŽธ๋ฆฌํ•˜๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ์Šคํ”„๋ง์—์„œ ์ง€์›ํ•˜๋Š” Annotation
  • ๊ธฐ์กด์ฝ”๋“œ
    		//@GetMapping("/")
        public String loginHomeV3(HttpServletRequest request, Model model){
            HttpSession session = request.getSession(false);
            if (session == null){
                return "home";
            }
            Object member = session.getAttribute(SessionConst.LOGIN_MEMBER);
            if (member == null){
                return "home";
            }
            model.addAttribute("member", member);
            return "loginHome";
        }
  • ๋ณ€๊ฒฝ ํ›„ ์ฝ”๋“œ
        @GetMapping("/")
        public String loginHomeV3Spring(@SessionAttribute(value = SessionConst.LOGIN_MEMBER, required = false) Member member, Model model){
            if (member == null){
                return "home";
            }
            model.addAttribute("member", member);
            return "loginHome";
        }
    }

โ€ป Traking Mode

  • ์„œ๋ฒ„์ž…์žฅ์—์„œ ์›น๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์ฟ ํ‚ค๋ฅผ ์ง€์›ํ•˜๋Š”์ง€ ๋ชจ๋ฅด๊ธฐ ๋•Œ๋ฌธ์— ์ตœ์ดˆ ์ ‘๊ทผ ์‹œ ์ฟ ๊ธฐ ๊ฐ’๋„ ์ „๋‹ฌํ•˜๊ณ  URL์—๋„ JSESSIONID ๋ฅผ ์ „๋‹ฌํ•œ๋‹ค.
    • URL ์ „๋‹ฌ ๋ฐฉ์‹์„ ๋„๊ณ  ์ฟ ํ‚ค๋ฅผ ํ†ตํ•ด์„œ๋งŒ ์„ธ์…˜์„ ์œ ์ง€ํ•˜๊ณ  ์‹ถ์„ ๋•Œ

์„ธ์…˜ ์ •๋ณด์™€ ํƒ€์ž„์•„์›ƒ ์„ค์ •

  • ์„ธ์…˜ ์ •๋ณด
    @GetMapping("/session-info")
        public String sessionInfo(HttpServletRequest request) {
            HttpSession session = request.getSession(false);
            if (session == null) {
                return "์„ธ์…˜์ด ์—†์Šต๋‹ˆ๋‹ค.";
            }
            //์„ธ์…˜ ๋ฐ์ดํ„ฐ ์ถœ๋ ฅ
            session.getAttributeNames().asIterator()
                    .forEachRemaining(name -> log.info("session name={}, value={}",
                            name, session.getAttribute(name)));
            log.info("sessionId={}", session.getId());
            log.info("maxInactiveInterval={}", session.getMaxInactiveInterval());
            log.info("creationTime={}", new Date(session.getCreationTime()));
            log.info("lastAccessedTime={}", new Date(session.getLastAccessedTime()));
            log.info("isNew={}", session.isNew());
            return "์„ธ์…˜ ์ถœ๋ ฅ";
        }

    sessionId : ์„ธ์…˜Id, JSESSIONID ์˜ ๊ฐ’์ด๋‹ค. ์˜ˆ) 34B14F008AA3527C9F8ED620EFD7A4E1

    maxInactiveInterval : ์„ธ์…˜์˜ ์œ ํšจ ์‹œ๊ฐ„, ์˜ˆ) 1800์ดˆ, (30๋ถ„)

    creationTime : ์„ธ์…˜ ์ƒ์„ฑ์ผ์‹œ

    lastAccessedTime : ์„ธ์…˜๊ณผ ์—ฐ๊ฒฐ๋œ ์‚ฌ์šฉ์ž๊ฐ€ ์ตœ๊ทผ์— ์„œ๋ฒ„์— ์ ‘๊ทผํ•œ ์‹œ๊ฐ„, ํด๋ผ์ด์–ธํŠธ์—์„œ ์„œ๋ฒ„๋กœ

    sessionId ( JSESSIONID )๋ฅผ ์š”์ฒญํ•œ ๊ฒฝ์šฐ์— ๊ฐฑ์‹ ๋œ๋‹ค.

    isNew : ์ƒˆ๋กœ ์ƒ์„ฑ๋œ ์„ธ์…˜์ธ์ง€, ์•„๋‹ˆ๋ฉด ์ด๋ฏธ ๊ณผ๊ฑฐ์— ๋งŒ๋“ค์–ด์กŒ๊ณ , ํด๋ผ์ด์–ธํŠธ์—์„œ ์„œ๋ฒ„๋กœ
    sessionId ( JSESSIONID )๋ฅผ ์š”์ฒญํ•ด์„œ ์กฐํšŒ๋œ ์„ธ์…˜์ธ์ง€ ์—ฌ๋ถ€

  • ์„ธ์…˜ ํƒ€์ž„์•„์›ƒ ์„ค์ •
    • ๋ฌธ์ œ์ 
      • ์‚ฌ์šฉ์ž๊ฐ€ ๋กœ๊ทธ์•„์›ƒ์„ ํ•˜์ง€์•Š๊ณ  ์›น ๋ธŒ๋ผ์šฐ์ €๋ฅผ ์ข…๋ฃŒํ•œ ๊ฒฝ์šฐ ์„œ๋ฒ„๋Š” ์›น ๋ธŒ๋ผ์šฐ์ €๋ฅผ ์ข…๋ฃŒํ•œ ๊ฒƒ์ธ์ง€ ์•„๋‹Œ์ง€๋ฅผ ์ธ์‹ํ•  ์ˆ˜ ์—†์–ด์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์–ธ์ œ ์‚ญ์ œํ•ด์•ผ ํ•˜๋Š”์ง€ ํŒ๋‹จํ•˜๊ธฐ๊ฐ€ ์–ด๋ ต๋‹ค.
      • ์ด ๊ฒฝ์šฐ ๋‚จ์•„์žˆ๋Š” ์„ธ์…˜์„ ๋ฌดํ•œ์ • ๋ณด๊ด€ํ•˜๋ฉด ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.
    • HttpSession์˜ ํ•ด๊ฒฐ๋ฐฉ๋ฒ•
      • ์œ„์™€ ๊ฐ™์€ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ์„ธ์…˜์˜ ํƒ€์ž„์•„์›ƒ ์‹œ๊ฐ„์„ ๋งˆ์ง€๋ง‰ ์ ‘๊ทผ ์‹œ๊ฐ„์œผ๋กœ๋ถ€ํ„ฐ 30๋ถ„์„ Default๊ฐ’์œผ๋กœ ์žก๋Š”๋‹ค.
    • ํƒ€์ž„ ์•„์›ƒ ์‹œ๊ฐ„ ์„ค์ •๋ฐฉ๋ฒ•
      • ๊ธ€๋กœ๋ฒŒ ์„ค์ •: application.properties - server.servlet.session.timeout=60 (๊ธ€๋กœ๋ฒŒ ์„ค์ •์€ ๋ถ„ ๋‹จ์œ„๋กœ ์„ค์ •ํ•ด์•ผํ•œ๋‹ค.)
      • ํŠน์ • ์„ธ์…˜ ์„ค์ •: session.setMaxInactiveInterval(1800);
  • ์‹ค๋ฌด ์ฃผ์˜์ 
    • ์„ธ์…˜์—๋Š” ์ตœ์†Œํ•œ์˜ ๋ฐ์ดํ„ฐ๋งŒ ๋ณด๊ด€ํ•ด์•ผํ•œ๋‹ค. ํ˜„์žฌ ํ”„๋กœ์ ํŠธ์—๋Š” Member ๊ฐ์ฒด ์ „์ฒด๋ฅผ ๋‹ด์•˜์ง€๋งŒ ์‹ค๋ฌด์—์„œ ์‚ฌ์šฉํ•  ๋•Œ๋Š” ์•„์ด๋””, ๋‹‰๋„ค์ž„ ๋“ฑ ๊ผญ ํ•„์š”ํ•œ ์ •๋ณด๋“ค๋งŒ ํ•ํ•˜๊ฒŒ ๋‹ด๋Š”๊ฒŒ ์ข‹๋‹ค.

โœ”๏ธ ํ•„ํ„ฐ, ์ธํ„ฐ์…‰ํ„ฐ

๊ณตํ†ต ๊ด€์‹ฌ ์‚ฌํ•ญ

  • ๋กœ๊ทธ์ธํ•œ ์‚ฌ์šฉ์ž๋งŒ ์ƒํ’ˆ ๊ด€๋ฆฌ ํŽ˜์ด์ง€์— ๋“ค์–ด๊ฐˆ ์ˆ˜ ์žˆ์–ด์•ผํ•œ๋‹ค.
    • ํ˜„์žฌ๋Š” URL์„ ์ž…๋ ฅํ•˜๋ฉด ๋กœ๊ทธ์ธ์„ ํ•˜์ง€์•Š์•„๋„ ํ•ด๋‹น ํŽ˜์ด์ง€์— ๋“ค์–ด๊ฐˆ ์ˆ˜ ์žˆ๋‹ค.
  • ์ด๋Ÿฌํ•œ ๊ณตํ†ต๊ด€์‹ฌ์‚ฌ๋Š” AOP๋กœ๋„ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ์ฐŒ๋งŒ. ์›น๊ณผ ๊ด€๋ จ๋œ ๊ณตํ†ต ๊ด€์‹ฌ์‚ฌ๋ฅผ ์ฒ˜๋ฆฌํ•  ๋•Œ๋Š” HTTP ํ—ค๋”๋‚˜ URL ์ •๋ณด๊ฐ€ ํ•„์š”ํ•˜๊ธฐ ๋•Œ๋ฌธ์— HttpServletRequest๋ฅผ ์ œ๊ณตํ•˜๋Š” ์„œ๋ธ”๋ฆฟ ํ•„ํ„ฐ๋‚˜ ์Šคํ”„๋ง ์ธํ„ฐ์…‰ํ„ฐ๋ฅผ ์‚ฌ์šฉํ•˜๋Š”๊ฒŒ ์ข‹๋‹ค.

์„œ๋ธ”๋ฆฟ ํ•„ํ„ฐ

  • ํ•„ํ„ฐ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•˜๊ณ  ๋“ฑ๋กํ•˜๋ฉด ์„œ๋ธ”๋ฆฟ ์ปจํ…Œ์ด๋„ˆ๊ฐ€ ํ•„ํ„ฐ๋ฅผ ์‹ฑ๊ธ€ํ†ค ๊ฐ์ฒด๋กœ ์ƒ์„ฑํ•˜๊ณ  ๊ด€๋ฆฌํ•œ๋‹ค.

ํ•„ํ„ฐ ์ œํ•œ ํ”Œ๋กœ์šฐ

HTTP ์š”์ฒญ -> WAS -> ํ•„ํ„ฐ -> ์„œ๋ธ”๋ฆฟ -> ์ปจํŠธ๋กค๋Ÿฌ //๋กœ๊ทธ์ธ ์‚ฌ์šฉ์ž
HTTP ์š”์ฒญ -> WAS -> ํ•„ํ„ฐ(์ ์ ˆํ•˜์ง€ ์•Š์€ ์š”์ฒญ์ด๋ผ ํŒ๋‹จ, ์„œ๋ธ”๋ฆฟ ํ˜ธ์ถœX)
//๋น„ ๋กœ๊ทธ์ธ ์‚ฌ์šฉ์ž

ํ•„ํ„ฐ - ์š”์ฒญ ๋กœ๊ทธ

  • ํ•„ํ„ฐ ์ฝ”๋“œ (LogFilter)
    • ServletRequest request๋Š” HTTP ์š”์ฒญ์ด ์•„๋‹Œ ๊ฒฝ์šฐ๊นŒ์ง€ ๊ณ ๋ คํ•ด์„œ ๋งŒ๋“  ์ธํ„ฐํŽ˜์ด์Šค์ด๊ธฐ ๋•Œ๋ฌธ์— HTTP๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด HttpServletRequest๋กœ ๋‹ค์šด ์ผ€์ŠคํŒ…ํ•˜๋ฉด ๋œ๋‹ค.

    • chain.doFilter(request, response);
      - ์ด ๋กœ์ง์„ ํ˜ธ์ถœํ•˜์ง€ ์•Š์œผ๋ฉด ๋‹ค์Œ ๋‹จ๊ณ„๋กœ ์ง„ํ–‰๋˜์ง€ ์•Š๋Š”๋‹ค.

      @Slf4j
      public class LogFilter implements Filter {
          @Override
          public void init(FilterConfig filterConfig) throws ServletException {
              log.info("log filter init");
          }
      
          @Override
          public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
              log.info("log filter doFilter");
      
              HttpServletRequest httpRequest = (HttpServletRequest) request;
              String requestURI = httpRequest.getRequestURI();
      
              String uuid = UUID.randomUUID().toString();
      
              try {
                  log.info("REQUEST [{}][{}]", uuid, requestURI);
                  chain.doFilter(request, response);
              } catch (Exception e){
                  throw e;
              } finally {
                  log.info("RESPONSE [{}][{}]", uuid, requestURI);
              }
      
          }
          @Override
          public void destroy() {
              log.info("log filter destroy");
          }
      }
  • Configuration ์ฝ”๋“œ (WebConfig) - FilterRegistrationBean
    @Configuration
    public class WebConfig {
    
        @Bean
        public FilterRegistrationBean logFilter(){
            FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
            filterRegistrationBean.setFilter(new LogFilter());
            filterRegistrationBean.setOrder(1);
            filterRegistrationBean.addUrlPatterns("/*");
    
            return filterRegistrationBean;
        }
    }

@ServletComponentScan @WebFilter(filterName = "logFilter", urlPatterns = "/*") ๋กœ
ํ•„ํ„ฐ ๋“ฑ๋ก์ด ๊ฐ€๋Šฅํ•˜์ง€๋งŒ ํ•„ํ„ฐ ์ˆœ์„œ ์กฐ์ ˆ์ด ์•ˆ๋œ๋‹ค. ๋”ฐ๋ผ์„œ FilterRegistrationBean ์„ ์‚ฌ์šฉํ•˜์ž

ํ•„ํ„ฐ - ์ธ์ฆ ์ฒดํฌ

  • ํ•„ํ„ฐ ์ฝ”๋“œ (LoginCheckFilter)
    • sendRedirect์— redirectURL์„ Param์œผ๋กœ ์คŒ์œผ๋กœ์จ ๋กœ๊ทธ์ธํŽ˜์ด์ง€๋กœ ๋ฆฌ๋‹ค๋ ‰ํŠธ ํ•œ ํ›„ ๋กœ๊ทธ์ธ ์‹œ ์š”์ฒญํ–ˆ๋˜ ํŽ˜์ด์ง€๋กœ ๋‹ค์‹œ ๋Œ์•„๊ฐˆ ์ˆ˜ ์žˆ๋„๋ก ํ•œ๋‹ค.
      - LoginController login ๋กœ์ง return ๊ฐ’์— RequestParam์œผ๋กœ ๋ฐ›์€ redirectURL ์ถ”๊ฐ€

      @Slf4j
      public class LoginCheckFilter implements Filter {
      
          private static final String[] whitelist = {"/", "/members/add", "/login", "/logout", "/css/*"};
      
          @Override
          public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
      
              HttpServletRequest httpRequest = (HttpServletRequest) request;
              String requestURI = httpRequest.getRequestURI();
      
              HttpServletResponse httpResponse = (HttpServletResponse) response;
      
              try{
                  log.info("์ธ์ฆ ์ฒดํฌ ํ•„ํ„ฐ ์‹œ์ž‘{}", requestURI);
                  if (isLoginCheckPath(requestURI)){
                      log.info("์ธ์ฆ ์ฒดํฌ ๋กœ์ง ์‹คํ–‰ {}", requestURI);
                      HttpSession session = httpRequest.getSession();
                      if(session == null || session.getAttribute(SessionConst.LOGIN_MEMBER) == null){
                          log.info("๋ฏธ์ธ์ฆ ์‚ฌ์šฉ์ž ์š”์ฒญ {}", requestURI);
                          httpResponse.sendRedirect("/login?redirectURL=" + requestURI);
                          return;
                      }
                  }
                  chain.doFilter(request, response);
              } catch (Exception e){
                  throw e;
              } finally {
                  log.info("์ธ์ฆ ์ฒดํฌ ํ•„ํ„ฐ ์ข…๋ฃŒ {}", requestURI);
              }
          }
      
          private boolean isLoginCheckPath(String requestURI){
              return !PatternMatchUtils.simpleMatch(whitelist, requestURI);
          }
      }
  • Configuration ์ฝ”๋“œ
    @Bean
        public FilterRegistrationBean loginCheckFilter(){
            FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
            filterRegistrationBean.setFilter(new LoginCheckFilter());
            filterRegistrationBean.setOrder(2);
            filterRegistrationBean.addUrlPatterns("/*");
    
            return filterRegistrationBean;
        }

์Šคํ”„๋ง ์ธํ„ฐ์…‰ํ„ฐ

์Šคํ”„๋ง ์ธํ„ฐ์…‰ํ„ฐ ์ œํ•œ ํ”Œ๋กœ์šฐ

HTTP ์š”์ฒญ -> WAS -> ํ•„ํ„ฐ -> ์„œ๋ธ”๋ฆฟ -> ์Šคํ”„๋ง ์ธํ„ฐ์…‰ํ„ฐ -> ์ปจํŠธ๋กค๋Ÿฌ //๋กœ๊ทธ์ธ ์‚ฌ์šฉ์ž
HTTP ์š”์ฒญ -> WAS -> ํ•„ํ„ฐ -> ์„œ๋ธ”๋ฆฟ -> ์Šคํ”„๋ง ์ธํ„ฐ์…‰ํ„ฐ
(์ ์ ˆํ•˜์ง€ ์•Š์€ ์š”์ฒญ์ด๋ผ ํŒ๋‹จ, ์ปจํŠธ๋กค๋Ÿฌ ํ˜ธ์ถœX) // ๋น„ ๋กœ๊ทธ์ธ ์‚ฌ์šฉ์ž

์Šคํ”„๋ง ์ธํ„ฐ์…‰ํ„ฐ ํ˜ธ์ถœ ํ๋ฆ„

์ธํ„ฐ์…‰ํ„ฐ ์˜ˆ์™ธ ์ƒํ™ฉ

์Šคํ”„๋ง ์ธํ„ฐ์…‰ํ„ฐ - ์š”์ฒญ ๋กœ๊ทธ

  • ์ธํ„ฐ์…‰ํ„ฐ ์ฝ”๋“œ
    • setAttribute๋ฅผ ํ†ตํ•ด uuid ๊ฐ’์„ ์ „๋‹ฌํ•ด์„œ ๊ฐ™์€ ๊ฐ’์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

      @Slf4j
      public class LoginInterceptor implements HandlerInterceptor {
      
          public static final String LOG_ID = "logId";
      
          @Override
          public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
              String requestURI = request.getRequestURI();
              String uuid = UUID.randomUUID().toString();
              request.setAttribute(LOG_ID, uuid);
      
              if (handler instanceof HandlerMethod){
                  HandlerMethod hm = (HandlerMethod) handler;
              }
      
              log.info("REQUEST [{}][{}][{}]", uuid, requestURI, handler);
              return true;
          }
      
          @Override
          public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
              log.info("postHandle [{}]", modelAndView);
          }
      
          @Override
          public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
              String requestURI = request.getRequestURI();
              String uuid = (String) request.getAttribute(LOG_ID);
              log.info("RESPONSE [{}][{}]", uuid, requestURI);
              if(ex != null){
                  log.error("afterCompletion error!!", ex);
              }
          }
      }
  • Configuration ๋“ฑ๋ก ์ฝ”๋“œ
    @Configuration
    public class WebConfig implements WebMvcConfigurer {
    
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(new LoginInterceptor())
                    .order(1)
                    .addPathPatterns("/**")
                    .excludePathPatterns("/css/**", "/*.ico", "/error");
        }
    }

์Šคํ”„๋ง ์ธํ„ฐ์…‰ํ„ฐ - ์ธ์ฆ ์š”์ฒญ

  • ์ธํ„ฐ์…‰ํ„ฐ ์ฝ”๋“œ
    • URL ์ฒดํฌ๋ฅผ Configuration ํด๋ž˜์Šค์—์„œ ํ•˜๊ธฐ๋•Œ๋ฌธ์— ์ธํ„ฐ์…‰ํ„ฐ ๋กœ์ง์ด ๋‹จ์ˆœํ•ด์ง„๋‹ค.

      @Slf4j
      public class LoginCheckInterceptor implements HandlerInterceptor {
          @Override
          public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
              String requestURI = request.getRequestURI();
              log.info("์ธ์ฆ ์ฒดํฌ ์ธํ„ฐ์…‰ํ„ฐ ์‹คํ–‰ {}", requestURI);
      
              HttpSession session = request.getSession(false);
              if(session == null || session.getAttribute(SessionConst.LOGIN_MEMBER) == null){
                  log.info("๋ฏธ์ธ์ฆ ์‚ฌ์šฉ์ž ์š”์ฒญ");
                  response.sendRedirect("login?redirectURL=" + requestURI);
                  return false;
              }
              return true;
          }
      }
  • Configuration ๋“ฑ๋ก ์ฝ”๋“œ
    @Configuration
    public class WebConfig implements WebMvcConfigurer {
    
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(new LoginInterceptor())
                    .order(1)
                    .addPathPatterns("/**")
                    .excludePathPatterns("/css/**", "/*.ico", "/error");
    
            registry.addInterceptor(new LoginCheckInterceptor())
                    .order(2)
                    .addPathPatterns("/**")
                    .excludePathPatterns("/", "/members/add", "/login", "/logout", "/css/**", "/*.ico", "/error");
        }

Argument Resolver

  • @Login Annotation์ด ์žˆ์œผ๋ฉด ์ง์ ‘ ๋งŒ๋“  ArgumentResolver๊ฐ€ ๋™์ž‘ํ•ด์„œ ์ž๋™์œผ๋กœ ์„ธ์…˜์— ์žˆ๋Š” ๋กœ๊ทธ์ธ ํšŒ์›์„ ์ฐพ์•„์ฃผ๊ณ , ๋งŒ์•ฝ ์„ธ์…˜์— ์—†๋‹ค๋ฉด null ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•˜๋„๋ก ๊ฐœ๋ฐœ

๋กœ๊ทธ์ธ Annotation ์ƒ์„ฑ

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Login {
}
  • Target: ํŒŒ๋ผ๋ฏธํ„ฐ์—๋งŒ ์‚ฌ์šฉ
  • Retatetion: ๋ฆฌํ”Œ๋ ‰์…˜ ๋“ฑ์„ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ๋Ÿฐํƒ€์ž„๊นŒ์ง€ ์• ๋…ธํ…Œ์ด์…˜ ์ •๋ณด๊ฐ€ ๋‚จ์•„์žˆ์Œ.

LoginMenberArgumentResolver ์ƒ์„ฑ

@Slf4j
public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver {
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        log.info("supportParameter ์‹คํ–‰");
        boolean hasLoginAnnotation = parameter.hasParameterAnnotation(Login.class);
        boolean hasMemberType = Member.class.isAssignableFrom(parameter.getParameterType());

        return hasLoginAnnotation && hasMemberType;
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        log.info("resolveArgument ์‹คํ–‰");

        HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
        HttpSession session = request.getSession(false);
        if(session == null){
            return null;
        }

        return session.getAttribute(SessionConst.LOGIN_MEMBER);
    }
}
  • supportsParameter : @Login ์• ๋…ธํ…Œ์ด์…˜์ด ์žˆ์œผ๋ฉด์„œ Member ํƒ€์ž…์ด๋ฉด ํ•ด๋‹น ArgumentResolver๊ฐ€ ์‚ฌ์šฉ๋œ๋‹ค.
  • resolveArgument : ์ปจํŠธ๋กค๋Ÿฌ ํ˜ธ์ถœ ์ง์ „์— ํ˜ธ์ถœ๋˜์–ด์„œ ํ•„์š”ํ•œ ํŒŒ๋ผ๋ฏธํ„ฐ ์ •๋ณด๋ฅผ ์ƒ์„ฑ

Configuration์— ์„ค์ • ์ถ”๊ฐ€

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(new LoginMemberArgumentResolver());
    }
}

โœ”๏ธ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ์™€ ์˜ค๋ฅ˜ ํŽ˜์ด์ง€

์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜

์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์€ ์‚ฌ์šฉ์ž ์š”์ฒญ๋ณ„๋กœ ๋ณ„๋„์˜ ์“ฐ๋ ˆ๋“œ๊ฐ€ ํ• ๋‹น๋˜๊ณ , ์„œ๋ธ”๋ฆฟ ์ปจํ…Œ์ด๋„ˆ ์•ˆ์—์„œ ์‹คํ–‰๋œ๋‹ค. ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ–ˆ๋Š”๋ฐ, ์–ด๋””์„ ๊ฐ€ try ~ catch๋กœ ์˜ˆ์™ธ๋ฅผ ์žก์•„์„œ ์ฒ˜๋ฆฌํ•˜๋ฉด ์•„๋ฌด๋Ÿฐ ๋ฌธ์ œ๊ฐ€ ์—†๋‹ค. ๊ทธ๋Ÿฐ๋ฐ ๋งŒ์•ฝ์— ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ ์˜ˆ์™ธ๋ฅผ ์žก์ง€ ๋ชปํ•˜๊ณ , ์„œ๋ธ”๋ฆฟ ๋ฐ–์œผ๋กœ ๊นŒ์ง€ ์˜ˆ์™ธ๊ฐ€ ์ „๋‹ฌ๋˜๋ฉด ์–ด๋–ป๊ฒŒ ๋™์ž‘ํ• ๊นŒ?

Exception

  • ์˜ˆ์™ธ์ฒ˜๋ฆฌ๋ฅผ ํ•˜์ง€๋ชปํ•œ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•˜๊ฒŒ ๋˜๋ฉด ์„œ๋ธ”๋ฆฟ๋ฐ–์ธ WAS(Tomcat)๊นŒ์ง€ ์ „๋‹ฌ์ด ๋˜๊ณ  ํ†ฐ์บฃ์ด ๊ธฐ๋ณธ์œผ๋กœ ์ œ๊ณตํ•˜๋Š” ์˜ค๋ฅ˜ ํ™”๋ฉด์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค
    • 500 - Internal Server Error
  • ๋ผ์šฐํŒ…์„ ํ•ด๋†“์ง€ ์•Š์€ ์•„๋ฌด URL์„ ํ˜ธ์ถœ ์‹œ์—๋Š” 404 - Not Found ์˜ค๋ฅ˜ ํ™”๋ฉด ๋ฐ˜ํ™˜
โš™๏ธ **WAS(์—ฌ๊ธฐ๊นŒ์ง€ ์ „ํŒŒ) <- ํ•„ํ„ฐ <- ์„œ๋ธ”๋ฆฟ <- ์ธํ„ฐ์…‰ํ„ฐ <- ์ปจํŠธ๋กค๋Ÿฌ(์˜ˆ์™ธ๋ฐœ์ƒ)**

SendError

  • response.sendError(HTTP ์ƒํƒœ ์ฝ”๋“œ, ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€)
    • ์ƒํƒœ ์ฝ”๋“œ์™€ ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€๋„ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ๋‹ค.
  • ํ•ด๋‹น ๋ฉ”์„œ๋“œ๊ฐ€ ์‹คํ–‰๋˜๋ฉด ๋‹น์žฅ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•˜๋Š” ๊ฒƒ์€ ์•„๋‹ˆ์ง€๋งŒ, ์„œ๋ธ”๋ฆฟ ์ปจํ…Œ์ด๋„ˆ์—๊ฒŒ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ๋‹ค๋Š” ์ ์„ ์ „๋‹ฌ ํ•  ์ˆ˜ ์žˆ๋‹ค.
โš™๏ธ **WAS(sendError ํ˜ธ์ถœ ๊ธฐ๋ก ํ™•์ธ) <- ํ•„ํ„ฐ <- ์„œ๋ธ”๋ฆฟ <- ์ธํ„ฐ์…‰ํ„ฐ <- ์ปจํŠธ๋กค๋Ÿฌ**

์„œ๋ธ”๋ฆฟ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ

์„œ๋ธ”๋ฆฟ ์˜ค๋ฅ˜ ํŽ˜์ด์ง€ ๋“ฑ๋ก

  • ์ฝ”๋“œ
    package hello.exception;
    
    @Component
    public class WebServerCustomizer implements WebServerFactoryCustomizer<ConfigurableWebServerFactory> {
        @Override
        public void customize(ConfigurableWebServerFactory factory) {
            ErrorPage errorPage404 = new ErrorPage(HttpStatus.NOT_FOUND, "/error-page/404");
            ErrorPage errorPage500 = new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/error-page/500");
            ErrorPage errorPageEx = new ErrorPage(RuntimeException.class, "/error-page/500");
            factory.addErrorPages(errorPage404, errorPage500, errorPageEx);
        }
    }

์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ ์ปจํŠธ๋กค๋Ÿฌ ๋“ฑ๋ก

  • ์ฝ”๋“œ
    @Slf4j
    @Controller
    public class ServletExController {
    
        @GetMapping("/error-ex")
        public void errorEx(){
            throw new RuntimeException("์˜ˆ์™ธ ๋ฐœ์ƒ!");
        }
        @GetMapping("/error-404")
        public void error404(HttpServletResponse response) throws IOException {
            response.sendError(404, "404์˜ค๋ฅ˜!");
        }
        @GetMapping("/error-500")
        public void error500(HttpServletResponse response) throws IOException {
            response.sendError(500);
        }
    }

์„œ๋ธ”๋ฆฟ ์˜ค๋ฅ˜ ํŽ˜์ด์ง€ ์ž‘๋™์›๋ฆฌ

โš™๏ธ **WAS(์—ฌ๊ธฐ๊นŒ์ง€ ์ „ํŒŒ) <- ํ•„ํ„ฐ <- ์„œ๋ธ”๋ฆฟ <- ์ธํ„ฐ์…‰ํ„ฐ <- ์ปจํŠธ๋กค๋Ÿฌ(์˜ˆ์™ธ๋ฐœ์ƒ)

WAS /error-page/500 ๋‹ค์‹œ ์š”์ฒญ -> ํ•„ํ„ฐ -> ์„œ๋ธ”๋ฆฟ -> ์ธํ„ฐ์…‰ํ„ฐ -> ์ปจํŠธ๋กค๋Ÿฌ(/errorpage/500) -> View**

  1. ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•ด์„œ WAS๊นŒ์ง€ ์ „ํŒŒ๋œ๋‹ค.
  2. WAS๋Š” ์˜ค๋ฅ˜ ํŽ˜์ด์ง€ ๊ฒฝ๋กœ๋ฅผ ์ฐพ์•„์„œ ๋‚ด๋ถ€์—์„œ ์˜ค๋ฅ˜ ํŽ˜์ด์ง€๋ฅผ ํ˜ธ์ถœํ•œ๋‹ค. ์ด๋•Œ ์˜ค๋ฅ˜ ํŽ˜์ด์ง€ ๊ฒฝ๋กœ๋กœ ํ•„ํ„ฐ, ์„œ๋ธ”๋ฆฟ, ์ธํ„ฐ์…‰ํ„ฐ, ์ปจํŠธ๋กค๋Ÿฌ๊ฐ€ ๋ชจ๋‘ ๋‹ค์‹œ ํ˜ธ์ถœ๋œ๋‹ค.
    • ์˜ค๋ฅ˜ ์ •๋ณด ์ถ”๊ฐ€
      • WAS๋Š” ์˜ค๋ฅ˜ ํŽ˜์ด์ง€๋ฅผ ๋‹จ์ˆœํžˆ ๋‹ค์‹œ ์š”์ฒญ๋งŒ ํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ, ์˜ค๋ฅ˜ ์ •๋ณด๋ฅผ request ์˜ attribute์— ์ถ”๊ฐ€ํ•ด์„œ ๋„˜๊ฒจ์ค€๋‹ค.

        javax.servlet.error.exception : ์˜ˆ์™ธ
        javax.servlet.error.exception_type : ์˜ˆ์™ธ ํƒ€์ž…
        javax.servlet.error.message : ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€
        javax.servlet.error.request_uri : ํด๋ผ์ด์–ธํŠธ ์š”์ฒญ URI
        javax.servlet.error.servlet_name : ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•œ ์„œ๋ธ”๋ฆฟ ์ด๋ฆ„
        javax.servlet.error.status_code : HTTP ์ƒํƒœ ์ฝ”๋“œ

์„œ๋ธ”๋ฆฟ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ - ํ•„ํ„ฐ

  • ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด WAS์—์„œ ๋‹ค์‹œ ํ•œ๋ฒˆ ํ˜ธ์ถœ์ด ๋ฐœ์ƒํ•˜๋Š”๋ฐ ์ด๋•Œ ๋‹ค์‹œ ํ•„ํ„ฐ์™€ ์ธํ„ฐ์…‰ํ„ฐ๊ฐ€ ํ˜ธ์ถœ๋œ๋‹ค. ๊ทธ๋Ÿฐ๋ฐ ํ•„ํ„ฐ์™€ ์ธํ„ฐ์…‰ํ„ฐ๋Š” ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•œ ์š”์ฒญ์—์„œ ์ด๋ฏธ ํ•œ๋ฒˆ ์™„๋ฃŒ๋œ ์‚ฌํ•ญ์ด๊ธฐ ๋•Œ๋ฌธ์— ๊ตณ์ด ํ˜ธ์ถœํ•  ํ•„์š”๊ฐ€ ์—†๋‹ค.
  • ์„œ๋ธ”๋ฆฟ์€ ์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด DispatcherType ์ด๋ผ๋Š” ์ถ”๊ฐ€์ •๋ณด๋ฅผ ์ œ๊ณตํ•œ๋‹ค.
    • DispatcherType

      REQUEST : ํด๋ผ์ด์–ธํŠธ ์š”์ฒญ
      ERROR : ์˜ค๋ฅ˜ ์š”์ฒญ
      FORWARD : MVC์—์„œ ๋ฐฐ์› ๋˜ ์„œ๋ธ”๋ฆฟ์—์„œ ๋‹ค๋ฅธ ์„œ๋ธ”๋ฆฟ์ด๋‚˜ JSP๋ฅผ ํ˜ธ์ถœํ•  ๋•Œ
      RequestDispatcher.forward(request, response);
      INCLUDE : ์„œ๋ธ”๋ฆฟ์—์„œ ๋‹ค๋ฅธ ์„œ๋ธ”๋ฆฟ์ด๋‚˜ JSP์˜ ๊ฒฐ๊ณผ๋ฅผ ํฌํ•จํ•  ๋•Œ
      RequestDispatcher.include(request, response);
      ASYNC : ์„œ๋ธ”๋ฆฟ ๋น„๋™๊ธฐ ํ˜ธ์ถœ

  • ํ•ด๊ฒฐ๋ฐฉ๋ฒ•
    • Configuration์—์„œ Filter๋ฅผ ์ถ”๊ฐ€ํ•  ๋•Œ ํ•ด๋‹น ์ฝ”๋“œ๋ฅผ ์ถ”๊ฐ€ํ•˜๋ฉด ๋œ๋‹ค.
      • filterRegistrationBean.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ERROR);
      • setDispatcherTypes์˜ Default Value๋Š” Request

์„œ๋ธ”๋ฆฟ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ - ์ธํ„ฐ์…‰ํ„ฐ

  • ์ธํ„ฐ์…‰ํ„ฐ์—๋Š” DispatcherType๊ณผ ๊ฐ™์€ ๊ฒƒ์ด ์—†๋‹ค.
  • excludePathPatterns์— error-page ๊ฒฝ๋กœ๋ฅผ ์ถ”๊ฐ€ํ•ด์ฃผ๋ฉด ๋œ๋‹ค.

์Šคํ”„๋ง๋ถ€ํŠธ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ

  • ์Šคํ”„๋ง ๋ถ€ํŠธ๋Š” ์—๋ŸฌํŽ˜์ด์ง€๋ฅผ ์ž๋™์œผ๋กœ ๋“ฑ๋กํ•œ๋‹ค.

    • /error ๋ผ๋Š” ๊ฒฝ๋กœ๋กœ ๊ธฐ๋ณธ ์˜ค๋ฅ˜ ํŽ˜์ด์ง€๋ฅผ ์„ค์ •ํ•œ๋‹ค.
  • WebServerCustomizer๋กœ ์ƒํƒœ์ฝ”๋“œ์™€ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๋ฅผ ์„ค์ •ํ•˜์ง€ ์•Š์œผ๋ฉด ๊ธฐ๋ณธ ์˜ค๋ฅ˜ ํŽ˜์ด์ง€๋กœ ์‚ฌ์šฉ๋œ๋‹ค.

  • BasicErrorController๋ผ๋Š” ์Šคํ”„๋ง ์ปจํŠธ๋กค๋Ÿฌ ๋˜ํ•œ ์ž๋™์œผ๋กœ ๋“ฑ๋กํ•œ๋‹ค.

    • ErrorPage์—์„œ ๋“ฑ๋กํ•œ /error๋ฅผ ๋งคํ•‘ํ•ด์„œ ์ฒ˜๋ฆฌํ•˜๋Š” ์ปจํŠธ๋กค๋Ÿฌ
  • ๊ฐœ๋ฐœ์ž๋Š” ์˜ค๋ฅ˜ํŽ˜์ด์ง€(๋ทฐ)๋งŒ ๋“ฑ๋กํ•˜๋ฉด ๋œ๋‹ค.

    • ์šฐ์„ ์ˆœ์œ„
      1. ๋ทฐํ…œํ”Œ๋ฆฟ
        1. resources/templates/error/500.html
        2. resources/templates/error/5xx.html
      2. ์ •์  ๋ฆฌ์†Œ์Šค
      3. ์ ์šฉ ๋Œ€์ƒ์ด ์—†์„ ๋•Œ ๋ทฐ ์ด๋ฆ„(error)
  • ์˜ค๋ฅ˜ ์ •๋ณด๋ฅผ ๋ทฐ ํŽ˜์ด์ง€์— ์‰ฝ๊ฒŒ ๋…ธ์ถœ์‹œํ‚ฌ ์ˆ˜ ์žˆ๋‹ค.

    • ์ฝ”๋“œ
      <!DOCTYPE HTML>
      <html xmlns:th="http://www.thymeleaf.org">
      <head>
       <meta charset="utf-8">
      </head>
      <body>
      <div class="container" style="max-width: 600px">
       <div class="py-5 text-center">
       <h2>500 ์˜ค๋ฅ˜ ํ™”๋ฉด ์Šคํ”„๋ง ๋ถ€ํŠธ ์ œ๊ณต</h2>
       </div>
       <div>
       <p>์˜ค๋ฅ˜ ํ™”๋ฉด ์ž…๋‹ˆ๋‹ค.</p>
       </div>
       <ul>
       <li>์˜ค๋ฅ˜ ์ •๋ณด</li>
       <ul>
       <li th:text="|timestamp: ${timestamp}|"></li>
       <li th:text="|path: ${path}|"></li>
       <li th:text="|status: ${status}|"></li>
       <li th:text="|message: ${message}|"></li>
       <li th:text="|error: ${error}|"></li>
       <li th:text="|exception: ${exception}|"></li>
       <li th:text="|errors: ${errors}|"></li>
       <li th:text="|trace: ${trace}|"></li>
       </ul>
       </li>
       </ul>
       <hr class="my-4">
      </div> <!-- /container -->
      </body>
      </html>
    • ์˜ต์…˜
      • application.properties
        server.error.include-exception=true
        server.error.include-message=on_param
        server.error.include-stacktrace=on_param
        server.error.include-binding-errors=on_param
      • ๊ธฐ๋ณธ ๊ฐ’์ด never ์ธ ๋ถ€๋ถ„์€ ๋‹ค์Œ 3๊ฐ€์ง€ ์˜ต์…˜์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.
        • never : ์‚ฌ์šฉํ•˜์ง€ ์•Š์Œ
        • always :ํ•ญ์ƒ ์‚ฌ์šฉ
        • on_param : ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ์žˆ์„ ๋•Œ ์‚ฌ์šฉ

โœ”๏ธ API ์˜ˆ์™ธ์ฒ˜๋ฆฌ

์ง์ ‘ ์ปจํŠธ๋กค๋Ÿฌ ์ฒ˜๋ฆฌ

		@RequestMapping(value = "/error-page/500", produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity <Map<String, Object>> errorPage500Api(HttpServletRequest request, HttpServletResponse response){
        Map<String, Object> result = new HashMap<>();
        Exception ex = (Exception) request.getAttribute(RequestDispatcher.ERROR_EXCEPTION);
        result.put("status", request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE));
        result.put("message", ex.getMessage());

        Integer statusCode = (Integer) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
        return new ResponseEntity(result, HttpStatus.valueOf(statusCode));
    }
  • produces=MediaType.APPLICATION_JSON_VALUE๋ฅผ ์„ ์–ธํ•˜๋ฉด ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์š”์ฒญํ•˜๋Š” HTTP Header์˜ Accpet์˜ ๊ฐ’์ด application/json์ผ ๋•Œ ํ•ด๋‹น ๋ฉ”์„œ๋“œ๊ฐ€ ์šฐ์„ ํ˜ธ์ถœ๋œ๋‹ค. โ€ป Jackson ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋Š” Map์„ Json ๊ตฌ์กฐ๋กœ ๋ณ€ํ™˜ํ•  ์ˆ˜ ์žˆ๋‹ค.

์Šคํ”„๋ง ๋ถ€ํŠธ ๊ธฐ๋ณธ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ

  • Request Accept ํƒ€์ž…์ด HTML์ผ ๋•Œ์™€ ๊ฐ™์ด Application/json ์ผ ๋•Œ์—๋„ BasicErrorController์—์„œ ์ž๋™์œผ๋กœ JSON ํ˜•์‹์œผ๋กœ ์˜ค๋ฅ˜์ฒ˜๋ฆฌ๋ฅผ ํ•ด์ค€๋‹ค.
    • BasicErrorController๋ฅผ ํ™•์žฅํ•˜๋ฉด JSON ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€๋„ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ๋‹ค.
  • ํ•˜์ง€๋งŒ HTML ํ˜•์‹์ด ์•„๋‹Œ JSONํ˜•์‹์˜ ๊ฒฝ์šฐ์—๋Š” API๋งˆ๋‹ค ๋ฐœ์ƒํ•˜๋Š” ์˜ˆ์™ธ์˜ ๊ฒฝ์šฐ๊ฐ€ ๋‹ค์–‘ํ•˜๊ณ  ๋ณต์žกํ•˜๋‹ค. ๋”ฐ๋ผ์„œ BasicErrorController๋ฅผ ์ด์šฉํ•œ ๋ฐฉ์‹์€ 4xx, 5xx ์˜ค๋ฅ˜์™€ ๊ฐ™์ด ์ผ๊ด€๋˜๊ฒŒ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” HTML ํ™”๋ฉด์„ ์ฒ˜๋ฆฌํ•  ๋•Œ ์‚ฌ์šฉํ•˜๊ณ  API ์˜ค๋ฅ˜์ฒ˜๋ฆฌ๋Š” @ExceptionHandler ๋ฅผ ์‚ฌ์šฉํ•˜์ž

HandlerExceptionResolver

  • ๊ธฐ์กด์—๋Š” ์ปจํŠธ๋กค๋Ÿฌ์—์„œ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•ด์„œ ์„œ๋ธ”๋ฆฟ์„ ๋„˜์–ด WAS๊นŒ์ง€ ์ „๋‹ฌ๋˜๋ฉด 500์—๋Ÿฌ๋ฅผ ๋˜์ ธ์คฌ๋‹ค. ์ด ์—๋Ÿฌ๋ฅผ 400์—๋Ÿฌ, 404์—๋Ÿฌ ๋“ฑ ์ž์‹ ์ด ์›ํ•˜๋Š” ์—๋Ÿฌ๋กœ ๋ฐ”๊ฟ”์„œ ๋˜์ ธ์ฃผ๊ฑฐ๋‚˜ ๋ฉ”์‹œ์ง€, ํ˜•์‹์„ ๋ฐ”๊พธ๊ธฐ ์œ„ํ•ด์„œ๋Š” ExceptionResolver๋ฅผ ์กฐ์ž‘ํ•ด์•ผํ•œ๋‹ค.

- ์ฝ”๋“œ1
    
    ```java
    @Slf4j
    public class MyHandlerExceptionResolver implements HandlerExceptionResolver {
     @Override
     public ModelAndView resolveException(HttpServletRequest request,
    HttpServletResponse response, Object handler, Exception ex) {
    	 try {
    			 if (ex instanceof IllegalArgumentException) {
    			 log.info("IllegalArgumentException resolver to 400");
    			 response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage());
    			 return new ModelAndView();
    	 }
    	 } catch (IOException e) {
    			 log.error("resolver ex", e);
    	 }
    	 return null;
     }
    }
    ```
    

๋ฐ˜ํ™˜ ๊ฐ’์— ๋”ฐ๋ฅธ ๋™์ž‘ ๋ฐฉ์‹

  • HandlerExceptionResolver ์˜ ๋ฐ˜ํ™˜ ๊ฐ’์— ๋”ฐ๋ฅธ DispatcherServlet ์˜ ๋™์ž‘ ๋ฐฉ์‹์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.
    • ๋นˆ ModelAndView
      • new ModelAndView() ์ฒ˜๋Ÿผ ๋นˆ ModelAndView ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋ฉด ๋ทฐ๋ฅผ ๋ Œ๋”๋ง ํ•˜์ง€ ์•Š๊ณ , ์ •์ƒ ํ๋ฆ„์œผ๋กœ ์„œ๋ธ”๋ฆฟ์ด ๋ฆฌํ„ด๋œ๋‹ค.
    • ModelAndView ์ง€์ •
      • ModelAndView ์— View , Model ๋“ฑ์˜ ์ •๋ณด๋ฅผ ์ง€์ •ํ•ด์„œ ๋ฐ˜ํ™˜ํ•˜๋ฉด ๋ทฐ๋ฅผ ๋ Œ๋”๋งํ•œ๋‹ค.
    • null
      • null ์„ ๋ฐ˜ํ™˜ํ•˜๋ฉด, ๋‹ค์Œ ExceptionResolver ๋ฅผ ์ฐพ์•„์„œ ์‹คํ–‰ํ•œ๋‹ค. ๋งŒ์•ฝ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š”
        ExceptionResolver ๊ฐ€ ์—†์œผ๋ฉด ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๊ฐ€ ์•ˆ๋˜๊ณ , ๊ธฐ์กด์— ๋ฐœ์ƒํ•œ ์˜ˆ์™ธ๋ฅผ ์„œ๋ธ”๋ฆฟ ๋ฐ–์œผ๋กœ ๋˜์ง„๋‹ค.

ExceptionResolver ํ™œ์šฉ

  • ์˜ˆ์™ธ ์ƒํƒœ ์ฝ”๋“œ ๋ณ€ํ™˜
    • ์˜ˆ์™ธ๋ฅผ response.sendError(xxx) ํ˜ธ์ถœ๋กœ ๋ณ€๊ฒฝํ•ด์„œ ์„œ๋ธ”๋ฆฟ์—์„œ ์ƒํƒœ ์ฝ”๋“œ์— ๋”ฐ๋ฅธ ์˜ค๋ฅ˜๋ฅผ
      ์ฒ˜๋ฆฌํ•˜๋„๋ก ์œ„์ž„
    • ์ดํ›„ WAS๋Š” ์„œ๋ธ”๋ฆฟ ์˜ค๋ฅ˜ ํŽ˜์ด์ง€๋ฅผ ์ฐพ์•„์„œ ๋‚ด๋ถ€ ํ˜ธ์ถœ, ์˜ˆ๋ฅผ ๋“ค์–ด์„œ ์Šคํ”„๋ง ๋ถ€ํŠธ๊ฐ€ ๊ธฐ๋ณธ์œผ๋กœ ์„ค์ •ํ•œ /error ๊ฐ€ ํ˜ธ์ถœ๋จ
  • ๋ทฐ ํ…œํ”Œ๋ฆฟ ์ฒ˜๋ฆฌ
    • ModelAndView ์— ๊ฐ’์„ ์ฑ„์›Œ์„œ ์˜ˆ์™ธ์— ๋”ฐ๋ฅธ ์ƒˆ๋กœ์šด ์˜ค๋ฅ˜ ํ™”๋ฉด ๋ทฐ ๋ Œ๋”๋ง ํ•ด์„œ ๊ณ ๊ฐ์—๊ฒŒ ์ œ๊ณต
  • API ์‘๋‹ต ์ฒ˜๋ฆฌ
    • response.getWriter().println("hello"); ์ฒ˜๋Ÿผ HTTP ์‘๋‹ต ๋ฐ”๋””์— ์ง์ ‘ ๋ฐ์ดํ„ฐ๋ฅผ ๋„ฃ์–ด์ฃผ๋Š”
      ๊ฒƒ๋„ ๊ฐ€๋Šฅํ•˜๋‹ค. ์—ฌ๊ธฐ์— JSON ์œผ๋กœ ์‘๋‹ตํ•˜๋ฉด API ์‘๋‹ต ์ฒ˜๋ฆฌ๋ฅผ ํ•  ์ˆ˜ ์žˆ๋‹ค.
  • ์ฝ”๋“œ
    @Slf4j
    public class UserHandlerExceptionResolver implements HandlerExceptionResolver {
     private final ObjectMapper objectMapper = new ObjectMapper();
     @Override
     public ModelAndView resolveException(HttpServletRequest request,
    HttpServletResponse response, Object handler, Exception ex) {
     try {
    	 if (ex instanceof UserException) {
    		 log.info("UserException resolver to 400");
    		 String acceptHeader = request.getHeader("accept");
    		 response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
    			 if ("application/json".equals(acceptHeader)) {
    				 Map<String, Object> errorResult = new HashMap<>();
    				 errorResult.put("ex", ex.getClass());
    				 errorResult.put("message", ex.getMessage());
    
    				 String result = objectMapper.writeValueAsString(errorResult);
    				
    				 response.setContentType("application/json");
    				 response.setCharacterEncoding("utf-8");
    				 response.getWriter().write(result);
    				 return new ModelAndView();
    			 } else {
    			 //TEXT/HTML
    				 return new ModelAndView("error/500");
    			 }
    	 }
     } catch (IOException e) {
     log.error("resolver ex", e);
     }
     return null;
     }
    }

์Šคํ”„๋ง์ด ์ œ๊ณตํ•˜๋Š” ExceptionResolver

โš™๏ธ 1.**ExceptionHandlerExceptionResolver** - `@ExceptionHandler` 2.ResponseStatusExceptionResolver - HTTP ์ƒํƒœ ์ฝ”๋“œ ์ง€์ •3.DefaultHandlerExceptionResolver - ์Šคํ”„๋ง ๋‚ด๋ถ€ ๊ธฐ๋ณธ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ

2. ResponseStatusExceptionResolver

  1. **@ResponseStatus ๊ฐ€ ๋‹ฌ๋ ค์žˆ๋Š” ์˜ˆ์™ธ ์ฒ˜๋ฆฌ**

    @ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "์ž˜๋ชป๋œ ์š”์ฒญ ์˜ค๋ฅ˜")
    public class BadRequestException extends RuntimeException {
    }
    • sendError๋ฅผ ํ†ตํ•ด ์ง€์ •ํ•œ ์ƒํƒœ์ฝ”๋“œ, ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ โ†’ WAS์—์„œ ๋‹ค์‹œ ์˜ค๋ฅ˜ํŽ˜์ด์ง€๋ฅผ ๋‚ด๋ถ€ ์š”์ฒญ
  2. ResponseStatusException ์˜ˆ์™ธ ์ฒ˜๋ฆฌ

    @GetMapping("/api/response-status-ex2")
    public String responseStatusEx2() {
     throw new ResponseStatusException(HttpStatus.NOT_FOUND, "error.bad", new
    IllegalArgumentException());
    }
    • @ResponseStatus๋Š” ๊ฐœ๋ฐœ์ž๊ฐ€ ์ง์ ‘ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์—†๋Š” ์˜ˆ์™ธ์—๋Š” ์ ์šฉํ•  ์ˆ˜ ์—†๋‹ค.
      • ๋™์  ๋ณ€๊ฒฝ์„ ์œ„ํ•ด์„œ๋Š” ReponseStatusException ์‚ฌ์šฉ

    โ€ป ๋ฉ”์‹œ์ง€ ๊ธฐ๋Šฅ ์‚ฌ์šฉ ๊ฐ€๋Šฅ - messages.properties์— ๋ฉ”์‹œ์ง€ ์ง€์ • ํ›„ reason ๊ฐ’์— ๋Œ€์ž…

1. ExceptionHandlerExceptionResolver - @ExceptionHandler

  • @ExceptionHandler ๋ฅผ ์„ ์–ธํ•˜๊ณ , ํ•ด๋‹น ์ปจํŠธ๋กค๋Ÿฌ์—์„œ ์ฒ˜๋ฆฌํ•˜๊ณ  ์‹ถ์€ ์˜ˆ์™ธ๋ฅผ ์ง€์ •ํ•ด์ฃผ๋ฉด ๋œ๋‹ค.
  • ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋‹ค์–‘ํ•œ ์˜ˆ์™ธ๋ฅผ ํ•œ๋ฒˆ์— ์ฒ˜๋ฆฌ ํ•  ์ˆ˜ ์žˆ๋‹ค.
    • @ExceptionHandler({AException.class, BException.class})
  • โ—์‹คํ–‰ํ๋ฆ„
    • ์ปจํŠธ๋กค๋Ÿฌ๋ฅผ ํ˜ธ์ถœํ•œ ๊ฒฐ๊ณผ IllegalArgumentException ์˜ˆ์™ธ๊ฐ€ ์ปจํŠธ๋กค๋Ÿฌ ๋ฐ–์œผ๋กœ ๋˜์ ธ์ง„๋‹ค.
      • ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ–ˆ์œผ๋กœ ExceptionResolver ๊ฐ€ ์ž‘๋™ํ•œ๋‹ค. ๊ฐ€์žฅ ์šฐ์„ ์ˆœ์œ„๊ฐ€ ๋†’์€
        ExceptionHandlerExceptionResolver ๊ฐ€ ์‹คํ–‰๋œ๋‹ค.
      • ExceptionHandlerExceptionResolver ๋Š” ํ•ด๋‹น ์ปจํŠธ๋กค๋Ÿฌ์— IllegalArgumentException ์„ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” @ExceptionHandler ๊ฐ€ ์žˆ๋Š”์ง€ ํ™•์ธํ•œ๋‹ค.
      • illegalExHandle() ๋ฅผ ์‹คํ–‰ํ•œ๋‹ค.
      • @RestController ์ด๋ฏ€๋กœ illegalExHandle() ์—๋„ @ResponseBody ๊ฐ€ ์ ์šฉ๋œ๋‹ค. ๋”ฐ๋ผ์„œ HTTP ์ปจ๋ฒ„ํ„ฐ๊ฐ€ ์‚ฌ์šฉ๋˜๊ณ , ์‘๋‹ต์ด ๋‹ค์Œ๊ณผ ๊ฐ™์€ JSON์œผ๋กœ ๋ฐ˜ํ™˜๋œ๋‹ค.
      • @ResponseStatus(HttpStatus.BAD_REQUEST) ๋ฅผ ์ง€์ •ํ–ˆ์œผ๋ฏ€๋กœ HTTP ์ƒํƒœ ์ฝ”๋“œ 400์œผ๋กœ ์‘๋‹ตํ•œ๋‹ค.

@ControllerAdvice

  • ๋Œ€์ƒ์œผ๋กœ ์ง€์ •ํ•œ ์—ฌ๋Ÿฌ ์ปจํŠธ๋กค๋Ÿฌ์— @ExceptionHandler, @InitBinder ๊ธฐ๋Šฅ์„ ๋ถ€์—ฌํ•ด์ค€๋‹ค. ์ง€์ •๋ฐฉ๋ฒ•
    // Target all Controllers annotated with @RestController
    @ControllerAdvice(annotations = RestController.class)
    public class ExampleAdvice1 {}
    
    // Target all Controllers within specific packages
    @ControllerAdvice("org.example.controllers")
    public class ExampleAdvice2 {}
    
    // Target all Controllers assignable to specific classes
    @ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class})
    public class ExampleAdvice3 {}

โœ”๏ธ ์Šคํ”„๋ง ํƒ€์ž… ์ปจ๋ฒ„ํ„ฐ

์Šคํ”„๋ง ํƒ€์ž… ๋ณ€ํ™˜ ์ ์šฉ ์˜ˆ

  • ์Šคํ”„๋ง MVC ์š”์ฒญ ํŒŒ๋ผ๋ฏธํ„ฐ
    • @RequestParam, @ModelAttribute, @PathVariable
  • ๋ทฐ๋ฅผ ๋ Œ๋”๋ง ํ•  ๋•Œ

ํƒ€์ž… ์ปจ๋ฒ„ํ„ฐ - Converter

  • Integer๋ฅผ String์œผ๋กœ ๋ณ€ํ™˜ํ•˜๋Š” Converter์™€ ๊ฐ™์ด ๋ฒ”์šฉ์ ์ธ ์ปจ๋ฒ„ํ„ฐ๋Š” ์Šคํ”„๋ง์ด ์ œ๊ณตํ•œ๋‹ค. ํ•˜์ง€๋งŒ ์ง์ ‘ ๋งŒ๋“  ๊ฐ์ฒดํ˜• ํƒ€์ž…์œผ๋กœ์˜ ๋ณ€ํ™˜์„ ์œ„ํ•œ ์ปจ๋ฒ„ํ„ฐ๋Š” ์ง์ ‘ ์„ ์–ธํ•ด์„œ ์ปจ๋ฒ„์ „ ์„œ๋น„์Šค๋ฅผ ์ด์šฉํ•˜๋Š” addFormatters๋กœ ์ถ”๊ฐ€ํ•ด์•ผํ•œ๋‹ค.
  1. ์ปจ๋ฒ„ํ„ฐ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ํ†ตํ•ด ์ปจ๋ฒ„ํ„ฐ ํด๋ž˜์Šค๋ฅผ ๊ตฌํ˜„ํ•œ๋‹ค.
  2. WebConfig์˜ addFormatters ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•ด ์ปจ๋ฒ„ํ„ฐ๋ฅผ ์ถ”๊ฐ€ํ•œ๋‹ค.
    1. ์Šคํ”„๋ง ๋‚ด๋ถ€์—์„œ ConversionService๋ฅผ ์ œ๊ณตํ•œ๋‹ค.

ConversionService

  • ํƒ€์ž… ์ปจ๋ฒ„ํ„ฐ๋ฅผ ์„ ์–ธํ•ด๋‘๊ณ  ํ•˜๋‚˜์”ฉ ์ง์ ‘ ์ฐพ์•„์„œ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์€ ๋งค์šฐ ๋ถˆํŽธํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์Šคํ”„๋ง์€ ๊ฐœ๋ณ„ ์ปจ๋ฒ„ํ„ฐ๋ฅผ ๋ชจ์•„๋‘๊ณ  ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ConversionService ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•œ๋‹ค.

์ธํ„ฐํŽ˜์ด์Šค ๋ถ„๋ฆฌ ์›์น™ - ISP

  • ISP๋Š” ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์ž์‹ ์ด ์ด์šฉํ•˜์ง€ ์•Š๋Š” ๋ฉ”์„œ๋“œ์— ์˜์กดํ•˜์ง€ ์•Š์•„์•ผ ํ•œ๋‹ค.
  • DefaultConversionService ๋‹ค์Œ ๋‘ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ–ˆ๋‹ค.
    • ConversionService : ์ปจ๋ฒ„ํ„ฐ ์‚ฌ์šฉ์— ์ดˆ์ 

    • ConversionRegistry : ์ปจ๋ฒ„ํ„ฐ ๋“ฑ๋ก์— ์ดˆ์ 

      ์ด๋ ‡๊ฒŒ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๋ถ„๋ฆฌํ•˜๋ฉด ์ปจ๋ฒ„ํ„ฐ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ํด๋ผ์ด์–ธํŠธ์™€ ์ปจ๋ฒ„ํ„ฐ๋ฅผ ๋“ฑ๋กํ•˜๊ณ  ๊ด€๋ฆฌํ•˜๋Š” ํด๋ผ์ด์–ธํŠธ์˜ ๊ด€์‹ฌ์‚ฌ๋ฅผ ๋ช…ํ™•ํ•˜๊ฒŒ ๋ถ„๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค. ํŠนํžˆ ์ปจ๋ฒ„ํ„ฐ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ํด๋ผ์ด์–ธํŠธ๋Š” ConversionService๋งŒ ์˜์กดํ•˜๋ฉด ๋˜๋ฏ€๋กœ, ์ปจ๋ฒ„ํ„ฐ๋ฅผ ์–ด๋–ป๊ฒŒ ๋“ฑ๋กํ•˜๊ณ  ๊ด€๋ฆฌํ•˜๋Š”์ง€ ์ „ํ˜€ ๋ชฐ๋ผ๋„ ๋œ๋‹ค.

๋ทฐ ํ…œํ”Œ๋ฆฟ์— ์ปจ๋ฒ„ํ„ฐ ์ ์šฉํ•˜๊ธฐ

  • ํƒ€์ž„๋ฆฌํ”„๋Š” ${{...}} ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์ž๋™์œผ๋กœ ์ปจ๋ฒ„์ „ ์„œ๋น„์Šค๋ฅผ ์‚ฌ์šฉํ•ด์„œ ๋ณ€ํ™˜๋œ ๊ฒฐ๊ณผ๋ฅผ ์ถœ๋ ฅํ•œ๋‹ค.
    • ๋ณ€์ˆ˜ํ‘œํ˜„์‹: ${...}
    • ์ปจ๋ฒ„์ „ ์„œ๋น„์Šค ์ ์šฉ: ${{...}}
  • th:field ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ ์ปจ๋ฒ„์ „ ์„œ๋น„์Šค๊ฐ€ ์ž๋™์œผ๋กœ ์ ์šฉ๋˜๊ธฐ ๋•Œ๋ฌธ์— ์ผ๋ฐ˜์ ์ธ ๋ณ€์ˆ˜ํ‘œํ˜„์‹์„ ์‚ฌ์šฉํ•˜๋ฉด ๋œ๋‹ค.

ํฌ๋งทํ„ฐ - Formatter

  • ์ปจ๋ฒ„ํ„ฐ๋Š” ๋ฒ”์šฉ (๊ฐ์ฒด โ†’ ๊ฐ์ฒด)
  • ํฌ๋งทํ„ฐ๋Š” ๋ฌธ์ž์— ํŠนํ™” (๊ฐ์ฒด โ†’ ๋ฌธ์ž, ๋ฌธ์ž โ†’ ๊ฐ์ฒด) + ํ˜„์ง€ํ™”(Locale)
  • ์ฝ”๋“œ
    public class MyNumberFormatter implements Formatter<Number> {
        @Override
        public Number parse(String text, Locale locale) throws ParseException {
            NumberFormat format = NumberFormat.getInstance(locale);
            return format.parse(text);
        }
        @Override
        public String print(Number object, Locale locale) {
            return NumberFormat.getInstance(locale).format(object);
        }
    }

ํฌ๋งทํ„ฐ๋ฅผ ์ง€์›ํ•˜๋Š” ์ปจ๋ฒ„์ „ ์„œ๋น„์Šค - FormattingConversionService

  • DefaultFormattingConversionService ๋Š” FormattingConversionService ์— ๊ธฐ๋ณธ์ ์ธ ํ†ตํ™”, ์ˆซ์ž ๊ด€๋ จ ๋ช‡๊ฐ€์ง€ ๊ธฐ๋ณธ ํฌ๋งทํ„ฐ๋ฅผ ์ถ”๊ฐ€ํ•ด์„œ ์ œ๊ณตํ•œ๋‹ค.
  • FormattingConversionService ๋Š” ConversionService๊ด€๋ จ ๊ธฐ๋Šฅ์„ ์ƒ์†๋ฐ›๊ธฐ ๋–„๋ฌธ์— ๊ฒฐ๊ณผ์ ์œผ๋กœ ์ปจ๋ฒ„ํ„ฐ๋„ ํฌ๋งทํ„ฐ๋„ ๋ชจ๋‘ ๋“ฑ๋กํ•  ์ˆ˜ ์žˆ๋‹ค.

โ†’ ์Šคํ”„๋ง ๋ถ€ํŠธ๋Š” DefaultFormattingConversionService๋ฅผ ์ƒ์† ๋ฐ›์€ WebConversionService๋ฅผ ๋‚ด๋ถ€์—์„œ ์‚ฌ์šฉํ•œ๋‹ค.

์Šคํ”„๋ง์ด ์ œ๊ณตํ•˜๋Š” ๊ธฐ๋ณธ ํฌ๋งทํ„ฐ

  • Formatter ์ธํ„ฐํŽ˜์ด์Šค์˜ ๊ตฌํ˜„ ํด๋ž˜์Šค๋ฅผ ์ฐพ์•„๋ณด๋ฉด ์ˆ˜ ๋งŽ์€ ๋‚ ์งœ๋‚˜ ์‹œ๊ฐ„ ๊ด€๋ จ ํฌ๋งทํ„ฐ๊ฐ€ ์ œ๊ณต๋˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค. ๊ทธ๋Ÿฐ๋ฐ ํฌ๋งทํ„ฐ๋Š” ๊ธฐ๋ณธํ˜•์‹์ด ์ง€์ •๋˜์–ด ์žˆ๊ธฐ ๋–„๋ฌธ์— ๊ฐ์ฒด์˜ ๊ฐ ํ•„๋“œ๋งˆ๋‹ค ๋‹ค๋ฅธ ํ˜•์‹์œผ๋กœ ํฌ๋งท์„ ์ง€์›ํ•˜๊ธฐ๋Š” ์–ด๋ ต๋‹ค. โ†’ ์ด๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ์Šคํ”„๋ง์—์„œ๋Š” ๋‘๊ฐ€์ง€ ์• ๋…ธํ…Œ์ด์…˜์„ ์ œ๊ณตํ•œ๋‹ค.
    • @NumberFormat

      • ์ˆซ์ž ๊ด€๋ จ ํ˜•์‹ ์ง€์ • ํฌ๋งทํ„ฐ ์‚ฌ์šฉ
    • @DateTimeFormat
      - ๋‚ ์งœ ๊ด€๋ จ ํ˜•์‹ ์ง€์ • ํฌ๋งทํ„ฐ ์‚ฌ์šฉ

      	@Data
       static class Form {
      	 @NumberFormat(pattern = "###,###")
      	 private Integer number;
      	 @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
      	 private LocalDateTime localDateTime;
       }

์ฃผ์˜

  • ๋ฉ”์‹œ์ง€ ์ปจ๋ฒ„ํ„ฐ(HttpMessageConverter)์—๋Š” ์ปจ๋ฒ„์ „ ์„œ๋น„์Šค๊ฐ€ ์ ์šฉ๋˜์ง€ ์•Š๋Š”๋‹ค.
  • HttpMessageConverter์˜ ์—ญํ• ์€ HTTP ๋ฉ”์‹œ์ง€ ๋ฐ”๋””์˜ ๋‚ด์šฉ์„ ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜ํ•˜๊ฑฐ๋‚˜ ๊ฐ์ฒด๋ฅผ HTTP๋ฉ”์‹œ์ง€๋ฐ”๋””์— ์ž…๋ ฅํ•˜๋Š” ๊ฒƒ์ด๋‹ค.
  • ์˜ˆ๋ฅผ ๋“ค์–ด JSON๋ฅผ ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ๋ฉ”์‹œ์ง€ ์ปจ๋ฒ„ํ„ฐ๋Š” ๋‚ด๋ถ€์—์„œ Jackson ๊ฐ™์€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค. JSON ๊ฒฐ๊ณผ๋กœ ๋งŒ๋“ค์–ด์ง€๋Š” ์ˆซ์ž๋‚˜ ๋‚ ์งœ ํฌ๋งท์„ ๋ณ€๊ฒฝํ•˜๊ณ  ์‹ถ์œผ๋ฉด ํ•ด๋‹น ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ์ œ๊ณตํ•˜๋Š” ์„ค์ •์„ ํ†ตํ•ด์„œ ํฌ๋งท์„ ์ง€์ •ํ•ด์•ผ ํ•œ๋‹ค.
  • ๊ฒฐ๊ณผ์ ์œผ๋กœ ์ด๊ฒƒ์€ ์ปจ๋ฒ„์ „ ์„œ๋น„์Šค์™€ ์ „ํ˜€ ๊ด€๊ณ„๊ฐ€ ์—†๋‹ค.
  • ์ปจ๋ฒ„์ „ ์„œ๋น„์Šค๋Š” @RequestParam, @ModelAttribute, @PathVariable, ๋ทฐ ํ…œํ”Œ๋ฆฟ ๋“ฑ์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

โœ”๏ธ ํŒŒ์ผ ์ „์†ก

HTML ํผ ์ „์†ก ๋ฐฉ์‹

  • application/x-www-form-urlencoded
  • multipart/form-data

๋ฌธ์ž์™€ ๋ฐ”์ด๋„ˆ๋ฆฌ ํŒŒ์ผ์„ ๋™์‹œ์— ์ „์†กํ•˜๋Š” ๊ฒƒ์ฒ˜๋Ÿผ ์—ฌ๋Ÿฌ ํ˜•์‹์˜ ํผ์„ ์ „์†ก์‹œํ‚ค๊ธฐ ์œ„ํ•ด์„œ๋Š” multipart/form-data๋ผ๋Š” ์ „์†ก ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•ด์•ผํ•œ๋‹ค.

โ†’ enctype="multipart/form-data" ๋ฅผ Form ํƒœ๊ทธ์— ์„ ์–ธ

๋ฉ€ํ‹ฐ ํŒŒํŠธ ์‚ฌ์šฉ ์˜ต์…˜

์—…๋กœ๋“œ ์‚ฌ์ด์ฆˆ ์ œํ•œ

spring.servlet.multipart.max-file-size=1MB
spring.servlet.multipart.max-request-size=10MB

์„œ๋ธ”๋ฆฟ๊ณผ ํŒŒ์ผ ์—…๋กœ๋“œ

  • ์ฝ”๋“œ
    		@PostMapping("/upload")
        public String saveFileV1(HttpServletRequest request) throws ServletException, IOException {
            log.info("request={}", request);
            String itemName = request.getParameter("itemName");
            log.info("itemName={}", itemName);
            Collection<Part> parts = request.getParts();
            log.info("parts={}",parts);
    
            for (Part part : parts) {
                log.info("==== PART ====");
                log.info("name={}", part.getName());
                Collection<String> headerNames = part.getHeaderNames();
                for (String headerName : headerNames) {
                    log.info("header {}: {}", headerName, part.getHeader(headerName));
                }
    
                //ํŽธ์˜ ๋ฉ”์„œ๋“œ
                log.info("submittedFileName={}", part.getSubmittedFileName());
                log.info("size={}", part.getSize());
    
                //๋ฐ์ดํ„ฐ ์ฝ๊ธฐ
                InputStream inputStream = part.getInputStream();
                String body = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
                log.info("body={}", body);
    
                //ํŒŒ์ผ์— ์ €์žฅํ•˜๊ธฐ
                if (StringUtils.hasText(part.getSubmittedFileName())){
                    String fullPath = fileDir + part.getSubmittedFileName();
                    log.info("ํŒŒ์ผ ์ €์žฅ fullPath={}", fullPath);
                    part.write(fullPath);
                }
            }
  • ๋ฉ€ํ‹ฐํŒŒํŠธ ํ˜•์‹์€ ์ „์†ก ๋ฐ์ดํ„ฐ๋ฅผ ํ•˜๋‚˜ํ•˜๋‚˜ ๊ฐ๊ฐ PART๋กœ ๋‚˜๋ˆ„์–ด ์ „์†กํ•œ๋‹ค.
  • ์„œ๋ธ”๋ฆฟ์ด ์ œ๊ณตํ•˜๋Š” PART๋Š” ๋ฉ€ํ‹ฐํŒŒํŠธ ํ˜•์‹์„ ํŽธ๋ฆฌํ•˜๊ฒŒ ์ฝ์„ ์ˆ˜ ์žˆ๋Š” ๋ฉ”์„œ๋“œ๋ฅผ ์ œ๊ณตํ•œ๋‹ค.

์Šคํ”„๋ง๊ณผ ํŒŒ์ผ ์—…๋กœ๋“œ

  • ์ฝ”๋“œ
    		@PostMapping("/upload")
        public String saveFile(@RequestParam String itemName,
                               @RequestParam MultipartFile file, HttpServletRequest request) throws IOException {
            log.info("request={}", request);
            log.info("itemName={}", itemName);
            log.info("multiPartFile={}", file);
    
            if(!file.isEmpty()){
                String fullPath = fileDir + file.getOriginalFilename();
                log.info("fullPath={}", fullPath);
                file.transferTo(new File(fullPath));
            }
            return "upload-form";
        }
  • @ModelAttribute์—์„œ๋„ MultipartFile์„ ๋™์ผํ•˜๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

0๊ฐœ์˜ ๋Œ“๊ธ€