영한킴: spring MVC2 (1) : 타임리프

이 포스트는 강의 내용에 관한 노트나 필자가 코딩한 것을 기록했습니다


스프링 MVC 2편 - 백엔드 웹 개발 활용 기술

타임리프 - 반복문 / th:block

  • th:block의 사용예시를 알려주었지만.. 왠지 th:each로도 기능적으로 가능할 것 같아서 해 보았다
<th:block th:each="user : ${users}">
  <div>
    사용자 이름1 <span th:text="${user.username}"></span> 사용자 나이1
    <span th:text="${user.age}"></span>
  </div>
  <div>요약 <span th:text="${user.username} + ' / ' + ${user.age}"></span></div>
</th:block>
<h2>성우쿤의 each</h2>
<div th:each="user : ${users}">
  <div>
    사용자 이름1 <span th:text="${user.username}"></span> 사용자 나이1
    <span th:text="${user.age}"></span>
  </div>
  <div>요약 <span th:text="${user.username} + ' / ' + ${user.age}"></span></div>
</div>
  • 결과적으로 된다 !!

  • bootstap 3.0 은 templates 디렉터리가 아닌 static 밑에 있어야 한다 !!

  • 스프링에서 반환 타입이 void이면 viewResolver를 동작시킬 수 없다.. 이렇게 보면 당연하지만 아래의 코드를 보면 혼동이 올 수 있다

@GetMapping(...)
public void 메서드1() {
  애너테이션없는_메서드();
}

public String 애너테이션없는_메서드() {
  ...
  return "뷰_이름";
}

쪼금만 생각해보면 당연하단 것을 알 수 있다.. 아래와 같이 수정하면 작동한다

@GetMapping(...)
public String 메서드1() {
  return 애너테이션없는_메서드();
}
  • th:replace 속성에 리터럴식이 들어있으면 parsing 에러 난다 !!

    • 예) th:replace="|~{${fragmentPath} :: ${fragmentName}}|"
  • 타임리프 자바스크립트 인라인은 booleanlist값도 처리해준다 !

아래는 공식 문서 내용이다

An important thing to note regarding JavaScript inlining is that this expression evaluation is intelligent and not limited to Strings. Thymeleaf will correctly write in JavaScript syntax the following kinds of objects:

- Strings
- Numbers
- Booleans
- Arrays
- Collections
- Maps
- Beans (objects with getter and setter methods)
  • 자바스크립트 인라인에서, 네츄럴 리터럴의 주석문처럼 보이는 것은 띄어쓰면 안된다 !! 예)
let updateForm = /*[[${idReadonly}]]*/ "no-data";
  • 위 내용중 네츄럴 인터럴/*[[${idReadonly}]]*/ 이부분을 /* [[${idReadonly}]] */ 이렇게 사용하면 안된다 !

  • 타임리프의 주석 중 정말 유용한 주석 !

html 자체의 내용은 css/js를 적용시키고, 렌더링된 내용은 css/js를 없애고 싶을 때 정말 유용하다 만일 아래의 주석이 없었으면 계속 file not found 에러가 뜨게 된다 ㅠ

<!--/*-->
타임리프 렌더링시 안보이게 할 태그들
<!--*/-->
  • form에서 값을 서버로 넘길때 값이 null이 넘어갈 수 있다 !!

    • @ModelAttribute 같은 경우는 setter로 대입을 받는데
      도메인 객체의 id가 long인 경우라면 이를 대비해서 null을 수용할 수 있는 Long 으로 바꾸자 !
@Data
@NoArgsConstructor
public class Item {

    private Long id;
    ...

  • 서버에서 객체를 넘기지 않은 상황에서, 타임리프 html에서 해당 객체의 프로퍼티에 접근하는 코드가 있을 경우
    if문으로 분기를 태우지 않더라도 렌더링 오류가 난다 (아래의 문장)
  <script th:inline="javascript">
      let updateForm;
      ...
      $(document).ready(function() {
        updateForm = /*[[${idReadonly}]]*/  "no-data";
        if(updateForm !== null && updateForm !== "no-data" && updateForm === true) {
          $("input[id='id']").prop('value', [[${item.id}]]);
		      $("input[id='name']").prop('value', [[${item.name}]]);
      ...

이럴 때는 form뷰를 보낼때 아래의 빈 Item 객체도 같이 보내자 !

model.addAttribute("item", new Item());

체크박스 멀티

addForm.html 에서 *{regions}${item.regions}와 같다
*{regions} 는 form태그의 th:object="${item}" 을 참조한다

bindingResult와 @ModelAttribute 순서를 바꾸면..

한번 해보았다. 아래와 같은 로그 메세지가 나온다

java.lang.IllegalStateException:
An Errors/BindingResult argument is expected to be declared immediately after the model attribute,
the @RequestBody or the @RequestPart arguments to which they apply: ...

th:errorclassth:field

<input
  type="text"
  id="itemName"
  th:field="*{itemName}"
  th:errorclass="field-error"
  ...
/>

th:errors가 필드명을 몰라도 처리가 되는 이유는 th:field 가 필드명을 알려주기 때문이다

FieldErrorObjectError

FieldError 는 넘어온 argument가 있지만 ObjectError는 그러한 경우가 아니다.
왜냐하면 어느 한 필드에 속한 에러가 아니라 전반적이고 복합적인 에러이기 때문이다

reject, rejectValue

  • rejectValue는 필드에러 , reject는 오브젝트에러이다.

javax

javax 는 처음에는 확장 패키지였으나 java와 같은 1급 패키지로 승격되었다 javax -> java로 이름 수정시.. 소스를 수정해야하는 이유로 개발자들의 원성이 있었고 결국 javax의 의미 자체를 표준으로 승격하였다

@Range 의 정확한 범위

본인은 아래의 범위를 보고 헷갈렸다..

@Range(min = 1000, max = 1000000)
private Integer price;

과연 저게

1. min < price < max
2. min < price <= max
3. min <= price < max
4. min <= price <= max

위 보기중 어떤 것에 해당하는지 …

테스트 해보니 4번이 맞았다

  • 참고로 @Range@Min@Value의 조합으로 쓸 수 있다

즉,

@Range(min = 1000, max = 1000000)

는 아래와 같다

@Min(value = 1000)
@Max(value = 1000000)

Bean 검증 오류 메세지 우선순위

@NotBlank 기준으로 .properties 에서 아래의 순서대로 찾는다.


1 NotBlank.item.itemName
2 NotBlank.itemName
3 NotBlank.java.lang.String
4 NotBlank

만일 위에서도 없으면

    @NotBlank(message = "공백 X {0}")
    private String itemName;

위 애너테이션에서 찾는다

@Range 의 MAX

아래와 같이 범위를 설정할 경우 기본 범위가 9223372036854775807로 잡혀 있는데.. 이건 int의 범위가 아니다 이건 long의 범위인데 분명 price는 Integer이다

public class ItemUpdateForm {
  ...
    @Range(min = 1000)
    private Integer price;

알고보니 아래처럼 설정되어 있기 때문인데..

public @interface Range {
	@OverridesAttribute(constraint = Max.class, name = "value") long max() default Long.MAX_VALUE;

실제로는 요청에 int의 범위를 넘어서는 값을 입력하면 요청이 되지 않는다. (400 Bad Request 발생)

@ModelAttribute 복습

@ModelAttribute("item") ItemSaveForm form

위처럼 되어 value를 지정하지 않을시에

model.addAtt..("itemSaveForm", form)

이렇게 담기게 된다 아니면 뷰의 th:object의 값을 수정하면 된다

빈 배열로 오는 경우를 @NotBlank로 하면

타임리프-스프링 폼으로 체크 박스를 서버로 전송하면

[]가 전송된다. 즉 비어있는 List 이다.

@NotNull 로 null 뿐만 아니라 위의 경우 까지도 커버가 된다.

하지만 @NotBlank로 하면 500 Error 발생

There was an unexpected error (type=Internal Server Error, status=500).
HV000030: No validator could be found for constraint 'javax.validation.constraints.NotBlank' validating type 'java.util.List<java.lang.String>'. Check configuration for 'regions'
javax.validation.UnexpectedTypeException: HV000030: No validator could be found for constraint 'javax.validation.constraints.NotBlank' validating type 'java.util.List<java.lang.String>'. Check configuration for 'regions'

Bean Validation을 적용할때 MessageSource의 에러코드 관리 (방법을 찾지 못함)

기존의 BindingResult를 통해서 검증을 했을 때는 에러코드와 인자를 넘길 수 있었다

때문에 errors.properties, errors_en.properties 와 같은 곳에서 메시지를 관리할 수 있었다

그러나 Bean Validation 같은 경우는 MessageSource까지는 관리 가능하나 인자를 어디서 넘기는지.. 아직 발견을 하지 못했다 !!

이곳에서 해답을 구할 수 있었다 ! https://kapentaz.github.io/spring/Spring-Boo-Bean-Validation-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EC%95%8C%EA%B3%A0-%EC%93%B0%EC%9E%90/#

Validation시 input:password 은 초기화

type이 password인 input 은 검증실패한 데이터를 초기화한다

영속쿠키, 세션쿠키에서 세션은 서버와 상관 X

세션쿠키의 세션은 서버의 HttpSession과 무관!

브라우저의 세션(로그인 유지)을 의미하는 것으로 보인다

주석의 TODO 는 앞으로 진행할 것을 미

// 로그인 성공 처리 TODO

위와 같은 곳에서 TODO는 앞으로 구현하겠단 의미

쿠키 처리

  // 쿠키에 시간 정보를 주지 않으면 세션 쿠키(브라우저 종료시 모두 종료)
  Cookie idCookie = new Cookie("memberId", String.valueOf(loginMember.getId()));
  response.addCookie(idCookie);

쿠키 저장해서 내려 받음

image

저장된 쿠키를 올려 보냄

image

저장된 쿠키 목록

image

쿠키 제거

image

커스텀 Filter 등록

강의에서는 @Configuration을 통해 등록하는 과정이 나왔지만

@Component로 등록을 시키는 것도 가능하다

강의에서 등록한 방식은 아래와 같다

스프링 부트는 WAS를 내장하고 구동하므로

FilterRegistrationBean 를 이용해서 필터를 등록한다

타임리프의 친절하지 않은 에러 메세지…

뷰의 오타 하나로 한참을 해매고 있었다..

문제가 된 건 아래의 코드

image

위 코드 한 글자로 아래와 같은 에러 트레이스가 나오는데…

image

이게 불친절하다 ㅠㅠ 저 = 하나만 빼면 되는 코드인데… 아까 전 디버깅할 때는 저게 원인이었다고 도저히 유추해낼 수가 없다 ㅠㅠ 우연히 테스트파일 하나 생성해서 알아냈는데.. 우찌……………..

log.info, debug

인텔리제이 Run, Debug 모두 log.info, debug 는 찍힌다.. 수업중에서는 운영은 .info, .debug를 권장한 기억이 있는데.. 다소 혼돈스럽다 나중에 알아보자 !

  • 내 로깅 레벨이
logging:
  level:
    swcho.mini.mvc: debug

위와 같이 설정되어 있는 걸 확인했다

나중에 알아보자

@ComponentScan

하는 방법

앱이름_Application 에 아래와 같이 설정한다

@ComponentScan(basePackages = {"swcho.mini.mvc.domain", "swcho.mini.mvc.web"})

basePackages 는 생략 가능하다

Exception Page 처리 과정

image

처음 요청은 정상 요청이 간다.. 이후 필터를 거치면서 WAS로 돌아오고

이후 다시 예외에 대한 2차적인 ERROR 페이지 요청을 한다

image

image

protected 메서드

BasicErrorController 등의 클래스를 상속받아서 에러페이지 처리를 커스터마이징 하고자할 때

protected 메서드로 되어 있다면 해당 메서드를 오버라이딩하여 구현할 수 있다

protected는 자손에게 열린 메서드이다




© 2020.12. by 따라쟁이

Powered by philz