-
[스프링 인 액션] Chapter 2 - 웹 애플리케이션 개발하기 :: 정보 보여주기개발서적읽기/Spring in Action 제 5판 2020. 7. 28. 13:39
1장에서는 애플리케이션 홈페이지를 보여주기 위해 최초의 스프링 MVC 컨트롤러를 생성하였다.
이번 장에서는 타코 클라우드 애플리케이션에 가장 중요한 커스텀 타고 디자인 기능을
추가할 것이다. 그리고 이렇게 하면서 스프링 MVC를 더 깊이 있게 알아볼 것이다.
또한 모델 데이터를 보여주고 사용자 입력을 처리하는 방법을 알게 될 것이다.
웹 브라우저를 사용하는 클라이언트가 타코를 커스터마이징할 수 있도록
DB로부터 식자재를 가져와 브라우저에 보여줘야 한다.
스프링 웹 어플리케이션에서는 데이터를 가져오고 처리하는 것이 컨트롤러의 일이다.
그리고 브라우저에 보여주는 데이터를 HTML로 나타내는 것은 뷰가 하는 일이다.
컨트롤러와 뷰를 이용해 식자재를 브라우저에 보여주도록 하자.
먼저 다음 컴포넌트를 생성해야 한다.
- 타코 식자재의 속성을 정의하는 도메인 클래스
- 식자재 정보를 가져와서 뷰에 전달하는 스프링 MVC 컨트롤러 클래스
- 식자재의 내역을 사용자의 브라우저에 보여주는 뷰 템플릿
■도메인 설정하기
애플리케이션의 도메인은 해당 애플리케이션의 이해에 필요한 개념을 다루는 영역이다.
타코 클라우드 애플리케이션의 도메인에는 다음과 같은 객체가 포함된다.
- 디자인을 구성하는 식자재
- 고객이 선택한 타코 디자인
- 고객
- 고객의 타코 주문
먼저 각 식자재는 타입(고기류, 치즈류 등)과 이름을 갖는다.
그리고 각 식자재는 쉽고 분명하게 참조할 수 있는 ID를 갖는다.
package tacos;
/*
* @USER JungHyun
* @DATE 2020-07-04
* @DESCRIPTION
*/
import lombok.Data;
import lombok.RequiredArgsConstructor;
@Data
@RequiredArgsConstructor
public class Ingredient {
private final String id;
private final String name;
private final Type type;
public enum Type {
WRAP, PROTEIN, VEGGIES, CHEESE, SAUCE
}
}Ingredient 클래스에서 특이한 점은 final 속성들을 초기화하는 생성자가 없다는 것이다.
또한 속성들에 대한 getter와 setter도 없고
equals, hashCode, toString 등의 유용한 메서드도 정의하지 않았다.
이는 Lombok 라이브러리가 위와 같은 유용한 메서드들을
런타임 시에 자동으로 생성하기 때문이다.
클래스에 @Data 애노테이션을 지정하면
소스 코드에 누락된 final 속성들을 초기화하는 생성자는 물론이고
속성들의 getter와 setter 등을 생성하라고 Lombok에 알려준다.
이렇게 Lombok을 사용하면 소스 코드 분량을 줄일 수 있다.
Lombok을 사용하려면 프로젝트에 Lombok 의존성을 추가해야 한다.
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>의존성을 추가한 후 @Data와 같은 Lombok 애노테이션을 코드에 추가하면
컴파일 시에 빌드 명세(pom.xml)에 정의한 Lombok이 실행된다.
그리고 클래스의 속성들을 초기화하는 생성자는 물론이고
속성들의 getter와 setter 등을 Lombok이 자동 생성해 주므로 에러가 생기지 않는다.
■컨트롤러 클래스 생성하기
컨트롤러 클래스를 생성하기 전에 Taco 클래스를 추가하자.
이 클래스는 평범한 자바 도메인 객체를 나타내며 두 개의 속성을 갖는다
그리고 Ingredient 클래스처럼 Taco 클래스에도 @Data 애노테이션이 지정되었다.
package tacos;
/*
* @USER JungHyun
* @DATE 2020-07-04
* @DESCRIPTION
*/
import lombok.Data;
import java.util.List;
@Data
public class Taco {
private String name;
private List<String> ingredients;
}Taco 클래스가 왜 필요한지는 나중에 설명할 것이다.
컨트롤러는 HTTP 요청을 처리하고 브라우저에 보여줄 html을 뷰에 요청한다.
또는 Rest 형태의 응답 몸체에 직접 데이터를 추가한다.
타코 클라우드 애플리케이션은 다음 작업들을 수행하는 컨트롤러가 필요하다.
- 요청 경로가 /design인 HTTP GET요청을 처리한다
- 식자재의 내역을 생성한다.
- 식자재 데이터의 HTML 작성을 뷰 템플릿에 요청하고
작성된 HTML을 웹 브라우저에 전송한다.이런 요구사항을 처리하는 DesignTacoController 클래스는 다음과 같다.
package tacos.web;
/*
* @USER JungHyun
* @DATE 2020-07-04
* @DESCRIPTION
*/
import tacos.Ingredient;
import tacos.Taco;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
@Slf4j
@Controller
@RequestMapping("/design")
public class DesignTacoController {
@GetMapping
public String showDesignForm(Model model) {
List<Ingredient> ingredients = Arrays.asList(
new Ingredient("A", "AA", Ingredient.Type.WRAP),
new Ingredient("B", "BB", Ingredient.Type.WRAP),
new Ingredient("C", "CC", Ingredient.Type.PROTEIN),
new Ingredient("D", "DD", Ingredient.Type.PROTEIN),
new Ingredient("E", "EE", Ingredient.Type.VEGGIES),
new Ingredient("F", "FF", Ingredient.Type.VEGGIES),
new Ingredient("G", "GG", Ingredient.Type.CHEESE),
new Ingredient("H", "HH", Ingredient.Type.CHEESE),
new Ingredient("I", "II", Ingredient.Type.SAUCE),
new Ingredient("J", "JJ", Ingredient.Type.SAUCE)
);
Ingredient.Type[] types = Ingredient.Type.values();
for (Ingredient.Type type : types) {
model.addAttribute(type.toString().toLowerCase(), filterByType(ingredients, type));
}
model.addAttribute("taco", new Taco());
return "design";
}
@PostMapping
public String processDesign(Taco design) {
log.info("Processing design: " + design);
return "redirect:/orders/current";
}
private Object filterByType(List<Ingredient> ingredients, Ingredient.Type type) {
return ingredients.stream().filter(x -> x.getType().equals(type))
.collect(Collectors.toList());
}
}@Slf4j는 컴파일 시에 Lombok에 제공되며,
이 클래스에 자동으로 SLF4J Logger를 생성한다.
이 애노테이션은 다음 코드를 추가한 것과 같은 효과를 낸다.
private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(DesignTacoController.class);
@Controller 애노테이션은 DesignTacoController 클래스가 컨트롤러로 식별되게 하며
스프링 애플리케이션 컨텍스트가 컴포넌트 검색을 해야 한다는 것을 나타낸다.
스프링 애플리케이션 컨텍스트가 DesignTacoController 클래스를 찾은 후
이 클래스의 인스턴스를 생성하여 Bean으로 등록한다.
@RequestMapping 애노테이션이 클래스 수준으로 적용될 때는
해당 컨트롤러 클래스가 처리하는 모든 요청의 종류를 지정하게 된다.
여기서는 "/design" 경로로 시작하는 요청을 처리함을 나타낸다.
GET 요청 처리하기
@GetMapping 애노테이션은 "/design" 이라는 HTTP GET 요청이 수신될 때
그 요청을 showDesignForm 메소드로 매핑(지정)한다.
@GetMapping 애노테이션은 스프링 4.3에서 소개된 새로운 애노테이션이다.
스프링 4.3 이전에는 메서드 수준의 @RequestMapping 애노테이션을 사용했다.
Get이외에도 Post, Put, Delete, Patch 등을 지원하는 애노테이션이 각각 있다.
이제 showDesignForm 메서드 내부를 보자.
우선 식자재를 나타내는 Ingredient 객체를 저장하는 List를 생성한다.
지금은 Ingredient 객체들을 직접 코드에서 추가하였지만
이후엔 DB로부터 가져와서 저장할 것이다.
그 다음 코드에서는 식자재의 유형(고기, 치즈, 소스 등)을 List에서 필터링한 후
showDesignForm()의 인자로 전달되는 Model 객체의 속성으로 추가한다.
Model은 컨트롤러와 데이터를 보여주는 뷰 사이에서 데이터를 운반하는 객체다.
궁극적으로 Model 객체의 속성에 있는 데이터는
뷰가 알 수 있는 서블릿 요청 속성들로 복사된다.
showDesignForm 메서드는 마지막에 "design"을 반환한다.
이것은 모델 데이터를 브라우저에 나타내는 데 사용될 뷰의 논리적인 이름이다.
■뷰 디자인하기
컨트롤러가 완성되었으므로 이제는 뷰를 만들 것이다.
스프링은 뷰를 정의하는 여러 가지 방법을 제공한다.
JSP, Thymeleaf, FreeMarker, Mustache, 그루비 등의 템플릿들이 그것이다.
여기선 Thymeleaf를 사용한다.
Thymeleaf의 의존성은 아래와 같이 추가한다.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>Thymeleaf와 같은 뷰 라이브러리들은 어떤 웹 프레임워크와도 사용 가능하도록 설계되었다.
따라서 스프링의 추상화 모델을 알지 못하며
컨트롤러가 데이터를 넣는 Model 대신 서블릿 요청 속성들을 사용한다.
그러므로 그런 뷰에게 요청을 전달하기 앞서 스프링은 Thymeleaf 또는 다른 뷰 템플릿이
사용하는 요청 속성에 컨트롤러에서 생성한 모델 데이터를 복사한다.
모든 식자재의 유형과 폼을 포함하는 전체 Thymeleaf 템플릿의 코드는 다음과 같다.
design.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org" lang="en">
<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 th:text="${#fields.hasErrors('name')}">XXX</span>
<span class="validationError"
th:if="${#fields.hasErrors('name')}"
th:errors="*{name}">Name Error</span>
<br/>
<button>Submit your taco</button>
</div>
</form>
</body>
</html>styles.css
@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;
}■프로그램 실행
컨트롤러와 뷰를 실행해서 결과를 확인하자
방법은 두가지다.
1. java -jar
2. mvn spring-boot:run
위 방법중 하나를 선택하여 서버를 실행한 후
브라우저로 위 url을 싱행하면 타코 페이지가 등장한다.
'개발서적읽기 > Spring in Action 제 5판' 카테고리의 다른 글
[스프링 인 액션] Chapter 2 - 웹 애플리케이션 개발하기 :: 폼 입력 유효성 검사하기 (0) 2020.07.28 [스프링 인 액션] Chapter 2 - 웹 애플리케이션 개발하기 :: 폼 제출 처리하기 (0) 2020.07.28 [스프링 인 액션] Chapter 1 - 스프링 시작하기 :: 스프링 애플리케이션 작성하기 (0) 2020.07.28 [스프링 인 액션] Chapter 1 - 스프링 시작하기 :: 스프링 애플리케이션 초기 설정하기 (0) 2020.07.28 [스프링 인 액션] Chapter 1 - 스프링 시작하기 :: 스프링이란? (0) 2020.07.28 댓글