도서 'Spring in Action' 제 5판을 보고 책 내용과 그 이외의 부족한 부분을 채워가며 공부한 내용입니다.
코드 첨부는 package명과 import문은 생략했습니다.
키워드 : 스프링 MVC, HTTP, Template, Lombok, Model
1. 정보 보여주기
현재 만드려는 애플리케이션
타코 클라우드 애플리케이션
- 온라인으로 타코를 주문할 수 있는 어플리케이션
- 풍부한 식자재를 보여주는 팔레트를 사용해서 고객이 창의적으로 커스텀 타코를 디자인할 수 있도록 고안
스프링 웹 어플리케이션 필요 Component
- 타코의 식자재의 속성을 정의하는 Domain Class
- 식자재 정보를 가져와서 View에 전달하는 스프링 MVC Controller Class
- 식자재의 내역을 사용자의 브라우저에 보여주는 View Template
1. Domain 설정하기
객체 - 타코 식자재(Ingredient
- Type
- 고기류
- 치즈류
- 소스류
- 이름
- ID
package tacos;
import lombok.Data;
import lombok.RequiredArgsConstructor;
@Data
@RequiredArgsConstructor
public class Ingridient {
private final String id;
private final String name;
private final Type type;
public static enum Type{
WRPA, PROTEIN, VEGGIES, CHEES, SAUCE
}
}
2. Controller Class 생성하기
Controller Class -> 스프링 MVC 프레임워크의 핵심
역할
- HTTP 요청을 처리
- 브라우저에 보여줄 HTML을 View에 요청
- REST 형태의 응답 몸체에 직접 데이터 추가
현재 목표
- 요청 경로가 /design인 HTTP GET 요청을 처리
- 식자재의 내역을 생성
- 식자재 데이터의 HTML 작성을 View Template에 요청
- 작성된 HTML을 웹 브라우저에 전송
@Slf4j
@Controller
@RequestMapping("/desing")
public class DesignTacoController {
@GetMapping
public String showDesignFrom(Model model) {
List<Ingredient> ingredients = Arrays.asList(
new Ingredient("FLTO", "Flour Tortilla", Type.WRAP),
new Ingredient("COTO", "Corn Tortilla", Type.WRAP),
new Ingredient("GRBF", "Ground Beef", Type.PROTEIN),
new Ingredient("CARN", "Carnitas", Type.PROTEIN),
new Ingredient("TMTO", "Diced Tomatoes", Type.VEGGIES),
new Ingredient("LETC", "Lettuce", Type.VEGGIES),
new Ingredient("CHED", "Cheddar", Type.CHEESE),
new Ingredient("JACK", "Monterrey Jack", Type.CHEESE),
new Ingredient("SLSA", "Salsa", Type.SAUCE),
new Ingredient("SRCR", "Sour Cream", Type.SAUCE)
);
Type[] types = Ingredient.Type.values();
for(Type type : types) {
model.addAttribute(type.toString().toLowerCase(),
filterByType(ingredients, type));
}
model.addAttribute("taco", new Taco());
return "design";
}
private List<Ingredient> filterByType(
List<Ingredient> ingredients, Type type){
return ingredients
.stream()
.filter(x -> x.getType().equals(type))
.collect(Collectors.toList());
}
}
- @SLF4J
자바에서 사용하는 Simple Logging Facade
SLF4J Logger를 생성(아래의 구문을 추가한 것과 같은 효과
private static final org.slf4j.Logger log =
org.slf4j.LoggerFactory.getLogger(DesingTacoController.class);
- @Controller
DesignTacoController.class가 컨트롤러로 식별
컴포넌트 검색을 해야한다는 것을 나타냄.
Spring이 DesingTacoController.class를 찾은 후 Spring Application Context의 Bean으로 해당 Class의 인스턴스를 자동 생성
- @RequestMapping("/design")
DesignTacoController에서 /design으로 시작하는 경로의 요청을 처리함(아래의 GET 요청 처리하기에서 자세하게 다룸)
- public String showDesignFrom(Model model)
식자재를 나타내는 Ingredient 객체를 저장하는 List를 생성(현재 Arrays.asList를 사용해서 List에 들어갈 Ingredient들을 직접 new로 추가함(현재는 예시이고 앞으로는 DataBase로부터 가져와서 저장할 것)
- model.addAttribute(type.toString().toLowerCase(), filterByType(ingredients, type));
type을 소문자로 하여 key 값으로 받고 필터링(filterByType 메서드를 통해 같은 type을 필터)한 ingredients를 value로 model에 넣음
Model은 Controller와 Data를 보여주는 View 사이에서 데이터를 운반하는 객체
Model 객체의 속성에 있는 데이터는 View가 알 수 있는 서블릿(Servlet) 요청 속성들로 복사
- private List<Ingredient> filterByType(List<Ingredient> ingredients, Type type)
식자재의 유형(고기, 치즈, 소스 등)을 List에서 필터링(FilterByType 메서드)할때 사용한 메서드
ingredients에서 각각의 요소들 x의 Type을 받아왔을 때(.getType()) 그 값이 type 과 일치한다면(.equals(type)) 해당 요소를 추출
-> 결과적으로 위의 코딩이 하는 일
브라우저에서 /design 경로에 접속한다면 DesignTacoController의 showDesignForm() 메서드가 실행되고 View에 요청이 전달되기 전에 List에 저장된 식자재 데이터를 모델 객체(Model)에 넣음.
3. View 디자인하기
스프링에서 View를 정의하는 여러가지 방법
- JSP(JavaServer Page)
- Thymeleaf
- FreeMarker
- Mustache
- Groovy 기반의 템플릿
Thymeleaf
- 어떠한 프레임워크와도 사용 가능하도록 설계
- 스프링의 추상화 모델을 알지 못함
- Controller가 데이터를 넣는 Model 대신 Servlet요청 속성을 사용
- 스프링은 View에게 요청을 전달하기 앞서 Thymeleaf와 이외의 다른 View 템플릿이 사용하는 요청 속성에 모델 데이터를 복사
- 템플릿에 영향을 주지 않는(HTML의 구조를 깨지 않고 기존의 HTML 코드를 변경하지 않고 덧붙이는 코드)방식을 사용
Thymeleaf Template : 요청 데이터를 나타내는 요소 속성을 추가로 갖는 HTML
ex) key가 "message"인 요청 속성이 있고 이것을 Thymeleaf를 사용해서 HTML <p> 태그로 나타내고자 한다면 Thymeleaf Template의 작성방법
<p th:text="${message}">placeholder message</p>
핵심 라이브러리
Standard Dialect
대부분으 프로세서는 'Attribute Processor'
- 이를 통해 브라우저는 단순히 추가 속성을 무시하기 때문에 처리되기 전에도 HTML 템플릿 파일을 올바르게 표시
비교예시
1. JSP
<input type="text" name="userName" value="${user.name}">
2. Thymeleaf Standard Dialect
<input type="text" name="userName" value="Gorany" th:value="${user.name}">
장점
- 디자이너와 개발자가 동일한 템플릿 파일에서 작업하고 정적인 HTML을 동적인 HTML로 변환하는데 필요한 노력을 줄일 수 있음.(Natural Templating이 해당 기능 수행)
Variable Expression: ${...}
${...} : Controller로부터 받은 Model에서 값을 꺼내 사용하는 것
Message Expressions: #{...}
#{...} : properties파일에서 메세지(값)을 꺼내어 사용하는 것.
Selection Variable Expressions: *{...}
*{...} : 상위 태그에 선언되어 있는 변수에 대한 메서드를 바로 표현(java의 static import와 유사)
Link URL Expressions: @{...}
@{...} : 리소스를 찾는 URL이라면 static 폴더부터 시작해서 경로표현식을 사용
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="EUC-KR">
<title>Taco Cloud</title>
<link rel="stylesheet" th:href="@{/styles.css}" />
</head>
<body>
<h1>Design your taco!</h1>
<img th:src="@{/images/TacoCloud.png}" />
<form method="POST" th:object="${taco}">
<span class="validationError"
th:if="${#fields.hasErrors('ingredients')}"
th:errors="*{ingredients}">Ingredient Error</span>
<div class="grid">
<div class="ingredient-group" id="wraps">
<h3>Designate your wrap:</h3>
<div th:each="ingredient : ${wrap}">
<input name="ingredients" type="checkbox"
th:value="${ingredient.id}" />
<span th:text="${ingredient.name}">INGREDIENT</span><br />
</div>
</div>
<div class="ingredient-group" id="proteins">
<h3>Pick your protein:</h3>
<div th:each="ingredient : ${protein}">
<input name="ingredients" type="checkbox"
th:value="${ingredient.id}" />
<span th:text="${ingredient.name}">INGREDIENT</span><br />
</div>
</div>
<div class="ingredient-group" id="cheeses">
<h3>Choose your cheese:</h3>
<div th:each="ingredient : ${cheese}">
<input name="ingredients" type="checkbox"
th:value="${ingredient.id}" />
<span th:text="${ingredient.name}">INGREDIENT</span><br />
</div>
</div>
<div class="ingredient-group" id="veggies">
<h3>Determine your veggies:</h3>
<div th:each="ingredient : ${veggies}">
<input name="ingredients" type="checkbox"
th:value="${ingredient.id}" />
<span th:text="${ingredient.name}">INGREDIENT</span><br />
</div>
</div>
<div class="ingredient-group" id="sauces">
<h3>Select your sauce:</h3>
<div th:each="ingredient : ${sauce}">
<input name="ingredients" type="checkbox"
th:value="${ingredient.id}" />
<span th:text="${ingredient.name}">INGREDIENT</span><br />
</div>
</div>
</div>
<div>
<h3>Name your taco creation:</h3>
<input type="text" th:field="*{name}" />
<span
class="validationError" th:if="${#fields.hasErrors('name')}"
th:errors="*{name}">Name Error</span>
<br />
<button>Submit your taco</button>
</div>
</form>
</body>
</html>
<코드 분석>
- <p: th:text="${message}">placeholder message</p>
key가 message인 서블릿 요청 속성의 값으로 교체
- <h3>Designate your wrap:</h3>
<div th:each="ingredient : ${wrap}">
<input name="ingredients" type="checkbox" th:value="${ingredient.id}" />
<span th:text="${ingredient.name}">INGREDIENT</span><br />
</div>
wrap 요청 속성에 있는 컬렉션의 각 항목에 대해 하나씩 <div>를 반복해서 나타내기 위해 th:each 속성을 사용
(for loop와 유사)
각 반복에서 식자재 항목이 ingredients라는 이름의 Thymeleaf 변수와 바인딩
<div>요소 내부에는 체크 상자인 <input>요소와 해당 체크 상자의 라벨을 제공하기위한 <span>요소가 있음.
th.value를 사용해서 <input>요소의 value 속성을 해당 식자재의 id 속성 값으로 설정
<span>요소는 th:text를 사용해서 "INGREDIENT"텍스트를 해당 식자재의 name 속성값으로 교체
- <img th:src="@{/images/TacoCloud.png}" />
resources에서 TacoCloud 이미지를 가져옴
- <link rel="stylesheet" th:href="@{/styles.css}" />
@{} 연산자 사용 : 참조되는 정적 콘텐츠의 위치(컨텍스트 상대 경로)를 알려줌.
4. 스타일 시트
@charset "EUC-KR";
div.ingredient-group:nth-child(odd) {
float: left;
padding-right: 20px;
}
div.ingredient-group:nth-child(even) {
float: left;
padding-right: 0;
}
div.ingredient-group {
width: 50%;
}
.grid:after {
content: "";
display: table;
clear: both;
}
*, *:after, *:before {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
span.validationError {
color: red;
}
2. Lombok
어노테이션 기반으로 코드를 자동완성 해주는 라이브러리
Getter, Setter, Equals, ToString 등의 코드를 자동완성 시킬 수 있음.
장점
- 어노테이션 기반의 코드 자동 생성을 통한 생산성 향상
- 반복되는 코드 다이어트를 통한 가독성 및 유지보수성 향상
- Getter, Setter외에 빌더 패턴이나 로그 생성 등 다양한 방면으로 활용 가능
어노테이션
class 이름 위에 적용시 모든 변수들에 적용이 가능
변수이름 위에 적용시 해당 변수들만 적용 가능
- @Getter @Setter
변수들에 대해 Getter/Setter 메서드를 만듬.
- @AllArgsConsturctor
모든 변수를 사용하는 생성자를 자동완성 시켜주는 어노테이션
class위에 사용
- @NoArgsConstructor
어떠한 변수도 사용하지 않는 기본 생성자를 자동완성 시켜주는 어노테이션
- @RequiredArgsConstructor
특정 변수만을 활용하는 생성자를 자동완성 시켜주는 어노테이션
생성자의 인자로 추가할 변수에 @NonNull 어노테이션을 붙여서 해당 변수를 생성자의 인자로 추가
해당 변수를 final로 선언해도 의존성을 주입받을 수 있음
- @EqualsAndHashCode
class에 대한 equals 함수와 hashCode함수를 자동으로 생성
만약 서로 다른 두 객체에서 특정 변수의 이름이 똑같은 경우 같은 객체로 판단하고 싶다면 아래와 같이 설정
@EqualsAndHashCode(of = {"Name", "ID"}, callSuper = false)
추가 설명
- of = {"Name", "ID"}
- Name과 ID가 동일하다면 같은 객체로 인식하도록 해줌.
- callSuper = false
- 상위 클래스를 적용시키지 않기 위해 사용
- @ToString
class의 변수들을 기반으로 ToString 메서드를 자동 완성출력을 원하지 않는 변수 위에 @ToString.Exclude을 붙여주면 출력 제외(해당 변수만 ToString메서드가 안생김)
- @Data
@String, @EqualsAndHashCode, @Getter, @Setter, @RequiredArgsConstructor를 자동르로 완성
실무에선 너무 무겁워 활용을 지양
- @Builder
해당 Class의 객체의 생성에 Builder 패턴을 적용
Class의 위에 작성 시 모든 변수에 적용
해당 변수만을 build하려면 생성자를 작성하고 그 위에 @Builder 어노테이션 추가
- @Delegate
한 객체의 메서드를 다른 객체로 위임
- @Log4j2
해당 Class의 Log Class를 자동 완성
3.GET 요청 처리하기
Class 수준의 @RequestMapping과 함께 사용된 @GetMapping 어노테이션은 /design의 HTTP GET요청이 수신될 때 그 요청을 처리하기 위해 showDesignForm() 메서드가 호출됨을 나타냄.
- 스프링 MVC 요청-대응 어노테이션
어노테이션 | 설명 |
@RequestMapping | 다목적 요청을 처리 |
@GetMapping | HTTP GET요청을 처리 |
@PostMapping | HTTP POST요청을 처리 |
@PutMapping | HTTP PUT 요청을 처리 |
@DeleteMapping | HTTP DELETE 요청을 처리 |
@PatchMapping | HTTP PATCH 요청을 처리 |
4. Model에 데이터를 담기
- Model addAttribute(String name, Object value)
value 객체를 name(key 역할) 이름으로 추가, View 코드에서는 name으로 지정한 이름을 통해서 value를 사용
- Model addAttribute(Object value)
value만 추가
@Controller 클래스 안의 매핑되는 모든 URL에 공통적으로 넘겨줘야할 데이터가 있을 때 사용
value의 패키지 이름을 제외한 단순 class 이름을 모델 이름으로 사용(첫글자는 소문자로 처리)
value가 배열이거나 컬렉션인 경우 첫 번째 원소의 클래스 이름 뒤에 "List"를 붙인 걸 Model 이름으로 사용(해당 케이스의 경우 클래스 이름의 첫자는 소문자 처리)
참고 자료
1. 크레이그 월즈, Spring in Action, Fifth Edition(출판지 : 제이펍, 2020)
'Spring' 카테고리의 다른 글
Spring 데이터로 작업하기 1 (0) | 2022.05.18 |
---|---|
Spring 웹 애플리케이션 개발 3 (0) | 2022.05.11 |
Spring 웹 애플리케이션 개발 2 (0) | 2022.05.11 |
Spring 기초 - 2 (0) | 2022.05.09 |
Spring 기초 - 1 (0) | 2022.05.09 |