Super Kawaii Cute Cat Kaoani [인스타그램 클론 코딩][Spring boot] 3. 회원 가입 구현

[인스타그램 클론 코딩][Spring boot] 3. 회원 가입 구현

2024. 2. 17. 02:25
728x90
SMALL
더보기

✔️ username, password, email, name을 입력 받고 회원 가입을 진행하도록 하겠음!

📌 User 엔티티 구현

package yerong.InstagramCloneCoding.domain.user;

import jakarta.persistence.*;
import lombok.*;
import yerong.InstagramCloneCoding.domain.BaseTimeEntity;

@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Entity
public class User extends BaseTimeEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "userId")
    private Long id;

    @Column(nullable = false, unique = true, length = 20)
    private String username;
    @Column(nullable = false)
    private String password;
    @Column(nullable = false)
    private String email;
    @Column(nullable = false)
    private String name;
    
    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private Role role;

    private String bio; //자기소현
    private String website;
    private String phone;
    private String gender;
}
  • BaseTimeEntity를 통해 생성 시간 및 수정 시간을 받아올 것임!
  • @GeneratedValue(strategy = GenerationType.IDENTITY)
    • 번호 증가 전략이 데이터베이스를 따라가는 것!
  • @Column(unique = true)
    • 중복 불가 제약 조건 설정
    • 중복되면 오류 발생 ! (이 오류를 처리하는 방법은 밑에서 다시 설명하겠음)

 

✅ BaseTimeEntity코드

package yerong.InstagramCloneCoding.domain;

import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.time.LocalDateTime;

@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseTimeEntity {

    @CreatedDate
    private LocalDateTime createdAt;

    @LastModifiedDate
    private LocalDateTime updatedAt;
}

 

➡️ JPAAuditing 활성화 시켜줘야 함!

@EnableJpaAuditing
@SpringBootApplication
public class InstagramCloneCodingApplication {

	public static void main(String[] args) {
		SpringApplication.run(InstagramCloneCodingApplication.class, args);
	}

}
728x90

📌 UserRepository

package yerong.InstagramCloneCoding.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import yerong.InstagramCloneCoding.domain.user.User;

@Repository
public interface UserRepository extends JpaRepository<User, Long> {


}

➡️ JPA Repository 사용

 

 

📌 AuthService

package yerong.InstagramCloneCoding.service;


import yerong.InstagramCloneCoding.web.dto.SignupDto;

public interface AuthService {

    void join(SignupDto signupDto);
}
package yerong.InstagramCloneCoding.service.impl;

import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import yerong.InstagramCloneCoding.domain.user.User;
import yerong.InstagramCloneCoding.repository.UserRepository;
import yerong.InstagramCloneCoding.service.AuthService;
import yerong.InstagramCloneCoding.web.dto.SignupDto;

@Service
@RequiredArgsConstructor
public class AuthServiceImpl implements AuthService {

    private final UserRepository userRepository;
    @Override
    @Transactional
    public void join(SignupDto signupDto){
        User user = signupDto.toEntity();
        userRepository.save(user);
    }
}

 

➡️ 회원가입 서비스 기본적인 구현

(비밀번호 해시 등 구체적인 작업은 밑에서 다시 설명!)

 

반응형

📌 AuthController

  • csrf 토큰 때문에 회원 가입을 완료한 후 로그인으로 이동하는 것이 실행 안됨
    • 서버는 시큐리티가 감싸고 있기 때문에 서버에 도착하기 전에 시큐리티가 검사를 진행함
    • 비활성화 시키는 것이 편함
  • SecurityConfig 코드 중 일부
return http
	.csrf(csrfConfig ->
        csrfConfig.disable())
        	.build();

 

 

    • signup.html 코드에 해당 코드를 구현해주면 해당 url을 post로 요청 시 아래의 코드 실행됨
<form class="login__input" action="/auth/signup" method="post">

 

 

✅ AuthController 코드

package yerong.InstagramCloneCoding.web.controller;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import yerong.InstagramCloneCoding.domain.user.User;
import yerong.InstagramCloneCoding.service.AuthService;
import yerong.InstagramCloneCoding.web.dto.SignupDto;

@Controller
@Slf4j
@RequiredArgsConstructor
public class AuthController {

    private final AuthService authService;

    @GetMapping("/auth/signin")
    public String signinForm(){
        return "views/auth/signin";
    }

    @GetMapping("/auth/signup")
    public String signupForm(){
        return "views/auth/signup";
    }

    @PostMapping("/auth/signup")
    public String signup(SignupDto signupDto){
        authService.join(signupDto);
        return "views/auth/signin";
    }
}

 

 

📌 비밀번호 해시

 

✅ SecurityConfig 

    @Bean
    public BCryptPasswordEncoder encoder(){
        return new BCryptPasswordEncoder();
    }

➡️ 빈으로 등록

 

 

✅ AuthServiceImpl 코드에 비밀번호 encode 추가 및 Role 설정

    private final BCryptPasswordEncoder passwordEncoder;

    @Override
    @Transactional
    public void join(SignupDto signupDto){
        String rawPassword = signupDto.getPassword();
        String encPassword = passwordEncoder.encode(rawPassword);
        signupDto.setPassword(encPassword);
        User user = signupDto.toEntity();
        user.setRole(Role.USER);
        userRepository.save(user);
    }

 

 

 

 

📌 유효성 검증

✅ build.gradle

implementation 'org.springframework.boot:spring-boot-starter-validation'

 

✅ SignupDto

  • Dto로 요청받을 것이기 때문에 여기에 조건 걸어줌
package yerong.InstagramCloneCoding.web.dto;

import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.NotBlank;
import lombok.Getter;
import lombok.Setter;
import yerong.InstagramCloneCoding.domain.user.User;

@Getter
@Setter
public class SignupDto {

    @Size(min = 2, max = 20)
    @NotBlank        
    String username;

    @NotBlank
    String password;
    @NotBlank
    String email;
    @NotBlank
    String name;

    public User toEntity(){
        return User.builder()
                .username(username)
                .password(password)
                .email(email)
                .name(name)
                .build();
    }
}
  • @Max() : 문자열/배열의 길이 제한
  • @NotBlank : 빈칸이면 안됨

 

✅ 엔티티에도 적용

package yerong.InstagramCloneCoding.domain.user;

import jakarta.persistence.*;
import lombok.*;
import yerong.InstagramCloneCoding.domain.BaseTimeEntity;

@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Builder
@Entity
public class User extends BaseTimeEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "userId")
    private Long id;

    @Column(nullable = false, unique = true, length = 20)
    private String username;

    @Column(nullable = false)
    private String password;
    @Column(nullable = false)
    private String email;

    @Column(nullable = false)
    private String name;

    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private Role role;

    private String bio; //자기소개
    private String website;
    private String phone;
    private String gender;

    public void setRole(Role role){
        this.role = role;
    }

}

 

✅ Controller에도 적용

    @PostMapping("/auth/signup")
    public String signup(@Valid SignupDto signupDto, BindingResult bindingResult){
        if(bindingResult.hasErrors()){
            Map<String, String> errorMap = new HashMap<>();

            for(FieldError error : bindingResult.getFieldErrors()){
                errorMap.put(error.getField(), error.getDefaultMessage());
                log.info(error.getDefaultMessage());
            }
        }
        authService.join(signupDto);
        return "views/auth/signin";
    }
  • Dto 앞에 @Valid 설정해두면 앞서 설정한 조건들에 맞게 유효성 검사를 자동으로 해줌
  • 검증할 객체 바로 옆에 BindingResult 생성
    • 자동으로 안에 에러들이 담김
    • Map을 하나 만들어서 생성된 에러들을 담아줌
      ➡️ 20 이하여야 합니다 로그가 자동으로 생성됨!

 

✔️ @ResponseBody 적용 ver

    @PostMapping("/auth/signup")
    public @ResponseBody String signup(@Valid SignupDto signupDto, BindingResult bindingResult){
        if(bindingResult.hasErrors()){
            Map<String, String> errorMap = new HashMap<>();

            for(FieldError error : bindingResult.getFieldErrors()){
                errorMap.put(error.getField(), error.getDefaultMessage());
                log.info(error.getDefaultMessage());
            }
        }
        authService.join(signupDto);
        return "views/auth/signin";
    }
  • @ResponseBody + @Controller = @RestController➡️ 즉, 데이터를 반환함

✅ Handler 적용

  • 예외가 터지면 Handler가 가로챔
    ➡️ @ControllerAdvice 사용
  • 따로 handler 패키지에 ControllerExceptionHandler 만들어서 사용
    • @RestController 사용해서 데이터 전달

 

✔️ 공통응답DTO

package yerong.InstagramCloneCoding.web.dto;

import lombok.*;

import java.util.Map;

@Getter
@Setter
@RequiredArgsConstructor
@NoArgsConstructor
@AllArgsConstructor
public class CMRespDto<T> {

    private String message;
    private T data;

}

➡️ 왜 T로 하는가? : 그때그때 리턴하고 싶은 형태가 다를 수도 있기 때문!

 

✔️ CustomValidationException

package yerong.InstagramCloneCoding.handler.exception;

import java.util.Map;

public class CustomValidationException extends RuntimeException{

    private String message;
    private Map<String, String> errorMap;

    public CustomValidationException(String message, Map<String, String> errorMap){
        super(message);
        this.message = message;
        this.errorMap = errorMap;
    }

    public Map<String, String> getErrorMap(){
        return errorMap;
    }
}

 

✔️ ControllerExceptionHandler

package yerong.InstagramCloneCoding.handler;

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestController;
import yerong.InstagramCloneCoding.handler.exception.CustomValidationException;
import yerong.InstagramCloneCoding.web.dto.CMRespDto;

import java.util.Map;

@RestController
@ControllerAdvice
@Slf4j
public class ControllerExceptionHandler {

    @ExceptionHandler(CustomValidationException.class)
    public CMRespDto<Map<String, String>> validationException(CustomValidationException e){
        return new CMRespDto(-1, e.getMessage(), e.getErrorMap());
    }
}

➡️ 뭘 리턴할 지 모를 때는 <> 사이의 값을 <?>로 해도 됨! (괜히 없는 타입을 해서 오류가 날 수도 있음)

 

✔️ AuthController

    @PostMapping("/auth/signup")
    public String signup(@Valid SignupDto signupDto, BindingResult bindingResult){
        if(bindingResult.hasErrors()){
            Map<String, String> errorMap = new HashMap<>();

            for(FieldError error : bindingResult.getFieldErrors()){
                errorMap.put(error.getField(), error.getDefaultMessage());
                log.info(error.getDefaultMessage());
            }
            throw new CustomValidationException("유효성 검사 실패", errorMap);
        }
        else {
            authService.join(signupDto);
            return "views/auth/signin";
        }
    }

 

➡️ 일단 이렇게 예외 처리를 해놓으면 예외가 터지면(username의 길이가 20 이상, 나머지 값들이 빈칸) CustomValidationException 실행

 

 

❗️JavaScript 사용해서 예외 처리

✅ util/Script

  • 에러가 발생해도 해당 페이지에서 에러를 처리해야지 다른 페이지로 넘어가면 안됨
  • 프론트에서 에러를 처리하면 되는 것 같지만 postman으로 실행하면 적용이 안됨
    ➡️ 즉, 서버에서도 에러처리를 해줘야 함
package yerong.InstagramCloneCoding.util;

public class Script {
    public static String back(String message){
        StringBuffer sb = new StringBuffer();
        sb.append("<script>");
        sb.append("alert('"+message+"');");
        sb.append("history.back();");
        sb.append("</script>");
        return sb.toString();
    }
}

 

 

✔️ ControllerExceptionHandler

package yerong.InstagramCloneCoding.handler;

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestController;
import yerong.InstagramCloneCoding.handler.exception.CustomValidationException;
import yerong.InstagramCloneCoding.util.Script;
import yerong.InstagramCloneCoding.web.dto.CMRespDto;

import java.util.Map;

@RestController
@ControllerAdvice
@Slf4j
public class ControllerExceptionHandler {

    @ExceptionHandler(CustomValidationException.class)
    public String validationException(CustomValidationException e){
        return Script.back(e.getErrorMap().toString());
    }
}

 

 

 

➡️ 보기 좋게 에러 처리

➡️ 확인 누르면 다시 회원가입 창으로 넘어옴!

 

 

💡CMRespDto vs Script

  1. 클라이언트에게 응답할 때는 Script 가 좋음
  2. Ajax 통신할 때는 CMRespDto
  3. Android 통신 : CMRespDto

 

 

 


PREV

 

[인스타그램 클론코딩] 2. Security 설정

Security 라이브러리를 등록하면 인증되지 않은 모든 사용자를 /login 으로 redirection 됨 상태 코드 302 = redirect SecurityConfig를 통해 설정해줌 📌 SecurityConfig 세팅 package yerong.InstagramCloneCoding.config; import

nyeroni.tistory.com

NEXT

 

[인스타그램 클론코딩] 4. 로그인 구현

📌 로그인 과정 설명 보통 insert는 get 방식으로 처리하지만, 로그인은 중요한 정보를 처리하는 것이기 때문에 post 방식으로 처리한다. 우리가 처리하기보다는 Spring Security가 처리해준다 (➡️ con

nyeroni.tistory.com

 

SMALL

 

728x90
LIST

BELATED ARTICLES

more