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

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

개발학생 2026. 7. 19. 19:19
반응형

*책 내용과 살짝 다르게, 다음과 같은 환경에서 프로젝트를 생성

2026.05.25 - [스프링(Spring), 스프링부트(SpringBoot)/스프링(Spring) 기초] - 스프링 인 액션 제5판(크레이그 월즈 지음) - 스프링 시작하기 1

 

스프링 인 액션 제5판(크레이그 월즈 지음) - 스프링 시작하기 1

0. 스프링과 스프링 부트의 차이1) 스프링(Spring Framework)의존성 주입(DI), AOP, 트랜잭션 관리 등 엔터프라이즈 애플리케이션의 기반을 제공XML, Java Config, Annotation 등을 이용해 원하는 설정을 직접 해

keep-programming-study.tistory.com

 

 

0. 스프링으로 구축할 애플리케이션

  • 데이터를 처리하고, 데이터베이스로부터 정보를 읽고, 다른 애플리케이션과 상호작용
  • 애플리케이션에 대한 사용자의 첫 인상은 UI(User Interface, 사용자 인터페이스)에서 갖게 되는데, 
    이는 대부분 웹 브라우저에 나타남 
    -> 스프링 MVC는 간단하게 정적인 콘텐츠를 보여주는 것 외에도, 훨씬 더 많은 UI들을 구현할 수 있음

1. 정보 보여주기

  • 이전 글들에서 만들었던 '타코 클라우드'에서 풍부한 타코 속제료를 보여주는 팔레트를 사용하여,
    고객이 창의적으로 커스텀 타코를 만들 수 있게 하고자 함
  • 이때 고객 자신이 원하는 타코를 디자인할 때 타코 속재료를 보여주고 선택할 수 있는 페이지가 있어야 하며,
    선택 가능한 속재료는 수시로 변경될 수 있으므로 하드코딩하는 방식이 아닌 데이터베이스에서 전달되는 방식으로 해야 함
  • 타코 디자인 페이지를 지원하기 위해 3개의 컴포넌트 생성
    1. 타코 식자재의 속성을 정의하는 도메인(domain) 클래스
    2. 타코 속재료 정보를 가져와 뷰에 전달하는 스프링 MVC 컨트롤러
    3. 선택 가능한 속재료 내역을 사용자 화면에 보여주는 뷰 템플릿 

스프링 MVC의 요청 처리 흐름(타코 애플리케이션)

1) 도메인(domain) 설정

  • 도메인은 해당 애플리케이션 이해에 필요한 개념을 다루는 영역
  • 타코 클라우드 애플리케이션의 도메인 객체: 고객이 선택한 타코 디자인, 디자인을 구성하는 속재료, 고객, 고객의 타코 주문

(1) 속재료 정의

  • pom.xml에서 마우스 우클릭 후, Spring -> Add starters를 선택한 후,
    Lombok을 검색해서 추가한 다음 OK 버튼을 누르는 과정 필수!
package tacos;

import lombok.Data;
import lombok.RequiredArgsConstructor;

// 타코 속재료(Ingredient)를 정의하는 도메인 클래스
@Data // Lombok: getter/setter, toString, equals, hashCode 자동 생성
@RequiredArgsConstructor // Lombok: final 필드를 매개변수로 받는 생성자 자동 생성
public class Ingredient {
    
    // 재료의 고유 ID (예: "CARN" 같은 코드값)
    private final String id;
    
    // 재료 이름 (예: "Carnitas", "Cheddar Cheese")
    private final String name;
    
    // 재료 종류 (랩, 단백질, 채소, 치즈, 소스)
    private final Type type;
    
    // 재료 종류를 정의하는 enum
    public static enum Type {
        WRAP,     // 또띠야, 타코 껍질 같은 랩류
        PROTEIN,  // 고기, 두부 등 단백질류
        VEGGIES,  // 양상추, 토마토 등 채소류
        CHEESE,   // 치즈류
        SAUCE     // 살사, 마요네즈 등 소스류
    }
}

 

(2) 타코 디자인 정의

package tacos;

import java.util.List;

import lombok.Data;

// 타코(Taco) 디자인을 정의하는 도메인 클래스
@Data // Lombok: getter/setter, toString, equals, hashCode 자동 생성
public class Taco {
    
    // 타코 이름 (예: "Carnitas Taco", "Veggie Taco")
    private String name;
    
    // 타코에 들어가는 재료 목록
    // 여기서는 재료 ID(String) 리스트로 관리
    private List<String> ingredients;
}

2) 컨트롤러 클래스 생성

  • 컨트롤러는 스프링 MVC 프레임워크의 중심적인 역할을 수행
    -> HTTP 요청을 처리하고, 브라우저에 보여줄 HTML을 뷰에 요청하거나, REST 형태의 응답 몸체에 직접 데이터 추가
  • 타코 클라우드 애플리케이션에서 수행하는 작업들
    1. 요청 경로가 /design인 HTTP GET 요청 처리
    2. 속재료 내역 생성
    3. 속재료 데이터의 HTML 작성을 뷰 템플릿에 요청한 다음, HTML을 웹 브라우저에 전송
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.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 {
    
    // GET 요청이 들어오면 타코 디자인 페이지를 보여주는 메서드(HTTP GET 요청 처리)
    @GetMapping
    public String showDesignForm(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 Beaf", 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)
        );
        
        // Ingredient.Type enum 값들을 가져옴 (WRAP, PROTEIN, VEGGIES, CHEESE, SAUCE)
        Type[] types = Ingredient.Type.values();
        
        // 각 타입별로 재료를 필터링해서 모델에 추가
        // 예: model에 "wrap" → 랩 재료 리스트, "protein" → 단백질 재료 리스트
        for (Type type : types) {
            model.addAttribute(type.toString().toLowerCase(),
                    filterByType(ingredients, type));
        }
        
        // 뷰에서 바인딩할 빈 타코 객체를 모델에 추가
        model.addAttribute("taco", new Taco());
        
        // "design" 뷰 이름 반환 → templates/design.html 렌더링
        return "design";
    }
    
    // 재료 리스트에서 특정 타입만 필터링하는 헬퍼 메서드
    private List<Ingredient> filterByType(List<Ingredient> ingredients, Type type) {
        return ingredients
                .stream()
                .filter(x -> x.getType().equals(type)) // 타입이 일치하는 재료만 선택
                .collect(Collectors.toList()); // 리스트로 반환
    }
}

 

3) 뷰 디자인하기 (지금은 뷰 라이브러리 중 Thymeleaf 사용)

  • 뷰 라이브러리들은 어떤 웹 프레임워크와도 사용 가능하도록 설계되어,
    스프링의 추상화 모델을 알지 못하고 컨트롤러가 데이터를 넣는 Model 대신 서블릿 요청 속성들을 사용 
    -> 스프링은 뷰에게 요청을 전달하기 전에, 뷰 템플릿이 사용하는 요청 속성에 모델 데이터를 복사해야 함
  • pom.xml에 다음과 같은 코드가 있는지 한번 더 확인!
    -> 스프링 부트의 자동-구성에서 런타임 시 classpath의 Thymeleaf를 찾아 Bean을 자동으로 생성 
    <dependencies>
        ...
		
        <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

(1) /src/main/resources/templates에서 우클릭한 후 New->Other를 선택하고 design.html 파일을 만든 후 코드 입력

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xthml"
	xmlns:th="http://www.thymeleaf.org">
<head>
	<meta charset="UTF-8">
	<title>Taco Cloud - Design</title>
	<link rel="stylesheet" th:href="@{style.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}">
			속재료 에러
		</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>
				</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>
				</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>
				</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>

(2) /src/main/resources/templates에서 우클릭한 후 New->Other를 선택하고 style.css 파일을 만든 후 코드 입력

@charset "UTF-8"; /* CSS 파일의 문자 인코딩을 UTF-8로 지정 */

/* 홀수 번째 ingredient-group div 스타일 */
div.ingredient-group:nth-child(odd) {
    float: left;              /* 왼쪽으로 배치 */
    padding-right: 20px;      /* 오른쪽 여백 추가 */
}

/* 짝수 번째 ingredient-group div 스타일 */
div.ingredient-group:nth-child(even) {
    float: left;              /* 왼쪽으로 배치 */
    padding-right: 0;         /* 오른쪽 여백 없음 */
}

/* ingredient-group 공통 스타일 */
div.ingredient-group {
    width: 50%;               /* 전체 너비의 절반 차지 */
}

/* grid 컨테이너의 float 해제 (clearfix) */
.grid:after {
    content: "";              /* 가상 요소 생성 */
    display: table;           /* 블록 요소처럼 동작 */
    clear: both;              /* float 속성 해제 */
}

/* 모든 요소와 가상 요소에 박스 크기 계산 방식 지정 */
*, *:after, *:before {
    -webkit-box-sizing: border-box; /* 크롬/사파리용 */
    -moz-box-sizing: border-box;    /* 파이어폭스용 */
    /* 기본적으로 border-box 적용: padding, border 포함한 크기 계산 */
}

/* 유효성 검사 에러 메시지 스타일 */
span.validationError {
    color: red;               /* 에러 메시지를 빨간색으로 표시 */
}

(3) 스프링부트 서버를 실행하고 http://localhost:8080/design에 접속해서 확인

 

 

*다음 글에서 계속됩니다

반응형