inblog logo
|
sxias
    스프링부트자바스크립트

    [스프링부트] 25. 유효성 검사

    문정준's avatar
    문정준
    Apr 15, 2025
    [스프링부트] 25. 유효성 검사
    Contents
    1. 유효성 검사2. 모듈화3. Proxy Pattern4. 적용
     

    1. 유효성 검사

    • 사용자에게 올바른 입력값을 받기 위한 사전 검사
    • Pattern.matches(regex, value) 사용
      • regex : 정규 표현식, 입력을 허용하는 규칙
      • value : 입력받을 값
     
    • 한글만 받고 싶을 때에 사용할 수 있는 Regular Expression Test
      • 자음, 모음, 한글을 전부 포함
      • ^ : 시작
      • $ : 끝
      • [ ] : 유효성 검사 패턴
        • - : 범위 (a-z, A-Z 등, a에서 z까지)
        • ^ : 제외 (!과 같은 의미, 대괄호 안에 있어야 작동)
      • { } : 글자 수 (정수 or 범위 (-))
      • + : 뒤에 추가 정규 표현식을 작성
    @Test public void 한글만된다_test() { // 1. given String value = ""; // 2. when boolean result = Pattern.matches("^[ㄱ-ㅎㅏ-ㅣ가-힣]+$", value); // 3. eye System.out.println("Test : " + result); }
    notion image
    notion image
     
    package shop.mtcoding.blog.temp; import org.junit.jupiter.api.Test; import java.util.regex.Pattern; // https://regex101.com public class RegexTest { @Test public void 한글만된다_test() { String value = "ㅏㅏㅑ"; boolean result = Pattern.matches("^[가-힣]+$", value); System.out.println("테스트 : " + result); } @Test public void 한글은안된다_test() throws Exception { String value = "saltedcaramel1234"; // String value = "$86..ssa"; boolean result = Pattern.matches("^[^가-힣ㄱ-ㅎㅏ-ㅣ]+$", value); System.out.println("테스트 : " + result); } @Test public void 영어만된다_test() throws Exception { String value = "ssar"; // String value = "ssar2"; boolean result = Pattern.matches("^[a-zA-Z]+$", value); System.out.println("테스트 : " + result); } @Test public void 영어는안된다_test() throws Exception { String value = "1한글$%^"; // String value = "ssar"; boolean result = Pattern.matches("^[^a-zA-Z]+$", value); System.out.println("테스트 : " + result); } @Test public void 영어와숫자만된다_test() throws Exception { String value = "ssar2+3"; // String value = "ssar2&"; // String value = "ssar한글"; boolean result = Pattern.matches("^[a-zA-Z0-9]+$", value); System.out.println("테스트 : " + result); } @Test public void 영어만되고_길이는최소2최대4이다_test() throws Exception { String value = "ssar"; // String value = "ssarm"; boolean result = Pattern.matches("^[a-zA-Z]{2,4}$", value); System.out.println("테스트 : " + result); } @Test public void user_username_test() throws Exception { String username = "ssar"; // String username = "ssa^"; boolean result = Pattern.matches("^[a-zA-Z0-9]{2,20}$", username); System.out.println("테스트 : " + result); } @Test public void user_email_test() throws Exception { String email = "s...s@fGf.ccm"; // String username = "@fGf.ccm"; // +를 *로 변경해보기 boolean result = Pattern.matches("^[a-zA-Z0-9.]+@[a-zA-Z0-9]+\\.[a-zA-Z]{2,3}$", email); System.out.println("테스트 : " + result); } @Test public void user_fullname_test() throws Exception { String fullname = "코스"; // String fullname = "코스ss1"; boolean result = Pattern.matches("^[a-zA-Z가-힣]{1,20}$", fullname); System.out.println("테스트 : " + result); } @Test public void account_gubun_test() throws Exception { String gubun = "TRANSFER"; // WITHDRAW(8), DEPOSIT(7), TRANSFER(8) boolean result = Pattern.matches("^(WITHDRAW|DEPOSIT|TRANSFER)$", gubun); System.out.println("테스트 : " + result); } @Test public void account_gubun_test2() throws Exception { String gubun = "TRANSFER"; // WITHDRAW(8), DEPOSIT(7), TRANSFER(8) boolean result = Pattern.matches("^(TRANSFER)$", gubun); System.out.println("테스트 : " + result); } @Test public void account_tel_test() throws Exception { String tel = "01022227777"; boolean result = Pattern.matches("^[0-9]{3}[0-9]{4}[0-9]{4}$", tel); System.out.println("테스트 : " + result); } }
     
    💡

    대괄호 ([ ]) 뒤에 바로 $를 붙이면 안될까?

    • 대괄호 뒤에 바로 $를 붙이면 문자 1개를 검사
      • A = true, Ab = false
    • +를 붙이면 이후에 이어지는 문자들도 추가로 검사
     

    2. 모듈화

    • Controller에 부착하여 재사용하기 위해, 유효성 검사를 모듈화
      • DTO에서 값을 검사 : Controller의 책임 전가
      • Annotation 활용
     
    • 기존 코드 : Controller에서 입력받는 값에 따른 유효성 검사의 차이 - 입력값에 따라 달라짐
      • 모듈화 불가
    @PostMapping("/join") public String join(UserRequest.JoinDTO joinDTO) { // 유효성 검사 boolean r1 = Pattern.matches("^[a-zA-Z0-9]{2,20}$", joinDTO.getUsername()); boolean r2 = Pattern.matches("^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[!@#$%^&*()])[a-zA-Z\\d!@#$%^&*()]{6,20}$", joinDTO.getPassword()); boolean r3 = Pattern.matches("^[a-zA-Z0-9.]+@[a-zA-Z0-9]+\\.[a-zA-Z]{2,3}$", joinDTO.getEmail()); if (!r1) throw new Exception400("유저네임은 2-20자이며, 특수문자,한글이 포함될 수 없습니다"); if (!r2) throw new Exception400("패스워드는 4-20자이며, 특수문자,영어 대문자,소문자, 숫자가 포함되어야 하며, 공백이 있을 수 없습니다"); if (!r3) throw new Exception400("이메일 형식에 맞게 적어주세요"); userService.회원가입(joinDTO); return "redirect:/login-form"; }
     
    • 새 코드 : Annotation과 Errors를 활용하여 공통모듈을 생성 - 입력값이 달라도 코드 동일
      • 모듈화 가능
    // @Valid : 매개변수 내 Validation (Pattern, Size 등) 검사 @PostMapping("/join") public String join(@Valid UserRequest.JoinDTO joinDTO, Errors errors) { /* // 유효성 검사 boolean r1 = Pattern.matches("^[a-zA-Z0-9]{2,20}$", joinDTO.getUsername()); boolean r2 = Pattern.matches("^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[!@#$%^&*()])[a-zA-Z\\d!@#$%^&*()]{6,20}$", joinDTO.getPassword()); boolean r3 = Pattern.matches("^[a-zA-Z0-9.]+@[a-zA-Z0-9]+\\.[a-zA-Z]{2,3}$", joinDTO.getEmail()); if (!r1) throw new Exception400("유저네임은 2-20자이며, 특수문자,한글이 포함될 수 없습니다"); if (!r2) throw new Exception400("패스워드는 4-20자이며, 특수문자,영어 대문자,소문자, 숫자가 포함되어야 하며, 공백이 있을 수 없습니다"); if (!r3) throw new Exception400("이메일 형식에 맞게 적어주세요"); */ // pass if no error exists if (errors.hasErrors()) { // gather errors List<FieldError> fErrors = errors.getFieldErrors(); // alert users ( for (FieldError fieldError : fErrors) { throw new Exception400(fieldError.getField() + " : " + fieldError.getDefaultMessage()); } } userService.회원가입(joinDTO); return "redirect:/login-form"; }
     
    notion image
     

    3. Proxy Pattern

    • 공통 모듈을 프록시 패턴을 활용해서 분리
      • 코드의 재사용 용이
     
    • GlobalValidationHandler : 유효성 검사를 위한 Advice
      • Before, After, Around
      • Before : Target이 실행되기 전에 실행되는 함수
        • Target의 사전 동작을 관리 가능
      • After : Target이 실행되고 난 후 실행되는 함수
        • Target의 사후 동작을 관리 가능
      • Around : Target이 실행되기 전, 실행 후 둘 다 실행되는 함수
        • Target의 사전, 사후 동작을 전부 관리 가능
    @Component @Aspect // Proxy public class GlobalValidationHandler { @Before("@annotation(shop.mtcoding.blog._core.error.anno.MyBefore)") public void beforeAdvice(JoinPoint jp) { String name = jp.getSignature().getName(); System.out.println("before advice : " + name); } @After("@annotation(shop.mtcoding.blog._core.error.anno.MyAfter)") public void afterAdvice(JoinPoint jp) { String name = jp.getSignature().getName(); System.out.println("after advice : " + name); } @Around("@annotation(shop.mtcoding.blog._core.error.anno.MyAround)") public Object aroundAdvice(ProceedingJoinPoint jp) { String name = jp.getSignature().getName(); System.out.println("around advice 직전 : " + name); try { Object result = jp.proceed(); // 컨트롤러 함수 호출 System.out.println("around advice 직후 : " + name); System.out.println("result : " + result); return result; } catch (Throwable e) { throw new RuntimeException(e); } } }
     
    💡

    @Around와 @Before, @After의 차이점

    • Before, After는 함수가 실행되기 전과 후를 나누어서 관리하므로, DS의 invoke 권한이 없음
      • 매개변수 또는 리턴 값을 분석할 수 있긴 하지만 invoke 타이밍을 조절할 수 없음
    • Around에서 사용하는 ProceedingJoinPoint는 invoke의 제어권을 가지고 있음
      • ProceedingJoinPoint.proceed()를 통해 invoke를 직접 수행 가능
      • 함수 전,후반의 매개변수, 리턴 값, invoke 타이밍을 전부 관리 가능
     
    With ChatGPT : ✅ 보완된 요약: @Around, @Before, @After 차이점
    🤖

    @Around vs @Before / @After

    • @Before와 @After는 관심 대상 메서드의 실행 전후에만 개입할 수 있음
      • @Before: 메서드 실행 직전에 실행됨
      • @After: 메서드 실행 직후에 실행됨
      • 이들 어노테이션은 **핵심 로직(= invoke)**의 실행을 차단하거나 재정의할 수 없음
      • 단, JoinPoint를 통해 메서드 이름, 매개변수, 타겟 객체 정보 등은 읽을 수 있음
      • 예외 발생 여부와 무관하게 실행되는 특징이 있음 (@After는 finally처럼 작동)
     
    • @Around는 핵심 메서드 실행 자체를 제어할 수 있는 유일한 어노테이션
      • ProceedingJoinPoint.proceed()를 호출해야 실제 메서드가 실행됨
      • 따라서, 실행 시점 지연, 실행 생략, 반복 실행, 예외 감싸기 등도 가능
      • @Before, @After, @AfterReturning, @AfterThrowing의 모든 기능을 포함 가능
      • 리턴값 변형이나 예외 핸들링을 포함한 전체 흐름 커스터마이징에 적합
     
    • 실무 팁
      • 단순히 로깅, 인증체크, 파라미터 확인 정도라면 @Before / @After로 충분
      • 트랜잭션 처리, 리턴 값 변조, 예외 래핑 등 전체 흐름이 필요한 경우는 @Around 사용
     
     

    4. 적용

    • GlobalExceptionHandler의 Pointcut을 PostMapping, PutMapping에 적용
    • 매개 변수 내에 Errors를 찾음
    • 있으면, Errors 내에 항목이 있는지 검사 (Errors.hasErrors())
    • 있으면 유효성 검사
    @Aspect @Component public class GlobalValidationHandler { // 관심사를 분리 // PostMapping 혹은 PutMapping이 붙어 있는 메서드를 실행하기 직전에 Advice 호출 @Before("@annotation(org.springframework.web.bind.annotation.PostMapping) || @annotation(org.springframework.web.bind.annotation.PutMapping)") public void badRequestAdvice(JoinPoint jp) { // 실행될 실제 메서드의 모든 것을 투영 (Reflect) Object[] args = jp.getArgs(); // 메서드의 매개변수들 (배열로 리턴) for (Object arg : args) { // 매개변수 갯수만큼 (Annotation 제외) // execute when method's args have Errors if (arg instanceof Errors) { System.out.println("Exception400 처리 필요"); Errors errors = (Errors) arg; // Downcasting // execute when Errors.size() > 0 -> errors exist if (errors.hasErrors()) { // gather errors List<FieldError> fErrors = errors.getFieldErrors(); // alert users ( for (FieldError fieldError : fErrors) { throw new Exception400(fieldError.getField() + " : " + fieldError.getDefaultMessage()); } } } } } }
     
    • Target : PostMapping + Body가 있는 메서드 (update, save)
    • BoardController
    @PostMapping("/board/save") public String save(@Valid BoardRequest.SaveDTO saveDTO, Errors errors) { User sessionUser = (User) session.getAttribute("sessionUser"); boardService.글쓰기(saveDTO, sessionUser); return "redirect:/"; } @PostMapping("/board/{id}/update") public String update(@PathVariable("id") Integer id, @Valid BoardRequest.UpdateDTO updateDTO, Errors errors) { User sessionUser = (User) session.getAttribute("sessionUser"); boardService.글수정(updateDTO, sessionUser.getId(), id); return "redirect:/board/" + id; }
     
    • BoardRequest
    @Data public static class SaveDTO { // title=제목1&content=내용1 -> isPublic은 null // title=제목1&content=내용1&isPublic -> isPublic은 공백 // title=제목1&content=내용1&isPublic= -> isPublic은 스페이스 @NotEmpty(message = "제목을 입력해주세요.") // null, 공백(""), 스페이스(" ") 안됨 private String title; @NotEmpty(message = "내용을 입력해주세요.") private String content; private String isPublic; public Board toEntity(User user) { return Board.builder() .title(title) .content(content) .isPublic(isPublic == null ? false : true) .user(user) // user객체 필요 .build(); } } @Data public static class UpdateDTO { @NotEmpty(message = "제목을 입력해주세요.") private String title; @NotEmpty(message = "내용을 입력해주세요.") private String content; private String isPublic; // "on" or null public Boolean isPublicChecked() { return "on".equals(isPublic); } }
     
    • UserController
    @PostMapping("/user/update") public String update(@Valid UserRequest.UpdateDTO updateDTO, Errors errors) { User sessionUser = (User) session.getAttribute("sessionUser"); // update user_tb set password = ?, email = ? where id = ? User userPS = userService.회원정보수정(updateDTO, sessionUser.getId()); // 세션 동기화 session.setAttribute("sessionUser", userPS); return "redirect:/"; } @PostMapping("/join") public String join(@Valid UserRequest.JoinDTO joinDTO, Errors errors) { /* // 유효성 검사 boolean r1 = Pattern.matches("^[a-zA-Z0-9]{2,20}$", joinDTO.getUsername()); boolean r2 = Pattern.matches("^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[!@#$%^&*()])[a-zA-Z\\d!@#$%^&*()]{6,20}$", joinDTO.getPassword()); boolean r3 = Pattern.matches("^[a-zA-Z0-9.]+@[a-zA-Z0-9]+\\.[a-zA-Z]{2,3}$", joinDTO.getEmail()); if (!r1) throw new Exception400("유저네임은 2-20자이며, 특수문자,한글이 포함될 수 없습니다"); if (!r2) throw new Exception400("패스워드는 4-20자이며, 특수문자,영어 대문자,소문자, 숫자가 포함되어야 하며, 공백이 있을 수 없습니다"); if (!r3) throw new Exception400("이메일 형식에 맞게 적어주세요"); */ userService.회원가입(joinDTO); return "redirect:/login-form"; } @PostMapping("/login") public String login(@Valid UserRequest.LoginDTO loginDTO, HttpServletResponse response, Errors errors) { User sessionUser = userService.로그인(loginDTO); session.setAttribute("sessionUser", sessionUser); if (loginDTO.getRememberMe() == null) { Cookie cookie = new Cookie("username", null); cookie.setMaxAge(0); // 즉시 만료 response.addCookie(cookie); } else { Cookie cookie = new Cookie("username", loginDTO.getUsername()); cookie.setMaxAge(60 * 60 * 24 * 7); response.addCookie(cookie); } return "redirect:/"; }
     
    • UserRequest
    @Data public static class UpdateDTO { @Pattern(regexp = "^[a-zA-Z0-9]{2,20}$", message = "유저네임은 2-20자이며, 특수문자,한글이 포함될 수 없습니다") private String username; @Size(min = 4, max = 20, message = "크기가 4자에서 20자 사이여야 합니다.") private String password; @Pattern(regexp = "^[a-zA-Z0-9.]+@[a-zA-Z0-9]+\\.[a-zA-Z]{2,3}$", message = "이메일 형식으로 적어주세요") private String email; } // insert 용도의 dto에는 toEntity 메서드를 만든다. @Data public static class JoinDTO { // DTO 유효성 검사 annotation // 1. Pattern @Pattern(regexp = "^[a-zA-Z0-9]{2,20}$", message = "유저네임은 2-20자이며, 특수문자,한글이 포함될 수 없습니다") private String username; // 2. Size @Size(min = 4, max = 20, message = "크기가 4자에서 20자 사이여야 합니다.") private String password; @Pattern(regexp = "^[a-zA-Z0-9.]+@[a-zA-Z0-9]+\\.[a-zA-Z]{2,3}$", message = "이메일 형식으로 적어주세요") private String email; public User toEntity() { return User.builder() .username(username) .password(password) .email(email) .build(); } } @Data public static class LoginDTO { @Pattern(regexp = "^[a-zA-Z0-9]{2,20}$", message = "유저네임은 2-20자이며, 특수문자,한글이 포함될 수 없습니다") private String username; @Size(min = 4, max = 20, message = "크기가 4자에서 20자 사이여야 합니다.") private String password; private String rememberMe; // check되면 on, 안되면 null }
     
    • LoveController
    @PostMapping("/love") public Resp<?> saveLove(@Valid @RequestBody LoveRequest.SaveDTO reqDTO, Errors errors) { User sessionUser = (User) session.getAttribute("sessionUser"); LoveResponse.SaveDTO respDTO = loveService.좋아요(reqDTO, sessionUser.getId()); return Resp.ok(respDTO); }
     
    • LoveRequest : boardId는 Integer 타입 : NotBlank, NotEmpty 사용 불가 → NotNull
    @Data public static class SaveDTO { @NotNull(message = "해당하는 글이 없습니다.") private Integer boardId; public Love toEntity(Integer sessionUserId) { return Love.builder() .board(Board.builder().id(boardId).build()) .user(User.builder().id(sessionUserId).build()) .build(); } }
     
    • ReplyController
    @PostMapping("/reply/save") public String save(@Valid ReplyRequest.SaveDTO saveDTO, Errors errors) { User sessionUser = (User) session.getAttribute("sessionUser"); replyService.댓글저장(saveDTO, sessionUser); return "redirect:/board/" + saveDTO.getBoardId(); }
     
    • ReplyRequest
    @Data public static class SaveDTO { @NotNull(message = "해당하는 글이 없습니다.") private Integer boardId; @NotEmpty(message = "내용을 입력해주세요.") private String content; public Reply toEntity(User sessionUser) { return Reply.builder() .content(content) .user(sessionUser) .board(Board.builder().id(boardId).build()) .build(); } }
    Share article
    Contents
    1. 유효성 검사2. 모듈화3. Proxy Pattern4. 적용

    Sxias ㆍ a32176740@gmail.com

    RSS·Powered by Inblog