Spring Boot

Spring Boot + MariaDB 1 회원가입구현하기(제약조건, 암호화,제약조건, 전후처리, scrpit)

svdjcuwg4638 2023. 6. 7. 18:09

DTO만들기

 

우선 패키지와 클래스구성은 이러하다 밑에서 User.java와 SignupDto.java가 어떻게 생겼고 안의 코드는 어떤 역할을 하는지 알아보자.

 

SignupDto.java

@Data는 lombok의 기능으로 getter과 setter을 생성해줌 toString()까지

package com.cos.photogramstart.web.dto.auth;

import lombok.Data;

@Data //Getter, Setter
public class SignupDto {

	private String username;
	private String password;
	private String email;
	private String name;
	
}

 

User.java

아래에서도 lombok의 @Data를 사용하고 시간은 db에 입력하기위해 LocalDateTime 클래스를 이용하여 현재시간을 가져와 기입하여 데이터 입력

package com.cos.photogramstart.domain.user;

import java.time.LocalDateTime;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

// JPA - Java Persistence API(자바로 데이터를 영구적으로 저장할 수 있는 API를 제공)

@AllArgsConstructor
@NoArgsConstructor
@Data
@Entity // 디비에 테이블 생성
public class User {

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY) // 번호증가 전략이 데이터베이스를 따라간다
	private int id;
	// 원래는 id가 idx라 1번부터 시작하게 될건데 사용자가 1억유저라하면
	// long로 데이터타입을 잡는게 맞다 지금은 연습이니 int
	
	private String username;
	private String password;
	
	private String name;
	private String website; // 웹 사이트
	private String bio; //자기소개
	private String email;
	private String phone;
	private String gender;
	
	private String profileimageUrl; // 사진
	private String role; //권한
	
	private LocalDateTime createDate;
	
	public void createDate() {
		this.createDate =  LocalDateTime.now();
	}
	
}

 


회원가입

 

1.어노테이션 추가

User.java에 @Builder이라는 어노테이션을 추가해준다.

 

2. builder를 이용한 함수만들기

SignupDto에 밑과같이 함수 만들기 이함수가 입력받은 데이터를 기반을 유저를 생성해줄거임

 

 

3. 데이터 저장을 위해 저장 인터페이스만들기

UserRepository.java를 domain.user패키지에 만들어주었다

 

UserRepository.java

package com.cos.photogramstart.domain.user;

import org.springframework.data.jpa.repository.JpaRepository;

// 어노테이션이 없어도 JpaRepository를 상속하면 Ioc 등록이 자동으로 된다.
public interface UserRepository extends JpaRepository<User, Integer>{
	
}

안의 내용은 없더라도 저장소의 역할이 가능하니 이정도만 적어주자

 

4.서비스생성하기

AuthService.java

package com.cos.photogramstart.service;

import org.springframework.stereotype.Service;

import com.cos.photogramstart.domain.user.User;
import com.cos.photogramstart.domain.user.UserRepository;

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Service
public class AuthService {

	private final UserRepository userRepository;
	
	public User 회원가입(User user) {
		// 회원가입 진행
		User userEntity =  userRepository.save(user);
		// save는 저장한 데이터를 리턴한다
		return userEntity;
	} 
	
}

 

5.컨트롤러에 기능추가하기

AuthController.java

package com.cos.photogramstart.web;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;

import com.cos.photogramstart.domain.user.User;
import com.cos.photogramstart.service.AuthService;
import com.cos.photogramstart.web.dto.auth.SignupDto;

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor // final에 대한 모든 생성자를 만들어준다.(final을 DI할때 사용)
@Controller // 1. IoC 2. 파일을 리턴하는 컨트롤러
public class AuthController { 

	
	private static final Logger log = LoggerFactory.getLogger(AuthController.class);

	// 컨트롤러에서 서비스로 값을 넘겨줘야하기때문에 맴버필드에 불러오셔야합니다.
	private final AuthService authService;
	
    // final키워드가 있으면 생성자가 필수이다 하지만 
    // @RequiredArhsConstructor이 밑과같이 생성자를 만들어주는 역할을 한다.
//	public AuthController(AuthService authService) {
//		this.authService = authService;
//	}
	
	@GetMapping("/auth/signin")
	public String signinForm() {
		return "auth/signin";
	}

	@GetMapping("/auth/signup")
	public String signupForm() {
		return "auth/signup";
	}
	
	// 회원가입 버튼 -> /auth/signup -> /auth/signin
	@PostMapping("/auth/signup")
	public String signup(SignupDto signupDto) { // key=vaule (x-www-form-urlencoded)
		log.info(signupDto.toString()); 
		// User < - SinupDto
		User user = signupDto.toEntity();
		User userEntity = authService.회원가입(user);
		System.out.println(userEntity);
		log.info(user.toString());
		return "auth/signin";
	}	

}

 

6. 실행확인

실행해보니 id가없다며 실패문구를 확인하였다 해결방법은 밑과같다

id auto_increment(1씩증가하는 idx)를 맨처음에는 yml파일 update부분을 create로수정하고 저장해주어야 idx가 만들어진다 저장하고 밑의 로그에서 idx생성되었다는걸 확인하고 다시 update로 해줘야 데이터가 안사라집니다.

실행은 컨트롤러가 데이터를 받고 서비스에게 넘겨 서비스는 Repository에  저장한다

 


트렌젝션 관리 등록하기

Write(insert,update,delete)작업을할땐 @Transactional을 걸어주어 ACID알아서 관리해주게하여 예기치못한 오류로부터 보호할 수 있다.

위와같이 insert작업에 @Transactional을 걸어준 모습니다.


비밀번호 해시 암호화

 

1.SecurifyConfig.java에 BCryptPasswordEncoder Bean 작성

BCryptPasswordEncoder을 import할때 스프링 프레임워크껄 사용하셔야 합니다.

 

2. AuthService.java에서 암호화 진행하기

package com.cos.photogramstart.service;

import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.cos.photogramstart.domain.user.User;
import com.cos.photogramstart.domain.user.UserRepository;

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Service
public class AuthService {

	private final UserRepository userRepository;
	private final BCryptPasswordEncoder bCryptPasswordEncoder;
	
	@Transactional //Write(insert,Update,Delete)
	public User 회원가입(User user) {
		// 회원가입 진행
		String rawpassword = user.getPassword();
		String encPassword = bCryptPasswordEncoder.encode(rawpassword);
		// 암호화된 패스워드가 담기게 된다.
		user.setPassword(encPassword);
		user.setRole("ROLE_USER");
		User userEntity =  userRepository.save(user);
		// save는 저장한 데이터를 리턴한다
		return userEntity;
	} 
	
}

 


 

제약조건 추가하기

위와같은 방법으로 비밀번호 암호화 까진 성공하였다 하지만 아이디가 중복이가능하면 로그인을 시도할때 같은 아이디가 두개일시 오류가 발생할것이다 그것을 방지하기 위해 unique제약조건을 추가해보자

 

User.java에 username(id인 속성)에 @Column을 추가하여 제약조건을 줄 수 있다.

오류방지를 위해 위와같이 제약조건 설정한뒤 yml에서 

update > create로 수정하여 저장한뒤 제약조건이 추가된걸 확인하고 다시 update로 바꿔주는게 좋습니다.

 

만약 내가 문자길이도 20으로 제한을 주고싶다면

위와같이 ,로 구분을 하면 두개의 제약조건도 넣을수있고 계속추가가 가능하다

주의해야할점은 제약조건이 변경되면 바로 반영이 안되기때문에 yml에서 create로 변경한후 저장하고 변경확인 다음 update로 수정해준다.


전처리 후처리 개념

이제 제약조건이 생기고 길이가 20 아이디는 무조건 중복이되면 안된다 설정하여 오류가 발생하게되는데 이오류를 어떻게 처리하는게 좋을지에 대하여 알아보자

 

전처리

전처리는 1번 벽에 해당하고 길이가 20이다 이걸 db까지 거쳐야할까? 

절대 아니다 1번에서도 돌려보내면된다 이런것을 전처리라고한다

 

후처리

후처리는 db까지 거쳐야하는것을 말하고 아이디같은것들은 무조건 db에서 조회를 해봐야하기 때문에 db를 거쳐 2번벽에서 처리하여 오류를 해결하는것을 후처리라고한다.

ExceptionHandler로 보통 후처리를한다

 


전처리 후처리

 

1. 라이브러리 받아오기

Maven Repository에서 Validation검색하여 spring용 받아오기

주의

validation버젼은 pom.xml상단의 springBoot의 버젼을 참고하여 같이 맞춰주셔야합니다.

 

2. 컨트롤러에 @Valid 매개필드에 추가

 

@Valid의 어노테이션 종류

밑의글에선 dependency를 해줄 필요가 없다고하지만 지금은 넣어줘야 사용가능합니다. 

현재는 탑재가 안되어있음 따로 넣어줘야함

https://bamdule.tistory.com/35

 

[Spring Boot] @Valid 어노테이션으로 Parameter 검증하기

java.validation의 @Valid 어노테이션 사용법 정리 글입니다. Spring Boot 라이브러리에서 기본적으로 탑재된 기능이며 따로 dependency해 줄 필요가 없습니다. Spring Boot Version은 2.2.2.RELEASE 입니다. 1. java.valid

bamdule.tistory.com

 

3. 제약조건 설정하기

비번,이메일,네임에 NotBlank를 주어 공백 스페이스가들어간 빈문자열도 허용안하게 설정

max는 사용하니 오류가 나서 Size를 사용했습니다.

위와같이 User.java에서도 똑같이 설정해주기 

User에서는 persistence의 @Column을 사용합니다.

nullable = false는 null을 허용하지 않겠다라는 뜻

설정이 다되었다면 yml create로 설정한뒤 저장하고 db에서 DESC하여 설정적용된걸 확인하고 다시 update로 바꿔주기

 

여기서 의문인게 왜 프론트에서 태그에 그냥 require하면 notnull값은 안들어올거같은데 왜 백엔드에서도 막아주는 걸까 이유는 모든 클라이언트가 홈페이지에서 요청한다는 보장이 없다 우리가 흔히 사용하는 postman에서 유저이름만 적고 전송을해보면 데이터가 간다 즉 프론트에서 1차적으로 막고 백엔드에서 2차적으로 막아야 잘못된 데이터가 들어오는것을 방지 할 수가있다.

 

4. 컨트롤러에서 전처리확인하기

	// 회원가입 버튼 -> /auth/signup -> /auth/signin
	@PostMapping("/auth/signup")
	// ResponsBody가 붙어있다면 상단에 @Controller이라도 데이터를 반환한다	
									// BindingResult를 매개변수에 추가하여 잘못된게있는지 확인
	public @ResponseBody String signup(@Valid SignupDto signupDto, BindingResult bindingResult) { // key=vaule (x-www-form-urlencoded)
		if(bindingResult.hasErrors()) {
			Map<String, String>errorMap = new HashMap<>();
			
										// 모든 에러가 getFieldErrors에 리스트 형태로 담겨있다.
			for(FieldError error : bindingResult.getFieldErrors()) {
				errorMap.put(error.getField(), error.getDefaultMessage());
				System.out.println("================");
				System.out.println(error.getDefaultMessage());
				System.out.println("================");
			}
			return "오류남";
		}else {
			// User < - SinupDto
			User user = signupDto.toEntity();
			User userEntity = authService.회원가입(user);
			System.out.println(userEntity);
			return "auth/signin";	
			
		}
			
		
	}

지금은 결과를 확인하기위해 @ResponsBody를 붙였지만 나중에 사라질것이다

에러가 있다면 오류남이 홈페이지에 보일것이고 에러가 없다면 auth/signin이 보일것이다.

 

5.에러 처리하기 내가 원하는 형태로

CustomValidationException.java는 내가 커스텀할 에러처리문법이있고

ControllerExceptionHandler.java에는 위에서처리한 값이 넘어와 클라이언트에게 가게된다.

우선 제일먼저 받게되는 컨트롤러를 수정해보자

 

AuthController.java 에 회원가입을 처리하는 함수

	public String signup(@Valid SignupDto signupDto, BindingResult bindingResult) { // key=vaule (x-www-form-urlencoded)
		if(bindingResult.hasErrors()) {
			Map<String, String>errorMap = new HashMap<>();
			
										// 모든 에러가 getFieldErrors에 리스트 형태로 담겨있다.
			for(FieldError error : bindingResult.getFieldErrors()) {
				errorMap.put(error.getField(), error.getDefaultMessage());
				System.out.println("================");
				System.out.println(error.getDefaultMessage());
				System.out.println("================");
			}
			// exception을 발동시켜 에러를 던진다
			throw new CustomValidationException("유효성검사 실패함",errorMap);
		}else {
			// User < - SinupDto
			User user = signupDto.toEntity();
			User userEntity = authService.회원가입(user);
			System.out.println(userEntity);
			return "auth/signin";	
			
		}
			
		
	}

에러발생시 CustomValidationException에 에러Map이 전달되고 처리되는 과정을 알아보자

 

handler.ex패키지의 CustomValidationException.java

package com.cos.photogramstart.handler.ex;

import java.util.Map;

public class CustomValidationException extends RuntimeException{

	// 시리얼번호는 객체를 구분할때 사용된다
	private static final long serialVersionUID = 1L;

	private Map<String, String> errorMap;
	
	public CustomValidationException(String massage,  Map<String, String> errorMap) {
		super(massage); // 메세지는 부모에게 넘김(Exception)
		this.errorMap = errorMap;
	}
	
	public Map<String, String> getErrorMap(){
		return errorMap;
	}
}

 

handler.ControllerExceptionHandler.java

package com.cos.photogramstart.handler;

import java.util.Map;

import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestController;

import com.cos.photogramstart.handler.ex.CustomValidationException;

@RestController // 데이터를 보낼거라서 restcontroller로 설정
@ControllerAdvice
public class ControllerExceptionHandler {

	// CustomValidationException을 사용할거라고 명시
	@ExceptionHandler(CustomValidationException.class)
	public Map<String, String> validationException(CustomValidationException e) {
		// 매개변수로 받아온 내가만든 에러의 map을받아와 반환시킨다.
		return e.getErrorMap();
	}
	
}

결과 확인하기

보기와 같이 json형태로 잘 넘어 온다면 성공이다.

왜 이렇게 처리를하냐면은 어디서 무슨 에러가 발생했는지 명확하게 나오기때문에 우리가 클라이언트에게 오류문구로 알려주기 편하기 때문이다.

 

하지만 위에서 컨트롤러에 throw exception에서 보이는 유효성 검사 실패함이란 메세지도 가져오고싶다면 어떻게 해야할까 방법은 밑에 있다.

 

일단은 에러를 담을 객체를 만들 DTO를 만들어주자

 

CMRespDto.java

package com.cos.photogramstart.web.dto;

import java.util.Map;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@AllArgsConstructor
@NoArgsConstructor
@Data
public class CMRespDto {

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

위와같이 메세지와 에러map을담을 맴버필드를 만들고

ControllerExceptionHandler.java를 수정해주자

 

ControllerExceptionHandler.java

@RestController // 데이터를 보낼거라서 restcontroller로 설정
@ControllerAdvice
public class ControllerExceptionHandler {

	// CustomValidationException을 사용할거라고 명시
	@ExceptionHandler(CustomValidationException.class)
	public CMRespDto validationException(CustomValidationException e) {
		// 매개변수로 받아온 내가만든 에러의 map을받아와 반환시킨다.
		return new CMRespDto(e.getMessage(),e.getErrorMap());
	}
	
}

위와같이 반환값과 리턴값을 방금만든 CMRespDto로 바꿔주고

생성자에는 메세지와 map을 넣어준다 생성자 순서는 우리가 정의한 맴버필드 순서로 된다.

 

위와같이 설정한다면 어떤 형태로 데이터가 넘어오는지 확인해보자

postman에서 id는 20자이상으로하고 pw는 정상입력나머지는 입력안하고 전송해보았다 결과는 이러하다

위에선 정해진 틀 String과 Map을 반환하고있는데

내가 만약 String과 String을 반환하고 싶거나 아니면 String과 int를 반환하고 싶다면 어떻게 해야할까

다른 클래스를 만들어야할까 아니다 제내릭을 사용하여 처리하는 방법을 알아보자

 

CMRespDto수정하기

@AllArgsConstructor
@NoArgsConstructor
@Data
public class CMRespDto<T> {

	private int code; // 1성공 , -1실패
	private String message;
	private T data;
}

제네릭타입 T를 설정하여 data의 자료형은 T에반영되서 유동적으로 바뀔수 있다

 

ControllerExceptionHandler.java수정

@RestController // 데이터를 보낼거라서 restcontroller로 설정
@ControllerAdvice
public class ControllerExceptionHandler {

	// CustomValidationException을 사용할거라고 명시
	@ExceptionHandler(CustomValidationException.class)
	// ? 라고 지정해놓으면 리턴 타입에 맞춰 자료형을 알맞게 기입해준다
	public CMRespDto<?> validationException(CustomValidationException e) {
		// 매개변수로 받아온 내가만든 에러의 map을받아와 반환시킨다.
		return new CMRespDto<Map<String,String>>(-1, e.getMessage(),e.getErrorMap());
	}
}

리턴값의 제네릭타입만 잘 정해주면 오류없이 잘 반환되는 모습을 볼 수 있다.

 

근대 위방식처럼 오류를 클라이언트에게 json형태로 보여주는것은 웃긴거같다 보통 페이지 같은경우에는 alert으로 무엇이 잘못됫는지 알려주고 원래 적던 회원가입페이지로 넘어가는게 맞다 보통은 잘못 입력한 태그밑에 빨간글씨로 틀렸다는 안내문구가 보인다

 

alert로 script로 처리할려면 어떻게 해야할까

 

util이란 패키지 생성후 Script.java생성하기

package com.cos.photogramstart.util;

public class Script {

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

위와 같이 만들고 ControllerExceptionHandler의 validationException함수를 수정해주면 완성이다.

	@ExceptionHandler(CustomValidationException.class)
	public String validationException(CustomValidationException e) {
		
		// 오류에 대한 Map내용을 String화하여 String반환
		return Script.back(e.getErrorMap().toString());
	}

 

정리

1. 클라이언트에게 응답할 대는 Script 좋음. (브라우저에게 데이터 전달한방식)

2. Ajax통신 - CMRespDto

3. Android통신 - CMRespDto

 

회원가입과 같이  history.back()이나 alert같은것을 사용하기 좋기때문에 Script방식이 좋아보인다

하지만 Ajax방식을 나중에 많이 사용하기때문에 CMRespDto방식도 꼭 알아두어야한다.