Home Project Lab 15. 유효성 검사(javascript) - 1
Post
Cancel

Project Lab 15. 유효성 검사(javascript) - 1

Java bean 유효성 검사(spring-boot-starter-validation)

  • Bean Validation 2.0(JSR-380)은 Java 데이터 유효성 검사 표준 기술로, 애노테이션을 사용하여 bean 유효성 검사를 하는 Java API 명세다.
  • Hibernate-validator: Bean Validation을 구현한 Java API다.
  • 프로젝트에서는 Bean Validation 2.0 API를 사용하여 Java 유효성 검사를 수행한다. spring-boot-starter-validation 의존성에는 Bean Validation 2.0 API와 이를 구현한 Hibernate-validator API가 존재한다.

출처: https://medium.com/@SlackBeck/javabean-validation%EA%B3%BC-hibernate-validator-%EA%B7%B8%EB%A6%AC%EA%B3%A0-spring-boot-3f31aee610f5

  • javax annotation으로 validation 방법은 하단 출처를 참고하였다.

출처: https://jeong-pro.tistory.com/203

파일 유효성 검사(파일 크기, 파일 확장자, MIME Type)

  • 기본적인 파일 validation 방법은 파일 확장자를 검사하는 방법이다. 보안 위협이 될 수 있는 확장자를 가지는 파일(.exe 파일, .jar 파일)이 서버에 업로드 되지 않도록 차단한다.
  • 그러나 파일 확장자가 .txt 고 보안 공격을 시도하는 파일 binary 구조를 가진다면, 큰 보안 위협이 된다. 벡엔드에서는 .txt 파일 확장자를 유효한 파일 확장자라고 판단하여 파일 업로드를 허용하기 때문이다. 이렇듯 웹에서 파일 확장자는 큰 의미를 가지지 않으며, 파일 MIME type을 사용한 파일 유효성 검사가 필요하다.
  • MIME(Multipurpose Internet Mail Extensions) type은 웹에서 클라이언트에게 전송된 문서의 다양성을 알려주기 위한 메커니즘으로서, 첨부된 파일을 텍스트 문자 형태로 변환해서 이메일과 함께 전송하기 위해 개발된 포맷이다. 파일의 MIME type을 분석한다면, 위에서 설명한 보안 위협 사례를 예방할 수 있다.
  • 프로젝트에서는 파일 확장자, 파일 크기(한 번 업로드시 20MB 이하의 파일만 업로드 허용), MIME type을 사용하여 유효성 검사를 수행한다. 다양한 파일 MIME type 확인 방법 중에서, Apache Tika 라이브러리를 사용하였다.

출처: https://offbyone.tistory.com/330
https://www.baeldung.com/java-file-mime-type
https://stackoverflow.com/questions/5541694/how-to-get-file-extension-from-content-type

예외처리 설계

  • Spring에서 예외처리 방법에는 3가지가 있다.
  • 전역에서 예외처리(전체 애플리케이션) Global Level using - @ControllerAdvice
  • Controller에서 예외처리 Controller Level using - @ExceptionHandler
  • 메소드에서 예외처리 Method Level using - try- catch

출처: https://jeong-pro.tistory.com/195
https://springboot.tistory.com/33

전역에서 예외처리 @ControllerAdvice

  • 프로젝트에서는 애플리케이션에서 발생하는 예외는 전역에서 예외처리하여 에러 메시지를 응답한다.
  • Bean Validation 2.0 API의 유효성 검사에 실패하면 예외 정보를 Controller 파라미터에 BindingResult bindingResult를 선언 후 bindingResult.hasError() 메소드를 사용하여 확인할 수 있다.
  • 그러나 해당 방법은 모든 응답마다 유효성 검사를 위한 소스 코드가 추가되기에 비효율적이므로, 유효성 검사에 실패하면 발생하는 예외를 전역에서 예외처리 하도록 구현하였다.
1
2
3
4
5
6
7
8
9
10
11
<test.java>

@PostMapping("/books") public void save(@RequestBody @Valid AddBookRequestDto addBookRequestDto, BindingResult bindingResult)
{
    if (bindingResult.hasErrors())
    {
        bindingResult.getAllErrors().forEach(objectError->{ System.err.println("code : " + objectError.getCode()); System.err.println("defaultMessage : " + objectError.getDefaultMessage()); System.err.println("objectName : " + objectError.getObjectName()); });
        return;
    }
    bookService.save(addBookRequestDto.toEntity());
}

출처: https://velog.io/@hellozin/Valid-%EC%98%88%EC%99%B8%EB%A5%BC-%EC%A0%84%EC%97%AD-%EC%BB%A8%ED%8A%B8%EB%A1%A4%EB%9F%AC%EB%A1%9C-%EA%B0%84%EB%8B%A8%ED%95%98%EA%B2%8C-%EC%B2%98%EB%A6%AC%ED%95%98%EA%B8%B0

Controller에서 예외처리 @ExceptionHandler

  • Controller내에서 발생하는 모든 예외를 처리하는 방법이다. 전역에서 예외처리 하는 방법은 소스 코드 중복을 줄이고 가독성을 향상시키므로, 해당 방법을 적용하지 않았다. 메소드에서 예외처리 try-catch
  • 메소드에서 예외처리 방법은 예외마다 다르게 처리할 수 있고 메소드 단위로 예외처리를 수행하기 때문에 소스 코드의 안전성과 신뢰성을 높일 수 있다. 그러나 반복되는 try-catch 구문은 가독성을 떨어뜨리고 알고리즘 로직의 흐름이 끊긴다.
  • 하단 출처에서는 try-cath 사용할 때 전략을 다음과 같이 소개한다.
    1. try-catch를 최대한 지양해라.
    2. try-catch로 에러를 먹고 죽는 코드는 지양해라.(이런 코드가 있다면 로그라도 추가해주세요…) try { // 비즈니스 로직 수행… }catch (Exception e){ e.printStackTrace(); }
    3. try catch를 사용하게 된다면 더 구체적인 Exception을 발생시키는 것이 좋다.

출처: https://cheese10yun.github.io/spring-guide-exception/

의존성 관리

  • Java bean 유효성 검사를 위한 spring-boot-starter-validation 의존성과 파일 유효성 검사(MIME Type)에 필요한 Apache Tika 의존성을 추가한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<build.gradle>

   // 프로젝트 개발에 필요한 공통 의존성 라이브러리를 선언한다.
   dependencies {
       // spring boot
       implementation "org.springframework.boot:spring-boot-starter-web"
       implementation "org.springframework.boot:spring-boot-starter-data-jpa"
       implementation "org.springframework.boot:spring-boot-starter-security"
       implementation "org.springframework.boot:spring-boot-starter-validation"
       runtimeOnly "org.springframework.boot:spring-boot-devtools"

       ...
   }
}

...

project(":module-domain-core") {
   dependencies {
       compile project(":module-system-common")

       implementation "org.apache.tika:tika-parsers:1.24.1"
       implementation "com.querydsl:querydsl-core"
       implementation "com.querydsl:querydsl-jpa"
   }
}

...

Config

  • Web 애플리케이션의 설정 및 공통적으로 사용하는 Web Resource를 다루는 모듈을 추가하였다.
  • 해당 모듈은 프로젝트 진행 중 각 역할을 세분화하고 모듈간의 의존성을 낮추기 위해서 새로 추가하였으며, 이후 모듈의 역할을 구체적으로 정하고 세분화 작업을 수행할 예정이다.
  • Error 클래스, Handler 클래스, Validation 클래스, module-web-core와 module-app-admin이 공통적으로 사용하는 Web Resources(html, css, javascript 등) 파일이 위치한다. Web Resource는 추후 리펙토링 때 분리할 예정이다.
1
2
3
4
5
6
7
8
9
<settings.gradle>

rootProject.name = 'lab'
include 'module-system-common'
include 'module-domain-core'
include 'module-app-api'
include 'module-app-web'
include 'module-app-admin'
include 'module-web-core'


  • max-swallow-size: 요청 body의 크기를 설정한다. 업로드되는 파일 크기가 제한(20MB)을 초과하여 예외가 발생하는 경우, 사용자 정의 예외처리 방식으로 수행되도록 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<module-app-api/src/main/resources/application.yml>

spring:
 jpa:
   open-in-view: false
 devtools:
   # 프론트 수정 사항을 자동으로 반영한다.
   livereload:
     enabled: false
 servlet:
   multipart:
     # 한개의 파일의 최대 크기
     max-file-size: 20MB
     # form-data 요청에 따른 모든 파일의 최대 크기
     max-request-size: 20MB
     enabled: true
server:
 tomcat:
   max-swallow-size: -1
...

Java bean 유효성 검사

  • 게시글에서는 noticeBoard 게시판을 기준으로 소개한다.
  • 모든 DTO에서 공통적으로 사용하는 DTO다.
  • private String createdBy: @NotBlank 생성자 데이터가 공란인지 유효성 검사한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<module-domain-core/src/main/java/kr/ac/univ/common/dto/CommonDto.java>

package kr.ac.univ.common.dto;

import kr.ac.univ.common.domain.enums.ActiveStatus;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.time.LocalDateTime;

@Getter
@Setter
@NoArgsConstructor
@ToString
public class CommonDto {
   private Long idx;
   private LocalDateTime createdDate;
   private LocalDateTime lastModifiedDate;
   @NotBlank(message = "The createdBy must not be blank.\nIf the message is alerted although you are logged in, please contact the admin.")
   private String createdBy;
   private String lastModifiedBy;
   private ActiveStatus activeStatus;
   private boolean isAccess;
}


  • NoticeBoard에서 사용하는 DTO다.
  • private String title: @NotBlank 제목 데이터가 공란인지 유효성 검사한다.
  • private String content: @Editor은 사용자 정의 validaton(ConstraintValidator)이다. editor 멤버 필드의 byte 크기가 16MB 이하인지 유효성 검사한다.

출처: https://cheese10yun.github.io/ConstraintValidator/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<module-domain-core/src/main/java/kr/ac/univ/noticeBoard/dto/NoticeBoardDto.java>

package kr.ac.univ.noticeBoard.dto;

import kr.ac.univ.common.dto.CommonDto;
import kr.ac.univ.common.validation.Editor;
import kr.ac.univ.noticeBoard.domain.NoticeBoardAttachedFile;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;

import javax.validation.constraints.NotBlank;
import java.util.ArrayList;
import java.util.List;

@Getter
@Setter
@NoArgsConstructor
@ToString
public class NoticeBoardDto extends CommonDto {
   /* CommonDto: JPA Audit */

   /* 기본 정보 */
   @NotBlank(message = "The title must not be blank.")
   private String title;
   @Editor(max = 16777215, message="The editor's input size of bytes is exceeded.")
   private String content;
   private Long viewCount;

   /* newIcon */
   private boolean isNewIcon;

   /* 첨부 파일 */
   private List<NoticeBoardAttachedFile> attachedFileList = new ArrayList<NoticeBoardAttachedFile>();
}


  • 사용자 정의 validaton(ConstraintValidator)의 애노테이션을 생성한다.
  • String message(): @Editor를 통해 입력되는 message다.
  • long max(): @Editor를 통해 입력되는 최대 byte 크기다. byte 크기를 제한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<module-domain-core/src/main/java/kr/ac/univ/common/validation/Editor.java>

package kr.ac.univ.common.validation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import javax.validation.Constraint;
import javax.validation.Payload;

@Documented
@Constraint(validatedBy = EditorValidator.class)
@Target({ ElementType.METHOD, ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
public @interface Editor {
   String message();
   long max();
   Class<?>[] groups() default {};
   Class<? extends Payload>[] payload() default {};
}


  • 사용자 정의 validaton(ConstraintValidator)의 유효성 검사 로직이 존재한다.
  • initialize: @Editor를 통해 입력되는 최대 byte 크기를 초기화 한다.
  • long max(): ConstraintValidator의 유효성 검사 로직이다. byte 크기가 16MB 이하인지 유효성 검사한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<module-domain-core/src/main/java/kr/ac/univ/common/validation/EditorValidator.java>

package kr.ac.univ.common.validation;

import kr.ac.univ.util.ByteSizeUtil;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class EditorValidator implements ConstraintValidator<Editor, String> {
   long max;

   @Override
   public void initialize(Editor editor) {
       max = editor.max();
   }

   @Override
   public boolean isValid(String str, ConstraintValidatorContext cxt) {
       return ByteSizeUtil.getByteSize(str) < max;
   }
}


  • Java: 문자열의 byte 크기를 반환한다.

출처: https://photoress.tistory.com/entry/String%ED%98%95-Bytes%EB%A1%9C-length%EA%B5%AC%ED%95%98%EA%B8%B0
https://blog.miyam.net/125

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<module-system-common/src/main/java/kr/ac/univ/util/ByteSizeUtil.java>

package kr.ac.univ.util;

import java.io.UnsupportedEncodingException;

public class ByteSizeUtil {

   public static int getByteSize(String str) {
       int byteSize = 0;

       try {
           byteSize = str.getBytes("UTF-8").length;
       } catch (Exception e) {
           e.printStackTrace();
       }

       return byteSize;
   }

}


  • NoticeBoard 관련 클라이언트의 요청을 json 타입으로 응답한다.
  • @Valid를 사용하면 View에서 전달되는 데이터에 대해서 유효성 검사가 수행된다. 만약 NoticeBoardDto 유효성 검사에 실패하여 예외가 발생한다면, 해당 예외는 전역에서 예외처리 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
<module-app-api/src/main/java/kr/ac/univ/controller/NoticeBoardRestController.java>

package kr.ac.univ.controller;

import kr.ac.univ.common.validation.FileValidator;
import kr.ac.univ.error.ErrorCode;
import kr.ac.univ.error.ErrorResponse;
import kr.ac.univ.exception.FileTypeException;
import kr.ac.univ.noticeBoard.dto.NoticeBoardDto;
import kr.ac.univ.noticeBoard.dto.mapper.NoticeBoardMapper;
import kr.ac.univ.noticeBoard.service.NoticeBoardAttachedFileService;
import kr.ac.univ.noticeBoard.service.NoticeBoardService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MaxUploadSizeExceededException;
import org.springframework.web.multipart.MultipartFile;

import javax.validation.Valid;
import java.util.List;

@RestController
@RequestMapping("/api/notice-boards")
public class NoticeBoardRestController {
   private final NoticeBoardService noticeBoardService;
   private final NoticeBoardAttachedFileService noticeBoardAttachedFileService;

   public NoticeBoardRestController(NoticeBoardService noticeBoardService, NoticeBoardAttachedFileService noticeBoardAttachedFileService) {
       this.noticeBoardService = noticeBoardService;
       this.noticeBoardAttachedFileService = noticeBoardAttachedFileService;
   }

   @PostMapping
   public ResponseEntity<?> postNoticeBoard(@RequestBody @Valid NoticeBoardDto noticeBoardDto) {
       Long idx = noticeBoardService.insertNoticeBoard(noticeBoardDto);

       return new ResponseEntity<>(idx, HttpStatus.CREATED);
   }

   @PutMapping("/{idx}")
   public ResponseEntity<?> putNoticeBoard(@PathVariable("idx") Long idx, @RequestBody @Valid NoticeBoardDto noticeBoardDto) {
       noticeBoardService.updateNoticeBoard(idx, noticeBoardDto);

       return new ResponseEntity<>("{}", HttpStatus.OK);
   }

   @DeleteMapping("/{idx}")
   public ResponseEntity<?> deleteNoticeBoard(@PathVariable("idx") Long idx) throws Exception {
       noticeBoardService.deleteNoticeBoardByIdx(idx);
       noticeBoardAttachedFileService.deleteAllAttachedFile(idx);

       return new ResponseEntity<>("{}", HttpStatus.OK);
   }

   ...
}

파일 유효성 검사(파일 확장자, MIME Type)

  • 유효하지 않은 파일 확장자, MIME type 데이터를 static 키워드로 선언된 Set Collection 멤버 필드에 초기화한다.
  • 파일 유효성 검사를 위한 파일 확장자와 MIME type 리스트는 하단 출처를 참고하여 정리하였다.

출처: https://www.howtogeek.com/137270/50-file-extensions-that-are-potentially-dangerous-on-windows/
https://kb.intermedia.net/article/23567
https://www.file-extensions.org/filetype/extension/name/dangerous-malicious-files

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
<module-domain-core/src/main/java/kr/ac/univ/common/validation/FileType.java>

package kr.ac.univ.common.validation;

import java.util.HashSet;
import java.util.Set;

public class FileType {
   public static Set<String> invalidMimeTypeSet = new HashSet<String>();
   public static Set<String> invalidExtensionSet = new HashSet<String>();
   public static Set<String> validImageTypeSet = new HashSet<String>();

   // Invalid mime type Set
   static {
       // Application
       invalidMimeTypeSet.add("application/x-msdownload"); // .exe
       invalidMimeTypeSet.add("application/x-msdownload; format=pe32"); // .exe
       invalidMimeTypeSet.add("application/java-archive"); // .jar
       invalidMimeTypeSet.add("application/javascript"); // .js
       invalidMimeTypeSet.add("application/x-shockwave-flash"); // .swf
       invalidMimeTypeSet.add("application/octet-stream"); // .bin 등의 실행 파일
       invalidMimeTypeSet.add("application/x-msmetafile"); // .wmf
       invalidMimeTypeSet.add("application/java-vm"); // .class
       invalidMimeTypeSet.add("application/vnd.ms-htmlhelp"); // .chm
       invalidMimeTypeSet.add("image/x-portable-graymap"); // .pgm
       invalidMimeTypeSet.add("image/x-pcx"); // .pcx
       invalidMimeTypeSet.add("application/winhlp"); // .hlp
       invalidMimeTypeSet.add("application/vnd.americandynamics.acc"); // .acc
       invalidMimeTypeSet.add("text/css"); // .css
       invalidMimeTypeSet.add("application/x-sh"); //.sh
   }

   // Invalid Extension Set
   static {
       // Programs
       invalidExtensionSet.add(".pif");
       invalidExtensionSet.add(".gadget");
       invalidExtensionSet.add(".msi");
       invalidExtensionSet.add(".msp");
       invalidExtensionSet.add(".com");
       invalidExtensionSet.add(".hta");
       invalidExtensionSet.add(".cpl");
       invalidExtensionSet.add(".msc");
       invalidExtensionSet.add(".exe");
       // Scripts
       invalidExtensionSet.add(".bat");
       invalidExtensionSet.add(".cmd");
       invalidExtensionSet.add(".vb");
       invalidExtensionSet.add(".vbs");
       invalidExtensionSet.add(".vbe");
       invalidExtensionSet.add(".jse");
       invalidExtensionSet.add(".ws");
       invalidExtensionSet.add(".wsf");
       invalidExtensionSet.add(".wsc");
       invalidExtensionSet.add(".wsh");
       invalidExtensionSet.add(".ps1");
       invalidExtensionSet.add(".ps2");
       invalidExtensionSet.add(".ps1xml");
       invalidExtensionSet.add(".ps2xml");
       invalidExtensionSet.add(".psc1");
       invalidExtensionSet.add(".psc2");
       invalidExtensionSet.add(".msh");
       invalidExtensionSet.add(".msh1");
       invalidExtensionSet.add(".msh2");
       invalidExtensionSet.add(".mshxml");
       invalidExtensionSet.add(".msh1xml");
       invalidExtensionSet.add(".msh2xml");
       // Shortcuts
       invalidExtensionSet.add(".scf");
       invalidExtensionSet.add(".lnk");
       invalidExtensionSet.add(".inf");
       invalidExtensionSet.add(".reg");
       // Others
       invalidExtensionSet.add(".dll");
       invalidExtensionSet.add(".sys");
       invalidExtensionSet.add(".gzquar");
       invalidExtensionSet.add(".zix");
       invalidExtensionSet.add(".aru");
       invalidExtensionSet.add(".ozd");
       invalidExtensionSet.add(".drv");
       invalidExtensionSet.add(".sjs");
       invalidExtensionSet.add(".dev");
       invalidExtensionSet.add(".xlm");
       invalidExtensionSet.add(".0_full_0_tgod_signed");
       invalidExtensionSet.add(".boo");
       invalidExtensionSet.add(".tps");
       invalidExtensionSet.add(".tsa");
       invalidExtensionSet.add(".sop");
       invalidExtensionSet.add(".bkd");
       invalidExtensionSet.add(".cih");
       invalidExtensionSet.add(".iik");
       invalidExtensionSet.add(".dyz");
       invalidExtensionSet.add(".dyv");
       invalidExtensionSet.add(".kcd");
       invalidExtensionSet.add(".s7p");
       invalidExtensionSet.add("dlb");
       invalidExtensionSet.add(".9");
       invalidExtensionSet.add(".dom");
       invalidExtensionSet.add(".php3");
       invalidExtensionSet.add(".dxz");
       invalidExtensionSet.add(".mjg");
       invalidExtensionSet.add(".mfu");
       invalidExtensionSet.add(".cla");
       invalidExtensionSet.add(".hlw");
       invalidExtensionSet.add(".rsc_tmp");
       invalidExtensionSet.add(".mjz");
       invalidExtensionSet.add(".bup");
       invalidExtensionSet.add(".upa");
       invalidExtensionSet.add(".bhx");
       invalidExtensionSet.add(".mcq");
       invalidExtensionSet.add(".dli");
       invalidExtensionSet.add(".txs");
       invalidExtensionSet.add(".fnr");
       invalidExtensionSet.add(".xir");
       invalidExtensionSet.add(".xlv");
       invalidExtensionSet.add(".bxz");
       invalidExtensionSet.add(".cxq");
       invalidExtensionSet.add(".xdu");
       invalidExtensionSet.add(".ska");
       invalidExtensionSet.add(".wlpginstall");
       invalidExtensionSet.add(".cfxxe");
       invalidExtensionSet.add(".tti");
       invalidExtensionSet.add(".vexe");
       invalidExtensionSet.add(".qrn");
       invalidExtensionSet.add(".dllx");
       invalidExtensionSet.add(".faq");
       invalidExtensionSet.add(".xtbl");
       invalidExtensionSet.add(".smtmp");
       invalidExtensionSet.add(".ceo");
       invalidExtensionSet.add(".tko");
       invalidExtensionSet.add(".uzy");
       invalidExtensionSet.add(".oar");
       invalidExtensionSet.add(".bll");
       invalidExtensionSet.add(".plc");
       invalidExtensionSet.add(".spam");
       invalidExtensionSet.add(".ssy");
       invalidExtensionSet.add(".dbd");
       invalidExtensionSet.add(".smm");
       invalidExtensionSet.add(".ce0");
       invalidExtensionSet.add(".zvz");
       invalidExtensionSet.add(".cc");
       invalidExtensionSet.add(".blf");
       invalidExtensionSet.add(".ctbl");
       invalidExtensionSet.add(".iws");
       invalidExtensionSet.add(".vzr");
       invalidExtensionSet.add(".nls");
       invalidExtensionSet.add(".hsq");
       invalidExtensionSet.add(".lkh");
       invalidExtensionSet.add(".aepl");
       invalidExtensionSet.add(".rna");
       invalidExtensionSet.add(".hts");
       invalidExtensionSet.add(".let");
       invalidExtensionSet.add(".aut");
       invalidExtensionSet.add(".delf");
       invalidExtensionSet.add(".buk");
       invalidExtensionSet.add(".fuj");
       invalidExtensionSet.add(".atm");
       invalidExtensionSet.add(".ezt");
       invalidExtensionSet.add(".fjl");
       invalidExtensionSet.add(".bmw");
       invalidExtensionSet.add(".dx");
       invalidExtensionSet.add(".cyw");
       invalidExtensionSet.add(".iva");
       invalidExtensionSet.add(".pid");
       invalidExtensionSet.add(".bps");
       invalidExtensionSet.add(".capxml");
       invalidExtensionSet.add(".bqf");
       invalidExtensionSet.add(".pr");
       invalidExtensionSet.add(".qit");
       invalidExtensionSet.add(".xnt");
       invalidExtensionSet.add(".lpaq5");
       invalidExtensionSet.add(".lok");
       invalidExtensionSet.add(".shs");
       invalidExtensionSet.add(".mcs");
       invalidExtensionSet.add(".dmg");
       invalidExtensionSet.add(".grp");
       invalidExtensionSet.add(".ocx");
       invalidExtensionSet.add(".ovl");
       invalidExtensionSet.add(".vdl");
       invalidExtensionSet.add(".vxd");
       invalidExtensionSet.add(".asp");
       invalidExtensionSet.add(".htx");
       invalidExtensionSet.add(".php");
       invalidExtensionSet.add(".crt");
       invalidExtensionSet.add(".ins");
       invalidExtensionSet.add(".isp");
       invalidExtensionSet.add(".sbs");
       invalidExtensionSet.add(".sct");
       invalidExtensionSet.add(".shb");
       invalidExtensionSet.add(".shd");
       invalidExtensionSet.add(".wst");
   }

   // Valid image mime type set
   static {
       validImageTypeSet.add("image/jpeg"); // .jpg, .jpeg
       validImageTypeSet.add("image/x-citrix-jpeg"); // .jpg, .jpeg
       validImageTypeSet.add("image/png"); //.png
       validImageTypeSet.add("image/x-citrix-png"); //.png
       validImageTypeSet.add("image/x-png"); //.png
   }

}


  • MIME type, 파일 확장자 유효성 검사가 수행된다. Apache Tika 라이브러리를 사용하여 파일의 MIME type을 유효성 검사하고, 이후 파일 확장자를 유효성 검사한다.
  • 만약 MIME type과 파일 확장자 유효성 검사에 실패한다면 에러 메시지가 Controller에 반환되고, 유효성 검사에 성공한다면 “valid” 문자열이 반환된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
<module-domain-core/src/main/java/kr/ac/univ/common/validation/FileValidator.java>

package kr.ac.univ.common.validation;

import java.io.IOException;

import kr.ac.univ.util.FileUtil;
import org.apache.tika.Tika;
import org.springframework.web.multipart.MultipartFile;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class FileValidator {
   /**
    * file type이 유효한지 검사
    *
    * @param files
    * @return
    * @throws IOException
    */
   public static String isFileValid(MultipartFile[] files) throws IOException {
       Tika tika = new Tika();
       String result = "valid";

       for (MultipartFile file : files) {
           String mimeType = tika.detect(file.getBytes());
           String extension = FileUtil.getExtension(file.getOriginalFilename());

           if (FileType.invalidMimeTypeSet.contains(mimeType)) {
               log.info(mimeType + ", " + extension);
               result = "The file " + file.getOriginalFilename() + " [mime type: " + mimeType + "] doesn't support to upload because it supposed to dangerous and malicious.";
               break;
           }

           if (FileType.invalidExtensionSet.contains(extension)) {
               log.info(mimeType + ", " + extension);
               result = "The file " + file.getOriginalFilename() + " [extension: " + extension + "] doesn't support to upload because it supposed to dangerous and malicious.";
               break;
           }
       }

       return result;
   }

   public static String isImageFileValid(MultipartFile[] files) throws IOException {
       Tika tika = new Tika();
       String result = "valid";

       for (MultipartFile file : files) {
           String mimeType = tika.detect(file.getBytes());
           String extension = FileUtil.getExtension(file.getOriginalFilename());

           log.info(mimeType + ", " + extension);

           if (!FileType.validImageTypeSet.contains(mimeType)) {
               result = "The file " + file.getOriginalFilename() + "[mimet ype: " + mimeType + "] doesn't support to upload because it supposed to not image type.";
               break;
           }
       }

       return result;
   }

}


  • NoticeBoard 관련 클라이언트의 요청을 json 타입으로 응답한다.
  • View에서 전달되는 파일에 대해서 MimeType과 파일 확장자 유효성 검사가 수행된다.
  • MimeType과 파일 확장자 유효성 검사에 실패한다면(Service에서 반환한 문자열이 에러 메시지인 경우), FileTypeException 예외가 발생하고 해당 예외는 전역에서 예외처리 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<module-app-api/src/main/java/kr/ac/univ/controller/NoticeBoardRestController.java>

...

    // 첨부 파일 업로드
    @PostMapping("/attachedFile")
    public ResponseEntity<?> uploadAttachedFile(Long idx, String createdBy, MultipartFile[] files) throws Exception {
        String fileValidationResult = FileValidator.isFileValid(files);

        // 파일 mime type 검사
        if (!"valid".equals(fileValidationResult)) {
            throw new FileTypeException(fileValidationResult);
        }

        noticeBoardAttachedFileService.uploadAttachedFile(idx, createdBy, files);

        return new ResponseEntity<>("{}", HttpStatus.CREATED);
    }
...

예외처리

  • 메소드 또는 컨트롤로러에서 예외를 처리하지 못한다면, @RestControllerAdvice 애노테이션이 선언된 클래스가 전역에서 예외처리를 수행된다.
  • 메소드에 선언된 @ExceptionHandler 애노테이션 통하여 지정한 예외를 처리한다.
  • 또한 사용자에게 전달되는 예외 메시지가 일관성을 가지기 위해서, 예외 데이터를 저장하는 ErrorResponse 클래스를 생성하였다. 예외 관련 데이터는 ErrorResponse 클래스에 초기화된 다음 클라이언트에 응답한다.
  • 각 예외에 대한 설명은 주석을 참고하면 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
<module-web-core/src/main/java/kr/ac/univ/handler/GlobalExceptionHandler.java>

package kr.ac.univ.handler;

import kr.ac.univ.error.ErrorCode;
import kr.ac.univ.error.ErrorResponse;
import kr.ac.univ.exception.BusinessException;
import kr.ac.univ.exception.FileTypeException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.validation.BindException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.multipart.MaxUploadSizeExceededException;

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
   /**
    * @Valid binding error가 발생할 때 발생
    */
   @ExceptionHandler(MethodArgumentNotValidException.class)
   protected ResponseEntity<ErrorResponse> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
       log.error("handleMethodArgumentNotValidException", e);
       final ErrorResponse response = ErrorResponse.of(ErrorCode.INVALID_INPUT_VALUE, e.getBindingResult());

       return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
   }

   /**
    * @ModelAttribute bindingResult error가 발생할 때 발생
    */
   @ExceptionHandler(BindException.class)
   protected ResponseEntity<ErrorResponse> handleBindException(BindException e) {
       log.error("handleBindException", e);
       final ErrorResponse response = ErrorResponse.of(ErrorCode.INVALID_TYPE_VALUE, e.getBindingResult());

       return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
   }

   /**
    * enum type binding error가 발생할 때 발생
    */
   @ExceptionHandler(MethodArgumentTypeMismatchException.class)
   protected ResponseEntity<ErrorResponse> handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e) {
       log.error("handleMethodArgumentTypeMismatchException", e);
       final ErrorResponse response = ErrorResponse.of(e);

       return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
   }

   /**
    * 지원하지 않은 HTTP method를 호출 할 때 발생
    */
   @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
   protected ResponseEntity<ErrorResponse> handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) {
       log.error("handleHttpRequestMethodNotSupportedException", e);
       final ErrorResponse response = ErrorResponse.of(ErrorCode.METHOD_NOT_ALLOWED);

       return new ResponseEntity<>(response, HttpStatus.METHOD_NOT_ALLOWED);
   }

   /**
    * Authentication 객체가 필요한 권한을 보유하지 않은 경우 발생
    */
   @ExceptionHandler(AccessDeniedException.class)
   protected ResponseEntity<ErrorResponse> handleAccessDeniedException(AccessDeniedException e) {
       log.error("handleAccessDeniedException", e);
       final ErrorResponse response = ErrorResponse.of(ErrorCode.HANDLE_ACCESS_DENIED);

       return new ResponseEntity<>(response, HttpStatus.valueOf(ErrorCode.HANDLE_ACCESS_DENIED.getStatus()));
   }

   /**
    * multipart에서 설정한 file size보다 큰 파일이 업로드 되는 경우 발생
    */
   @ExceptionHandler(MaxUploadSizeExceededException.class)
   protected ResponseEntity<?> handleMaxUploadSizeExceededException(MaxUploadSizeExceededException e) {
       log.error("handleMaxUploadSizeExceededException", e);
       final ErrorResponse response = ErrorResponse.of(ErrorCode.FILE_SIZE_ERROR);

       return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
   }

   /**
    * file type이 위험하고 악의적인 것으로 판별되는 경우 발생
    */
   @ExceptionHandler(FileTypeException.class)
   protected ResponseEntity<?> handleFileTypeException(Exception e) {
       log.error("handleFileTypeException", e);
       final ErrorResponse response = ErrorResponse.of(ErrorCode.FILE_TYPE_ERROR, e.getMessage());

       return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
   }

   /**
    * 비즈니스 요구사항에 따른 Exception
    * 비즈니스 요구사항에 예외일 경우 BusinessException으로 통일성 있게 처리
    * 추가로 늘어날 수는 있지만 exception 개수를 최소화 해야함
    */
   @ExceptionHandler(BusinessException.class)
   protected ResponseEntity<ErrorResponse> handleBusinessException(final BusinessException e) {
       log.error("handleBusinessException", e);
       final ErrorCode errorCode = e.getErrorCode();
       final ErrorResponse response = ErrorResponse.of(errorCode);

       return new ResponseEntity<>(response, HttpStatus.valueOf(errorCode.getStatus()));
   }

   /**
    * 그 밖에 발생하는 모든 예외 처리, Null Point Exception 등
    * 개발자가 직접 핸들링해서 다른 예외로 던지지 않으면 발생
    */
   @ExceptionHandler(Exception.class)
   protected ResponseEntity<ErrorResponse> handleException(Exception e) {
       log.error("handleException", e);
       final ErrorResponse response = ErrorResponse.of(ErrorCode.INTERNAL_SERVER_ERROR);

       return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR);
   }
}


  • ErrorCode는 ErrorResponse 클래스에 저장되는 예외 메시지 관련 데이터 중 예외 코드에 대한 사용자 정의 enum 클래스다.
  • ErrorCode는 HTTP 상태 코드, 에러 코드, 에러 메시지로 구성되며 추후 예외가 추가 되면 에러 코드를 추가할 예정이다.
  • 예외가 발생할 때 사용자에게 전달하는 예외 관련 데이터는 크게 두 가지 종류로 분류할 수 있다. 해당 데이터는 유효성 검사에 실패한 경우 클라이언트에 에러 메시지로(json 자료형이 아님) 전달된다.
  • 첫 번째는 새로운 예외가 발생할 때 생성자를 통해 예외 메시지를 초기화 하는 방법이다.(new Exception(“Error message”);) 해당 메시지를 json 자료형으로 형변환 하면, 에러 메시지를 errors 배열에서 확인할 수 있다.
  • 두 번째는 ErrorResponse의 ErrorCode enum 자료형을 통해 에러 메시지 데이터에 의해서 초기화 된다. 해당 메시지를 json 자료형으로 형변환 하면, 에러 메시지를 message json에서 확인할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<module-web-core/src/main/java/kr/ac/univ/error/ErrorCode.java>

package kr.ac.univ.error;


import com.fasterxml.jackson.annotation.JsonFormat;

@JsonFormat(shape = JsonFormat.Shape.OBJECT)
public enum ErrorCode {

   // Common
   INVALID_INPUT_VALUE(400, "C001", "The input value is invalid."),
   INVALID_TYPE_VALUE(400, "C002", " Invalid Type Value."),
   METHOD_NOT_ALLOWED(405, "C003", " The Method is not allowed."),
   HANDLE_ACCESS_DENIED(403, "C004", "Access is Denied."),
   INTERNAL_SERVER_ERROR(500, "C005", "Internal Server Error."),

   // File
   FILE_SIZE_ERROR(500, "F001", "The file size must be less than 20 MB."),
   FILE_TYPE_ERROR(500, "F002", "The file type is supposed to dangerous and malicious."),

   // User
   INVALID_USERNAME(500, "U001", "The ID is duplicated or ID can be used for more than 6 characters and less than 16 characters.")

   ;

   private final String code;
   private final String message;
   private int status;

   ErrorCode(final int status, final String code, final String message) {
       this.status = status;
       this.message = message;
       this.code = code;
   }

   public String getMessage() {
       return this.message;
   }

   public String getCode() {
       return code;
   }

   public int getStatus() {
       return status;
   }

}


  • ErrorResponse 클래스는 예외 메시지 관련 데이터를 저장하는 클래스다.
  • 전달되는 파라미터에 따라 다양한 경우의 클래스를 초기화 하기 위한 생성자와 메소드로 구성된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
<module-web-core/src/main/java/kr/ac/univ/error/ErrorResponse.java>

package kr.ac.univ.error;


import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;

import org.springframework.validation.BindingResult;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;

@Getter
@ToString
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class ErrorResponse {
   private String message;
   private String code;
   private List<FieldError> errors;

   private ErrorResponse(final ErrorCode code, final List<FieldError> errors) {
       this.message = code.getMessage();
       this.code = code.getCode();
       this.errors = errors;
   }

   private ErrorResponse(final ErrorCode code) {
       this.message = code.getMessage();
       this.code = code.getCode();
       this.errors = new ArrayList<>();
   }

   private ErrorResponse(final ErrorCode code, final String message) {
       this.message = message;
       this.code = code.getCode();
       this.errors = new ArrayList<>();
   }

   public static ErrorResponse of(final ErrorCode code, final BindingResult bindingResult) {
       return new ErrorResponse(code, FieldError.of(bindingResult));
   }

   public static ErrorResponse of(final ErrorCode code) {
       return new ErrorResponse(code);
   }

   public static ErrorResponse of(final ErrorCode code, String message) {
       return new ErrorResponse(code, message);
   }

   public static ErrorResponse of(final ErrorCode code, final List<FieldError> errors) {
       return new ErrorResponse(code, errors);
   }

   public static ErrorResponse of(MethodArgumentTypeMismatchException e) {
       final String value = e.getValue() == null ? "" : e.getValue().toString();
       final List<ErrorResponse.FieldError> errors = ErrorResponse.FieldError.of(e.getName(), value, e.getErrorCode());
       return new ErrorResponse(ErrorCode.INVALID_TYPE_VALUE, errors);
   }

   @Getter
   @NoArgsConstructor(access = AccessLevel.PROTECTED)
   public static class FieldError {
       private String field;
       private String value;
       private String reason;

       private FieldError(final String field, final String value, final String reason) {
           this.field = field;
           this.value = value;
           this.reason = reason;
       }

       public static List<FieldError> of(final String field, final String value, final String reason) {
           List<FieldError> fieldErrors = new ArrayList<>();
           fieldErrors.add(new FieldError(field, value, reason));
           return fieldErrors;
       }

       private static List<FieldError> of(final BindingResult bindingResult) {
           final List<org.springframework.validation.FieldError> fieldErrors = bindingResult.getFieldErrors();
           return fieldErrors.stream()
                   .map(error -> new FieldError(
                           error.getField(),
                           error.getRejectedValue() == null ? "" : error.getRejectedValue().toString(),
                           error.getDefaultMessage()))
                   .collect(Collectors.toList());
       }
   }
}


  • BussinessException 클래스는 사용자 정의 예외다.
  • 비즈니스 로직에서 예외가 발생하는 경우 예외 메시지 관련 데이터를 초기화하기 위한 생성자가 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<module-web-core/src/main/java/kr/ac/univ/exception/BusinessException.java>

package kr.ac.univ.exception;

import kr.ac.univ.error.ErrorCode;

public class BusinessException extends RuntimeException {
   private ErrorCode errorCode;

   public BusinessException(String message) {
       super(message);
   }

   public BusinessException(String message, ErrorCode errorCode) {
       super(message);
       this.errorCode = errorCode;
   }

   public BusinessException(ErrorCode errorCode) {
       super(errorCode.getMessage());
       this.errorCode = errorCode;
   }

   public ErrorCode getErrorCode() {
       return errorCode;
   }

}


  • FileTypeException 클래스는 사용자 정의 예외다.
  • 파일 유효성 검사에서 예외가 발생하는 경우 예외 메시지 관련 데이터를 초기화하기 위한 생성자가 있다.
1
2
3
4
5
6
7
8
9
10
<module-web-core/src/main/java/kr/ac/univ/exception/FileTypeException.java>

package kr.ac.univ.exception;

public class FileTypeException extends BusinessException {
   public FileTypeException(String message) {
      super(message);
   }

}


  • InvalidUsernameException 클래스는 사용자 정의 예외다.
  • 사용자 로그인시 ID가 유효하지 않아 예외가 발생하는 경우 예외 메시지 관련 데이터를 초기화하기 위한 생성자가 있다.
1
2
3
4
5
6
7
8
9
10
11
12
<module-web-core/src/main/java/kr/ac/univ/exception/InvalidUsernameException.java>

package kr.ac.univ.exception;

import kr.ac.univ.error.ErrorCode;

public class InvalidUsernameException extends BusinessException {
   public InvalidUsernameException() {
      super(ErrorCode.INVALID_USERNAME);
   }

}


  • 서버에서 예외 처리하여 사용자에게 전달된 에러 메시지를 View에서 parsing 하여 출력하는 함수다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<module-app-web/src/main/resources/static/js/validation.js>

/*
* validation response message alert
*/
function paraseErrorMsg(msg) {
   var parseMsg = JSON.parse(msg.responseText);
   var alertMsg = null;

   if (isEmpty(parseMsg.errors)) {
       alertMsg = parseMsg.message;
   } else {
       alertMsg = parseMsg.message + "\n" + parseMsg.errors[0].reason;
   }

   alert(alertMsg);
}


  • Javsacript: 객체가 비어있는지 확인하는 함수다.
1
2
3
4
5
6
7
8
9
10
11
<module-app-web/src/main/resources/static/js/util.js>

/* 객체 empty 여부 반환 */
function isEmpty(obj) {
   for(var prop in obj) {
       if(obj.hasOwnProperty(prop))
           return false;
   }

   return true;
}


  • ajax를 통하여 요청하는 경우 에러가 발생하여 에러 메시지를 받는다면, parseErrorMsg 함수를 통하여 받은 메시지를 사용자에게 alert 경고창으로 알려준다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<module-app-web/src/main/resources/templates/noticeBoard/form.html>

$.ajax({
   url: "http://localhost:8081/api/notice-boards/attachedFile",
   type: "post",
   data: formData,
   dataType: "text",
   enctype: 'multipart/form-data',
   processData: false,
   contentType: false,
   async: false,
})
   .done(function (msg) {
       location.href = "/notice-board?idx=" + noticeBoardIdx;
   })
   .fail(function (msg) {
       paraseErrorMsg(msg);
       console.log("Update attached file is fail.");
       deleteNoticeBoard(noticeBoardIdx);
   });

프로젝트 실행 및 결과

  • 벡엔드에서 title(@NotBlank)의 유효성 검사에 실패할 때 다음 이미지와 같이 응답 메시지를 확인할 수 있다.

image


  • 벡엔드에서 title(@NotBlank)의 유효성 검사에 실패할 때 다음 이미지와 같이 응답 메시지를 확인할 수 있다.

image


  • 20 MB 보다 큰 파일을 업로드 할 때 유효성 검사에 실패할 때 발생하는 에러 메시지다.

image


  • 파일 MimeType 유효성 검사와 파일 확장가 검사에 실패하여 발생하는에러 메시지다.

image

This post is licensed under CC BY 4.0 by the author.

Project Lab 14. 유효성 검사(javascript) - 1

Project Lab 16. UI 변경(Front-end) 및 코드 리펙토링 - 1

Comments powered by Disqus.

Trending Tags