풀스택 개발자 과정 60일차

너구·2026년 8월 3일

풀스택 성장과정

목록 보기
63/79

Spring

서버 사이드 렌더링(SSR)

  1. 미리 정의된 템플릿을 만들고 동적으로 HTML 페이지를 만들어서 클라이언트에게 전달
  2. 요청이 올 때마다 서버에서 새로운 HTML 페이지를 만들어서 주는 방식 (서버 사이드랜드)

서버 사이드 템플릿 엔진 - Thymeleaf, JSP
JSP는 무조건 서버사이드 렌더링
Thymeleaf - 무조건 서버사이드 렌더링을 하지 않아도 웹 브라우저에서 정상적인 화면이 나온다.

layouts

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">

<head>
    <meta charset="UTF-8">
    <title>Title</title>

    <!-- CSS only -->
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/css/bootstrap.min.css">
    <link th:href="@{/css/layout1.css}" rel="stylesheet">

    <!-- JS, Popper.js and Jquery -->
    <script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/js/bootstrap.bundle.min.js"></script>

    <th:block layout:fragment="script"></th:block>
    <th:block layout:fragment="css"></th:block>
</head>

<body>

<div th:replace="~{fragments/header::header}"></div>

<div layout:fragment="content" class="content"></div>

<div th:replace="~{fragments/footer::footer}"></div>

</body>

</html>

footer.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<div class="footer" th:fragment="footer">
    <footer class="page-footer font-small cyan darken-3">
        <div class="footer-copyright text-center py-3">
            2026 Shopping Mall WebSite
        </div>
    </footer>
</div>
</html>

header.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">

<div th:fragment="header">
<!--    네비게이션 바-->
    <nav class="navbar navbar-expand-lg bg-primary navbar-dark">
<!--        버튼-->
        <div class="container-fluid">
            <button class="navbar-toggler" type="button" data-bs-toggle="collapse"
                    data-bs-target="#navbarTogglerDemo03"
                    aria-controls="navbarTogglerDemo03"
                    aria-expanded="false" aria-label="Toggle navigation">
                <span class="navbar-toggler-icon"></span>
            </button>
<!--            홈화면-->
            <a class="navbar-brand" href="/">shop</a>
<!--            네비게이션 관리-->
            <div class="collapse navbar-collapse" id="navbarTogglerDemo03">
                <ul class="navbar-nav me-auto mb-2 mb-lg-0">
                    <li class="nav-item">
                        <a class="nav-link" href="/admin/item/new">상품 등록</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="/admin/item">상품 관리</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="/cart">장바구니</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="/orders">구매이력</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="/members/login">로그인</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="/members/logout">로그아웃</a>
                    </li>
                </ul>
<!--                홈화면에서 검색가능-->
                <form class="d-flex" th:action="@{/}" method="get">
                    <input name="searchQuery" class="form-control me-2" type="search"
                        placeholder="Search" aria-label="Search">
                    <button class="btn btn-outline-success my-2 my-sm-0" type="submit">Search</button>
                </form>
            </div>
        </div>
    </nav>
</div>
</html>

thymeleafEx01

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<p th:text="${data}">Hello Thymeleaf!!</p>
</body>
</html>

thymeleafEx02

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>상품 데이터 출력 예제</h1>
<div>
    상품명 : <span th:text="${itemDto.itemNm}"></span>
</div>
<div>
    상품상세설명 : <span th:text="${itemDto.itemDetail}"></span>
</div>
<div>
    상품등록일 : <span th:text="${itemDto.regTime}"></span>
</div>
<div>
    상품가격 : <span th:text="${itemDto.price}"></span>
</div>
</body>
</html>

thymeleafEx03

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>상품 리스트 출력 예제</h1>
<table border="1">
    <thead>
    <tr>
        <td>순번</td>
        <td>상품명</td>
        <td>상품설명</td>
        <td>가격</td>
        <td>상품등록일</td>
    </tr>
    </thead>
    <tbody>
    <tr th:each="itemDto, status : ${itemDtoList}">
        <td th:text="${status.index}"></td>
        <td th:text="${itemDto.itemNm}"></td>
        <td th:text="${itemDto.itemDetail}"></td>
        <td th:text="${itemDto.price}"></td>
        <td th:text="${itemDto.regTime}"></td>
    </tr>
    </tbody>
</table>
</body>
</html>

thymeleafEx04

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>

<body>

<h1>상품 리스트 출력 예제</h1>

<table border="1">
    <thead>
    <tr>
        <td>순번</td>
        <td>상품명</td>
        <td>상품설명</td>
        <td>가격</td>
        <td>상품등록일</td>
    </tr>
    </thead>

    <tbody>
    <tr th:each="itemDto, status : ${itemDtoList}">
        <td th:if="${status.even}" th:text="짝수"></td>
        <td th:unless="${status.even}" th:text="홀수"></td>

        <td th:text="${itemDto.itemNm}"></td>
        <td th:text="${itemDto.itemDetail}"></td>
        <td th:text="${itemDto.price}"></td>
        <td th:text="${itemDto.regTime}"></td>
    </tr>
    </tbody>

</table>

</body>
</html>

thymeleafEx05

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>Thymeleaf 링크처리 예제 페이지</h1>
<div>
    <a th:href="@{/thymeleaf/ex01}">예제1 페이지 이동</a>
</div>
<div>
    <a th:href="@{https://www.thymeleaf.org/}">공식 페이지 이동</a>
</div>
<div>
    <a th:href="@{/thymeleaf/ex06(param1 = '홍길동', param2 = '안녕하세요.')}">
        thymeleaf 파라미터 전달</a>
</div>
</body>
</html>

thymeleafEx06

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<p th:text="${param1}"></p>
<p th:text="${param2}"></p>
</body>
</html>

thymeleafEx07

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
        xmlns:layout="http://ultraq.net.nz/thymeleaf/layout"
        layout:decorate="~{layouts/layout1}">
<body>
<div layout:fragment="content">
    본문 영역 입니다.
</div>
</body>
</html>

layout1.css

html {
    position: relative;
    min-height: 100%;
    margin: 0;
}

body {
    min-height: 100%;
}

.footer {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    width: 100%;
    padding: 15px 0;
    text-align: center;
}

.content {
    margin-bottom: 100px;
    margin-top: 50px;
    margin-left: 200px;
    margin-right: 200px;
}

SecurityConfig

package com.shop.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

@Configuration // 설정 클래스
@EnableWebSecurity // 웹 보안을 가능하게하는 클래스
public class SecurityConfig {
    // 허용 여부 설정하는 메서드
    // @Bean -> 객체 / 스프링 컨테이너에서 관리하고 사용한다. 싱글턴
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return null;
    }
    // 암호 -> 암호화 기능
    @Bean
    public static PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }
}

Role

package com.shop.constant;

public enum Role {
    USER, ADMIN
}

ThymeleafExController

package com.shop.controller;

import com.shop.dto.ItemDto;
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 java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

@Controller
@RequestMapping(value = "/thymeleaf") // 이 클래스에 접근하는 url
public class ThymeleafExController {
    // /thymeleaf/ex01
    @GetMapping(value = "/ex01")
    public String thymeleafExample01(Model model) {
        // 모델 data
        model.addAttribute("data", "타임리프 예제입니다.");
        // 화면 부르기
        // 경로를 문자열로 리턴하면 그 경로에 있는 html이 나온다.
        return "thymeleafEx/thymeleafEx01";
    }
    @GetMapping (value = "/ex02")
    public String thymeleafExample02(Model model) { // 뷰에다 보내줄 데이터를 담는 게 모델
        ItemDto itemDto = new ItemDto();
        itemDto.setItemDetail("상품 상세 설명");
        itemDto.setItemNm("테스트 상품1");
        itemDto.setPrice(10000);
        itemDto.setRegTime(LocalDateTime.now());

        model.addAttribute("itemDto", itemDto);
        return "thymeleafEx/thymeleafEx02";
    }

    @GetMapping(value = "/ex03")
    public String thymeleafExample03(Model model) {
        List<ItemDto> itemDtoList = new ArrayList<>();

        for (int i = 1; i <= 10; i++) {
            ItemDto itemDto = new ItemDto();
            itemDto.setItemDetail("상품 상세 설명" + i);
            itemDto.setItemNm("테스트 상품" + i);
            itemDto.setPrice(1000 * i);
            itemDto.setRegTime(LocalDateTime.now());
            itemDtoList.add(itemDto);
        }

        model.addAttribute("itemDtoList", itemDtoList);
        return "thymeleafEx/thymeleafEx03";
    }

    @GetMapping(value = "/ex04")
    public String thymeleafExample04(Model model) {
        List<ItemDto> itemDtoList = new ArrayList<>();

        for (int i = 1; i <= 10; i++) {
            ItemDto itemDto = new ItemDto();
            itemDto.setItemDetail("상품 상세 설명" + i);
            itemDto.setItemNm("테스트 상품" + i);
            itemDto.setPrice(1000 * i);
            itemDto.setRegTime(LocalDateTime.now());
            itemDtoList.add(itemDto);
        }

        model.addAttribute("itemDtoList", itemDtoList);
        return "thymeleafEx/thymeleafEx04";
    }

    @GetMapping(value = "/ex05")
    public String thymeleafExample05(Model model) {
        return "thymeleafEx/thymeleafEx05";
    }

    @GetMapping(value = "/ex06")
    public String thymeleafExample06(String param1, String param2, Model model) {
        model.addAttribute("param1", param1);
        model.addAttribute("param2", param2);
        return "thymeleafEx/thymeleafEx06";
    }

    @GetMapping(value = "/ex07")
    public String thymeleafExample07() {
        return "thymeleafEx/thymeleafEx07";
    }
}

MemberFormDto

package com.shop.dto;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class MemberFormDto {
    private String name;
    private String email;
    private String password;
    private String address;
}

Member entity

package com.shop.entity;

import com.shop.constant.Role;
import com.shop.dto.MemberFormDto;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.security.crypto.password.PasswordEncoder;

@Entity
@Table(name="member")
@Getter
@Setter
@ToString
public class Member {
    @Id
    @Column(name = "member_id")
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    private String name;

    @Column(unique = true)
    private String email;

    private String password;

    private String address;

    @Enumerated(EnumType.STRING)
    private Role role;

    // static 객체 안만들어도 바로 사용
    // MemberFormDto, PasswordEncoder
    public static Member createMember(MemberFormDto memberFormDto,
                                      PasswordEncoder passwordEncoder) {
        Member member = new Member();
        member.setName(memberFormDto.getName());
        member.setEmail(memberFormDto.getEmail());
        member.setAddress(memberFormDto.getAddress());
        String password = passwordEncoder.encode(memberFormDto.getPassword());
        member.setPassword(password);
        member.setRole(Role.ADMIN);
        return member;
    }
}

MemberRepository

package com.shop.repository;

import com.shop.entity.Member;
import org.springframework.data.jpa.repository.JpaRepository;

public interface MemberRepository extends JpaRepository<Member, Long> {
    Member findByEmail(String email);
}

MemberSercive

package com.shop.service;

import com.shop.entity.Member;
import com.shop.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service // 서비스
@Transactional // 트랜잭션
@RequiredArgsConstructor // @Autowired -> 싱글톤 스프링 컨테이너에서 받아서 쓴다
// 변수 final 객체 붙여줄게
public class MemberService {
    private final MemberRepository memberRepository;

    // 회원가입
    public Member saveMember(Member member) {
        validateDuplicateMember(member);
        return memberRepository.save(member); // 데이터베이스에 저장하라는 명령
    }

    public void validateDuplicateMember(Member member) {
        Member findMember = memberRepository.findByEmail(member.getEmail());
        if (findMember != null) {
            throw new IllegalStateException("이미 가입된 회원입니다.");
        }
    }
}

마무리

Thymeleaf를 이용해 Controller에서 데이터를 전달하고, 반복 출력·조건문·링크 처리·레이아웃(Header/Footer)까지 구현하는 방법을 배웠다.
또한 Querydsl을 이용한 동적 조회, Thymeleaf를 활용한 화면 구성과 레이아웃, Spring Security의 로그인·로그아웃 개념, 그리고 트랜잭션(@Transactional)의 동작 원리까지 배워보았는데 한 번에 여러가지가 왕창 들어오니까 정리가 잘 안되는 느낌이다.
특히 패키지와 클래스들이 정말 많이 늘어나게 되었는데 이것들에 대한 정리가 아직 완벽히 되지 않아서 머릿속이 많이 복잡하다.
하지만 직접 테스트를 해보고 동작되는 것들을 눈으로 직접 보니 재미있기도 하고 흥미로웠다.
이게 어떤 원리로 동작하고 어디에 들어가있는지 이런 것들을 확실히 내 것으로 만들면 도움이 많이 될 거 같기도 하다.
이 부분을 공부를 많이 해야겠다.

0개의 댓글