23.06.15
DEVOTEE를 활성화 시키면
지금 작성한 커뮤니티 글에 대해 1개의 댓글을 달아줍니다.
버튼을 누르면 글 수정 시 ChatGPT가 작성한 댓글이 수정됩니다.
| 컨텐츠 유형 | 제목 | 저장일 | 삭제 |
|---|
본인인증 로그인에 실패하였습니다.
회원이 아니시거나 본인인증 등록이
완료되지 않은 사용자입니다.
Spring Web MVC 에서 살펴볼 기능은 다음과 같다. (1편 , 2편)
pom.xml 에 필요한 라이브러리 설정
Annotation 으로 Spring Bean 선언
HTTP Get 에 대응하는 Controller 사용
HTTP Post 에 대응하는 Controller 사용
Json 으로 응답하기
Server Side Validation 과 Error 처리
File Upload 와 download
Filter 와 Interceptor
CORS 등록
환경 변수 설정 방법 (application.properties)
Logger 사용
파일 업로드는 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 에 설정하지 않아도 정상적으로 파일을 다운로드 할 수 있다.
Servlet 에서는 사용자의 요청을 제일 먼저 처리할 수 있는 ServletFilter 를 제공한다.
Filter 를 사용하는 비즈니스 로직 중에 가장 일반적인 것은 사용자가 인증을 받았는지 체크하는 로직이다.
이와 비슷하게 SpringBoot 에서는 InterceptorHandler 를 지원한다.
Filter 와 Interceptor 는 모두 SpringBoot 이 인식할 수 있는 등록 과정이 필요하다.
이러한 등록 과정은 Spring Bean 으로 등록하는 방법과 Design Pattern 중에 Template Method Pattern 이 사용된 WebMvcConfigurer 의 특정 메소드를 구현하는 방법이 있다.
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 을 만들어서 사용했다.
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("/**");
}
}서로 다른 호스트 주소가 다를 때 요청을 허용하는 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 결과에 대한 캐싱하는 시간을 초 단위로 세팅한다.
환경 파일과 관련하여 제공하는 기본 파라미터인 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.propertiesapplication.properties 는 다음과 같다.
server.port=18080단순히 서버 포트를 18080 으로 변경하기만 했다.
application-dev.properties 는 다음과 같다.
## logging
logging.file.path=logsserver.port 값은 지정하지 않았지만 dev 프로퍼티에 없으면 default 프로퍼티를 찾기 때문에 서버 포트는 18080 을 사용한다.
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
DEVOTEE를 활성화 시키면
지금 작성한 댓글에 AI가 댓글을 달아줍니다.