<Product(상품) 설계>

1) Entity 생성

package com.ecproject.onlinestore.entity;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.*;

import java.time.LocalDateTime;

@Entity
@Getter
@Setter
@NoArgsConstructor
@ToString
public class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
    private String details;
    private int price;
    private int quantity;

    private LocalDateTime releaseTime;
    private LocalDateTime updateTime;
}

2) MySql 데이터베이스 테이블 생성

3) MySql 관련 의존성 추가

// build.gradle
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.mysql:mysql-connector-j'
// application.properties
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/(데이터베이스명)
spring.datasource.username=(아이디)
spring.datasource.password=(비밀번호)
spring.jpa.show-sql = true
spring.jpa.database = mysql

4) ProductRepository 생성

package com.ecproject.onlinestore.repository;

import com.ecproject.onlinestore.entity.Product;
import org.springframework.data.jpa.repository.JpaRepository;

public interface ProductRepository extends JpaRepository<Product, Long> {
}

 

<회원가입 기능>

1) Spring Security 의존성 추가

// build.gradle
implementation 'org.springframework.boot:spring-boot-starter-security'

2) SecurityConfig 작성

- Spring Security의 기본 설정을 커스터마이징하여 애플리케이션의 보안 요구 사항에 맞게 조정

- 'WebSecurityConfigurerAdapter' 클래스를 상속받아 구현하는 방식은 더이상 쓰이지 않음

- 'SecurityFilterChain' 사용이 권장됨

package com.ecproject.onlinestore.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.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

import static org.springframework.security.config.Customizer.withDefaults;

@Configuration      // 클래스가 Spring IoC 컨테이너에 의해 Bean 정의의 소스로 사용됨을 나타냄
@EnableWebSecurity  // Spring Security 설정을 활성화
public class SecurityConfig {

    @Bean
    // HttpSecurity 객체를 사용하여 HTTP 요청에 대한 보안 구성을 정의
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.authorizeRequests(authz -> authz
            .anyRequest().authenticated())  // 모든 요청에 대해 인증을 요구함
            .formLogin(withDefaults());  // 기본 로그인 폼을 제공함
        return http.build();
    }

    @Bean
    // 비밀번호를 안전하게 저장하고 검증하는 메커니즘을 제공하는 인터페이스
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(); // BCrypt 해싱 함수를 사용하여 비밀번호를 인코딩
    }
}

 

3) MemberRole enum 생성

- 각각의 멤버가 일반 유저인지 관리자인지 구분

package com.ecproject.onlinestore.entity;

public enum MemberRole {
    USER, ADMIN     // 각각의 멤버가 일반 유저인지 관리자인지 구분
}

4) DTO 생성

- DTO(Data Transfer Object): 계층 간 데이터 전송을 위해 사용되는 객체

- 클라이언트와 서버 간, 또는 서비스 계층과 데이터 액세스 계층 간의 데이터 교환을 위해 사용

- 'MemberFormDto': 사용자가 회원가입 양식에 입력하는 데이터를 담는 DTO

package com.ecproject.onlinestore.dto;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class MemberFormDto {

    private String name;
    private String email;
    private String password;
    private String address;
}

5) Member Entity 생성

- @Enumerated(EnumType.STRING): 열거형 값을 문자열로 데이터베이스에 저장, 열거형의 순서가 변경되더라도 데이터베이스에 영향 X

- createMember 메서드: MemberFormDto 객체와 비밀번호를 암호화하기 위한 PasswordEncoder를 받아 새로운 Member 인스턴스를 생성하고 반환

package com.ecproject.onlinestore.entity;

import com.ecproject.onlinestore.dto.MemberFormDto;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

import org.springframework.security.crypto.password.PasswordEncoder;


@Entity
@Getter
@Setter
@ToString
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @Column(unique = true)      // 회원은 이메일을 통해 유일하게 구분
    private String email;

    private String password;
    private String address;

    @Enumerated(EnumType.STRING)    
    private Role role;

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

}

6) MemberRepository 생성

package com.ecproject.onlinestore.repository;

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

public interface MemberRepository extends JpaRepository<Member, Long> {
    Member findByEmail(String email);   //회원 가입시 중복된 회원이 있는지 이메일로 검사
}

7) MemberService 생성

package com.ecproject.onlinestore.service;

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

@Service
@Transactional
@RequiredArgsConstructor
public class MemberService {

    private final MemberRepository memberRepository;
    // 의존성 주입, 회원 정보에 대한 CRUD 연산을 데이터베이스에 위임

    public Member saveMember(Member member) {   // 새 회원 저장 메서드
        validateDuplicateMember(member);        // 먼저 중복 검사 실행
        return memberRepository.save(member);
    }

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

8) MemberController 생성

- 회원가입이 성공하면 메인 페이지로 리다이렉트

package com.ecproject.onlinestore.controller;

import com.ecproject.onlinestore.dto.MemberFormDto;
import com.ecproject.onlinestore.entity.Member;
import com.ecproject.onlinestore.service.MemberService;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
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;

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

    // 의존성 주입
    private final MemberService memberService;
    private final PasswordEncoder passwordEncoder;

    @GetMapping("/new")
    public String memberForm(Model model){                      // 회원 가입 양식 요청 메서드
        model.addAttribute("memberFormDto", new MemberFormDto());
        return "member/memberForm";
    }
    @PostMapping("/new")
    public String memberForm(MemberFormDto memberFormDto){      // 회원 가입 양식 제출 메서드
        Member member = Member.createMember(memberFormDto, passwordEncoder);
        memberService.saveMember(member);

        return "redirect:/";
    }
}

9) MainController 생성

- 애플리케이션의 홈페이지 또는 메인 페이지를 사용자에게 보여주는 역할을 수행

- 사용자가 웹 브라우저에서 루트 URL을 방문하면, "main" 뷰가 렌더링되어 사용자에게 응답으로 전송

package com.ecproject.onlinestore.controller;

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

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

10) MemberFormDto 유효성 검증 어노테이션 추가

package com.ecproject.onlinestore.dto;

import lombok.Getter;
import lombok.Setter;
import org.hibernate.validator.constraints.Length;

import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;

@Getter
@Setter
public class MemberFormDto {

    @NotBlank(message = "이름은 필수 입력 값입니다.")
    private String name;

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

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

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

11) MemberController 기능 추가

회원 가입이 성공하면 메인 페이지로 리다이렉트

- 실패하면 다시 회원 가입 페이지로 돌아가 실패 이유 출력

- @Valid: MemberFormDto 객체에 설정된 검증 조건을 자동으로 검사

- BindingResult: 검증 과정에서 발생한 에러 정보를 담음

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

    // 의존성 주입
    private final MemberService memberService;
    private final PasswordEncoder passwordEncoder;

    @GetMapping("/new")         // 회원 가입 양식 요청 메서드
    public String memberForm(Model model){
        model.addAttribute("memberFormDto", new MemberFormDto());
        return "member/memberForm";
    }
    @PostMapping("/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:/";
        // 성공적으로 회원 가입이 완료되면 루트 경로(/)로 리다이렉트
    }
}

12) thymeleaf 설정, memberForm.html 작성

implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>회원가입</title>
</head>
<body>
<h1>회원가입</h1>
<form th:action="@{/members/new}" th:object="${memberFormDto}" method="post">
    <p>이름: <input type="text" th:field="*{name}" /></p>
    <p>이메일: <input type="email" th:field="*{email}" /></p>
    <p>비밀번호: <input type="password" th:field="*{password}" /></p>
    <p>주소: <input type="text" th:field="*{address}" /></p>
    <p><button type="submit">가입하기</button></p>
</form>
</body>
</html>

http://localhost:8080/members/new

 

13) SecurityConfig 수정

회원 가입이 성공하면 메인 페이지로 리다이렉트

@Configuration      // 클래스가 Spring IoC 컨테이너에 의해 Bean 정의의 소스로 사용됨을 나타냄
@EnableWebSecurity  // Spring Security 설정을 활성화
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .csrf(csrf -> csrf.disable()) // CSRF 보호 비활성화
                .authorizeRequests(authz -> authz
                        .requestMatchers("/members/new").permitAll() // '/members/new' 경로에 인증되지 않은 접근 허용
                        .anyRequest().authenticated() // 그 외 모든 요청에 대해 인증 요구
                )
                .formLogin(withDefaults()); // 기본 로그인 폼 사용

        return http.build();
    }

    @Bean
    // 비밀번호를 안전하게 저장하고 검증하는 메커니즘을 제공하는 인터페이스
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(); // BCrypt 해싱 함수를 사용하여 비밀번호를 인코딩
    }
}

 

+ Recent posts