DTO 객체 -> Member Entity -> Member Service -> Member Repository
DTO (Data Transfer Object)
사용자에게 입력받은 데이터를 담아 전달하는 객체이다.
Member Entity
회원 정보를 담는 객체이며, 데이터베이스의 테이블과 연결된다.
Member Service
회원가입, 중복 검사 등 비즈니스 로직을 처리하는 역할을 한다.
Member Repository
Service의 요청을 받아 데이터베이스에 데이터를 저장하거나 조회하는 역할을 한다.
Database(DB)
최종적으로 회원 정보를 저장하는 공간이다.
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();
}
}
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";
}
}
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";
}
}
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());
}
}
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;
}
}
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;
}
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());
}
}
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";
}
}
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에러
}
}
<!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>
<!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>
<!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>
<!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>
<!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>
<?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를 활용해 회원가입, 로그인, 관리자 권한 등 다양한 테스트 코드를 작성하면서 기능이 정상적으로 동작하는지 검증하는 방법도 학습했다.
사실 정리는 이렇게 했지만 머릿속이 너무 복잡하다!
어디갔다 저기갔다 데이터 흐름(?)이 너무 많고 복잡해서 정말 열심히 공부해야 될 거 같다라는 생각이 든다.
열심히 해서 나 혼자 스스로도 쇼핑몰 정도는 뚝딱 만드는 개발자가 되고 싶다.