풀스택 개발자 과정 61일차

너구·2026년 8월 4일

풀스택 성장과정

목록 보기
64/79

스프링 시큐리티

인증 / 인가

  • 인증이 필요없는 경우 -> 상품 상세 페이지 조회
  • 인증이 필요한 경우 -> 상품 주문
  • 관리자 권한이 필요한 경우 -> 상품 등록

회원가입 흐름 (Test)

DTO 객체 -> Member Entity -> Member Service -> Member Repository

  • DTO (Data Transfer Object)
    사용자에게 입력받은 데이터를 담아 전달하는 객체이다.

  • Member Entity
    회원 정보를 담는 객체이며, 데이터베이스의 테이블과 연결된다.

  • Member Service
    회원가입, 중복 검사 등 비즈니스 로직을 처리하는 역할을 한다.

  • Member Repository
    Service의 요청을 받아 데이터베이스에 데이터를 저장하거나 조회하는 역할을 한다.

  • Database(DB)
    최종적으로 회원 정보를 저장하는 공간이다.

MemberService.java

package com.shop.service;

import com.shop.entity.Member;
import com.shop.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service // 서비스
@Transactional // 트랜잭션
@RequiredArgsConstructor // @Autowired -> 싱글톤 스프링 컨테이너에서 받아서 쓴다
// 변수 final 객체 붙여줄게
public class MemberService implements UserDetailsService {
    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("이미 가입된 회원입니다.");
        }
    }

    @Override
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
        Member member = memberRepository.findByEmail(email);

        if (member == null) {
            throw new UsernameNotFoundException(email);
        }
        // 빌더패턴
        return User.builder().username(member.getEmail())
                .password(member.getPassword())
                .roles(member.getRole().toString())
                .build();
    }
}

MemberControllor.java

package com.shop.controller;

import com.shop.dto.MemberFormDto;
import com.shop.entity.Member;
import com.shop.service.MemberService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.context.properties.bind.BindResult;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping(value = "/members")
@RequiredArgsConstructor
public class MemberController {

    private final MemberService memberService;
    private final PasswordEncoder passwordEncoder;

    @GetMapping(value = "/new")
    public String memberForm(Model model) {
        model.addAttribute("memberFormDto", new MemberFormDto());
        return "member/memberForm";
    }

    @PostMapping(value = "/new")
    public String memberForm(@Valid MemberFormDto memberFormDto,
                             BindingResult bindingResult, Model model) {
        if (bindingResult.hasErrors()) {
            return "member/memberForm";
        }
        try {
            Member member = Member.createMember(memberFormDto, passwordEncoder);
            memberService.saveMember(member);
        } catch (IllegalStateException e) {
            model.addAttribute("errorMessage", e.getMessage());
            return "member/memberForm";
        }
        return "redirect:/";
    }

    @GetMapping(value = "/login")
    public String loginMember() {
        return "/member/memberLoginForm";
    }

    @GetMapping(value = "/login/error")
    public String loginError(Model model) {
        model.addAttribute("loginErrorMsg", "아이디 또는 비밀번호를 확인해주세요.");
        return "/member/memberLoginForm";
    }
}

MainController.java

package com.shop.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class MainController {
    @GetMapping(value = "/")
    public String main() {
        return "main";
    }
}

MemberControllerTest.java

package com.shop.controller;


import com.shop.dto.MemberFormDto;
import com.shop.entity.Member;
import com.shop.service.MemberService;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.transaction.annotation.Transactional;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin;

@SpringBootTest
@AutoConfigureMockMvc // MockMvc 테스트를 하기 위해서 -> 테스트에서 간단한 웹 환경 동작이 가능하게 함
@Transactional
@TestPropertySource(locations = "classpath:application-test.properties")
class MemberControllerTest {
    @Autowired
    private MemberService memberService;

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private PasswordEncoder passwordEncoder;

    public Member createMember(String email, String password) {
        MemberFormDto memberFormDto = new MemberFormDto();
        memberFormDto.setEmail(email);
        memberFormDto.setName("홍길동");
        memberFormDto.setAddress("서올시 마포구 합정동");
        memberFormDto.setPassword(password);
        Member member = Member.createMember(memberFormDto, passwordEncoder);
        return memberService.saveMember(member);
    }

    @Test
    @DisplayName("로그인 성공 테스트")
    public void loginSuccessTest() throws Exception {
        String email = "test@email.com";
        String password = "1234";
        this.createMember(email, password);
        mockMvc.perform(formLogin().userParameter("email")
                .loginProcessingUrl("/members/login")
                .user(email).password(password))
                .andExpect(SecurityMockMvcResultMatchers.authenticated());
    }

    @Test
    @DisplayName("로그인 실패 테스트")
    public void loginFailTest() throws Exception {
        String email = "test@email.com";
        String password = "1234";
        this.createMember(email, password);
        mockMvc.perform(formLogin().userParameter("email")
                        .loginProcessingUrl("/members/login")
                        .user(email).password("12345"))
                .andExpect(SecurityMockMvcResultMatchers.unauthenticated());
    }
}

Member.java

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;
    }
}

MemberFormDto.java

package com.shop.dto;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.validator.constraints.Length;

@Getter
@Setter
public class MemberFormDto {
    @NotBlank(message = "이름은 필수 입력 값입니다.")
    private String name;

    @NotEmpty(message = "이메일은 필수 입력 값입니다.")
    @Email(message = "이메일 형식으로 입력해주세요.")
    private String email;

    @NotEmpty(message = "비밀번호는 필수 입력 값입니다.")
    @Length(min = 8, max = 16, message = "비밀번호는 8자 이상, 16자 이하로 입력해주세요.")
    private String password;

    @NotEmpty(message = "주소는 필수 입력 값입니다.")
    private String address;
}

SecurityConfig.java

package com.shop.config;

import com.shop.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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 {
    @Autowired
    MemberService memberService;

    // 허용 여부 설정하는 메서드
    // @Bean -> 객체 / 스프링 컨테이너에서 관리하고 사용한다. 싱글턴
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests(auth -> auth
                .requestMatchers("/css/**", "/js/**", "/img/**", "/favicon.ico", "/error").permitAll()
                .requestMatchers("/", "/members/**", "/item/**", "/images/**").permitAll()
                .requestMatchers("/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
        )
        .formLogin(formLogin -> formLogin
                .loginPage("/members/login")
                .defaultSuccessUrl("/")
                .usernameParameter("email")
                .failureUrl("/members/login/error")
        )
        .logout(logout -> logout
                .logoutUrl("/members/logout")
                .logoutSuccessUrl("/")
        );

        return http.build();
    }
    // 암호 -> 암호화 기능
    @Bean
    public static PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

    @Autowired
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(memberService).passwordEncoder(passwordEncoder());
    }
}

ItemController.java

package com.shop.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class ItemController {
    @GetMapping(value = "/admin/item/new")
    public String itemForm() {
        return "/item/itemForm";
    }
}

ItemControllorTest.java

package com.shop.controller;


import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;

import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
import static org.springframework.test.web.servlet.result.
        MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.
        MockMvcResultMatchers.status;

@SpringBootTest
@AutoConfigureMockMvc
@TestPropertySource(locations = "classpath:application-test.properties")
class ItemControllerTest {
    @Autowired
    MockMvc mockMvc;

    @Test
    @DisplayName("상품 등록 페이지 권한 테스트")
    public void itemFormTest() throws Exception{
        mockMvc.perform(MockMvcRequestBuilders.get("/admin/item/new")
                        .with(user("admin").roles("ADMIN")))
                .andDo(print())
                .andExpect(status().isOk()); // 200
    }
    @Test
    @DisplayName("상품 등록 페이지 일반 회원 접근 테스트")
    public void itemFormNotAdminTest() throws Exception{
        mockMvc.perform(MockMvcRequestBuilders.get("/admin/item/new")
                        .with(user("user").roles("USER")))
                .andDo(print())
                .andExpect(status().isForbidden()); // 403에러
    }
}

memberLoginForm.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorate="~{layouts/layout1}">
<!--사용자 CSS 추가-->
<th:block layout:fragment="css">
    <style>
        .error {
            color: #db2130;
        }
    </style>
</th:block>
<div layout:fragment="content">
    <form action="/members/login" role="form" method="post">
        <div class="form-group">
            <label th:for="email">이메일 주소</label>
            <input type="email" name="email" class="form-control" placeholder="이메일을 입력해주세요.">
        </div>
        <div class="form-group">
            <label th:for="password">비밀번호</label>
            <input type="password" name="password" class="form-control" placeholder="비밀번호를 입력해주세요.">
        </div>
        <p th:if="${loginErrorMsg}" class="error" th:text="${loginErrorMsg}"></p>
        <button class="btn btn-primary">로그인</button>

        <button type="button" class="btn btn-primary" onclick="location.href='/members/new'">회원가입</button>
        <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
    </form>
</div>
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

</body>
</html>

main.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorate="~{layouts/layout1}">

    <div layout:fragment="content">
        <h1>메인 페이지 입니다.</h1>
    </div>
</html>

memberForm.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorate="~{layouts/layout1}">
<!--사용자 css 추가-->
<th:block layout:fragment="css">
    <style>
        .fieldError {
        color: #bd2130;
        }
    </style>
</th:block>
<!--사용자 스크립트 추가-->
<th:block layout:fragment="script">
    <script th:inline="javascript">
        $(document).ready(function() {
        var errorMessage = [[${errorMessage}]];
            if(errorMessage != null) {
                alert(errorMessage);
            }
        });
    </script>
</th:block>

<div layout:fragment="content">
    <form action="/members/new" role="form" method="post" th:object="${memberFormDto}">
        <div class="form-group">
            <label th:for="name">이름</label>
            <input type="text" th:field="*{name}" class="form-control" placeholder="이름을 입력해주세요.">
            <p th:if="${#fields.hasErrors('name')}" th:errors="*{name}" class="fieldError">Incorrect date</p>
        </div>
        <div class="form-group">
            <label th:for="email">이메일 주소</label>
            <input type="text" th:field="*{email}" class="form-control" placeholder="이메일을 입력해주세요.">
            <p th:if="${#fields.hasErrors('email')}" th:errors="*{email}" class="fieldError">Incorrect date</p>
        </div>
        <div class="form-group">
            <label th:for="password">비밀번호</label>
            <input type="password" th:field="*{password}" class="form-control" placeholder="비밀번호를 입력해주세요.">
            <p th:if="${#fields.hasErrors('password')}" th:errors="*{password}" class="fieldError">Incorrect date</p>
        </div>
        <div class="form-group">
            <label th:for="address">주소</label>
            <input type="text" th:field="*{address}" class="form-control" placeholder="주소를 입력해주세요.">
            <p th:if="${#fields.hasErrors('address')}" th:errors="*{address}" class="fieldError">Incorrect date</p>
        </div>
        <div style="text-align: center">
            <button type="submit" class="btn btn-primary" style="">Submit</button>
        </div>
        <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
    </form>
</div>
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

</body>
</html>

header.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/extras/spring-security">

<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" sec:authorize="hasAnyAuthority('ROLE_ADMIN')">
                        <a class="nav-link" href="/admin/item/new">상품 등록</a>
                    </li>
                    <li class="nav-item" sec:authorize="hasAnyAuthority('ROLE_ADMIN')">
                        <a class="nav-link" href="/admin/item">상품 관리</a>
                    </li>
                    <li class="nav-item" sec:authorize="isAuthenticated()">
                        <a class="nav-link" href="/cart">장바구니</a>
                    </li>
                    <li class="nav-item" sec:authorize="isAuthenticated()">
                        <a class="nav-link" href="/orders">구매이력</a>
                    </li>
                    <li class="nav-item" sec:authorize="isAnonymous()">
                        <a class="nav-link" href="/members/login">로그인</a>
                    </li>
                    <li class="nav-item" sec:authorize="isAuthenticated()">
                        <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>

itemForm.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorate="~{layouts/layout1}">
<div layout:fragment="content">
    <h1>상품등록 페이지입니다.</h1>
</div>
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

</body>
</html>

pom.xml (shop)

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>4.1.0</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.shop</groupId>
    <artifactId>shop</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name/>
    <description/>
    <url/>
    <licenses>
        <license/>
    </licenses>
    <developers>
        <developer/>
    </developers>
    <scm>
        <connection/>
        <developerConnection/>
        <tag/>
        <url/>
    </scm>
    <properties>
        <java.version>25</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-h2console</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webmvc</artifactId>
        </dependency>

        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webmvc-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.querydsl</groupId>
            <artifactId>querydsl-jpa</artifactId>
            <version>5.0.0</version>
            <classifier>jakarta</classifier>
        </dependency>
        <dependency>
            <groupId>com.querydsl</groupId>
            <artifactId>querydsl-apt</artifactId>
            <version>5.0.0</version>
            <classifier>jakarta</classifier>
        </dependency>
        <dependency>
            <groupId>com.querydsl</groupId>
            <artifactId>querydsl-core</artifactId>
            <version>5.0.0</version>
        </dependency>
        <dependency>
            <groupId>nz.net.ultraq.thymeleaf</groupId>
            <artifactId>thymeleaf-layout-dialect</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.thymeleaf.extras</groupId>
            <artifactId>thymeleaf-extras-springsecurity6</artifactId>
            <version>3.1.2.RELEASE</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <executions>
                    <execution>
                        <id>default-compile</id>
                        <phase>compile</phase>
                        <goals>
                            <goal>compile</goal>
                        </goals>
                        <configuration>
                            <annotationProcessorPaths>
                                <path>
                                    <groupId>org.projectlombok</groupId>
                                    <artifactId>lombok</artifactId>
                                </path>
                            </annotationProcessorPaths>
                        </configuration>
                    </execution>
                    <execution>
                        <id>default-testCompile</id>
                        <phase>test-compile</phase>
                        <goals>
                            <goal>testCompile</goal>
                        </goals>
                        <configuration>
                            <annotationProcessorPaths>
                                <path>
                                    <groupId>org.projectlombok</groupId>
                                    <artifactId>lombok</artifactId>
                                </path>
                            </annotationProcessorPaths>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>com.mysema.maven</groupId>
                <artifactId>apt-maven-plugin</artifactId>
                <version>1.1.3</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>process</goal>
                        </goals>
                        <configuration>
                            <outputDirectory>target/generated-sources/java</outputDirectory>
                            <processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>

역할 정리

파일역할
pom.xml프로젝트에서 사용하는 라이브러리(의존성)와 빌드 설정을 관리하는 파일이다.
itemForm.html관리자가 상품을 등록하는 화면이다.
header.html여러 페이지에서 공통으로 사용하는 헤더(메뉴)를 관리한다.
memberForm.html회원가입 화면을 제공하는 페이지이다.
main.html사용자가 처음 접속하는 메인 화면이다.
memberLoginForm.html로그인 화면을 제공하는 페이지이다.
ItemControllerTest.java상품 관련 기능이 정상적으로 동작하는지 테스트하는 클래스이다.
ItemController.java상품 등록, 조회 등 상품 관련 요청을 처리하는 컨트롤러이다.
SecurityConfig.java로그인, 로그아웃, 권한, 접근 허용 등 Spring Security를 설정하는 클래스이다.
MemberFormDto.java회원가입 시 입력한 데이터를 담고, 입력값 검증(@NotBlank, @Size 등)을 수행하는 DTO이다.
Member.java회원 정보를 저장하는 Entity이며 데이터베이스의 Member 테이블과 연결된다.
MemberControllerTest.java회원가입과 로그인 등 회원 기능을 테스트하는 클래스이다.
MainController.java메인 페이지 요청을 처리하는 컨트롤러이다.
MemberController.java회원가입, 로그인 등 회원 관련 요청을 처리하는 컨트롤러이다.
MemberService.java회원가입, 중복 검사, 비밀번호 암호화 등 회원 관련 비즈니스 로직을 처리하는 클래스이다.

전체 관계

회원가입

memberForm.html

MemberController

MemberFormDto

입력값 검증(@NotBlank, @Size)

MemberService

Member(Entity)

Repository

Database

로그인

memberLoginForm.html

Spring Security
(SecurityConfig)

MemberService

Member(Entity)

로그인 성공/실패

상품등록

itemForm.html

ItemController

Service

Repository

Database

화면 구성

main.html

├── header.html (공통 헤더)
├── memberForm.html
├── memberLoginForm.html
└── itemForm.html

테스트

MemberControllerTest

회원가입/로그인 기능 테스트

ItemControllerTest

상품 등록 및 권한 테스트


한 줄 요약

-HTML : 사용자에게 보여지는 화면(View)
-Controller : 사용자의 요청을 받아 처리
-DTO : 입력 데이터를 전달하고 검증
-Service : 실제 비즈니스 로직 수행
-Entity : 데이터베이스와 연결되는 객체
-SecurityConfig : 로그인과 권한 관리
-Test : 기능이 정상적으로 동작하는지 검증
-pom.xml : 프로젝트에 필요한 라이브러리와 빌드 설정 관리

마무리

오늘은 더 복잡한 걸 배웠다.
Spring Boot에서 회원가입과 로그인 기능의 전체 흐름을 작성하였는데 회원가입 과정에서 Controller → DTO → Service → Entity → Repository → Database로 이어지는 구조와 각 계층의 역할을 이해했다.
또한 @NotBlank, @Size를 사용한 입력값 검증과 @Valid, BindingResult를 이용한 예외 처리 방법도 배웠다.
Spring Security를 적용하여 로그인, 로그아웃, 접근 권한을 설정하는 방법을 익혔고, PasswordEncoder를 이용한 비밀번호 암호화와 인증 과정을 확인했다.
마지막으로 MockMvc를 활용해 회원가입, 로그인, 관리자 권한 등 다양한 테스트 코드를 작성하면서 기능이 정상적으로 동작하는지 검증하는 방법도 학습했다.

사실 정리는 이렇게 했지만 머릿속이 너무 복잡하다!
어디갔다 저기갔다 데이터 흐름(?)이 너무 많고 복잡해서 정말 열심히 공부해야 될 거 같다라는 생각이 든다.
열심히 해서 나 혼자 스스로도 쇼핑몰 정도는 뚝딱 만드는 개발자가 되고 싶다.

0개의 댓글