예제를 만들자 뚱땅뚱땅

API 만들기 연습 #6 유효성 검증

NEWDODORIPYO 2022. 8. 30. 09:41

@Valid를 이용한 유효성 검증

유효성 검사

ApiDTO

package com.apiservice.model;

import lombok.*;

import javax.validation.constraints.Email;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Pattern;
import java.time.LocalDateTime;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class ApiDTO {

    public interface insertGroup{}
    public interface updateGroup{}

    private Integer memberNumber; //사용자 번호

    @NotEmpty(message = "아이디를 입력해주세요" , groups = insertGroup.class)
    @Email(message = "ID 는 Email 형식으로 입력해주세요" , groups = updateGroup.class)
    private String memberId; // 사용자 ID

    @NotEmpty(message = "비밀번호를 입력해주세요", groups = insertGroup.class)
    private String memberPw; // 사용자 PW

    @NotEmpty(message = "이름을 입력해주세요" , groups = insertGroup.class)
    private String memberName; // 사용자 이름

    @Pattern(regexp = "(\\\\d{2,3})-?(\\\\d{3,4})-?(\\\\d{4})$",message = "휴대폰 번호를 다시 확인해주세요 ",groups = {insertGroup.class,updateGroup.class})
    private String memberPhone; // 사용자 휴대폰 번호

    @Pattern(regexp ="N|Y",message = "광고 수신은 Y 거부는 N 를 입력해주세요 ",groups = {insertGroup.class,updateGroup.class})
    private String advert; // 광고 수신

    private LocalDateTime regDate;

    private LocalDateTime updateDate;

}

새롭게 추가된 코드를 위에서부터 천천히 알아보도록 하겠습니다

  • interface로 선언된 Group { } 들은 insert와 update 각각 상황에 적용해야 하는 상황이 다르기 때문에 따로 만들어 주었습니다
  • @NotEmapty는 해당 값이 null이 아니고 , 빈 스트링(””)아닌지 검증 (” “은 가능함) 합니다
  • @Email는 입력 값의 형식이 이메일 양식인지 검증합니다
  • @Pattern는 regxp에 선언된 패턴과 일치하는지 검증합니다
insertGroup{ } updateGroup{ }
insert 할 때는 @NotEmpty로 설정된 값들이 반듯이 입력되어야 합니다 update 할 때는 모든 사항을 수정하지 않는 경우도 있기 때문에 @NotEmpty를 빼주고 입력 형식만을 적용시켜준다

이제 Service 에서 각 상황에서 디테일을 추가해보자

입력 상황 register

@Override
    public void register(ApiDTO apiDTO) {

        log.info("service insert");
        log.info(apiDTO);

        //광고 수신 값이 null 이면 N 이 기본값으로 들어가게
        String advert = apiDTO.getAdvert() == null ? "N" :apiDTO.getAdvert();
        apiDTO.setAdvert(advert);

        //DB 조회 (중복 ID 가 있는지 검사)
        ApiVO getId = apiRepository.getId(apiDTO);

        if (getId == null){
        }else {
            log.info("ID 중복");
            throw new CustomException(ErrorCode.BAD_ARTICLE);
        }

      apiRepository.insert(apiDTO);

    }
  • 사용자가 advert에 값을 입력하지 않으면 기본적으로 ‘ N ’ 값이 들어가게 처리해주었습니다 <광고는 귀찮으니까요 >
  • inset 할때 ID가 중복이 되면 안 되니 그걸 체크해 주고 있습니다
  • ID 가 중복일 경우에는 커스텀으로 만든 에러를 보여주고 있다

여기서 새롭게 추가된 repository와 ErrorCode 보기

ApiRepository

package com.apiservice.repository;

import com.apiservice.model.ApiDTO;
import com.apiservice.model.ApiVO;
import com.apiservice.model.ListDTO;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface ApiRepository {

    void insert(ApiDTO apiDTO);

    void update(ApiDTO apiDTO);

    void delete(Integer memberNumber);

    List<ApiVO>selectList(ListDTO listDTO);

    int getTotal(ListDTO listDTO);

    ApiVO getPk(ApiDTO apiDTO); //PK 체크

    ApiVO getId(ApiDTO apiDTO); //ID 체크
    
}
  • getPK는 update 로직에서 pk값이 있는지를 확인해주기 위한
  • getId는 insert 할 때 중복된 ID 가 있는지 확인해주기 위한

ApiRepository. xml

<select id="getPk" resultType="com.apiservice.model.ApiVO">
        select * from api
        where member_number = #{memberNumber}
    </select>

    <select id="getId" resultType="com.apiservice.model.ApiVO">
        select * from api
        where member_id = #{memberId}
    </select>
  • SQL은 단순하게 PK인 memberNumber 그리고 id 인 memberId를 확인합니다

ErrorCode

package com.apiservice.model.Error;

import lombok.AllArgsConstructor;
import lombok.Getter;

@AllArgsConstructor
@Getter
public enum ErrorCode {

    BAD_REQUEST(400 ,"Bad Request"),
    BAD_ENTITY(401,"Bad ENTITY"),
    BAD_ARTICLE(404,"article value"),
    NOT_INFO(405,"없는 정보입니다"),
    SYS_ERR(500,"시스템 에러 입니다.");

    private final int status;
    private final String message;

    //  Enum 클래스로 사용할 에러들을 적어준다.
    //  status 값과 error message 만 프론트에 넘겨줄 예정으로 두 개만 작성하였다.
    //  재사용성을 생각해서 범위를 너무 좁게설정하지 말고 넓은 범위로 설정해보자
}
  • 중복이거나 없는 정보를 처리할 BAD_ARTICLE , NOT_INFO
  • 잡지 못한 에러 상황일 때는 시스템 에러를 띄우기 위해 SYS_ERR 만들었습니다

GlobalExceptionHandler

package com.apiservice.controller.Handler;

import com.apiservice.model.Error.ErrorDTO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DataRetrievalFailureException;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.jdbc.support.MetaDataAccessException;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.io.FileNotFoundException;

import static com.apiservice.model.Error.ErrorCode.*;

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler({ BindException.class})
    protected ResponseEntity handleBindException(BindException ex) {

        FieldError fieldError = ex.getFieldError();

        String message = fieldError.getDefaultMessage();
        String param = fieldError.getField();

        message = message + "(" + param + ")";

        log.debug(" message : {}", message);
        log.debug(" param   : {}", param);

        return new ResponseEntity(new ErrorDTO( BAD_REQUEST.getStatus(), message + " " + BAD_REQUEST.getMessage()+ ":" + param), HttpStatus.valueOf(BAD_REQUEST.getStatus()));
    }

    @ExceptionHandler({ Exception.class })
    protected ResponseEntity handleServerException(Exception ex) {

        ex.printStackTrace();

        return new ResponseEntity(new ErrorDTO(SYS_ERR.getStatus(), SYS_ERR.getMessage()), HttpStatus.INTERNAL_SERVER_ERROR);
    }

    @ExceptionHandler({CustomException.class})
    protected ResponseEntity handleCustomException(CustomException ex){

        ex.getMessage();

        return new ResponseEntity(new ErrorDTO(ex.getErrorCode().getStatus() , ex.getErrorCode().getMessage()),HttpStatus.valueOf(ex.getErrorCode().getStatus()));
    }

}
  • handleServerException에서는 가장 상위인 Exception으로 처리해서 특정 상황이 아닌 상황에는 SYS_ERR(500, "시스템 에러입니다.") 에러가 발생하게 했습니다
  • handleCustomException는 코드를 짜며 특정한 상황에 각 상황에 맞는 에러 코드를 리턴하고 있습니다

이렇게 insert 하는 서비스 코드에 디테일을 조금 추가해보았습니다

수정 update

@Override
    public void update(ApiDTO apiDTO) {
        log.info("update service");
        log.info(apiDTO);

        //DB 조회 (PK 가 있는지 검사)
        ApiVO getPk = apiRepository.getPk(apiDTO);

        if(getPk == null){
            log.info("없는 PK 입니다");
            throw new CustomException(ErrorCode.NOT_INFO);
        }

        apiRepository.update(apiDTO);
    }
  • getCheck를 통해 수정할 데이터가 있는지 확인해주고 없다면 CustomException 이 발생하게 처리했습니다

❗❗우선 서비스 로직은 이렇게 마무리했습니다 로직을 만들면서 swagger-ui로 계속 테스트를 진행하면서 코드를 뿌슝빠슝 했습니다 하지만 분량상 이곳에서는 생략했습니다

😱문제 발생

  •  swagger-ui에서 update를 할 때 NumberFormatException 이 발생함…
  •  update 할때 수정하려고 하는 항목만 입력했을 때 입력 안 한 값들이 null로 DB에 들어가는 엄청난 상태….

swagger-ui에서 update를 할 때 NumberFormatException 이 발생

swagger-ui 에서 update를 할때 PK 값인 memberNumber을 확인해서 update 를 진행한다 그래서 controller 에서 PathVariable 로 memberNumber 을 받아서 처리하고 있는데 NumberFormatException 이 발생하고 있다 ….

@ApiOperation("api post update")
@PostMapping("/update/{memberNumber}")
public ResponseEntity update(@PathVariable("memberNumber") Integer memberNumber , @Validated(ApiDTO.updateGroup.class) ApiDTO apiDTO){

log.info("-----------------------");
log.info("update");
log.info("------------------");
    apiService.update(apiDTO);

    return new ResponseEntity(HttpStatus.OK);
}

에러

  • 정확한 원인은 모르겠지만….. Integer 인 memberNumber이 들어올 때 String로 들어오는것 같다…. 그래서 받는 값을 Integer이 아닌 String 로 수정해서 문제는 해결했지는 왜? 라는 질문에는 답하지 못했다… String 로 들어오는 거 까지는 확인헀지만 왜? 들어오는지는 모르겠다… 아시면 댓글 달아주세요…

코드 수정

@ApiOperation("api post update")
@PostMapping("/update/{memberNumber}")
public ResponseEntity update(@PathVariable("memberNumber") String memberNumber , @Validated(ApiDTO.updateGroup.class) ApiDTO apiDTO){

log.info("-----------------------");
log.info("update");
log.info("------------------");
    apiService.update(apiDTO);

    return new ResponseEntity(HttpStatus.OK);
}

update 할 때 수정하려고 하는 항목만 입력했을 때 입력 안 한 값들이 null로 DB에 들어감

원래 update sql 문

<update id="update">
    update api
    set update_date  = now()
      , member_id    = #{memberId}
      , member_pw    = #{memberPw}
      , member_name  = #{memberName}
      , member_phone = #{memberPhone}
      , advert       = #{advert}
    where member_number = #{memberNumber}
</update>

현재 DB 데이터

  • 여기서 이제 swagger-ui로 id 만 수정하게 되면

DB 결과가…처참…😱

아니… 이게 무슨…. 하…. 수정할 때 모든 데이터를 입력하는 것이 아니면 기존의 데이터가 날아가버리는 상황이 발생했다….

이걸 수정하기 위해 고민한 것은 각 칼럼에 새로운 값이 안들어오면 기존의 값이 유지되게 해야하는데…… where 조건을 여러개를 주어야하는건가..? 컬럼에 And 조건 같은걸 걸어야 하나..? 여러 방법을 고민해보고 시도했지만 결과는 전부 꽝이었다…. 그러던 중 set 조건에 if문을 사용할 수 있다는 걸 알게 되었다 그래서 바로 시도해보았습니다

수정한 update sql문

<update id="update">
    update api
    set update_date  = now()
      <if test=" memberId != null and memberId != '' ">
      , member_id    = #{memberId}
      </if>
      <if test="memberPw != null and memberPw != '' ">
      , member_pw    = #{memberPw}
      </if>
      <if test="memberName != null and memberName != '' ">
      , member_name  = #{memberName}
      </if>
      <if test="memberPhone != null and memberPhone != '' ">
      , member_phone = #{memberPhone}
      </if>
      <if test="advert != null and advert != '' ">
      , advert       = #{advert}
      </if>
    where member_number = #{memberNumber}
</update>
  • 테스트를 위해 기존 DB에 null 들어간 데이터를 채워주고 다시 시도했습니다

swagger-ui로 id 만 수정하게 되면

DB 결과

크….. 바로 이거지….. 그래 이거야 ㅠ 이런 방법이 있었네요… 역시 아직 공부할게 너무너무 많다는 걸 느끼게 되는 것 같습니다

이렇게 기존 코드에서 유효성 검사와 부족한 부분들을 보완하는 리펙토링을 진행해보았습니다