[socceranalyst] validation 적용1(백엔드에 추가) spring
Validation 설정 추가
validation 이란 간단히 말해서 사용자가 입력한 내용이 정확한지 검증하고 DB 에 반영하는 것입니다.
즉, 회원가입을 할 때 ID 를 “4자 이상 20자 이하, 영문소문자 필수, 숫자 선택” 과 같이 입력할 수 있는 값을 정하는 것입니다. 이 부분은 Spring Data JPA 를 사용한다면 어노테이션을 통해 간단하게 설정할 수 있지만, 놀랍게도 배포할 때까지 하지 않았습니다.(!) 필수인데도 말입니다… 대신 프론트엔드에서는 정규식표현을 이용하여 validation 을 추가해놓았습니다.
이런 validation 은 프론트엔드, 백엔드, DB 세군데에 반영하겠습니다. 저는 프론트, 백엔드만 반영하면 되는줄 알았는데 찾아보니 DB 에도 반영되어야하더군요. 생각해보면 DB 무결성을 보장하기 위해 DB 에도 적용해야 하는데 제가 생각이 짧았던 것 같습니다.
문제는 어떤 값을 null 을 허용할 것인지, 입력이 안되었을 때 default 값은 어떻게 할 것인지 등을 Auth 이외에는 구체적으로 고민하지 않았습니다.
그래서 일단 Validation 을 어떻게 할지부터 고민해보겠습니다.
Validation 설정
일단 필드명, 타입, 제약사항(Constraints), 유효 범위로 구분하고 각각 설정해보겠습니다.
id 는 자동생성으로 입력불가능한 사항이니 생략하겠습니다.
제약사항은 WorkBench 기준입니다.
- PK : 기본키(Primary Key)입니다. NotNull + Unique 이며 테이블 당 1개만 설정할 수 있습니다. 여러열로 기본키를 구성하려면 복합키(composite key) 를 사용할 수 있습니다. 복합키는 두 개의 열이 합쳐서 하나의 복합열을 구성하는 것으로, 각각의 열은 Unique 가 아닐 수 있지만 복합열은 Unique 가 되어야 합니다.
- NN : not Null 입니다. Null 값을 허용하지 않습니다. 하지만 0(숫자 영) 이나 ““(공백) 은 허용하므로 추가적인 조치가 필요합니다.
- UQ : unique 입니다. 열의 모든 값이 고유해야 합니다.
- CHECK : 유효 범위에 쓸 CHECK 입니다. 제약사항과 따로 구분해놨습니다. 열 값이 특정 조건을 충족해야 함을 지정합니다. (1 < 값 <= 10 과 같이 사용합니다.)
- DEFAULT : 열에 기본값을 설정합니다.
- B : Binary 입니다. 이 플래그는 열의 데이터 유형이 이진 데이터임을 나타냅니다. 이 경우, 해당 열은 이진 데이터를 저장하고 처리하는 데 사용됩니다.
- UN: Unsigned 입니다. 이 제약사항은 열의 데이터 유형이 부호 없는 정수임을 나타냅니다. 부호 없는 정수는 음수 값을 허용하지 않으며, 0 및 양수 값만 저장할 수 있습니다. 이렇게 하면 해당 열의 저장 공간을 최적화할 수 있습니다.
- ZF : Zero Fill 입니다. 이 제약사항은 열 값이 저장될 때 남은 공간을 0으로 채워야 함을 나타냅니다. 이는 주로 고정 길이의 숫자 값을 저장하는 데 사용되며, 일관된 형식을 유지할 때 도움이 됩니다.
- G : Generated 입니다. 이 제약사항은 열 값이 사용자가 입력한 것이 아니라, 데이터베이스 시스템에 의해 자동으로 생성됨을 나타냅니다. 이는 주로 생성된 열(Generated Columns)이나 자동 증가 열(Auto-Increment Columns)에 사용됩니다.
- AI : Auto Increment 입니다. 데이터베이스 관리 시스템에서 자동으로 값을 증가시켜 고유한 값을 생성하는 기능입니다. 모든 테이블의 기본키를 id 로 하고 AI 를 적용했습니다.
Member validation
필드명</ | 타입</ | 제약사항</ | 유효 범위</ |
---|---|---|---|
String | NN | 공백 미허용, 이메일 형식 | |
memberId | String | NN, UQ | 글자수 4~20/소문자필수, 숫자선택 |
password | String | NN | 글자 수 8~30/숫자, 영문자, 특수문자 조합 |
name | String | NN | 글자수 1~100, 영문자 또는 한글 |
nickname | String | NN | 글자수 1~20, 영문자 또는 한글로 시작, 숫자선택 |
authority | ENUM | NN | ENUM('ROLE_USER', 'ROLE_ADMIN') |
글자 수가 지정되어있으면 Not Null 이 적용된거지만 혹시 정규식표현이 변경될 수도 있으니 NotNull 도 함께 넣어주겠습니다.
만약에 NotNull 만 넣을거라면 백엔드서버에서는 JPA 를 사용하면서 @NotBlank 를 넣어줘야 합니다. 해당 문자열이 ""
(공백) 이라면 사실 의미가 없죠.
참고로 spring data JPA 의 @NotNull vs @NotBlank vs @NotEmpty 입니다.
- @NotBlank : 문자열이
null
이 아니고, 공백 문자를 제외한 길이가 1 이상인지 확인합니다.""
(공백) 도 허용하지 않습니다. - @NotNull :
null
값만 허용하지 않습니다.0
,""
은 가능합니다. - @NotEmpty : 문자열, 컬렉션, 배열, 맵 등의 객체가 비어있지 않음을 확인합니다. 문자열의 경우
null
이거나 길이가 0인지 확인합니다. 즉,""
(공백) 은 통과할 수 있습니다.
Player validation
필드명</ | 타입</ | 제약사항</ | 유효 범위</ |
---|---|---|---|
name | String | NN | 글자수 1~100, 영문자 또는 한글 |
position | ENUM | NN | ENUM([포지션 이름]) |
memberId | Long | NN, UN, FK | - |
memberId 는 어차피 1부터 시작하니까 UN(unsigned) 를 붙여서 쥐톨이나마 데이터를 효율적으로 사용해봅시다.
Game validation
필드명</ | 타입</ | 제약사항</ | 유효 범위</ |
---|---|---|---|
memberId | Long | NN, UN, FK | - |
gameName | String | NN | 글자수 1~100 |
opponent | String | - | 글자수 0~100 |
location | String | - | 글자수 0~100 |
GA, GF | Int | - | 0~999 숫자만 허용 |
createdAt | Date | NN | Date 형식 |
gameName, opponent, location, GA, GF 까지 넉넉하게 줬습니다.
gameName 은 글자수 1개 이상으로 NN 을 부여하지만, 나머지는 공백이나 0 이 가능하도록 합니다. 즉, game 을 생성할 때나 수정할 때 gameName 만 입력해도 되게 하는 겁니다. 왜냐하면 입력하기 귀찮을 수 있잖아요??
현재 서버에서 GA, GF 가 Integer 형태이지만 null 값을 넣어주지 않기 위해 Int 로 변경해주겠습니다.
createdAt 은 지금 프론트에서는 2023-04-06T04:12:27.000Z
이런 LocalDateTime 형식으로 보내는데요. 백엔드에서 LocalDate 로 받아서 변환해서 잘보내줍니다.
지금 알았는데 한국시간으로 안가네요 … ㅎㅎ 지금은 어차피 Date 만 필요하니 급할 건 없지만 locale 을 한국으로 해서 같이 수정해야겠습니다.
Game_player validation
필드명</ | 타입</ | 제약사항</ | 유효 범위</ |
---|---|---|---|
gameId | Long | NN, UN, FK | - |
playerId | Long | NN, UN, FK | - |
game 과 player 테이블의 매핑(연결) 테이블입니다. 특별할 건 없습니다.
Record validation
필드명</ | 타입</ | 제약사항</ | 유효 범위</ |
---|---|---|---|
gameId | Long | NN, UN, FK | - |
playerId | Long | NN, UN, FK | - |
gamePosition | ENUM | NN | ENUM([포지션 이름]) |
timeIn, timeOut | Int | - | 0~120 |
main | ENUM | NN | ENUM[MAIN, SUB] |
touch, goal, assist 등등 기록 | Int | - | 0~999 숫자만 허용 |
gameId 와 playerId 는 FK 입니다.
gamePosition 과 main 은 각각의 ENUM 타입입니다. NotNull 을 넣었는데요. 저번에 프론트에서 “경기생성 페이지” 에서 데이터를 넘길 때 실수가 있어서 main 값을 넘기지 못했었습니다. 그때 Null 값이 들어갔었습니다. ENUM 을 지정하더라도 Not Null 은 필수입니다.
timeIn, timeOut 은 값 범위를 120분 이상까지 할지 고민했는데, 맘편하게 120분까지만 했습니다. touch, goal, assist 등의 기록도 맘편하게 999까지 허용했습니다.
DotRecord validation
필드명</ | 타입</ | 제약사항</ | 유효 범위</ |
---|---|---|---|
gameId | Long | NN, UN, FK | - |
playerId | Long | NN, UN | - |
playerName | String | NN | 글자수 1~100, 영문자 또는 한글 |
gamePosition | ENUM | NN | ENUM([포지션 이름]) |
x, y, shootX, shootY | Float(10,4) | - | - |
shoot, validShoot | boolean | - | - |
gameTime | Int | - | 0~120 숫자만 허용 |
이 웹앱의 핵심 기능의 DB 입니다.
먼저 game 과는 다대일 관계이지만 Player 와는 아닙니다. 원래는 Player 와도 다대일 관계였지만… 익명 및 상대편 DotRecord 구현이 힘들어서 Player 다대일 관계는 뺏습니다.
그리고 백엔드에서 playerId 를 참조해서 해당 DotRecord 의 playerName 과 gamePosition 을 Join 하여 넘기는 것보다 DotRecord 에 필드를 포함시키는 게 코드짜기 편하겠다고 생각해서 playerName 과 gamePosition 을 테이블에 포함시켰습니다.
x, y, shootX, shootY 는 점이 찍히는 위치, 슛이 끝나고 화살표가 그려지는 위치(shoot, validShoot 이 True 일 때) 입니다. 그런데 지금 프론트엔드 payLoad 확인해보니 x, y 좌표값이 전부 정수형태로 갑니다. 0.xxx 가 중요할 수도 있으니 한번 확인해봐야겠습니다.
gameTime 은 어차피 최대 120분이니 복잡할 것 없이 Int 로 범위 설정했습니다.
백엔드 Validation 설정 바꾸기
이제 백엔드 스프링 코드를 하나하나 보면서 변경해봅시다.
중요한 점은 Validation 을 직접 Entity 에 설정하지 않고 DTO (Data Transfer Object) 에 적용한다는 점입니다. 방금 검색하다가 깨달았습니다…
깨닫고 나니 김영한님이 저에게 DTO 에 적용해야하지만 당시 코드가 간단해서 Entity 에 강의하신게 기억이 납니다. 복습의 중요성입니다. (물론 포스팅 내용에 적어놓지 않아서 복습해봤자 의미가 없었을겁니다.)
다시 보니 김영한님 강의에서도 Validation 을 DTO 에 적용해놨습니다. 버그 이슈의 대부분은 지능 이슈입니다!
복습합시다. Bean Validation 다시 복습
검증에 실패한다면 어떤 오류가?
spring validation 을 통해서 프론트에서 서버로 들어오는 RequestDto 를 검증하게 됩니다.
이때 검증에 실패하면 컨트롤러 메서드는 실행하지 않고 Error 를 반환합니다.
이때 에러는 MethodArgumentNotValidException, BindException 입니다.
- MethodArgumentNotValidException : BindException 을 상속받습니다. @RequestBody 에서 Validation 오류 시 해당 오류를 반환합니다.
- BindException : Exception 을 상속받습니다. @ModelAttribute 에서 Validation 오류 시 해당 오류를 반환합니다.
@validated, @valid 관련 참고 포스팅 링크입니다.
제가 작성한 @validated, @valid 관련 포스팅입니다.
결론적으로 MethodArgumentNotValidException 이 터졌고, 체크예외이기 때문에 RuntimeException 으로는 (당연히) 잡을 수 없어서 전역예외처리에 추가해야 했습니다.
GlobalExceptionHandler 클래스
package soccer.backend.aop;
import ...;
@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler(RuntimeException.class)
public ResponseEntity<?> handleRuntimeException(RuntimeException e) {
...
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<?> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
//이걸로 어떤 핸들러가 실행되는지 확인해봤습니다. ㅎㅎ
log.info("MethodArgumentNotValidException 실행");
BindingResult bindingResult = e.getBindingResult();
List<FieldError> errors = bindingResult.getFieldErrors();
List<String> errorMessages = errors.stream()
.map(error -> error.getField() + " : " + error.getDefaultMessage())
.collect(Collectors.toList());
String errorMessage = errorMessages.stream().collect(Collectors.joining(", "));
return ResponseEntity.badRequest().body(errorMessage);
}
}
@Valid
어노테이션이 붙은 파라미터 객체는 스프링 MVC에서 요청 처리를 시작하기 전에 검증이 수행됩니다. 검증 실패 시,BindingResult
객체에 검증 결과가 담긴다고 생각하시면 됩니다. 그래서 bindingResult 에서 FieldError 를 뽑아내게 됩니다.- 에러는 여러 개일 수 있습니다. (ID 도 검증에 걸리고 Email 도 걸리고…) 따라서 에러를 String List 형태로 변환한 후
String errorMessages
로 받아서 한줄로 만들어주겠습니다. - 그리고 badRequest 와 함께 반환합니다. 그러면 front 에서 alert 창으로 errorMessage 를 띄웁니다.
DTO 변경하기
이제 정말로 Dto 에 Validation 을 추가해보도록 하겠습니다. Request 를 받는 dto 는 총 9개입니다. (별거 없는 서비스에 참 많습니다.)
MemberRequestDto
회원가입 시 사용하는 Dto 입니다. 필요한 코드만 들고 왔습니다. builder 생성자나 oneToMany 는 지금은 필요없습니다.
@Getter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class MemberRequestDto {
@NotBlank(message = "ID 를 입력해주세요.")
@Pattern(regexp = "^[a-z]+\\d*$", message = "4~20글자의 소문자, 숫자로 구성해주세요.")
@Size(min = 4, max = 20, message = "4~20글자의 소문자, 숫자로 구성해주세요.")
private String memberId;
@Email(message="이메일 형식을 확인해주세요.")
private String email;
@NotBlank(message = "PassWord 를 입력해주세요.")
@Pattern(regexp = "^(?=.[0-9])(?=.[a-zA-Z])(?=.*[@#$%^&+=])(?=\S+$).{8,30}$", message = "8~30글자의 소문자, 숫자, 특수문자로 구성해주세요.")
private String password;
@Pattern(regexp = "^[a-zA-Z가-힣]{1,100}$")
private String name;
@Pattern(regexp = "^[a-zA-Z가-힣][a-zA-Z가-힣0-9]{1,20}$")
private String nickname;
}
ChangePasswordRequestDto
비밀번호 변경 시 사용하는 Dto 입니다. 위와 똑같이 해줍니다.
package soccer.backend.auth.dto;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter
@AllArgsConstructor
@NoArgsConstructor
public class ChangePasswordRequestDto {
@NotBlank(message = "ID 를 입력해주세요.")
@Pattern(regexp = "^[a-z]+\\d*$", message = "4~20글자의 소문자, 숫자로 구성해주세요.")
@Size(min = 4, max = 20, message = "4~20글자의 소문자, 숫자로 구성해주세요.")
private String memberId;
@Pattern(regexp = "^(?=.*[0-9])(?=.*[a-zA-Z])(?=.*[@#$%^&+=])(?=\\S+$).{8,30}$", message = "8~30글자의 소문자, 숫자, 특수문자로 구성해주세요.")
private String exPassword;
@Pattern(regexp = "^(?=.*[0-9])(?=.*[a-zA-Z])(?=.*[@#$%^&+=])(?=\\S+$).{8,30}$", message = "8~30글자의 소문자, 숫자, 특수문자로 구성해주세요.")
private String newPassword;
}
PlayerRequestDto
Player 등록 시 사용하는 Dto 입니다. 간단하게 name, position 만 받습니다.
Id 검증은 서비스에서 실시하니까 따로 Validation 으로 검증은 하지 않겠습니다. 나머지도 똑같습니다.
public class PlayerRequestDto {
private Long id;
@Pattern(regexp = "^[a-zA-Z가-힣]{1,100}$", message="1~100글자의 영문자 또는 한글을 입력해주세요.")
private String name;
@NotNull(message="포지션을 확인해주세요.")
//position 은 Enum 타입이므로 NotNull 만 넣어줍니다.
private Position position;
}
}
GameCreateRequestDto
경기를 생성할 때 사용하는 Dto 입니다.
public class GameCreateRequestDto {
@NotBlank(message = "경기명을 입력해주세요.")
@Size(min=1, max=100, message="경기명은 1~100 글자 사이로 작성해주세요.")
private String gameName;
@Size(max=100, message="상대팀은 100 글자 이하로 작성해주세요.")
private String opponent;
@Size(max=100, message="위치는 100 글자 이하로 작성해주세요.")
private String location;
@Min(0) @Max(999)
private int gf;
@Min(0) @Max(999)
private int ga;
@NotNull(message="날짜를 확인해주세요.")
private LocalDate createdAt;
//경기에 참가하는 선수의 정보를 gamePlayerAddRequestDto 로 리스트형태로 받습니다.
private List<GamePlayerAddRequestDto> gamePlayerAddRequestDto;
}
- LocalDate : 기본적으로 Spring Boot와 Jackson은 ISO-8601 형식의 날짜 문자열을 자동으로 Java의 LocalDate 및 LocalDateTime 객체로 변환할 수 있습니다. 이때, 문자열이 시간 정보를 포함하지 않으면 LocalDate로, 시간 정보를 포함하면 LocalDateTime으로 변환합니다.
gamePlayerAddRequestDto
경기에 참가하는 선수 정보를 받는 Dto 입니다. 몇몇은 playerRequestDto 와 똑같습니다.
public class GamePlayerAddRequestDto {
private Long playerId;
@NotNull(message="포지션을 확인해주세요.")
private Position gamePosition;
@Min(0) @Max(120)
private int timeIn;
@Min(0) @Max(120)
private int timeOut;
@NotNull(message="선발/교체 여부를 확인해주세요.")
private Main main;
}
gamePosition 을 가지고 있지 않은 ananymous, blue player 가 있지만, 해당 익명값은 Position Enum 에 NONE 을 넣어서 해결해줬습니다.
GameUpdateRequestDto
경기 내용을 업데이트할 때 필요한 Dto 입니다. 경기 내용에서는 수정할 부분이 ‘경기 정보’와 ‘경기에 참여한 선수 기록’으로 구분되는데요. 따로 구분하는 게 불필요한 http 요청을 줄일 수 있을 것 같아서 나눠놨습니다.
public class GameUpdateRequestDto {
private Long id;
@Size(min=1, max=100, message="경기명은 1~100 글자 사이로 작성해주세요.")
private String gameName;
@Size(max=100, message="상대팀은 100 글자 이하로 작성해주세요.")
private String opponent;
@Size(max=100, message="위치는 100 글자 이하로 작성해주세요.")
private String location;
@Min(0) @Max(999)
private int gf;
@Min(0) @Max(999)
private int ga;
@NotNull(message="날짜를 확인해주세요.")
private LocalDate createdAt;
}
GameCreateRequestDto
와 비슷하지만 List<gamePlayerAddRequestDto>
가 없습니다. 경기에 참여한 선수에 대한 정보는 필요없기 때문입니다.
RecordRequestDto
다음은 선수 및 경기 내 record 를 담당하는 RecordRequestDto 입니다. 게임 생성 시에는 모든 기록이 default 값 (주로 0) 이 들어가서 생성됩니다. 그리고 경기 세부내용 조회 페이지에서 선수들의 기록을 변경할 수 있습니다.
public class RecordRequestDto {
private Long id;
@NotNull(message="포지션을 확인해주세요.")
private Position gamePosition;
@Min(0) @Max(120)
private int timeIn;
@Min(0) @Max(120)
private int timeOut;
@NotNull(message="선발/교체 여부를 확인해주세요.")
private Main main;
@Min(0) @Max(999) //아래에 min, max 를 합치는 코드를 만들었습니다.
private int touch;
private int goal;
private int assist;
private int chanceMaking;
private int shoot;
private int validShoot;
private int dribble;
private int successDribble;
private int pass;
private int successPass;
private int longPass;
private int successLongPass;
private int crossPass;
private int successCrossPass;
private int tackle;
private int intercept;
private int contention;
private int successContention;
private int turnover;
}
반복되는 @Min, @Max 합치기
@Min(0) @Max(999) 가 반복되고, timeIn/timeOut/gameTime 등은 @Min(0) @Max(120) 이 반복되니 다음과 같이 custom validation annotation 을 만들어줍니다.
package soccer.backend.annotation;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Min(0)
@Max(120)
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MinMax120 {
}
@Min(0)
@Max(999)
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MinMax999 {
}
이제부터 @MinMax120, @MinMax999 를 적용하겠습니다. 사실 중복을 줄이려고 만든건데 큰 차이가 없습니다. 오히려 나중에 값을 바꿀 때 더 힘들어질 수도 있겠습니다... ㅎㅎ
GameFieldRequestDto
다음은 핵심로직을 위한 Dto 입니다. 히트맵에서 작성된 정보를 받습니다.
public class GameFieldRequestDto {
private Long id;
private DotRecordRequestDto[] dotRecordRequestDto;
}
여기서는 경기의 id 와 각 DotRecord 의 기록을 받습니다. Validation 처리할 건 없습니다.
DotRecordRequestDto
히트맵 상 점에 대한 기록을 받습니다.
public class DotRecordRequestDto {
private Long playerId;
@Pattern(regexp = "^[a-zA-Z가-힣]{1,100}$", message="1~100글자의 영문자 또는 한글을 입력해주세요.")
private String playerName;
//spring 에서는 float(10,4) 와 같이 지정할 수 없습니다.
//대신 스프링에서 데이터베이스와 상호 작용할 때, 이와 같은 제약 조건을 다음과 같이 데이터베이스 스키마에 적용할 수 있습니다.
//@Column(name = "x", columnDefinition = "float(10,4)") <- 하지만 원하는 건 아니니 사용하지 맙시다.
private Float x;
private Float y;
@NotNull
private Position gamePosition;
private boolean shoot;
private boolean validShoot;
private Float shootX;
private Float shootY;
@MinMax120
private Integer gameTime;
}
적다보니 서비스 로직에서 DotRecordRequestDto 를 dotRecord 로 받을 때 player 검증이 없었습니다… ㅎㅎ 다음과 같이 추가해줬습니다. 다음 로직은 dotRecordRequestDto 를 받아서 dotRecord 에 저장하고, List
public List<DotRecord> toDotRecordList(Game game, DotRecordRequestDto[] dotRecordRequestDto) {
//반환할 dotREcords 의 목록입니다.
List<DotRecord> dotRecords = new ArrayList<>();
//DotRecordRequestDto 반복문을 돌립니다.
for (DotRecordRequestDto request : dotRecordRequestDto) {
//DotRecord 를 새로 만들고 Request 에서 값을 받아서 넣습니다.
DotRecord dotRecord = new DotRecord();
...
//playerId 가 0 이 아니고, -1 이 아닌 player 가 repository 에 있는지 찾습니다.
//0은 ananymous, -1 은 blue player 입니다. 검증할 필요없이 dotRecord 를 만들면 됩니다.
if(request.getPlayerId() != 0 && request.getPlayerId() != -1){
Player player = playerRepository.findById(request.getPlayerId()).orElseThrow(
() -> new IllegalArgumentException("해당 선수가 존재하지 않습니다.")
);
//playerRepository 에는 있지만 해당 member 의 player 가 아닐 수도 있으니 검증해봅니다.
//isPlayer(player) 는 해당 member 의 player 가 맞는지 boolean 으로 반환하는 클래스입니다.
if (!isPlayer(player)) {
throw new IllegalArgumentException("해당 선수가 존재하지 않습니다.");
}
}
dotRecord.setPlayerId(request.getPlayerId());
...
dotRecords.add(dotRecord);
dotRecordRepository.save(dotRecord);
}
return dotRecords;
}
만들면서 생각났는데, RequestDto 에는 playerName, gamePosition 을 빼고 playerId 만 받아서 toDotRecordList
에서 넣어주면 될 것 같습니다. 그리고 reponseDto 에서는 playerName, gamePosition 넣어주는거죠.
근데 다시 생각나는건, 만약 player 가 삭제되면? 서비스에서는 player 가 사라져도 해당 id 를 가진 dotRecord 는 남기고 싶은데요. 해당 dotRecord 에 player 정보를 기록해두지 않으면 안됩니다. 이 부분은 다음에 player hide 옵션을 만든 후에 손보도록 합시다.
결론적으로 toDotRecordList 에서 PlayerId 검증은 빼겠습니다. 나중에 생각합시다. 위에 toDotRecordList 는 무시해주도록 합시다.
Dto 가 잘되는지 통합테스트로 검증해봤습니다. (비밀번호 변경 하나만 해봤습니다 ㅎ)
프론트에는 아직 검증로직이 없어서 백엔드에서 오류를 받아옵니다. (경고창도 좀 예쁜걸로 바꿔야겠어요.)
일단 백엔드에 검증을 추가하니 마음은 편합니다. 적어도 DB 에 직접 접근하지 않는 이상 제가 의도한 데이터만 저장될거잖아요.
다음 포스팅은 DB, 프론트엔드에 검증로직을 추가해보겠습니다. (DB 먼저입니다. 제가 프론트울렁증이 있어서…)
추가 이슈
*+추가 : @NotBlank or @NotNull 과 @Size 가 중복되면 그냥 빼주도록 하겠습니다. 에러 검증도 두번이나 되고 중복인데 굳이 넣을 필요 없네요.*
*+추가 : 로그인 기능에서는 validation 을 빼겠습니다. 애초에 Id, pw 검증을 하니까요. 게다가 중간에 로그인 검증 hint 가 변경되면 기존 pw 로 로그인이 안되는 문제가 있습니다.*
- 예를 들어 비밀번호를 1234 로 설정했는데, 개발자가 중간에 비밀번호 Validation 을 8자리 이상으로 변경한다면? 그런데 로그인에 validation 이 걸려있으면? 비밀번호는 맞는데 로그인이 안되는 문제가 발생…
- 같은 이유로 비밀번호를 변경하는 로직(ChangePasswordRequestDto) 에서 memberId 와 exPassword 는 검증하지 않도록 합시다.
댓글남기기