데보션앱 소개페이지 바로가기
로그인 선택

신고하기

CLOSE
신고사유 (대표 사유 1개)
상세내용 (선택)
0/200
  • 신고한 게시글은 더 이상 보이지 않습니다.
  • 이용약관과 운영정책에 따라 신고사유에 해당하는지 검토 후 조치됩니다.
  • 허위 신고인 경우, 신고자의 서비스 이용이 제한될 수 있으니 유의하시어 신중하게 신고해 주세요.
(이 회원이 작성한 모든 댓글과 커뮤니티 게시물이 보이지 않고, 알림도 오지 않습니다.)

미리보기

커뮤니티

      1,234

      badge 23.06.15

      글 등록

      카테고리를 선택해주세요.

      DEVOTEE를 활성화 시키면
      지금 작성한 커뮤니티 글에 대해 1개의 댓글을 달아줍니다.

      버튼을 누르면 글 수정 시 ChatGPT가 작성한 댓글이 수정됩니다.

      임시저장함에 저장되었습니다. 저장일시 : 2022.5.17 14:29:08

      임시저장함

      제목을 선택하시면 이어서 작성이 가능하며,
      최대 20건까지 저장합니다.
      컨텐츠 유형, 제목, 저장일시, 삭제로 이뤄진 임시저장 목록
      컨텐츠 유형 제목 저장일 삭제

      데보션 블로그 게재 요청

      CLOSE
      • *
      • *

      본인인증

      효율적인 데보션 서비스 이용 및
      고객님의 소중한 개인정보보호를 위해
      본인인증을 진행해주세요. 본인인증 미 진행 시 로그인이 제한됩니다.
      본인인증 실패

      본인인증 로그인에 실패하였습니다.
      회원이 아니시거나 본인인증 등록이
      완료되지 않은 사용자입니다.

      회원정보 연결

      SpringBoot 으로 MSA 구현하기(4) - Spring Web MVC 기능(3)

      seungkyua 23.10.20
      3,922 18 0
      DEVOTEE 요약
      Spring Web MVC에서 파일 업로드와 다운로드, 필터와 인터셉터 설정, CORS 등록, 환경 변수 설정 방법 등을 설명한다. 파일 업로드는 MultipartFile를 이용하며, 파일 다운로드는 Resource 를 사용한다. 서버 필터로는 ServletFilter 및 InterceptorHandler를, CORS 등록과 환경 변수 설정은 WebMvcConfigurer를 이용하며, 로그 사용은 Lombok의 @Slf4j 어노테이션을 활용한다.
      DEVOTEE 추천 블로그

      Spring Web MVC 에서 살펴볼 기능은 다음과 같다. (1편 , 2편)

      1. pom.xml 에 필요한 라이브러리 설정

      2. Annotation 으로 Spring Bean 선언

      3. HTTP Get 에 대응하는 Controller 사용

      4. HTTP Post 에 대응하는 Controller 사용

      5. Json 으로 응답하기

      6. Server Side Validation 과 Error 처리

      7. File Upload 와 download

      8. Filter 와 Interceptor

      9. CORS 등록

      10. 환경 변수 설정 방법 (application.properties)

      11. Logger 사용


      7. File Upload 와 download

      파일 업로드는 MultipartFile 로 쉽게 핸들링이 가능하다.

      @RestController
      public class AppController { 
      ...
         @PostMapping("/uploadFile")
          public ResponseEntity<FileUploadResponse> uploadFile(
                  @RequestParam("file")MultipartFile multipartFile) {
      
              String fileName = StringUtils.cleanPath(Objects.requireNonNull(multipartFile.getOriginalFilename()));
              long size = multipartFile.getSize();
      
              String fileGenerateName  = saveFile(fileName, multipartFile);
      
              FileUploadResponse response = new FileUploadResponse(
                      fileName,
                      "/downloadRile/" + fileGenerateName,
                      size);
              return new ResponseEntity<>(response, HttpStatus.OK);
          }
      
          private String saveFile(String fileName, MultipartFile multipartFile) {
              String directory = "upload_files";
              String fileGenerateName = "abcdefg";
      
              Path uploadPath = Paths.get(directory);
              if (!Files.exists(uploadPath)) {
                  try {
                      Files.createDirectories(uploadPath);
                  } catch (IOException ioe) {
                      throw new FileUploadException("Create directory error");
                  }
              }
      
              try (InputStream inputStream = multipartFile.getInputStream()) {
                  Path filePath = uploadPath.resolve(fileGenerateName + "-" + fileName);
                  Files.copy(inputStream, filePath, StandardCopyOption.REPLACE_EXISTING);
              } catch (IOException ioe) {
                  throw new FileUploadException("Could not save file: " + fileName);
              }
      
              return fileGenerateName;
          }
      }
      @Getter
      public class FileUploadResponse {
          final private String fileName;
          final private String downloadUri;
          final private long size;
      
          public FileUploadResponse(String fileName, String downloadUri, long size) {
              this.fileName = fileName;
              this.downloadUri = downloadUri;
              this.size = size;
          }
      }

      MultipartFile 객체에는 파일명, 파일사이즈, 파일 내용 등의 정보를 갖고 있어서 파일을 다루는데에는 매우 편리하다.

      java.nio 패키지의 Files 를 쓰면 inputstream 을 File 로 쉽게 쓸 수 도 있다.


      파일을 디렉토리에 저장하기 위해서는 고유의 파일명이 필요하다. 여기서는 abcdefg 로 되어 있지만 매번 random 하게 생성할 필요가 있다.

      파일 다운로드는 spring core 패키지의 Resource 를 사용하여 쉽게 구현할 수 있다.

          @GetMapping("/downloadFile/{fileGenerateName}")
          public ResponseEntity<?> downloadFile(@PathVariable("fileGenerateName") String fileGenerateName) {
              Resource resource = getFileAsResource(fileGenerateName);
      
              MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
              headers.add(HttpHeaders.CONTENT_TYPE, "application/octet-stream");
              headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + resource.getFilename() + "\"");
      
              return new ResponseEntity<>(resource, headers, HttpStatus.OK);
      
          }
      
          private Resource getFileAsResource(String fileGenerateName) {
              String directory = "upload_files";
      
              String found = Stream.of(Objects.requireNonNull(new File(directory).list()))
                      .filter(file -> file.startsWith(fileGenerateName))
                      .findFirst()
                      .orElse(null);
      
              if (Objects.isNull(found)) {
                  throw new BadRequestException("File not found. file = [" + fileGenerateName + "]");
              }
      
              Path foundFile = Paths.get(directory).resolve(Paths.get(Objects.requireNonNull(found)));
      
              try {
                  return new UrlResource(foundFile.toUri());
              } catch (MalformedURLException e) {
                  throw new FileDownloadException("Internal Server Error.");
              }
          }

      다만, 헤더에 필요한 값들만 추가로 전달해 주면 된다.


      다운로드는 HttpServletResponse 로도 쉽게 구현할 수 있다.

          @GetMapping(
                  value = "/downloadFile2/{fileId}",
                  produces = {MediaType.IMAGE_JPEG_VALUE, MediaType.IMAGE_JPEG_VALUE})
          public void downloadFile(
                  @PathVariable("fileId") String fileId,
                  HttpServletResponse response) {
      
              String directory = "upload_files";
              String fileName = "abcdefg-coursera.png";
              fileId = fileName;
              Path filePath = Paths.get(directory).resolve(fileName);
      
              try (InputStream is = Files.newInputStream(filePath);
                   OutputStream os = response.getOutputStream();) {
      
                  response.setStatus(HttpStatus.OK.value());
                  response.setContentType(MediaType.IMAGE_PNG_VALUE);
                  response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + fileName + "\"");
      
                  StreamUtils.copy(is, os);
              } catch (Throwable th) {
                  th.printStackTrace();
                  throw new FileDownloadException("file download error.");
              }
          }
      public class FileDownloadException extends RuntimeException {
      
          public FileDownloadException() {
              super();
          }
      
          public FileDownloadException(String message) {
              super(message);
          }
      
          public FileDownloadException(String message, Throwable cause) {
              super(message, cause);
          }
      
          public FileDownloadException(Throwable cause) {
              super(cause);
          }
      
          protected FileDownloadException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
              super(message, cause, enableSuppression, writableStackTrace);
          }
      }
      @RestControllerAdvice
      public class ApiExceptionHandler {
      ...
          @ExceptionHandler(FileDownloadException.class)
          public ResponseEntity handleFileDownloadException(FileDownloadException e) {
              return new ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR);
          }
      }

      GetMapping 의 속성 중에 produces 가 있다.

      다운로드 하는 파일이 어떤 타입 인지를 알려주는 것인데 여러타입을 넣을 수 있다.

      이를 세팅하면 setContentType 에 설정하지 않아도 정상적으로 파일을 다운로드 할 수 있다.


      8. Filter 와 Interceptor

      Servlet 에서는 사용자의 요청을 제일 먼저 처리할 수 있는 ServletFilter 를 제공한다.

      Filter 를 사용하는 비즈니스 로직 중에 가장 일반적인 것은 사용자가 인증을 받았는지 체크하는 로직이다.


      이와 비슷하게 SpringBoot 에서는 InterceptorHandler 를 지원한다.


      Filter 와 Interceptor 는 모두 SpringBoot 이 인식할 수 있는 등록 과정이 필요하다.

      이러한 등록 과정은 Spring Bean 으로 등록하는 방법과 Design Pattern 중에 Template Method Pattern 이 사용된 WebMvcConfigurer 의 특정 메소드를 구현하는 방법이 있다.


      1. Filter 등록

      Filter 는 Bean 으로 등록하면 바로 사용 가능하다.

      @Component
      @Order(2)
      public class LoggingFilter implements Filter {
      
          @Override
          public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
              System.out.println("============= Before LoggingFilter");
              chain.doFilter(request, response);
              System.out.println("============= After LoggingFilter");
          }
      
          @Override
          public void init(FilterConfig filterConfig) throws ServletException {
              Filter.super.init(filterConfig);
          }
      }

      Component 로 Bean 으로 등록하고 Order(2) 로 Bean 의 순서를 지정할 수 있다.

      chain.doFilter 메소드의 앞 뒤로 필요한 전처리, 후처리 작업을 할 수 있다.

      혹은 아래와 같이 메소드에 @Bean 어노테이션을 사용해서 Bean 을 만들 수 도 있다. 물론 이 때는 바로 Bean 을 사용하는 것이 아니라 FilterRegistrationBean 을 사용해서 등록한다.

      @Configuration
      public class WebServerConfig implements WebMvcConfigurer {
      ...
          @Bean
          public FilterRegistrationBean<CharacterEncodingFilter> encodingFilter() {
              CharacterEncodingFilter encodingFilter = new CharacterEncodingFilter();
              encodingFilter.setEncoding("UTF-8");
              encodingFilter.setForceEncoding(true);
      
              FilterRegistrationBean<CharacterEncodingFilter> registrationBean = new FilterRegistrationBean<>();
              registrationBean.setFilter(encodingFilter);
              registrationBean.addUrlPatterns("/*");
              registrationBean.setOrder(Integer.MIN_VALUE);
              return registrationBean;
          }
      }

      WebMvcConfigurer 인터페이스를 구현하여 Bean 을 만들어서 사용했다.


      2. Interceptor 등록

      Interceptor 는 HandlerInterceptor 인터페이스를 구현한다.

      단, interceptor WebMvcConfigurer 의 addInterceptors 메소드를 통해서 할 수 있다.

      public class LoggingInterceptor implements HandlerInterceptor {
      
          @Override
          public boolean preHandle(
                  HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
      
              System.out.println("================= preHandle LoggingInterceptor");
              return true;
          }
          @Override
          public void postHandle(
                  HttpServletRequest request, HttpServletResponse response, Object handler,
                  ModelAndView modelAndView) throws Exception {
      
              System.out.println("================= postHandle LoggingInterceptor");
          }
      
          @Override
          public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
                                      Object handler, Exception exception) throws Exception {}
      }
      @Configuration
      public class WebServerConfig implements WebMvcConfigurer {
      ...
          @Override
          public void addInterceptors(InterceptorRegistry registry) {
              // Custom Interceptor 등록
              registry.addInterceptor(new LoggingInterceptor())
                      .addPathPatterns("/**");
          }
      }


      9. CORS 등록

      서로 다른 호스트 주소가 다를 때 요청을 허용하는 CORS 는 WebMvcConfigurer 를 사용하면 쉽게 등록할 수 있다.

      @Configuration
      public class WebServerConfig implements WebMvcConfigurer {
      ...
          @Override
          public void addCorsMappings(CorsRegistry registry) {
              registry.addMapping("/**")
                      .allowedOrigins("ahnseungkyu.com")
                      .allowedMethods("OPTIONS", "GET", "POST", "PUT", "PATCH")
                      .allowedHeaders("*")
                      .maxAge(60 * 60 * 24);
          }
      }

      모든 url 매핑을 허용하고 [ahnseungkyu.com](http://ahnseungkyu.com) 도메인만 예외로 적용하면 이 때 사용 가능한 HTTP method 지정이 가능하며,

      클라이언트가 pre-flight request 결과에 대한 캐싱하는 시간을 초 단위로 세팅한다.


      10. 환경 변수 설정 방법 (application.properties)

      환경 파일과 관련하여 제공하는 기본 파라미터인 spring.profiles.active 가 있다. 이 값은 기본 값이 default 이며,

      자바 어플리케이션을 실행할 때 환경 변수 파라미터로 지정하는 -D 옵션과 같이 사용한다.

      $ java -jar java-app.jar -Dspring.profiles.active=dev

      프로파일 변수 값에 따라 해당 프라퍼티가 로딩된다.

      예를 들어 위와 같이 dev 값을 가지면 [application-dev.properties](http://application-dev.properties) 파일이 로딩된다.

      classapth 가 되는 기본 디렉토리는 ./src/main/resources 이므로 이곳에 properties 파일을 위치 시킨다.

      $ ls -al src/main/resources 
      total 24
      drwxr-xr-x  7 ahnsk  staff   224  7 27 17:27 .
      drwxr-xr-x  4 ahnsk  staff   128  7 27 17:27 ..
      -rw-r--r--  1 ahnsk  staff  1341  7 27 17:27 application-dev.properties
      -rw-r--r--  1 ahnsk  staff  1341  7 27 17:27 application.properties

      application.properties 는 다음과 같다.

      server.port=18080

      단순히 서버 포트를 18080 으로 변경하기만 했다.


      application-dev.properties 는 다음과 같다.

      ## logging
      logging.file.path=logs

      server.port 값은 지정하지 않았지만 dev 프로퍼티에 없으면 default 프로퍼티를 찾기 때문에 서버 포트는 18080 을 사용한다.


      11. Logger 사용

      Lombok 라이브러리에서 제공하는 @Slf4j 어노테이션을 사용하면 쉽게 Logger 객체를 만들 수 있다.

      SpringBoot 에서는 Slf4j 를 구현하면 Logback 로그 프레임워크를 사용할 수 있다.

      또한 각 로거 인스턴스마다 프로파일을 설정할 수 있어서 사용하기에 편리하다.


      클래스패스 최상위 경로에 logback-spring.xml 파일을 다음과 같이 선언한다.

      <?xml version="1.0" encoding="UTF-8" ?>
      <configuration>
      
          <logger name="org.springframework.beans.factory" level="warn"/>
      
          <springProfile name="local">
              <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
                  <encoder>
                      <pattern>%d{yyyy/MM/dd HH:mm:ss.SSS} %magenta([%thread]) %highlight(%-5level) %logger{36} - %msg%n</pattern>
                  </encoder>
              </appender>
      
              <root level="info">
                  <appender-ref ref="STDOUT"/>
              </root>
          </springProfile>
      
          <springProfile name="dev">
              <appender name="systemLogAppender" class="ch.qos.logback.core.rolling.RollingFileAppender">
                  <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
                      <fileNamePattern>${LOG_PATH}/system-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
                      <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                          <maxFileSize>100MB</maxFileSize>
                      </timeBasedFileNamingAndTriggeringPolicy>
                      <maxHistory>30</maxHistory>
                  </rollingPolicy>
                  <encoder>
                      <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
                  </encoder>
              </appender>
      
              <root level="info">
                  <appender-ref ref="systemLogAppender"/>
              </root>
          </springProfile>
      
          <springProfile name="production">
              <appender name="systemLogAppender" class="ch.qos.logback.core.rolling.RollingFileAppender">
                  <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
                      <fileNamePattern>${LOG_PATH}/system-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
                      <timeBasedFileNamingAndTriggeringPolicy
                              class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                          <maxFileSize>100MB</maxFileSize>
                      </timeBasedFileNamingAndTriggeringPolicy>
                      <maxHistory>30</maxHistory>
                  </rollingPolicy>
                  <encoder>
                      <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
                  </encoder>
              </appender>
      
              <root level="warn">
                  <appender-ref ref="systemLogAppender"/>
              </root>
          </springProfile>
      
      </configuration> 

      프로파일 값을 local 로 띄운 경우에는 root 로그 레벨이 info 이고 STDOUT 표준 출력으로 출력된다.


      프로파일 값을 dev 로 띄운 경우에는 root 로그 레벨이 warn 이고 파일로 출력된다.

      파일 패턴에 ${LOG_PATH} 값은 application-dev.properties 파일의 logging.file.path=logs 값으로 치환되기 때문에 서버를 띄운 디렉토리 아래 logs 디렉토리가 생기고 거기에 로그 파일들이 생성된다.

      @Slf4j
      @RestController
      public class AppController {
      ...
          @GetMapping(path = "/apps/{appId}/errors")
          public ResponseEntity<CommonResponse> getErrors(
              @PathVariable(value = "appId") Long appId) {
      
              log.info("Unknown bad request error.\nappId = {}", appId);
              throw new BadRequestException("Unknown bad request error.\nappId = " + appId);
          }
      }
      
      --------- output ------------
      Error Message: Unknown bad request error.
      appId = 1131

      댓글 0

      DEVOTEE를 활성화 시키면
      지금 작성한 댓글에 AI가 댓글을 달아줍니다.

      seungkyua 님의 최신 블로그

      더보기

      DEVOTEE 추천 블로그

      동영상 기고하기