[Inflearn] 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 정리 (A)✍️ 정리/Spring2024. 7. 31. 14:10
Table of Contents
📚 강의 출처
강사님께 항상 감사합니다. 🧑🏻💻
해당 글은 김영한님의 강의와 개인적 지식을 바탕으로 정리한 내용입니다.
모든 자료의 출처는 김영한 강사님임을 미리 밝힙니다.
섹션 1. 타임리프 - 기본 기능
- 타임리프 특징
- 서버 사이드 HTML 렌더링(SSR)
- 네츄럴 템플릿
- 스프링 통합 지원.
- 타임리프 사용 선언
<html xmlns:th="http://www.thymeleaf.org">
- 텍스트 - text, utext(Escape, [(...)])
<li>th:text = <span th:text="${data}"></span></li>
<li>th:utext = <span th:utext="${data}"></span></li> // 이스케이프 기능 X
<li><span th:inline="none">[[...]] = </span>[[${data}]]</li>
<li><span th:inline="none">[(...)] = </span>[(${data})]</li> // 이스케이프 기능 X
- 변수 - SpringEL
// 기본 변수
<li>${user.username} = <span th:text="${user.username}"></span></li>
<li>${user['username']} = <span th:text="${user['username']}"></span></li>
<li>${user.getUsername()} = <span th:text="${user.getUsername()}"></span></li>
// 배열
<li>${users[0].username} = <span th:text="${users[0].username}"></span></li>
<li>${users[0]['username']} = <span th:text="${users[0]['username']}"></span></li>
<li>${users[0].getUsername()} = <span th:text="${users[0].getUsername()}"></span></li>
// Map
<li>${userMap['userA'].username} = <span th:text="${userMap['userA'].username}"></span></li>
<li>${userMap['userA']['username']} = <span th:text="${userMap['userA']['username']}"></span></li>
<li>${userMap['userA'].getUsername()} = <span th:text="${userMap['userA'].getUsername()}"></span></li>
- 지역 변수
<div th:with="first=${users[0]}">
<p>처음 사람의 이름은 <span th:text="${first.username}"></span></p>
</div>
- HTTP 정보, 파라미터, 세션, Spring Bean(빈에 등록되어, 전달 없이 바로 사용 가능)
// HTTP 정보
<li>request = <span th:text="${request}"></span></li>
<li>response = <span th:text="${response}"></span></li>
<li>session = <span th:text="${session}"></span></li>
<li>servletContext = <span th:text="${servletContext}"></span></li>
<li>locale = <span th:text="${#locale}"></span></li>
// 파라미터, 세션, Spring Bean
<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>
- 타임리프가 지원하는 유틸리티 객체
- #message : 메시지, 국제화 처리
- #uris : URI 이스케이프 지원
- #dates : java.util.Date 서식 지원
- #calendars : java.util.Calendar 서식 지원
- #temporals : 자바8 날짜 서식 지원
- #numbers : 숫자 서식 지원
- #strings : 문자 관련 편의 기능
- #objects : 객체 관련 기능 제공
- #bools : boolean 관련 기능 제공
- #arrays : 배열 관련 기능 제공
- #lists , #sets , #maps : 컬렉션 관련 기능 제공
- #ids : 아이디 처리 관련 기능 제공, 뒤에서 설명
- 참고 사이트
- URL 링크
// 단순 URL, 쿼리 파라미터, 경로 변수, 경로 변수 + 쿼리 파라미터
<li><a th:href="@{/hello}">basic url</a></li>
<li><a th:href="@{/hello(param1=${param1}, param2=${param2})}">hello queryparam</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})}">pathvariable + 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 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>
- 연산
// 산술 연산
<li>10 + 2 = <span th:text="10 + 2"></span></li>
<li>10 % 2 == 0 = <span th:text="10 % 2 == 0"></span></li>
// 비교 연산
<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>
// 조건식
<li>(10 % 2 == 0)? '짝수':'홀수' = <span th:text="(10 % 2 == 0)? '짝 수':'홀수'"></span></li>
// Elvis 연산자
<li>${data}?: '데이터가 없습니다.' = <span th:text="${data}?: '데이터가 없습니다.'"></span></li>
<li>${nullData}?: '데이터가 없습니다.' = <span th:text="${nullData}?: '데이터가 없습니다.'"></span></li>
// No-Operation
<li>${data}?: _ = <span th:text="${data}?: _">데이터가 없습니다.</span></li>
<li>${nullData}?: _ = <span th:text="${nullData}?: _">데이터가 없습니다.</span></li>
- 속성 값 설정
// 속성 값의 뒤에 추가 ex) text large
- th:attrappend = <input type="text" class="text" th:attrappend="class=' large'"/><br/>
// 속성 값의 앞에 추가 ex) large text
- th:attrprepend = <input type="text" class="text" th:attrprepend="class='large '"/><br/>
// class 속성에 추가 ex) text large
- th:classappend = <input type="text" class="text" th:classappend="large"/><br/>
// checked 처리
- checked o <input type="checkbox" name="active" th:checked="true" /><br/>
- checked x <input type="checkbox" name="active" th:checked="false" /><br/>
- checked o <input type="checkbox" name="active" checked /><br/>
- checked x <input type="checkbox" name="active" /><br/>
- 반복
<tr th:each="user : ${users}">
<td th:text="${user.username}">username</td>
<td th:text="${user.age}">0</td>
</tr>
- 타임리프 반복문 추가 기능
<tr th:each="user, userStat : ${users}"> // userStat 생략 가능, 타임리프가 .. + Stat 지원
<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 조건문
<span th:text="'미성년자'" th:if="${user.age lt 20}"></span>
<span th:text="'미성년자'" th:unless="${user.age ge 20}"></span> // unless, if 반대
// switch 문
<td th:switch="${user.age}">
<span th:case="10">10살</span>
<span th:case="20">20살</span>
<span th:case="*">기타</span> // default 역할
</td>
- 주석
<h1>1. 표준 HTML 주석</h1> // 렌더링 후 주석 부분만 표시
<!--<span th:text="${data}">html data</span> -->
<h1>2. 타임리프 파서 주석</h1> // 렌더링 후 주석 부분 완전 제거
<!--/* [[${data}]] */-->
<h1>3. 타임리프 프로토타입 주석</h1> // HTML 정적일 경우 보이지 않고, 타임리프 렌더링 경우 보이는 주석
<!--/*/
<span th:text="${data}">html data</span>
/*/-->
- 블록
<th:block th:each="user : ${users}"> // div 블록 단위로 반복문 가능
<div>
사용자 이름1 <span th:text="${user.username}"></span>
<div>
요약 <span th:text="${user.username} + ' / ' + ${user.age}"></span>
</div>
</th:block>
- 자바스크립트 인라인
<script th:inline="javascript"> // 적용 상태 O
var username = [[${user.username}]];
var age = [[${user.age}]];
var username2 = /*[[${user.username}]]*/ "test username";
var user = [[${user}]];
</script>
// 적용X 결과
<script>
var username = userA;
var age = 10;
var username2 = /*userA*/ "test username";
var user = BasicController.User(username=userA, age=10);
</script>
// 적용O 결과
<script>
var username = "userA";
var age = 10;
var username2 = "userA";
var user = {"username":"userA","age":10};
</script>
- 템플릿 조각
// fragment 태그
<footer th:fragment="copy">
<footer th:fragment="copyParam (param1, param2)">
// 태그 활용 페이지 불러오기
<h2>부분 포함 insert</h2>
<div th:insert="~{template/fragment/footer :: copy}"></div>
<h2>부분 포함 replace</h2>
<div th:replace="~{template/fragment/footer :: copy}"></div>
<h2>부분 포함 단순 표현식</h2>
<div th:replace="template/fragment/footer :: copy"></div>
<h1>파라미터 사용</h1>
<div th:replace="~{template/fragment/footer :: copyParam ('데이터1', '데이터2')}"></div>
- 템플릿 레이아웃
<head th:fragment="common_header(title,links)"> // 아래 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>
<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>
// 레이아웃 전체 태그
<html th:fragment="layout (title, content)" xmlns:th="http://www.thymeleaf.org">
<head>
<title th:replace="${title}">레이아웃 타이틀</title> </head>
<body>
<h1>레이아웃 H1</h1>
<div th:replace="${content}">
<p>레이아웃 컨텐츠</p>
</div>
<footer>
레이아웃 푸터
</footer>
</body>
</html>
// 전체 삽입
<html th:replace="~{template/layoutExtend/layoutFile :: layout(~{::title}, ~{::section})}" xmlns:th="http://www.thymeleaf.org">
<head>
<title>메인 페이지 타이틀</title>
</head>
<body>
<section>
<p>메인 페이지 컨텐츠</p>
<div>메인 페이지 포함 내용</div>
</section>
</body>
</html>
🧑🏻💻 강의 실습 기록
섹션 2. 타임리프 - 스프링 통합과 폼
- th:object - 커맨드 객체 저장
- th:field - HTML 태그의 id, name, value 속성 자동 처리
<input type="text" th:field="*{itemName}" />
<input type="text" id="itemName" name="itemName" th:value="*{itemName}" />
<form action="item.html" th:action th:object="${item}" method="post"> // 커맨드 저장
<input type="text" id="itemName" th:field="*{itemName}" class="form- control" placeholder="이름을 입력하세요"> // *{..} 축약 가능
- 체크박스, 라디오 버튼, 셀렉트 박스
- 체크 박스(단일)
- HTML Checkbox는 선택이 안되면, 서버로 값 전송 자체 안함(null)
- 해결 방안은 hidden 필드를 생성해 항상 전송
<div>판매 여부</div> <div>
<div class="form-check">
<input type="checkbox" id="open" th:field="*{open}" class="form-check-input">
// field로 인해, hidden 생략 가능
<label for="open" class="form-check-label">판매 오픈</label>
</div>
</div>
// HTML 렌더링 시
<div>판매 여부</div> <div>
<div class="form-check">
<input type="checkbox" id="open" class="form-check-input" name="open"value="true">
<input type="hidden" name="_open" value="on"/>
<label for="open" class="form-check-label">판매 오픈</label>
</div>
</div>
- 체크 박스(멀티)
- @ModelAttribute 특별한 사용법
@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 객체에 담겨 전달
<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>
- 라디오 버튼
<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("itemTypes")
public ItemType[] itemTypes() {
return ItemType.values();
}
// @ModelAttribute 없이도 전달 가능
<div th:each="type : ${T(hello.itemservice.domain.item.ItemType).values()}">
- 셀렉트 박스
<div>
<div>배송 방식</div>
<select th:field="*{deliveryCode}" class="form-select">
<option value="">==배송 방식 선택==</option>
<option th:each="deliveryCode : ${deliveryCodes}" th:value="${deliveryCode.code}" th:text="${deliveryCode.displayName}">FAST</option>
</select>
</div>
🧑🏻💻 강의 실습 기록
섹션 3. 메시지, 국제화
- 국제화
- 하드코딩 대신, 메시지가 설정한 파일을 나라별로 별도로 관리 가능
- HTTP accept-language 헤더 값 사용 or 직접 언어 선택 및 쿠키 처리
- Spring Boot 메시지 소스 설정
- application.properties - 'spring.messages.basename=messages'
- 파일 이름 명시 - messages_{language}_{country}.properties
- 메시지 못 찾는 에러 'NoSuchMessageException'
- 타임리프 메시지 표현식 #{...} 사용
- 파라미터 th:text="#{hello.name(${item.itemName})}
🧑🏻💻 강의 실습 기록
섹션 4. 검증1 - Validation
- 클라이언트 검증, 서버 검증
- 클라이언트 검증은 조작 가능, 보안 취약
- 서버만 검증 시 고객 사용성이 부족. 서버 검증 필수
- 타임 리프 th:if - 조건문
<div th:if="${errors?.containsKey('globalError')}">
<p class="field-error" th:text="${errors['globalError']}">전체 오류 메시지</p>
</div>
// errors? '?' SpringEL 문법 중 하나 만약 값이 없으면, NullPointerException 발생을 방지해 null로 치환
<input type="text"
th:classappend="${errors?.containsKey('itemName')} ? 'field-error' : _"
class="form-control">
// classappend로 참일 경우 추가를 만들기 가능
- 필드 오류, 글로벌 오류
bindingResult.addError(new FieldError("item", "itemName", "상품 이름은 필수입니다."));
// @ModelAttribute 이름, 오류가 발생한 필드, 기본 메시지
bindingResult.addError(new ObjectError("item", "가격 * 수량의 합은 10,000원 이상이어야 합니다. 현재 값 = " + resultPrice));
// @ModelAttribute 이름, 기본 메시지
- 타임리프 검증 오류 기능
글로버 오류 처리
<div th:if="${#fields.hasGlobalErrors()}">
<p class="field-error" th:each="err : ${#fields.globalErrors()}" th:text="$ {err}">전체 오류 메시지</p> errors
</div>
필드 오류 처리
<input type="text" id="itemName" th:field="*{itemName}" th:errorclass="field-error" class="form-control" placeholder="이름을 입력하세요"> errorclass
<div class="field-error" th:errors="*{itemName}"> errors
상품명 오류
</div>
- 에러 메시지 국제화
- spring.messages.basename=messages, errors 설정 추가
- errors.properties 파일 추가
- 더욱 세밀하게 정의되어 있는 것 우선순위
void rejectValue(@Nullable String field, String errorCode,
@Nullable Object[] errorArgs, @Nullable String defaultMessage);
- field : 오류 필드명
- errorCode : 오류 코드
- errorArgs : 오류 메시지에서 {0}을 치환하기 위한 값
- defaultMessage : 기본 메시지
- Spring은 타입 오류 발생 시, typeMismatch 오류 발생
- typeMismatch 오류 메시지를 정의하지 않으면, 기본 메시지 출력으로 혼동
- 컨트롤러에 검증기 적용
@InitBinder
public void init(WebDataBinder dataBinder) {
log.info("init binder {}", dataBinder);
dataBinder.addValidators(itemValidator);
}
- 검증할 대상 앞에 @Validated 애노테이션 삽입
- 모든 컨트롤러에 적용
@SpringBootApplication
public class ItemServiceApplication implements WebMvcConfigurer {
public static void main(String[] args) {
SpringApplication.run(ItemServiceApplication.class, args);
}
@Override
public Validator getValidator() {
return new ItemValidator();
}
}
- @Validated : 스프링 전용 검증 애노테이션
- @Valid : 자바 표준 검증 애노테이션
🧑🏻💻 강의 실습 기록
섹션 5. 검증2 - Validation
- Bean Validation : 검증 로직을 공통화하고, 표준화
- 검증 애노테이션과 여러 인터페이스의 모음
- Spring Boot가 자동으로 글로벌 Validator 등록
- @Validated : 검증 컨트롤러에 위치
- @NotBlank : 빈값 + 공백만 있는 경우 허용 X
- @NotNull : null 허용 X
- @Range(min = 값, max = 값) : 범위 지정
- @Max(값) : 최댓값 지정
에러 메시지 ex)
- @NotBlank
- NotBlank.item.itemName
- NotBlank.itemName
- NotBlank.java.lang.String
- NotBlank
BeanValidation 메시지 찾는 순서
- 메시지 코드 순서대로 messageSource에서 찾기
- message 속성 사용 -> @NotBlank(message = "공백")
- 라이브러리 기본 값
필드오류가 아닌, 오브젝트 오류 관련은 직접 자바 코드로 작성하는 것을 추천. @ScriptAssert로 가능은 하나 복잡한 로직에는 대응이 어렵다.
동일한 모델 객체를 등록, 수정할 경우 다르게 검증하는 방법 2가지
- Bean Validation - groups
- 별도 모델 객체를 정의
- gropus
// 객체 필드
@NotNull(groups = {SaveCheck.class, UpdateCheck.class})
// 컨트롤러 로직
public String addItemV2(@Validated(SaveCheck.class) @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes)
- 모델 객체 정의
- HTML Form -> ItemSaveForm -> Controller -> Item 생성 -> Repository 방식
public String addItem(@Validated @ModelAttribute("item") ItemSaveForm form, BindingResult bindingResult, RedirectAttributes redirectAttributes)
// @ModelAttribute("item") item 이름에 주의
- Bean Validation - API 방식
public Object addItem(@RequestBody @Validated ItemSaveForm form, BindingResult bindingResult)
- 3가지 경우
- 성공 요청 : 성공
- 실패 요청 : JSON 데이터를 객체로 생성하는 자체 실패
- 검증 오류 요청 : JSON 데이터를 객체로 생성은 성공했으나, 검증 실패
- @ModelAttribute vs @RequestBody
- @ModelAttribute는 각각 필드 단위로 세밀하게 적용, 특정 필드가 타입이 맞지 않아도 나머지는 정상 처리 가능
- @RequestBody는 JSON 데이터를 객체로 변경하지 못하면 바로 오류 발생, Validator 적용 불가능
🧑🏻💻 강의 실습 기록
'✍️ 정리 > Spring' 카테고리의 다른 글
[Inflearn] 스프링 DB 1편 - 데이터 접근 핵심 원리 (A) (1) | 2024.08.11 |
---|---|
[Inflearn] 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 정리 (B) (0) | 2024.08.06 |
[Inflearn] 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 정리 (B) (3) | 2024.07.24 |
[Inflearn] 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 정리 (A) (0) | 2024.07.18 |
[Inflearn] 모든 개발자를 위한 HTTP 웹 기본 지식 정리 (2) | 2024.07.03 |
@택이✌️ :: Code::택이
Backend 개발자를 꿈꾸는 꿈나무💭 기술 블로그
꾸준함을 목표로 하는 꿈나무 개발자 택이✌️입니다. 궁금하신 점이나 잘못된 정보가 있으면 언제든지 연락주세요. 📩 함께 프로젝트 및 스터디도 언제든지 희망합니다! 📖