스프링(Spring), 스프링부트(SpringBoot)/스프링(Spring) 기초

스프링 인 액션 제5판(크레이그 월즈 지음) - 웹 애플리케이션 개발하기 2

개발학생 2026. 8. 30. 19:22
반응형

*이전 글과 이어집니다.

2026.07.19 - [스프링(Spring), 스프링부트(SpringBoot)/스프링(Spring) 기초] - 스프링 인 액션 제5판(크레이그 월즈 지음) - 웹 애플리케이션 개발하기 1

 

스프링 인 액션 제5판(크레이그 월즈 지음) - 웹 애플리케이션 개발하기 1

*책 내용과 살짝 다르게, 다음과 같은 환경에서 프로젝트를 생성2026.05.25 - [스프링(Spring), 스프링부트(SpringBoot)/스프링(Spring) 기초] - 스프링 인 액션 제5판(크레이그 월즈 지음) - 스프링 시작하기

keep-programming-study.tistory.com

 

2. 폼 제출 처리하기

  • 뷰 페이지 html의 <form> 태그에 action 속성을 "post"로 설정하면,
  • 브라우저가 폼의 모든 데이터를 모아서 폼에 나타난 GET 요청과 같은 "/design" 경로로 서버에 HTTP POST 요청을 전송할 수 있게 함

1) @PostMapping을 사용해서 POST 요청 처리

(1) DesignTacoController에 메서드 추가

package tacos.web;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

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 lombok.extern.slf4j.Slf4j;
import tacos.Ingredient;
import tacos.Ingredient.Type;
import tacos.Taco;

@Slf4j // Lombok: 로그 출력을 위한 Slf4j 로거 자동 생성
@Controller // Spring MVC 컨트롤러로 등록
@RequestMapping("/design") // "/design" URL 요청을 처리하는 컨트롤러
public class DesignTacoController {
    
    ...
    
    // 사용자가 폼에서 입력한 타코 디자인 데이터를 받아옴
    // Taco 객체에는 타코 이름(name)과 선택된 재료(ingredients)가 바인딩됨
    @PostMapping
    public String processDesign(Taco design) {
    	// 이 지점에서 타코 디자인(선택된 속재료)을 저장
    	log.info("속재료 저장:" + design);
    	// 추후 DB 저장 코드 추가
    	
    	// 타코 디자인을 처리한 후 주문 페이지로 리다이렉트
        // "/orders/current" URL로 이동하여 주문 진행 화면을 보여줌
    	return "redirect:/orders/current";
    }
}

 

(2) OrderController 생성

package tacos.web;

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 lombok.extern.slf4j.Slf4j;
import tacos.Order;

@Slf4j // Lombok: Slf4j 로거 자동 생성 → 로그 출력 가능
@Controller // Spring MVC 컨트롤러로 등록
@RequestMapping("/orders") // "/orders" URL 요청을 처리하는 컨트롤러
public class OrderController {
    
    // "/orders/current" 경로로 들어오는 GET 요청을 처리
    @GetMapping("/current")
    public String orderForm(Model model) {
        // 새로운 Order 객체를 생성해서 모델에 추가
        // 뷰(orderForm.html)에서 폼 바인딩에 사용됨
        model.addAttribute("order", new Order());
        
        // "orderForm" 뷰 이름 반환 → templates/orderForm.html 렌더링
        return "orderForm";
    }
    
    // "/orders" 경로로 들어오는 POST 요청 처리
    @PostMapping
    public String processOrder(Order order) {
        // 사용자가 주문 폼을 제출하면 Order 객체에 데이터가 바인딩됨
        // Order에는 이름, 주소, 결제 정보 등 주문 관련 필드가 들어 있음
        
        // 현재는 단순히 제출된 주문 내용을 로그로 출력
        // 추후에는 DB에 저장하거나 주문 처리 로직을 추가할 예정
        log.info("Order submitted: " + order);
        
        // 주문을 처리한 후 홈 화면("/")으로 리다이렉트
        // 즉, 주문 완료 후 메인 페이지로 이동시킴
        return "redirect:/";
    }
}

 

2) 타코 주문 폼 뷰(view) 생성

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xthml"
	xmlns:th="http://www.thymeleaf.org">
<head>
	<meta charset="UTF-8">
	<title>Taco Cloud - Order</title>
	<link rel="stylesheet" th:href="@{style.css}"/>
</head>
<body>
	<form method="POST" th:action="@{/orders}" th:object="${order}">
		<h1>Order your taco creations!</h1>
		
		<img th:src="@{/images/TacoCloud.png"/>
		<a th:href="@{/design}" id="another">Design another taco</a><br />
		
		<!-- 필드 오류 발생 시 나타남 -->
		<div th:if="${#fields.hasErrors()}">
			<span class="validationError">
				Please correct the problems blow and resubmit.
			</span>
		</div>
		
		<!-- 배송자 이름, 배송 주소 및 우편번호 입력 -->
		<h3>Deliver my taco masterpieces to...</h3>
		<label for="deliveryName">Name: </label>
		<input type="text" th:field="*{deliveryName}" />
		<br />
		
		<label for="deliveryStreet">Street address: </label>
		<input type="text" th:field="*{deliveryStreet}" />
		<br />
		
		<label for="deliveryCity">City: </label>
		<input type="text" th:field="*{deliveryCity}">
		<br />
		
		<label for="deliveryState">State: </label>
		<input type="text" th:field="*{deliveryState}">
		<br />
		
		<!-- 배송 우편번호 입력 -->
		<label for="deliveryZip">Zip code: </label>
		<input type="text" th:field="*{deliveryZip}">
		<br />
		
		<!-- 결제수단 입력(카드) -->
		<h3>Here's how I'll pay</h3>
		<!-- 카드 번호 -->
		<label for="ccNumber">Credit Card: </label>
		<input type="text" th:field="*{ccNumber}">
		<br />
		
		<!-- 카드 유효기간 입력 -->
		<label for="ccExpiration">Expiration: </label>
		<input type="text" th:field="*{ccExpirattion}"/>
		<br />
		
		<!-- 카드 CVV 3자리 입력 -->
		<label for="ccCVV">CVV: </label>
		<input type="text" th:field="*{ccCVV}"/>
		<br />
		
		<!-- 주문 양식 제출 버튼 -->
		<input type="submit" value="Sumbit order" />
	</form>
</body>
</html>

 

3) 주문 정보를 저장할 Order 클래스 생성

package tacos;

import lombok.Data;

@Data // Lombok: getter/setter, toString, equals, hashCode 자동 생성
public class Order {
    
    // 배송자 이름
    private String deliveryName;
    
    // 배송 거리(Street)
    private String deliveryStreet;
    
    // 배송 도시(City)
    private String deliveryCity;
    
    // 배송 주(State)
    private String deliveryState;
    
    // 배송 우편번호(Zip)
    private String deliveryZip;
    
    // 결제용 신용카드 번호
    private String ccNumber;
    
    // 신용카드 유효기간 (MM/YY 형식)
    private String ccExpiration;
    
    // 신용카드 보안코드(CVV)
    private String ccCVV;
}

 

3. 폼 입력 유효성 검사하기 

  • 자바의 Bean Validation API를 통한 Bean 유효성 검사로 쉽게 규칙을 선언

1) 유효성 검사 규칙 선언하기

(1) pom.xml에 의존성 추가

  • 코드 수정 후, 프로젝트 우클릭 → Maven → Update Project 실행
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

 

(2) Taco 도메인 클래스에 유효성 검사 규칙 추가

package tacos;

import java.util.List;

import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;

// 타코(Taco) 디자인을 정의하는 도메인 클래스
@Data // Lombok: getter/setter, toString, equals, hashCode 자동 생성
public class Taco {
    
    // 타코 이름 (예: "Carnitas Taco", "Veggie Taco")
	@NotNull  // 빈 값이 들어가는 것을 방지
	@Size(min=5, message="Name must be at least 5 characters long")  // 문자열, 배열, 컬렉션, Map 모두 길이/크기 검증: 5글자 이상
    private String name;
    
    // 타코에 들어가는 재료 목록
    // 여기서는 재료 ID(String) 리스트로 관리
	@Size(min=1, message="You must choose at least 1 ingredient")  // 문자열, 배열, 컬렉션, Map 모두 길이/크기 검증: 리스트 길이가 1 이상ㄴ
    private List<String> ingredients;
}

 

(3) Order 클래스에 유효성 검사 규칙 추가

package tacos;

import org.hibernate.validator.constraints.CreditCardNumber;

import jakarta.validation.constraints.Digits;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import lombok.Data;

@Data // Lombok: getter/setter, toString, equals, hashCode 자동 생성
public class Order {
    
    // 배송자 이름
	@NotBlank(message="Name is required")
    private String deliveryName;
    
    // 배송 거리(Street)
	// 값이 null이거나 공백("")이면 에러 발생
	@NotBlank(message="Street is required")
    private String deliveryStreet;
    
    // 배송 도시(City)
	// 값이 null이거나 공백("")이면 에러 발생
	@NotBlank(message="City is required")
    private String deliveryCity;
    
    // 배송 주(State)
	// 값이 null이거나 공백("")이면 에러 발생
	@NotBlank(message="State is required")
    private String deliveryState;
    
    // 배송 우편번호(Zip)
	// 값이 null이거나 공백("")이면 에러 발생
	@NotBlank(message="Zip code is required")
    private String deliveryZip;
    
    // 결제용 신용카드 번호
	// Luhn 알고리즘을 이용해 카드번호 유효성 검사
	// -> Luhn 알고리즘: 신용카드 번호 같은 식별 번호가 올바르게 입력되었는지 확인하기 위한 단순한 체크섬 공식으로, 카드가 실제로 존재하거나 잔액이 있는지 확인하지는 못함
	// => 체크섬: 데이터가 전송되거나 저장되는 과정에서 오류가 발생했는지 확인하기 위한 검증 값
	@CreditCardNumber(message="Not a valid credit card number")
    private String ccNumber;
    
    // 신용카드 유효기간 (MM/YY 형식)
	// @Pattern: 정규식을 이용해 형식 검사
    // ^(0[1-9]|1[0-2]) → 월은 01~12
    // (\\/([1-9][0-9])) → 슬래시 뒤에 두 자리 연도
	@Pattern(regexp="^(0[1-9]|1[0-2])([\\/]([1-9][0-9])$", message="Must be formatted MM/YY")
    private String ccExpiration;
    
    // 신용카드 보안코드(CVV)
	// @Digits: 숫자만 허용, 정수 3자리, 소수점 없음
	@Digits(integer=3, fraction=0, message="Invalid CVV")
    private String ccCVV;
}

 

(4) 폼과 바인딩될 때 유효성 검사 수행하기 

1. DesignTacoController.java 수정 - 제출한 Taco의 유효성 검사 

package tacos.web;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.Errors;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;

import lombok.extern.slf4j.Slf4j;
import tacos.Ingredient;
import tacos.Ingredient.Type;
import tacos.Taco;

@Slf4j // Lombok: 로그 출력을 위한 Slf4j 로거 자동 생성
@Controller // Spring MVC 컨트롤러로 등록
@RequestMapping("/design") // "/design" URL 요청을 처리하는 컨트롤러
public class DesignTacoController {
    
	... 생략 ... 
    
    // 사용자가 폼에서 입력한 타코 디자인 데이터를 받아옴
    // Taco 객체에는 타코 이름(name)과 선택된 재료(ingredients)가 바인딩됨
    @PostMapping
    public String processDesign(@Valid Taco design, Errors errors) {
    	// Taco 유효성 검사를 실행하여(@Valid) 에러 발생 시 Taco 디자인 저장 처리를 중지하고 "design" 뷰를 반환
    	if (errors.hasErrors()) {
    		return "design";
    	}
    	
    	// 이 지점에서 타코 디자인(선택된 속재료)을 저장
    	log.info("속재료 저장:" + design);
    	// 추후 DB 저장 코드 추가
    	
    	// 타코 디자인을 처리한 후 주문 페이지로 리다이렉트
        // "/orders/current" URL로 이동하여 주문 진행 화면을 보여줌
    	return "redirect:/orders/current";
    }
}

 

2. OrderController.java 수정 - 제출한 Order의 유효성 검사

package tacos.web;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.Errors;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;

import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import tacos.Order;
import tacos.Taco;

@Slf4j // Lombok: Slf4j 로거 자동 생성 → 로그 출력 가능
@Controller // Spring MVC 컨트롤러로 등록
@RequestMapping("/orders") // "/orders" URL 요청을 처리하는 컨트롤러
public class OrderController {
    
    ... 생략 ... 
    
    // "/orders" 경로로 들어오는 POST 요청 처리
    @PostMapping
    public String processOrder(@Valid Order order, Errors errors) {
        // 사용자가 주문 폼을 제출하면 Order 객체에 데이터가 바인딩됨
        // Order에는 이름, 주소, 결제 정보 등 주문 관련 필드가 들어 있음
       
        // Order의 유효성 검사를 실행하여(@Valid) 에러 발생 시 Order 처리를 중지하고 "orderForm" 뷰를 반환
    	if (errors.hasErrors()) {
    		return "orderForm";
    	}
        
        // 현재는 단순히 제출된 주문 내용을 로그로 출력
        // 추후에는 DB에 저장하거나 주문 처리 로직을 추가할 예정
        log.info("Order submitted: " + order);
        
        // 주문을 처리한 후 홈 화면("/")으로 리다이렉트
        // 즉, 주문 완료 후 메인 페이지로 이동시킴
        return "redirect:/";
    }
}

 

(5) 유효성 검사 에러 보여주기 - orderForm.html 수정 (모든 필드에 <span>을 추가)

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xthml"
	xmlns:th="http://www.thymeleaf.org">
<head>
	<meta charset="UTF-8">
	<title>Taco Cloud - Order</title>
	<link rel="stylesheet" th:href="@{style.css}"/>
</head>
<body>
	<form method="POST" th:action="@{/orders}" th:object="${order}">
		<h1>Order your taco creations!</h1>
		
		<img th:src="@{/images/TacoCloud.png"/>
		<a th:href="@{/design}" id="another">Design another taco</a><br />
		
		<!-- 필드 오류 발생 시 나타남 -->
		<div th:if="${#fields.hasErrors()}">
			<span class="validationError">
				Please correct the problems blow and resubmit.
			</span>
		</div>
		
		<!-- 배송자 이름, 배송 주소 및 우편번호 입력 -->
		<h3>Deliver my taco masterpieces to...</h3>
		<label for="deliveryName">Name: </label>
		<input type="text" th:field="*{deliveryName}" />
		<!-- 배송자 이름 유효성 검사 오류 -->
		<span class="validationError"
			th:if="${#fields.hasErrors('deliveryName')}"
			th:errors="*{deleveryName}">
			Name Error
		</span>
		<br />
		
		<label for="deliveryStreet">Street address: </label>
		<input type="text" th:field="*{deliveryStreet}" />
		<!-- 배송 거리 유효성 검사 오류 -->
		<span class="validationError"
			th:if="${#fields.hasErrors('deliveryStreet')}"
			th:errors="*{deleveryStreet}">
			Street Error
		</span>
		<br />
		
		<label for="deliveryCity">City: </label>
		<input type="text" th:field="*{deliveryCity}">
		<!-- 배송 도시 유효성 검사 오류 -->
		<span class="validationError"
			th:if="${#fields.hasErrors('deliveryCity')}"
			th:errors="*{deleveryCity}">
			City Error
		</span>
		<br />
		
		<label for="deliveryState">State: </label>
		<input type="text" th:field="*{deliveryState}">
		<!-- 배송 주 유효성 검사 오류 -->
		<span class="validationError"
			th:if="${#fields.hasErrors('deliveryState')}"
			th:errors="*{deleveryState}">
			State Error
		</span>
		<br />
		
		<!-- 배송 우편번호 입력 -->
		<label for="deliveryZip">Zip code: </label>
		<input type="text" th:field="*{deliveryZip}">
		<!-- 배송 우편번호 유효성 검사 오류 -->
		<span class="validationError"
			th:if="${#fields.hasErrors('deliveryZip')}"
			th:errors="*{deleveryZip}">
			Zip Error
		</span>
		<br />
		
		<!-- 결제수단 입력(카드) -->
		<h3>Here's how I'll pay</h3>
		<!-- 카드 번호 -->
		<label for="ccNumber">Credit Card: </label>
		<input type="text" th:field="*{ccNumber}">
		<!-- 카드 번호 유효성 검사 오류 -->
		<span class="validationError"
			th:if="${#fields.hasErrors('ccNumber')}"
			th:errors="*{ccNumber}">
			CC Num Error
		</span>
		<br />
		
		<!-- 카드 유효기간 입력 -->
		<label for="ccExpiration">Expiration: </label>
		<input type="text" th:field="*{ccExpiration}"/>
		<!-- 카드 유효기간 유효성 검사 오류 -->
		<span class="validationError"
			th:if="${#fields.hasErrors('ccExpiration')}"
			th:errors="*{ccExpiration}">
			CC Expiration Error
		</span>
		<br />
		
		<!-- 카드 CVV 3자리 입력 -->
		<label for="ccCVV">CVV: </label>
		<input type="text" th:field="*{ccCVV}"/>
		<!-- 배송자 이름 유효성 검사 오류 -->
		<span class="validationError"
			th:if="${#fields.hasErrors('ccCVV')}"
			th:errors="*{ccCVV}">
			CC CVV Error
		</span>
		<br />
		
		<!-- 주문 양식 제출 버튼 -->
		<input type="submit" value="Sumbit order" />
	</form>
</body>
</html>

 

반응형