이번장도 프로젝트를 나눠서 해보면 좋겠다.
- 예제 프로젝트
- Spring 6 + Gradle 빌드
- pro28
- Servlet 3 부터 서블릿 컨테이너 자체에 내장된 기능으로 사용.
- Spring 5 + Maven 빌드
- pro28-maven
- commons-fileupload 사용.
- ...
- ...
- 28장도 무사히 끝남... 🎉🎊🎄
- ...
클라이언트의 파일 시스템에 있는 원본 파일명을 반환합니다. 사용하는 브라우저에 따라 경로 정보가 포함될 수 있지만 일반적으로 Opera 이외의 브라우저에서는 포함되지 않습니다. 참고: 이 파일 이름은 클라이언트가 제공한 것이므로 무턱대고 사용해서는 안 된다는 점에 유의하세요. 디렉토리 부분을 사용하지 않는 것 외에도 파일 이름에 '..' 등의 문자가 포함될 수 있으며 악의적으로 사용될 수 있습니다. 이 파일명을 직접 사용하지 않는 것이 좋습니다. 가급적 고유한 파일명을 생성하고 필요한 경우 이 파일명을 참조할 수 있도록 어딘가에 저장하세요.
원본 파일 이름, 또는 다중 파트 형식에서 파일이 선택되지 않은 경우 빈 문자열, 정의되지 않았거나 사용할 수 없는 경우 null을 반환합니다.
이 요청에 포함된 다중 파트 파일의 매개변수 이름이 포함된 문자열 객체 이터레이터를 반환합니다. 이는 일반 매개변수와 마찬가지로 양식의 필드 이름이며, 원본 파일 이름이 아닙니다.
파일 이름
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>${commons-fileupload.version}</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>${commons-io.version}</version>
</dependency>
<?xml version="1.0" encoding="UTF-8"?>
<Context allowCasualMultipartParsing="true" path="/" >
<WatchedResource>WEB-INF/web.xml</WatchedResource>
<!-- <Resources cachingAllowed="true" cacheMaxSize="100000" /> --> <!-- 기본값이여서 명시할 필요없음-->
</Context>
Tomcat 에다 직접 설정해도 되는데, 프로젝트 별로 설정하기 위해서, 위의 위치에 설정했다.
- allowCasualMultipartParsing: 이 설정은 multipart/form-data 형식의 요청을 처리하는 데 사용됩니다. 이 설정을 true로 설정하면 Tomcat은 multipart/form-data 형식의 요청을 처리할 때 Servlet 3.0 API를 사용합니다. 이 설정을 false로 설정하면 Tomcat은 multipart/form-data 형식의 요청을 처리할 때 Servlet 2.5 API를 사용합니다. Servlet 3.0 API는 Servlet 2.5 API보다 더 강력하고 기능이 풍부합니다. 따라서 이 설정을 true로 설정하는 것이 좋습니다.
- cachingAllowed, cacheMaxSize: 이 설정은 리소스를 캐시하는 데 사용됩니다. 이 설정을 true로 설정하면 Tomcat은 리소스를 캐시합니다. 이 설정을 false로 설정하면 Tomcat은 리소스를 캐시하지 않습니다. cacheMaxSize는 캐시할 수 있는 리소스의 최대 크기를 지정합니다. 리소스를 캐시하면 리소스를 요청할 때마다 서버에서 리소스를 가져올 필요가 없기 때문에 성능을 향상시킬 수 있습니다. 따라서 이 설정을 true로 설정하는 것이 좋습니다. 그러나 cacheMaxSize를 너무 높게 설정하면 캐시가 너무 커져 성능이 저하될 수 있습니다. 따라서 cacheMaxSize를 적절한 값으로 설정하는 것이 중요합니다.
<filter>
<display-name>springMultipartFilter</display-name>
<filter-name>springMultipartFilter</filter-name>
<filter-class>org.springframework.web.multipart.support.MultipartFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>springMultipartFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- DelegatingFilterProxy ... -->
<!-- ... -->
<!-- DispatcherServlet 로딩 이전에 이미 있어야지만 MultipartFilter에서 로딩이 가능하므로 루트 컨텍스트에 선언 -->
<bean name="filterMultipartResolver"
class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<property name="maxUploadSize" value="52428800"/> <!-- 50MB -->
<property name="maxInMemorySize" value="10485760"/> <!-- 10MB -->
<property name="maxUploadSizePerFile" value="10485760"/> <!-- 10MB -->
<property name="defaultEncoding" value="utf-8"/>
</bean>
-
MultipartFilter가 기본으로 인식하는 이름이
filterMultipartResolver
인데, 맞춰주자. -
이 빈설정은... DispatcherServlet 로딩 이전에 되야하므로 Root 컨텍스트에 넣어줘야 제대로 동작한다.
-
이 Bean을 못읽을 경우..
StandardServletMultipartResolver
로 처리되기 때문에 commons-upload 도 사용하면서 Servlet의 Multipart 기능도 사용하게되서 설정이 꼬이게 된다.maxUploadSizePerFile 설정이 안먹어서, web.xml에 max-file-size 설정을 해줘야하는 설정이 중복된상태가 됨.. 😅
-
- 서블릿 컨테이너가 자체적으로 지원하는 기능을 사용해서, 추가할 내용 없음.
<servlet>
<servlet-name>action</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup> <!-- 값이 1 이상이면 Tomcat 실행시 미리 메모리에 로드 -->
<multipart-config>
<!-- 임시 파일을 저장할 공간 : 기본은 시스템 임시 폴더이다. -->
<!--<location>${java.io.tmpdir}</location>-->
<!-- 업로드되는 파일의 최대 크기 -->
<max-file-size>10485760</max-file-size> <!-- 10MB -->
<!-- 한번에 올릴 수 있는 최대 크기 -->
<max-request-size>52428800</max-request-size> <!-- 50MB -->
<!-- 파일이 메모리에 기록되는 임계값 -->
<file-size-threshold>10485760</file-size-threshold> <!-- 10MB -->
</multipart-config>
</servlet>
- location 같은 경우는 시스템 기본 값을 쓰게하는 게 낫겠다.
$("#d_file").append(
`<div class="mb-3">
<label for="formFile${count}" class="form-label">${count}번 파일</label>
<input class="form-control" type="file" id="formFile${count}" name="file${count}">
</div>`
);
✨ 예상외로 별로 바꿀 것이 없었다. 컨트롤러 코드자체가 Spring Web이하의 클래스만 쓰다보니.. 그대로 활용가능 했다.
Google 계정을 사용하므로, 계정 ID / PW을 Vault에다 저장해서 쓰자..
메일을 2개 가입해야함...
https://mvnrepository.com/artifact/jakarta.mail/jakarta.mail-api 이거 구현체 라이브러리가 따로 있니?
네,
jakarta.mail-api
의 구현 라이브러리가 있습니다. Jakarta Mail API의 구현체로 알려진 JavaMail/JakartaMail은 이제 Eclipse Angus라는 독립적인 프로젝트로 제공됩니다. Eclipse Angus 프로젝트 페이지에서 더 많은 정보를 찾을 수 있습니다.
https://mvnrepository.com/artifact/jakarta.mail/jakarta.mail-api/2.1.2
디펜던시 설정
implementation "jakarta.mail:jakarta.mail-api:${jakartaMailApiVersion}" implementation "org.eclipse.angus:jakarta.mail:${jakartaMailImplVersion}"
디펜던시 설정
<dependency> <groupId>javax.mail</groupId> <artifactId>javax.mail-api</artifactId> <version>${javax.mail.version}</version> </dependency> <dependency> <groupId>com.sun.mail</groupId> <artifactId>javax.mail</artifactId> <version>${javax.mail.version}</version> </dependency>
-
AsyncConfig
@Configuration @EnableAsync public class AsyncConfig implements AsyncConfigurer { @Override public Executor getAsyncExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setThreadNamePrefix("async-thread-"); executor.setCorePoolSize(5); executor.setMaxPoolSize(10); executor.setQueueCapacity(30); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); executor.initialize(); return executor; } }
제공하신 코드는
Executor
인스턴스를 반환하는 메소드입니다.Executor
는 비동기적으로 작업을 실행하는 데 사용할 수 있는 객체입니다. 코드는ThreadPoolTaskExecutor
인스턴스를 생성합니다.ThreadPoolTaskExecutor
는 작업을 실행하기 위해 스레드 풀을 사용하는Executor
의 한 유형입니다.ThreadPoolTaskExecutor
인스턴스는 다음 매개변수로 구성됩니다.threadNamePrefix
: 스레드 풀의 스레드 이름에 사용되는 접두사입니다.corePoolSize
: 스레드 풀에 항상 유지되는 스레드 수입니다.maxPoolSize
: 스레드 풀이 생성하는 최대 스레드 수입니다.queueCapacity
: 스레드 풀에 대기할 수 있는 최대 작업 수입니다.rejectedExecutionHandler
: 스레드 풀에서 거부된 작업을 처리하는 데 사용되는 정책입니다.
ThreadPoolTaskExecutor
인스턴스가 생성되면 초기화되고 반환됩니다.다음은 코드에 대한 보다 자세한 설명입니다.
코드 스니펫
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
이 줄은 새로운
ThreadPoolTaskExecutor
인스턴스를 생성합니다.코드 스니펫
executor.setThreadNamePrefix("async-thread-");
이 줄은 스레드 풀의 스레드 이름에 사용되는 접두사를 설정합니다. 접두사는 "async-thread-"입니다.
코드 스니펫
executor.setCorePoolSize(5);
이 줄은 스레드 풀에 항상 유지되는 스레드 수를 설정합니다. 스레드 수는 5입니다.
코드 스니펫
executor.setMaxPoolSize(10);
이 줄은 스레드 풀이 생성하는 최대 스레드 수를 설정합니다. 최대 스레드 수는 10입니다.
코드 스니펫
executor.setQueueCapacity(30);
이 줄은 스레드 풀에 대기할 수 있는 최대 작업 수를 설정합니다. 최대 대기열 용량은 30입니다.
코드 스니펫
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
이 줄은 스레드 풀에서 거부된 작업을 처리하는 데 사용되는 정책을 설정합니다. 사용되는 정책은
ThreadPoolExecutor.CallerRunsPolicy
입니다. 즉, 작업은 작업을 제출한 스레드에서 실행됩니다.코드 스니펫
executor.initialize();
이 줄은
ThreadPoolTaskExecutor
인스턴스를 초기화합니다.코드 스니펫
return executor;
이 줄은
ThreadPoolTaskExecutor
인스턴스를 반환합니다.executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
이게 스레드풀에 제출하려했는데... 풀이 꽉차서 거부된 스레드는 본인이 실행해라라는 의미이니?네, 맞습니다.
ThreadPoolExecutor.CallerRunsPolicy
는 스레드 풀에 작업을 제출했지만 풀이 꽉 차서 거부된 경우 작업을 제출한 스레드에서 작업을 실행하는 정책입니다.Pool도 꽉차있고, Queue도 전부다 꽉차있어서 할 수있는 것이 없을 때이지?
네, 맞습니다. 스레드 풀이 꽉 차서 작업을 실행할 스레드가 없고, 작업 대기열도 꽉 차서 작업을 추가할 수 없을 때
ThreadPoolExecutor.CallerRunsPolicy
는 작업을 제출한 스레드에서 작업을 실행합니다.
Gradle 프로젝트는 Thymeleaf로 템플릿을 만들고,
Maven 프로젝트는 FreeMarker를 사용해보면 좋을 것 같다.
-
https://freemarker.apache.org/docs/pgui_quickstart_all.html 에 나온대로
- 설정 부분은 설정 빈로 만들고, (어플리케이션 수명주기와 같이해야한다함..)
- 실행 부분은 서비스 빈으로 만듬.
-
거의 메뉴얼 보고 했는데... 주의할 점이.. 메뉴얼에서는 System.out에다가 출력을 하고 있어서.. 이걸 close해버리면 테스트가 제대로 안된다.. 당연하게도 실제환경에서도 큰 문제가 있겠지만... 🎃
public String bookEmailTemplate(Book book) { try { /* Get the template (uses cache internally) */ Template template = freemarkerCfg.getTemplate("book_mail.ftl"); try (Writer out = new OutputStreamWriter(System.out)) { template.process(Map.of("book", book), out); return out.toString(); } } catch (Exception e) { throw new IllegalStateException(e); } }
이거를 OutputStream 사용 부분을 StringWriter로 바꿔주면 잘 된다.
public String bookEmailTemplate(Book book) { try { /* Get the template (uses cache internally) */ Template template = freemarkerCfg.getTemplate("book_mail.ftl"); StringWriter out = new StringWriter(); template.process(Map.of("book", book), out); return out.toString(); } catch (Exception e) { throw new IllegalStateException(e); } }
이메일 템플릿 관련해서 구체적으로 가이드가 있어서 좋은데... 번역을 먼저 해야겠다.
가이드에는 메시지 처리등 더 복잡한 내용이 있었지만, FreeMarker 적용했던 수준과 비슷하게 간단하게 적용했다.
인터셉터 적용은 Spring 5, 6이나 별차이가 없겠다.
-
HandlerInterceptorAdapter
5.3 부터 Depreacted 되었다.- Java 8 부터 인터페이스에 default 메서드를 정의할 수 있어서
HandlerInterceptor
인터페이스를 구현해서 쓰라는 것 같다.
- Java 8 부터 인터페이스에 default 메서드를 정의할 수 있어서
-
LocaleInterceptor에서 다른 메서드는 구현할 필요가 없으니... preHandle만 구현하자!
-
파라미터 값이 없을 때, 기본 값 로케일을 설정하는 저자님 방법 보다는... locale 파라미터가 유입 되었을 때만, 로케일을 새로 설정하는 것이 낫겠다.
if (locale != null && !locale.isBlank()) { LOGGER.info("### new locale: {}", locale); session.setAttribute(SessionLocaleResolver.LOCALE_SESSION_ATTRIBUTE_NAME, new Locale(locale)); }
- 단순 페이지면 저자님 방법도 될 것 같긴한데, 나는 Tiles / Thymeleaf 적용도 했고 복잡해져서인지... 저자님 방법으로는 결국은 항상 기본값 로케일을 설정해버리는 문제가 생겼다.
-
JSP
<%-- 현재 URL에 쿼리 스트링만 추가하면 좋긴한데, 간단하게 로케일 변경시에는 메인 페이지에서 바뀌도록 했다. --%> <c:choose> <c:when test="${pageContext.response.locale eq 'en'}"> <a class="btn btn-sm btn-outline-info" href="${contextPath}/main.do?locale=ko">Korean</a> <a class="btn btn-sm btn-outline-info disabled">English</a> </c:when> <c:otherwise> <a class="btn btn-sm btn-outline-info disabled">한글</a> <a class="btn btn-sm btn-outline-info" href="${contextPath}/main.do?locale=en">영어</a> </c:otherwise> </c:choose>
-
Thymeleaf
<th:block th:if="${#locale.toLanguageTag() == 'en'}"> <a class="btn btn-sm btn-outline-info" th:href="@{/main.do?locale=ko}">Korean</a> <a class="btn btn-sm btn-outline-info disabled">English</a> </th:block> <th:block th:if="${#locale.toLanguageTag() == 'ko'}"> <a class="btn btn-sm btn-outline-info disabled">한글</a> <a class="btn btn-sm btn-outline-info" th:href="@{/main.do?locale=en}">영어</a> </th:block>
현시점 Spring 환경에서는 뷰이름을 가져오기위한 인터셉터가 필요할 것 같진않은데...
어노테이션 기반 컨트롤러가 없을 때는 좀 필요했을 것 같다.
- 이부분은 진행하지 않는 것이 나을 것 같다. 현시점에서는 별로 의미가 없는 일 같다. 😅
- 뷰네임을 얻는 로직 코드는 이미 테스트 해봤었고, 아래 코드를 참조할것.
- https://github.com/mklinkj/the-art-of-java-web-programming-study/tree/master/chap21#213-multiactioncontroller-%EC%9D%B4%EC%9A%A9%ED%95%B4-%EC%8A%A4%ED%94%84%EB%A7%81-mvc-%EC%8B%A4%EC%8A%B5%ED%95%98%EA%B8%B0
- https://github.com/mklinkj/the-art-of-java-web-programming-study/blob/master/chap21/pro21-spring4-ex02/src/main/java/org/mklinkj/taojwp/ex02/UserController.java
- 위의 클래스 내용 중 getViewName() 메서드의 내용을 인터셉터로 미리 처리한 것이였다.