Kotlin

Kotlin Springboot Rest API 만들기 4 - 회원가입, common response body 만들기

pepega 2023. 1. 17. 19:45

이전 포스팅에서는

앞으로 만들 Entity 구조를 간략하게 설명하고

예시로 우아하게 Entity를 구성하는 방법에 대해 작성하였습니다.

불필요한 쿼리는 사용하지 않는게 좋으니까요!

 

이번 포스팅에서는

위 Entity를 토대로 회원가입을 만들어보겠습니다!

 

동시에 i18n을 포함한

Common Response body를 만들어보겠습니다!

+ 약간의 SpringSecurity가 추가되어 있습니다. 다음 포스팅에서 다루겠습니다.

 

전체 코드는 여기에 있습니다.

 

다시 나온 간단한 ERD

 

BaseEntity 생성하기

common.domain package안에

user_master에 들어갈

BaseEntity를 생성합니다.

 

 

BaseEntity는

@MappedSuperclass

annotation을 활용해서 id, createDate, updateDate를 만듭니다.

UserMaster Entity는 BaseEntity를 상속해서 사용하면 됩니다.

BaseEntity는

UserMaster Entity의 createDate, updateDate, id를 담당하고 있습니다.

 

이외의 내용은 이전 포스팅을 참고해주세요!

 

UserMaster Entity 생성하기

회원 정보를 저장할

UserMaster Entity를 생성합니다.

 

가장 하단에

BaseEntity를 상속한 코드를 확인 할 수 있습니다!

 

 

UserMasterRepository 생성하기

이 글에서는 Jpa를 사용할 계획입니다.

common 아래 repository package 안에 UserMasterRepository를 생성합니다.

JpaRepository를 상속받은 interface class를 생성합니다.

 

이미 존재하는 계정에 대한 오류 처리를 위해

findByEmail 메서드를 추가했습니다.

회원가입이기 때문에 nullable한 메서드입니다! (email이 존재하지 않을 수 있기 때문에)

SignService 생성하기

회원가입과 로그인을 개발하기 때문에

Sign이라는 키워드를 가지고 Service를 만듭니다!

common 아래 service.sign package 안에 SignService 클래스를 생성합니다.

 

회원가입 만들기

1. request dto 만들기

common package 안에

dto package를 만들고 sign package를 생성합니다.

그 안에 SignUpRequestDto를 생성합니다.

dto 안에서 로직별로 나눌 계획입니다.

 

dto 클래스는 data 클래스로 생성하였습니다.

kotlin에서 data 클래스는

equals, hashCode

toString "User(name=John, age=42)"

copy, conponentN() 자동으로 제공합니다!

 

2. signUp service method 만들기

웹 사이트 경험으로 비춰보면

같은 이메일은 회원가입이 되면 안되고

특정한 권한을 가져야 합니다 (일반 회원은 관리자 권한을 가질 수 없습니다)

패스워드는 암호화 후 DB에 저장되어야 합니다. (이후에 구현하도록 하겠습니다.)

패스워드는 8자 이상 글자, 숫자, 특수문자가 포함되어야 합니다. (이 부분은 시간이 되면 구현하겠습니다.)

 

계정 조회 후

일단 글에서는 TODO로 이미 존재하는 회원인 경우 오류를 throw 하도록 comment를 달았습니다.

 

일반 회원 계정은

ROLE_USER

라는 권한을 가지도록 했습니다.

 

Common Response body 생성하기

Service를 만들었으니

Controller를 만들 차례입니다.

 

그 전에 Controller가 정상적으로 끝나거나

오류가 발생했을 때

결과 값을 우아하게 보여주기 위해 Common Response body를 생성합니다.

 

api 호출 후 response body 예시는 아래와 같습니다.

 

성공했을 때

실패했을 때

 

CommonResult 생성하기

common package 아래

model.response package를 생성합니다.

그 안에 CommonResult 클래스를 open으로 생성합니다 (추후 상속을 위해)

 

 

ResponseService 생성하기

 

CommonResult에는 성공에 대한 값만 set 하였습니다.

ResponseService를 통해

실패했을 경우에 대한 값을 set 해보겠습니다.

 

service package 아래 response package를 생성하여

그 안에 ResponseService 클래스를 생성합니다.

 

 

성공했을 때는

CommonResult를 그대로 return 하고

 

실패했을 때는

CommonResult에 실패에 대한 값을 set 하였습니다.

 

이제 Controller에서 오류가 발생했을 때

failResult라는 메서드가 실행될 수 있게 GlobalExcpetion 처리 클래스를 생성해보겠습니다.

 

ExceptionAdvice 생성하기

common package안에 advice package를 생성하고

그 안에 ExceptionAdvice 클래스를 생성합니다.

이 클래스는

특정 오류가 발생했을 때 (Exception, Customized Exception, MethodArgumentNotValidException, etc.)

catch 하여 오류를 핸들링 할 수 있는 클래스 입니다.

 

exception.properties, exception_en.properties에 오류메시지를 정의하여

response body에 보여줄 예정입니다.

 

getMessage 메서드는

properties에서 메시지를 가져올 수 있게 하는 메서드입니다.

 

메시지를 가져올 수 있도록 application.yml을 수정합니다.

yml의 의미는

resources directory 안에

i18n directory 안에 있는 exception으로 시작하는 properties를 가져온다는 의미입니다.

 

이에 맞춰서 똑같이 설정합니다.

 

안에는 아래와 같이 설정합니다.

 

 

defaultException 메서드는

이유 모를 오류가 발생했을 때 (Exception)

handling 할 수 있는 메서드 입니다.

 

이유를 모르기 때문에

이유를 찾을 수 있도록

endPoint, requestURI, queryString, exception message를 로깅하였습니다.

 

 

그 아래

userExistException 메서드는

이미 유저가 존재하는 경우 발생되는 Exception입니다.

오류 코드로 409 CONFLICT를 발생하게 했습니다.

 

관련된 코드와 오류 메시지가 도출됩니다.

 

custom된 Exception은 WET한 코드가 나올 것 같아서

sealed interface로 선언하였습니다.

 

advice 아래 Error sealed interface를 생성합니다.

 

이 interface에는 custom된 exception이 추가될 예정입니다.

 

sealed를 사용하지 않는다면

아래와 같은 방법으로도 가능합니다.

https://github.com/GHGHGHKO/memes-api/commit/c5b6656dfebd83f22eb8168057547a3addf1ec78

 

refactor: custom error to sealed interface · GHGHGHKO/memes-api@c5b6656

Show file tree Showing 4 changed files with 14 additions and 23 deletions.

github.com

 

이제 오류가 발생했을 때

우아하게 response body를 보여줄 수 있게 됐습니다.

 

TODO를 남겨두었던 if를 수정하겠습니다.

 

SignController 생성하기

common package 아래

controller package를 생성하고

SignController를 생성합니다.

 

 

signService.signUp에서 오류가 발생하면

ExceptionAdvice 클래스에서 오류를 핸들링하게 됩니다.

 

프로젝트를 실행하고 아래와 같이 세팅하면

오류가 발생합니다.

 

Spring Security때문에 권한 오류가 발생하고 있습니다.

이 오류는 다음 포스팅에서 다루겠습니다!

 

테스트 삼아서

POST -> GET으로 변경 후 Send 해보겠습니다.

 

이 글은 intellij를 사용하고 있습니다.

글자가 깨져서 표시됩니다.

 

intellij -> File -> Editor -> File Encodings에서

동그라미를 체크하면

properties 파일이 물음표로 변하게 됩니다.

 

다시 한글로 설정 후 실행해보겠습니다.

 

오류 메시지가 확인됩니다.

 

Exception 로그는 아래와 같습니다.

Exception endPoint: GET /sign/v1/signUp, queryString : null, exception : org.springframework.web.HttpRequestMethodNotSupportedException: Request method 'GET' is not supported

당연히 /sign/v1/signUp endPoint는 GET을 지원하지 않으니

HttpRequestMethodNotSupportedException 오류가 발생하고 있습니다.

 

Security Configuration 생성하기

뒤에서 더 다룰 예정입니다.

common package 밑에 config package를 생성합니다.

그 안에 SecurityConfig 클래스를 생성합니다.

 

authorizeRequests
antMatchers

위 2개는 Deprecated 되어 

 

authorizeHttpRequests
requestMatchers

로 대체하였습니다.

 

우선 api를 호출하기 위해 (여기선 "/sign/v1/signUp" api)

로그인과 회원가입은 열려있어야 하기 때문에

로그인과 회원가입을 열어두었습니다.

 

httpBasic().disable()

REST API + jwt token으로 로그인할 계획입니다.

username, password를 사용하여 로그인하지 않을 계획이기 때문에 disable 하였습니다.

 

csrf().disable()

세션을 사용하지 않을 예정입니다.

로그인이 완료되면 토큰을 발급하여

api 호출 시 토큰이 있어야 호출 되도록 설정 할 예정이기 때문에

csrf 은 disable 하였습니다.

 

.authorizeHttpRequests()

API 요청 시 인증을 요구합니다.

 

.requestMatchers("/sign/*/signIn", "/sign/*/signUp").permitAll()

그러나 위 API는 허용합니다.

 

프로젝트를 실행하여 테스트 해보겠습니다.

 

그 전에 수행되는 쿼리를 확인하기 위해

application-dev.yml을 생성합니다.

common은 application.yml

develop 환경은 application-dev.yml

production 환경은 application-prod.yml

로 구성 할 예정입니다.

 

hibernate 쿼리를 확인하기 위해 환경을 구분했습니다.

 

Active profiles을 dev로 변경하고 프로젝트를 실행합니다.

 

Using generated security password: 972f3c97-2596-4b6c-ab8b-13fa5cf30b1e

메시지가 나오지만 이제는 무시해도 됩니다.

 

API를 다시 호출해보겠습니다.

 

request body

response body

성공한 것을 확인했습니다!

 

실행된 쿼리는 아래와 같습니다.

 

이메일이 존재하는지 확인하는 쿼리를 실행합니다.

 

존재하지 않는 회원이면

insert 쿼리를 수행합니다.

 

insert 쿼리가 완료되었으면

계정의 권한을 위해 user_master_rules 테이블에 insert 합니다.

 

 

같은 request body를 다시 호출해보겠습니다.

이미 있는 계정이기 때문에

common response와 함께 오류 코드, 메시지가 도출될 것입니다.

 

 

우리는 영어 버전도 추가하였습니다.

api 호출 시

Headers에 아래 내용을 추가합니다.

Accept-Language: en

 

다시 API를 호출하면 영어 response body가 나옵니다.

 

이번 포스팅에서는

로그인, 회원가입, common response body + 약간의 SpringSecurity를 진행했습니다.

 

다음 포스팅에서는

SpringSecurity를 기반으로 jwt token을 도출하겠습니다.

 

 

참고자료

http://blog.storyg.co/rest-api-response-body-best-pratics

https://ko.wikipedia.org/wiki/%EA%B5%AD%EC%A0%9C%ED%99%94%EC%99%80_%EC%A7%80%EC%97%AD%ED%99%94

https://youtu.be/jafa3cqoAVM

https://dkswnkk.tistory.com/521

https://minholee93.tistory.com/entry/Spring-Security-HTTP-Basic-Authentication

https://junhyunny.github.io/information/security/spring-boot/spring-security/cross-site-reqeust-forgery/

https://docs.spring.io/spring-security/site/docs/4.2.5.RELEASE/apidocs/org/springframework/security/config/http/SessionCreationPolicy.html

https://stackoverflow.com/questions/74862254/how-to-log-sql-statements-with-query-param-values-in-spring-boot-3-hibernate-6