<ul>
<li>th:text ์ฌ์ฉ <span th:text="${data}"></span></li>
<li>์ปจํ
์ธ ์์์ ์ง์ ์ถ๋ ฅํ๊ธฐ = [[${data}]]</li>
</ul>
๋ฌธ์๋ฅผ ํ๊ทธ๋ก ๋ณํํ์ง ์๊ณ ๋ฌธ์ ๊ทธ๋๋ก ์ถ๋ ฅ
โป ์ด์ค์ผ์ดํ๋ฅผ ๊ธฐ๋ณธ์ผ๋ก ํ๊ณ ๊ผญ ํ์ํ ๋๋ง ์ธ์ด์ค์ผ์ดํ ์ฌ์ฉ
<div th:with="first=${users[0]}">
<p>์ฒ์ ์ฌ๋์ ์ด๋ฆ์ <span th:text="${first.username}"></span></p>
</div>
<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>
<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>
<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 > 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>- 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๋ false๋ก ์ค์ ํด๋ ๋ฌด์กฐ๊ฑด ์ฒดํฌ๊ฐ ๋์ด์ ธ์ ๋ ๋๋ง๋๋ค. ํ์ง๋ง th:ckecked๋ฅผ ์ฌ์ฉํ๋ฉด false๋ก ์ค์ ํ ๊ฒฝ์ฐ checked ์ค์ ์์ฒด๋ฅผ ์์ ์ค๋ค.
checked o
checked x
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>
<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>
<!--
<span th:text="${data}">html data</span>
--><!--/* [[${data}]] */-->
<!--/*-->
<span th:text="${data}">html data</span>
<!--*/--><!--/*/
<span th:text="${data}">html data</span>
/*/--><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>th:object*{...} : ์ ํ ๋ณ์์์ด๋ผ๊ณ ํ๋ฉฐ th:object ์์ ์ ํํ ๊ฐ์ฒด์ ์ ๊ทผํ๋ค.*{itemName} == ${item.itemName}th:field<input type="hidden" name="_open" value="on"><!-- 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๋ฅผ ์ ์ธํ๋ค.
๋ฑ๋กํผ, ์์ธํ๋ฉด, ์์ ํผ์์ ๋ชจ๋ ์ฒดํฌ๋ฐ์ค๋ฅผ ๋ณด์ฌ์ฃผ๊ธฐ ์ํด์๋ ๊ฐ๊ฐ์ ์ปจํธ๋กค๋ฌ์์ 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;
}
<!-- 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>
<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>
@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>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}";
}
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>
// 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.addError(new FieldError(objectName(ModelAttribute ์ธ์คํด์ค ์ด๋ฆ)
, field, rejectedValue, bindingFailure(ํ์
์ค๋ฅ๊ฐ์ ๋ฐ์ธ๋ฉ ์คํจ์ธ์ง, ๊ฒ์ฆ ์คํจ์ธ์ง ๊ตฌ๋ถ ๊ฐ),
codes(๋ฉ์์ง ์ฝ๋), arguments(๋ฉ์์ง์ ์ฌ์ฉํ๋ ์ธ์) defaultMessage)
bindingResult.addError(new ObjectError(objectName, defaultMessage)โป @ModelAttribute์ ๋ฐ์ธ๋ฉ ์ ํ์ ์ค๋ฅ๊ฐ ๋ฐ์ํ๋ฉด?
spring.messages.basename=messages,errors ๋ฅผ ์ถ๊ฐํ๋ค.// 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));
// FieldError , rejectValue
bindingResult.rejectValue("price", "range", new Object[]{1000, 1000000}, null);
// ObjectError, reject
bindingResult.reject("totalPriceMin", new Object[]{10000, resultPrice}, null);
MessageCodesResolver codesResolver = new DefaultMessageCodesResolver();
String [] messageCodes = codesResolver.resolveMessageCodes("required", "item");๊ฐ์ฒด ์ค๋ฅ
๊ฐ์ฒด ์ค๋ฅ์ ๊ฒฝ์ฐ ๋ค์ ์์๋ก 2๊ฐ์ง ์์ฑ
1.: code + "." + object name
2.: code
์) ์ค๋ฅ ์ฝ๋: required, object name: item
- : required.item
- : required
ํ๋ ์ค๋ฅ
ํ๋ ์ค๋ฅ์ ๊ฒฝ์ฐ ๋ค์ ์์๋ก 4๊ฐ์ง ๋ฉ์์ง ์ฝ๋ ์์ฑ
1.: code + "." + object name + "." + field
2.: code + "." + field
3.: code + "." + field type
4.: code
if (!StringUtils.hasText(item.getItemName())){
bindingResult.rejectValue("itemName", "required");
} // ==
ValidationUtils.rejectIfEmptyOrWhitespace(bindingResult, "itemName", "required");
typeMismatch ๋ผ๋ ์ค๋ฅ ์ฝ๋๋ฅผ ์ฌ์ฉํ๋๋ฐ ์ด ์ฝ๋๊ฐ MessageCodesResolver๋ฅผ ํตํ๋ฉด์ 4๊ฐ์ง ๋ฉ์์ง ์ฝ๋๊ฐ ์์ฑ๋ ๊ฒ์ด๋ค. @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}";
}์ปจํธ๋กค๋ฌ ๋ก์ง์์ ์ฒ๋ฆฌํ๋ ๊ฒ์ด ๋๋ฌด ๋ง๋ค
@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);
}
}
}
}@InitBinder
public void init(WebDataBinder dataBinder){
dataBinder.addValidators(itemValidator);
}
โป ๊ธ๋ก๋ฒ ์ค์
@SpringBootApplication
public class ItemServiceApplication implements WebMvcConfigurer {
public static void main(String[] args) {
SpringApplication.run(ItemServiceApplication.class, args);
}
@Override
public Validator getValidator() {
return new ItemValidator();
}
}
implementation 'org.springframework.boot:spring-boot-starter-validation@NotBlank : ๋น๊ฐ + ๊ณต๋ฐฑ๋ง ์๋ ๊ฒฝ์ฐ๋ฅผ ํ์ฉํ์ง ์๋๋ค.
@NotNull : null ์ ํ์ฉํ์ง ์๋๋ค.
@Range(min = 1000, max = 1000000) : ๋ฒ์ ์์ ๊ฐ์ด์ด์ผ ํ๋ค.
@Max(9999) : ์ต๋ 9999๊น์ง๋ง ํ์ฉํ๋ค.
์ฑ๊ณตํ๋ฉด ๋ค์์ผ๋ก
์คํจํ๋ฉด typeMismatch ๋ก FieldError ์ถ๊ฐ
โ errors.properties์ ์ ์ธํ ์๋ฌ๋ฉ์์ง ์ถ๋ ฅ
๋ฐ์ธ๋ฉ์ ์ฑ๊ณตํ ํ๋๋ง Bean Validation ์ ์ฉ
BeanValidator๋ ๋ฐ์ธ๋ฉ์ ์คํจํ ํ๋๋ BeanValidation์ ์ ์ฉํ์ง ์๋๋ค.
ํ์
๋ณํ์ ์ฑ๊ณตํด์ ๋ฐ์ธ๋ฉ์ ์ฑ๊ณตํ ํ๋์ฌ์ผ BeanValidation ์ ์ฉ์ด ์๋ฏธ ์๋ค
//errors.properties
#Bean Validation ์ถ๊ฐ
NotBlank={0} ๊ณต๋ฐฑX
Range={0}, {2} ~ {1} ํ์ฉ
Max={0}, ์ต๋ {1}
@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}";
}
@ModelAttribute๋ HTTP์์ฒญ ํ๋ผ๋ฏธํฐ(URL ์ฟผ๋ฆฌ์คํธ๋ง, Post Form)๋ฅผ ๋ค๋ฃฐ ๋ ์ฌ์ฉํ๋ค.
@RequestBody๋ HTTP Body์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ฒด๋ก ๋ณํํ ๋ ์ฌ์ฉํ๋ค. (API JSON ์์ฒญ)
๋ฐ๋ผ์ ๋ฉ์์ง ์ปจ๋ฒํฐ์ ์๋์ด ์ฑ๊ณตํด์ ItemSaveForm ๊ฐ์ฒด๋ฅผ ๋ง๋ค์ด์ผ @Validated๊ฐ ์ ์ฉ๋๋ค.
โ **RequestBody๋ ๋ฉ์์ง ์ปจ๋ฒํฐ ๋จ๊ณ์์ JSON ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ฒด๋ก ๋ณ๊ฒฝํ์ง ๋ชปํ๋ฉด Validator ๊ฒ์ฆ ๋ถ๊ฐ๋ฅ**
โ HttpMessageConverter ๋จ๊ณ์์ ์คํจํ ์์ธ ์ฒ๋ฆฌ๋ฐฉ๋ฒ์ ๋ค์ ์์ธ ์ฒ๋ฆฌ ๋ถ๋ถ์์ ๋ค๋ฃธ.
Member Data Class
MemberRepository
// ์๋ฐ ๋ฌธ๋ฒ ์ฐธ๊ณ
public Optional<Member> findByLoginId(String loginId) {
return findAll().stream()
.filter(m -> m.getLoginId().equals(loginId))
.findFirst();
}
MemberController
HTML
@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);
}

@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();
}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)session.invalidate() //@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
JSESSIONID ๋ฅผ ์ ๋ฌํ๋ค.@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 )๋ฅผ ์์ฒญํด์ ์กฐํ๋ ์ธ์ ์ธ์ง ์ฌ๋ถ
server.servlet.session.timeout=60 (๊ธ๋ก๋ฒ ์ค์ ์ ๋ถ ๋จ์๋ก ์ค์ ํด์ผํ๋ค.)session.setMaxInactiveInterval(1800);ํํฐ ์ ํ ํ๋ก์ฐ
HTTP ์์ฒญ -> WAS -> ํํฐ -> ์๋ธ๋ฆฟ -> ์ปจํธ๋กค๋ฌ //๋ก๊ทธ์ธ ์ฌ์ฉ์
HTTP ์์ฒญ -> WAS -> ํํฐ(์ ์ ํ์ง ์์ ์์ฒญ์ด๋ผ ํ๋จ, ์๋ธ๋ฆฟ ํธ์ถX)
//๋น ๋ก๊ทธ์ธ ์ฌ์ฉ์
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
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 ์ ์ฌ์ฉํ์
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);
}
}
@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
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
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");
}@Login Annotation์ด ์์ผ๋ฉด ์ง์ ๋ง๋ ArgumentResolver๊ฐ ๋์ํด์ ์๋์ผ๋ก ์ธ์
์ ์๋ ๋ก๊ทธ์ธ ํ์์ ์ฐพ์์ฃผ๊ณ , ๋ง์ฝ ์ธ์
์ ์๋ค๋ฉด null ๊ฐ์ ๋ฐํํ๋๋ก ๊ฐ๋ฐ@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Login {
}
@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
public class WebConfig implements WebMvcConfigurer {
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new LoginMemberArgumentResolver());
}
}
์น ์ ํ๋ฆฌ์ผ์ด์
์น ์ ํ๋ฆฌ์ผ์ด์ ์ ์ฌ์ฉ์ ์์ฒญ๋ณ๋ก ๋ณ๋์ ์ฐ๋ ๋๊ฐ ํ ๋น๋๊ณ , ์๋ธ๋ฆฟ ์ปจํ ์ด๋ ์์์ ์คํ๋๋ค. ์ ํ๋ฆฌ์ผ์ด์ ์์ ์์ธ๊ฐ ๋ฐ์ํ๋๋ฐ, ์ด๋์ ๊ฐ try ~ catch๋ก ์์ธ๋ฅผ ์ก์์ ์ฒ๋ฆฌํ๋ฉด ์๋ฌด๋ฐ ๋ฌธ์ ๊ฐ ์๋ค. ๊ทธ๋ฐ๋ฐ ๋ง์ฝ์ ์ ํ๋ฆฌ์ผ์ด์ ์์ ์์ธ๋ฅผ ์ก์ง ๋ชปํ๊ณ , ์๋ธ๋ฆฟ ๋ฐ์ผ๋ก ๊น์ง ์์ธ๊ฐ ์ ๋ฌ๋๋ฉด ์ด๋ป๊ฒ ๋์ํ ๊น?
response.sendError(HTTP ์ํ ์ฝ๋, ์ค๋ฅ ๋ฉ์์ง)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 /error-page/500 ๋ค์ ์์ฒญ -> ํํฐ -> ์๋ธ๋ฆฟ -> ์ธํฐ์
ํฐ -> ์ปจํธ๋กค๋ฌ(/errorpage/500) -> View**
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 ์ํ ์ฝ๋
DispatcherType ์ด๋ผ๋ ์ถ๊ฐ์ ๋ณด๋ฅผ ์ ๊ณตํ๋ค.REQUEST : ํด๋ผ์ด์ธํธ ์์ฒญ
ERROR : ์ค๋ฅ ์์ฒญ
FORWARD : MVC์์ ๋ฐฐ์ ๋ ์๋ธ๋ฆฟ์์ ๋ค๋ฅธ ์๋ธ๋ฆฟ์ด๋ JSP๋ฅผ ํธ์ถํ ๋
RequestDispatcher.forward(request, response);
INCLUDE : ์๋ธ๋ฆฟ์์ ๋ค๋ฅธ ์๋ธ๋ฆฟ์ด๋ JSP์ ๊ฒฐ๊ณผ๋ฅผ ํฌํจํ ๋
RequestDispatcher.include(request, response);
ASYNC : ์๋ธ๋ฆฟ ๋น๋๊ธฐ ํธ์ถ
filterRegistrationBean.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ERROR);์คํ๋ง ๋ถํธ๋ ์๋ฌํ์ด์ง๋ฅผ ์๋์ผ๋ก ๋ฑ๋กํ๋ค.
/error ๋ผ๋ ๊ฒฝ๋ก๋ก ๊ธฐ๋ณธ ์ค๋ฅ ํ์ด์ง๋ฅผ ์ค์ ํ๋ค.WebServerCustomizer๋ก ์ํ์ฝ๋์ ์์ธ ์ฒ๋ฆฌ๋ฅผ ์ค์ ํ์ง ์์ผ๋ฉด ๊ธฐ๋ณธ ์ค๋ฅ ํ์ด์ง๋ก ์ฌ์ฉ๋๋ค.
BasicErrorController๋ผ๋ ์คํ๋ง ์ปจํธ๋กค๋ฌ ๋ํ ์๋์ผ๋ก ๋ฑ๋กํ๋ค.
๊ฐ๋ฐ์๋ ์ค๋ฅํ์ด์ง(๋ทฐ)๋ง ๋ฑ๋กํ๋ฉด ๋๋ค.
์ค๋ฅ ์ ๋ณด๋ฅผ ๋ทฐ ํ์ด์ง์ ์ฝ๊ฒ ๋ ธ์ถ์ํฌ ์ ์๋ค.
<!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>server.error.include-exception=true
server.error.include-message=on_param
server.error.include-stacktrace=on_param
server.error.include-binding-errors=on_param @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 ๊ตฌ์กฐ๋ก ๋ณํํ ์ ์๋ค.@ExceptionHandler ๋ฅผ ์ฌ์ฉํ์
- ์ฝ๋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;
}
}
```
@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;
}
}**@ResponseStatus ๊ฐ ๋ฌ๋ ค์๋ ์์ธ ์ฒ๋ฆฌ**
@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "์๋ชป๋ ์์ฒญ ์ค๋ฅ")
public class BadRequestException extends RuntimeException {
}
ResponseStatusException ์์ธ ์ฒ๋ฆฌ
@GetMapping("/api/response-status-ex2")
public String responseStatusEx2() {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "error.bad", new
IllegalArgumentException());
}
โป ๋ฉ์์ง ๊ธฐ๋ฅ ์ฌ์ฉ ๊ฐ๋ฅ - messages.properties์ ๋ฉ์์ง ์ง์ ํ reason ๊ฐ์ ๋์
@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 {}@RequestParam, @ModelAttribute, @PathVariableDefaultConversionService ๋ค์ ๋ ์ธํฐํ์ด์ค๋ฅผ ๊ตฌํํ๋ค.ConversionService : ์ปจ๋ฒํฐ ์ฌ์ฉ์ ์ด์
ConversionRegistry : ์ปจ๋ฒํฐ ๋ฑ๋ก์ ์ด์
์ด๋ ๊ฒ ์ธํฐํ์ด์ค๋ฅผ ๋ถ๋ฆฌํ๋ฉด ์ปจ๋ฒํฐ๋ฅผ ์ฌ์ฉํ๋ ํด๋ผ์ด์ธํธ์ ์ปจ๋ฒํฐ๋ฅผ ๋ฑ๋กํ๊ณ ๊ด๋ฆฌํ๋ ํด๋ผ์ด์ธํธ์ ๊ด์ฌ์ฌ๋ฅผ ๋ช ํํ๊ฒ ๋ถ๋ฆฌํ ์ ์๋ค. ํนํ ์ปจ๋ฒํฐ๋ฅผ ์ฌ์ฉํ๋ ํด๋ผ์ด์ธํธ๋ ConversionService๋ง ์์กดํ๋ฉด ๋๋ฏ๋ก, ์ปจ๋ฒํฐ๋ฅผ ์ด๋ป๊ฒ ๋ฑ๋กํ๊ณ ๊ด๋ฆฌํ๋์ง ์ ํ ๋ชฐ๋ผ๋ ๋๋ค.
${{...}} ๋ฅผ ์ฌ์ฉํ๋ฉด ์๋์ผ๋ก ์ปจ๋ฒ์ ์๋น์ค๋ฅผ ์ฌ์ฉํด์ ๋ณํ๋ ๊ฒฐ๊ณผ๋ฅผ ์ถ๋ ฅํ๋ค.${...}${{...}}th:field ๋ฅผ ์ฌ์ฉํ๋ ๊ฒฝ์ฐ ์ปจ๋ฒ์ ์๋น์ค๊ฐ ์๋์ผ๋ก ์ ์ฉ๋๊ธฐ ๋๋ฌธ์ ์ผ๋ฐ์ ์ธ ๋ณ์ํํ์์ ์ฌ์ฉํ๋ฉด ๋๋ค.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);
}
}FormattingConversionServiceDefaultFormattingConversionService ๋ FormattingConversionService ์ ๊ธฐ๋ณธ์ ์ธ ํตํ, ์ซ์ ๊ด๋ จ ๋ช๊ฐ์ง ๊ธฐ๋ณธ ํฌ๋งทํฐ๋ฅผ ์ถ๊ฐํด์ ์ ๊ณตํ๋ค.FormattingConversionService ๋ ConversionService๊ด๋ จ ๊ธฐ๋ฅ์ ์์๋ฐ๊ธฐ ๋๋ฌธ์ ๊ฒฐ๊ณผ์ ์ผ๋ก ์ปจ๋ฒํฐ๋ ํฌ๋งทํฐ๋ ๋ชจ๋ ๋ฑ๋กํ ์ ์๋ค.โ ์คํ๋ง ๋ถํธ๋ DefaultFormattingConversionService๋ฅผ ์์ ๋ฐ์ WebConversionService๋ฅผ ๋ด๋ถ์์ ์ฌ์ฉํ๋ค.
@NumberFormat
@DateTimeFormat
- ๋ ์ง ๊ด๋ จ ํ์ ์ง์ ํฌ๋งทํฐ ์ฌ์ฉ
@Data
static class Form {
@NumberFormat(pattern = "###,###")
private Integer number;
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime localDateTime;
}
@RequestParam, @ModelAttribute, @PathVariable, ๋ทฐ ํ
ํ๋ฆฟ ๋ฑ์์ ์ฌ์ฉํ ์ ์๋ค.๋ฌธ์์ ๋ฐ์ด๋๋ฆฌ ํ์ผ์ ๋์์ ์ ์กํ๋ ๊ฒ์ฒ๋ผ ์ฌ๋ฌ ํ์์ ํผ์ ์ ์ก์ํค๊ธฐ ์ํด์๋ 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);
}
} @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";
}