2019년 3월 18일 월요일

SpringBoot에 Thymeleaf, JSP 같이 쓰기

JAVA로 구축된 대부분의 레거시 웹 소스 코드에서는 아직도 JSP가 많이 사용되고 있을 것이다.

SpringBoot에서도 JSP를 쓰는 것을 본 적이 있다.

이를 Thymeleaf 등 다른 템플릿 엔진으로 전환하고자 할 때 순차적으로 진행하기 위해 과도기가 필요할 수 있겠는데, 단일 프로젝트에서 Thymeleaf와 JSP를 같이 쓰는 방법을 정리해본다.

JSP

  • Java Server Pages. Java를 이용한 서버 사이드 스크립트 언어.
  • Servlet Container가 <% … %> 스크립트 영역을 Java 코드로 컴파일 하여 실행됨
  • 서버 사이드 코드와 클라이언트 사이드 코드가 섞여있음
  • 디버깅이 어렵고 코드량 증가에 비해 유지보수가 대폭 어려워짐
  • Spring 기본 지원에서 제외

Thymeleaf

  • (현재) 스프링 생태계에서 권장하는 텍스트 템플릿 엔진
    • Natural Template 템플릿 코드가 HTML이기 때문에 WAS기동없이 웹 브라우저에서 확인 가능
    • 프런트 엔드(HTML, Javascript, CSS), 백 엔드(JAVA) 분업에 용이
  • 문법이 엄격한 편

개발자들은…

좋다는 사람도 있고 별로라는 사람도 있는데 솔직히 나도 별로인 거 같다. 그냥 못쓸 정도는 아니라는거… 

SpringBoot에서 JSP를 쓰기 위한 설정

build.gradle에 2개 라이브러리 의존성 추가

1
2
compile 'org.apache.tomcat.embed:tomcat-embed-jasper'
compile 'javax.servlet:jstl:1.2'
cs

application.yml에 View Resolver 설정

1
2
3
4
5
6
spring:
  mvc:
    throw-exception-if-no-handler-found: false
    view:
      prefix: /WEB-INF/jsp/
      suffix: .jsp
cs

Controller 클래스 하나 간단하게 만든다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.walter.project.controller;
 
import com.walter.project.service.LanguageService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import reactor.core.publisher.Mono;
 
@Controller
@RequiredArgsConstructor
public class LanguageController {
 
    final private LanguageService languageService;
 
    @GetMapping(value = "/language")
    public Mono<String> index(Model model) {
        model.addAttribute("list", languageService.getList());
        return Mono.just("language/index");
    }
}
cs

application.yml에 설정한 경로대로 src/main 디렉터리 밑에 webapp/WEB-INF/jsp 경로를 만들고 Controller 내용에 맞춰 JSP파일을 만든다.

index.jsp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
<div>
    Language List
    <ul>
        <c:forEach var="lang" items="${list}">
            <li>
                <a href="/language/${lang.id}">${lang.name}</a>
            </li>
        </c:forEach>
    </ul>
</div>
</body>
</html>
cs

그럼 아래와 같은 구조가 된다.

애플리케이션 구동시킨 뒤 http://localhost:9002/language로 접속

이렇게 해서 일단 화면은 JSP로 뿌려진다.

원하는 것은 여기에 Thymeleaf도 사용할 수 있는 설정을 만들고, Controller로 전달되어 온 요청을 처리한 다음 결과를 표시할 뷰를 처음에는 Thymeleaf 경로에서 찾아서 있으면 Thymeleaf 템플릿으로 보여주고, Thymeleaf 템플릿이 없으면 그 다음 JSP설정으로 넘어가서 JSP로 보여주게 하는 것이다.

Thymeleaf ViewResolver 설정 추가

build.gradle에 SpringBoot용 thymeleaf 라이브러리 추가

1
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
cs

아니면 아래처럼 일반 Spring프로젝트에 넣을 때처럼 넣어도 된다.

1
2
3
compile group: 'org.thymeleaf', name: 'thymeleaf', version: '3.0.11.RELEASE'
compile group: 'org.thymeleaf', name: 'thymeleaf-spring4', version: '3.0.11.RELEASE' // SpringBoot 1.5일 때
compile group: 'org.thymeleaf', name: 'thymeleaf-spring5', version: '3.0.11.RELEASE' // SpringBoot 2일 때
cs

application.yml에 JSP용으로 View Resolver 설정한 것 날려버리고 Java Config로 수동 설정한다.

1
2
3
4
5
6
7
spring:
  mvc:
    throw-exception-if-no-handler-found: false
#    view:
#      prefix: /WEB-INF/jsp/
#      suffix: .jsp
 
cs

Java Config 클래스 파일 하나 작성한다.

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
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
import org.springframework.web.servlet.view.JstlView;
import org.thymeleaf.spring5.SpringTemplateEngine;
import org.thymeleaf.spring5.view.ThymeleafViewResolver;
 
@Configuration
@EnableWebMvc
@RequiredArgsConstructor
public class WebMvcConfig {
 
    final private SpringTemplateEngine springTemplateEngine;
 
    @Bean
    public ThymeleafViewResolver thymeleafViewResolver() {
        ThymeleafViewResolver thymeleafViewResolver = new ThymeleafViewResolver();
        thymeleafViewResolver.setTemplateEngine(springTemplateEngine);
        thymeleafViewResolver.setCharacterEncoding("UTF-8");
        thymeleafViewResolver.setAlwaysProcessRedirectAndForward(true);
        thymeleafViewResolver.setOrder(1);
        return thymeleafViewResolver;
    }
 
    @Bean
    public InternalResourceViewResolver jspViewResolver() {
        InternalResourceViewResolver jspViewResolver = new InternalResourceViewResolver();
        jspViewResolver.setPrefix("/WEB-INF/jsp/");
        jspViewResolver.setSuffix(".jsp");
        jspViewResolver.setViewClass(JstlView.class);
        jspViewResolver.setOrder(2);
        return jspViewResolver;
    }
}
cs

ViewResolver에는 setOrder()메서드로 우선 순위를 정할 수 있다.

thymeleaf가 1순위, JSP를 2순위로 설정하였다. 우선 순위로 설정한 ViewResolver에서 view파일을 못찾으면 다음 순위로 넘어가게 되어있다.

JSP용으로 사용하는 InternalResourceViewResolver는 view를 찾지 못하면 다음 Resolver로 넘어가는 것이 아니라 Exception을 발생시켜 버리므로 가장 마지막 순위로 설정한다.

Thymeleaf 템플릿 파일을 작성한다. Thymeleaf 기본 설정대로 프로젝트의 src/main/resources 경로 아래 templates 디렉터리를 만들고 그 안에 Controller에서 정의한 view이름으로 html파일을 생성한다.

index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8"/>
    <title>Title</title>
</head>
<body>
    Language List (Thymeleaf)
    <ul>
        <th:block th:each="lang : ${list}">
            <li>
                <a th:href="@{/language/{id}(id=${lang.id})}" th:text="${lang.name}">lang.name</a>
            </li>
        </th:block>
    </ul>
</body>
</html>
cs

위와 같이 템플릿 파일까지 작성한 다음 서버 재기동하고 재접속하면 아래와 같이 잘 나온다.

ViewResolver Chaining

여기까지 하면 그럼 ViewResolver의 Chaining설정까지 해놓았으니 Thymeleaf의 템플릿 파일이 없으면 당연히 다음 순위인 JSP파일을 찾아서 보여주겠지? 라는 생각이 든다.

그래서 테스트 삼아 index.html 파일의 이름을 index2.html로 바꿔보고 다시 돌려본다.

위처럼 예상 외로 안돌아간다. ViewResolver에서 Exception을 뱉어내면서 500에러가 떨어진다.

Thymeleaf ViewResolver에서 템플릿을 구성하지 못하면 ViewResolver Chaining에 의해서 다음 순위 ViewResolver로 넘어간다. 그렇지만 템플릿을 구성하려 View파일을 로딩할 때 Thymeleaf ViewResolver에서는 View파일이 실제로 존재하는지를 미리 알 수가 없다. 파일이 없으면 Chaining이 작동하기 전에 Exception이 발생해버린다.

이를 해결하려면 두가지 방법이 있는데 가장 쉬운 방법은 ViewResolver에 ViewNames 속성을 정의하여 View파일 경로에 특정 패턴이 적용된 것만 Thymeleaf 템플릿으로 연결하고 그 이외는 Chaining이 적용되어 다음 순번 ViewResolver으로 넘어가게 하는 방법이다.

Thymeleaf ViewResolver Bean설정에 ViewNames 셋팅

1
    thymeleafViewResolver.setViewNames(new String[] {"thymeleaf/*"});
cs


Controller

1
2
3
4
5
6
7
8
9
10
11
    @GetMapping(value = "/language")
    public Mono<String> index(Model model) {
        model.addAttribute("list", languageService.getList());
        return Mono.just("language/index");     // JSP
    }
    
    @GetMapping(value = "/test")
    public Mono<String> test(Model model) {
        model.addAttribute("test"new Date());
        return Mono.just("thymeleaf/test");     // Thymeleaf
    }
cs


이게 사실 정석이라 할 수 있는데 이렇게 하면 JSP를 Thymeleaf로 마이그레이션이 다 끝나도 설정을 원복하지 않는 한 View이름에 앞으로 계속 thymeleaf/를 붙여야 한다. 이게 솔직히 좀 마음에 안든다. -_-

그래서 좀 변칙적인 수단일 수도 있지만 Thymeleaf ViewResolver 클래스를 상속해서 템플릿 파일이 존재하는지 여부를 체크한 다음 없으면 false를 리턴하게 하여 Chaining이 동작하게 하는 방법이 있다.

CustomThymeleafViewResolver.java

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
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import org.apache.commons.lang3.StringUtils;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver;
import org.thymeleaf.spring5.view.ThymeleafViewResolver;
 
import java.net.MalformedURLException;
import java.util.Locale;
 
@RequiredArgsConstructor
public class CustomThymeleafViewResolver extends ThymeleafViewResolver {
 
    public static final String RESOURCE_PREFIX_CLASSPATH = "classpath:";
    public static final String RESOURCE_PREFIX_FILE = "file:";
 
    final private SpringResourceTemplateResolver springResourceTemplateResolver;
 
    @Setter
    private String prefix;
 
    @Setter
    private String suffix;
 
    @Override
    protected boolean canHandle(String viewName, Locale locale) {
        boolean isExistView = isExistView(viewName);
        if (isExistView) {
            return super.canHandle(viewName, locale);
        }
        return isExistView;
    }
 
    protected boolean isExistView(String viewName) {
        String viewPath =
                StringUtils.defaultString(this.prefix, springResourceTemplateResolver.getPrefix()) +
                viewName +
                StringUtils.defaultString(this.suffix, springResourceTemplateResolver.getSuffix());
 
        Resource resource = null;
        if (viewPath.startsWith(RESOURCE_PREFIX_CLASSPATH)) {
            resource = new ClassPathResource(StringUtils.removeStart(viewPath, RESOURCE_PREFIX_CLASSPATH));
        } else if (viewPath.startsWith(RESOURCE_PREFIX_FILE)) {
            resource = new FileSystemResource(StringUtils.removeStart(viewPath, RESOURCE_PREFIX_FILE));
        } else {
            try {
                resource = new UrlResource(viewPath);
            } catch (MalformedURLException e) {
                return false;
            }
        }
 
        if (!resource.exists()) {
            return false;
        }
        return true;
    }
}
cs

isExistView() 메서드에서 View파일 존재 여부를 체크하여 canHandle() 메서드에서 파일이 있으면 수퍼 클래스의 메서드를 수행하고, 반대의 경우 false를 리턴한다.

Java Config파일에는 ThymeleafViewResolver클래스를 상속한 CustomThymeleafViewResolver클래스를 Bean에 셋팅한다.

WebMvcConfig.java

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
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
import org.springframework.web.servlet.view.JstlView;
import org.thymeleaf.spring5.SpringTemplateEngine;
import org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver;
import org.thymeleaf.spring5.view.ThymeleafViewResolver;
 
@Configuration
@EnableWebMvc
@RequiredArgsConstructor
public class WebMvcConfig {
 
    final private SpringResourceTemplateResolver springResourceTemplateResolver;
    final private SpringTemplateEngine springTemplateEngine;
 
    @Bean
    public ThymeleafViewResolver thymeleafViewResolver() {
        ThymeleafViewResolver thymeleafViewResolver = new CustomThymeleafViewResolver(springResourceTemplateResolver);
        thymeleafViewResolver.setTemplateEngine(springTemplateEngine);
        thymeleafViewResolver.setCharacterEncoding("UTF-8");
        thymeleafViewResolver.setAlwaysProcessRedirectAndForward(true);
        thymeleafViewResolver.setOrder(1);
        return thymeleafViewResolver;
    }
 
    @Bean
    public InternalResourceViewResolver jspViewResolver() {
        InternalResourceViewResolver jspViewResolver = new InternalResourceViewResolver();
        jspViewResolver.setPrefix("/WEB-INF/jsp/");
        jspViewResolver.setSuffix(".jsp");
        jspViewResolver.setViewClass(JstlView.class);
        jspViewResolver.setOrder(2);
        return jspViewResolver;
    }
}
cs

이렇게 하면 오류없이 처음에는 Thymeleaf 템플릿을 찾고, 템플릿 파일이 없으면 JSP로 넘어간다.

2019년 2월 11일 월요일

JPA 기술 노트

2018.11.22 T아카데미 JPA 기술세미나 참석시 기록.

JPA는 Spring Data로 실제 담당 시스템에 적용되어 사용하고 있기는 하나 기본 개념의 재정립이 절실하던 중 T아카데미에서 관련 세미나 소식이 들려왔다.

강의하시는 분이 자바 ORM 표준 JPA 프로그래밍 저자이기도 해서 바로 신청했는데 짧은 시간 내에 굉장히 유익했던 내용이었다.

실습 준비

H2 데이터베이스 설치 (http://www.h2database.com)

JPA와 모던 자바 데이터 저장 기술
객체와 관계형 데이터베이스의 차이

  1. 상속 객체 [상속 관계 - Table 수퍼타입 서브타입 관계]
  2. 연관관계
    • 객체는 참조를 사용: member.getTeam()
    • 테이블은 외래 키를 사용: JOIN ON M.TEAM_ID = T.TEAM_ID
  3. 데이터 타입

ORM?

  • Object-relational mapping(객제 관계 매핑)
  • 객체는 객체대로 설계
  • 관계형 데이터베이스는 관계형 데이터베이스 대로 설계
  • ORM 프레임워크가 중간에서 매핑

JPA는 표준 명세

  • JPA는 인터페이스이고 실제로 구현된 것은 하이버네이트이다.
  • SQL 중심적인 개발에서 객체 중심으로 개발

JPA의 성능 최적화 기능

  1. 1차 캐시와 동일성 보장
    • 같은 트랜잭션 안에서는 같은 엔티티를 반환 - 약간의 조회 성능 향상
  2. 트랜잭션을 지원하는 쓰기 지연
    • 트랜잭션을 커밋할 때까지 INSERT SQL을 모음
    • JDBC BATCH SQL를 사용해서 한꺼번에 DB에 전송하고 커밋
  3. 지연 로딩(Lazy Loading)
    • 지연 로딩 - 객체가 실제 사용될 때 로딩
    • 즉시 로딩 - JOIN SQL로 한번에 연관된 객체까지 미리 조회

ORM은 객체와 RDB 두 기둥 위에 있는 기술

실습

1. H2 DB에서 테스트 테이블 생성

2. Maven 프로젝트 생성하여 jpa, h2database 라이브러리 추가

3. Main클래스와 Entity클래스를 생성

4. persistence.xml 파일을 생성하여 /resources/META-INF/ 디렉터리에 넣음

5. Main클래스에 데이터 등록 코드 작성

6. H2 DB에 실제 데이터가 등록되었는지 확인해본다.

  • 엔터티 매니저 팩토리는 하나만 생성해서 애플리케이션 전체에서 공유
  • 엔터티 매니저는 쓰레드 간에 공유하면 안된다. (사용하고 버려야 됨)

데이터베이스 스키마 자동 생성하기

  • DDL을 애플리케이션 실행 시점에 자동 생성
  • 테이블 중심 -> 객체 중심
  • 데이터베이스 방언을 활용해서 데이터베이스에 맞는 적절한 DDL생성
  • 이렇게 생성된 DDL은 개발 장비에서만 사용

hibernate.hbm2ddl.auto

  • create: 기존 테이블 삭제 후 다시 생성 (DROP + CREATE)
  • create-drop: create와 같으나 종료시점에 테이블 drop
  • update: 변경분만 반영(운영에는 적용하면 안됨)
  • validate: 엔터티와 테이블이 정상 매핑되었는지 확인
  • none: 사용하지 않음

식별자 매핑 방법 @Id (직접 매핑)

  • IDENTITY : 데이터베이스에 위임, MYSQL
  • SEQUENCE: 데이터베이스 시퀀스 오브젝트 사용

권장하는 식별자 전략

  • 기본키 제약 조건 : not null, 유일, 변하면 안된다.
  • 미래까지 이 조건을 만족하는 자연키는 찾기 어렵다. 대리키(대체키)를 사용하자.
  • 예를 들어 주민등록번호도 기본 키로 적절하지 않다.
  • 권장 : Long + 대체키 + 키 생성전략 사용 

연관관계의 주인과 mappedBy

  • 객체의 양방향 관계는 사실 양방향 관계가 아니라 서로 다른 단방향 관계 2개다.
    • 객체의 두 관계 중 하나를 주인으로 설정하고 주인 객체에서 일어나는 업데이트 동작만 실제 DB에 적용
    • ‘mappedBy’ 는 주인이 아니고 조회만 되는 것
    • 주인은 mappedBy 속성 사용하지 않음
    • 주인이 아니면 mappedBy 속성으로 주인을 지정
    • 외래 키가 있는 곳을 주인으로 정해라
      • 어지간하면 단방향으로 설계를 끝내라
      • 필요하면 반대쪽 객체에 mappedBy 설정만 추가해라

주인이 아닌 객체에 member객체를 추가하고 저장

외래키가 null로 들어감

주인인 객체에 team객체를 셋팅 후 저장

외래키가 저장됨

  • 테이블은 외래 키 하나로 두 테이블의 연관관계를 정리

JPA 내부 구조

  • 영속성 컨텍스트
    • 객체를 생성한 상태(비영속 - new)
    • 객체를 저장한 상태(영속 - managed)

트랜잭션을 커밋하거나 JPQL를 사용할 때 플러시가 자동으로 되나 Spring Data등 다른 라이브러리랑 섞어 쓸 때는 수동으로 .flush()메서드로 수행한다.

연관관계와 함께 지연로딩을 설정했을 때, 엔터티 매니저를 클로징한 뒤(em.close();) 조회를 시도하면 에러가 발생한다.

JPA와 객체지향 쿼리

  • JPQL
  • 외부에 제공하는 API는 엔터티가 아닌 별도의 dto를 만들어서 제공하는 것을 권장 (엔터티 변경 시마다 API도 변경되버려 운영에 어려움)

JPA 기반 프로젝트

  • Spring Data JPA
  • QueryDSL - 문자가 아닌 코드로 작성

2019년 1월 5일 토요일

Tomcat 8.5에서의 특수문자 파일명/경로 처리

최근 웹서버를 새로 셋팅할 일이 생겨 하는 김에 웹 애플리케이션의 WAS(Tomcat 7)를 Tomcat 8.5 버전으로 업그레이드를 시도했는데 이슈 하나를 발견하여 적용을 연기했다. 이제 해결했기 때문에 조만간 다시 반영 시도를 할 것이지만…

웹서버에는 NAS가 마운트되어 있고 웹에서 이 NAS에 위치한 (다른 시스템에서 업로드한)이미지 파일을 읽어들여 보여줘야 하는데 유독 몇개 파일에서 404에러가 떨어져서 웹 브라우저에서 엑박으로 표시되는 것이었다. 같은 경로의 다른 파일들은 멀쩡히 잘 보이는데, 처음에는 한글파일명을 인식못하나? 싶어서 봤더니 이상없이 잘 보이는 이미지파일 중에서도 한글명으로 된 파일들이 있었고 Tomcat의 server.xml 설정 파일에도 Connector 속성에 URIEncoding="UTF-8" 옵션이 잘 들어가 있었다.

404에러가 발생하는 CASE를 보니 파일명 형식이

1
(#알파벳#)#한글#[#숫자].#확장자# 
cs

예를 들면 (abc)한글명[123].jpg 이런 식이다.

파일명 앞의 괄호가 문제가 되나? 싶어서 파일명을 바꿔봤더니 그래도 안된다. 혹시나 해서 파일명 뒤의 대괄호를 URI Encoding해서 (abc)한글명%5B123%5D.jpg 로 바꾸어서 호출해봤더니 된다…

UTF-8 처리해줬는데 여기에서 더 뭘 추가해줘야 하나 찾아봐야 했는데, WAS 보안상 URL경로와 파라메터에서 특수문자를 아예 막아놓은 것이었다.

AS-IS의 Tomcat 7 설정파일에서

1
<Listener className="org.apache.catalina.core.JasperListener" />
cs

이 부분만 주석처리해서 엎어친 다음(8버전에서 삭제된 클래스라 안하면 오류발생) 해봐도 안되는걸 봐서는 아마도 버전업하면서 기본 보안 설정을 더 엄하게 한 모양이다.

어떻게 처리해야 할지는 Tomcat 설정 파일 중 catalina.properties 파일 내용을 보면 가장 마지막 라인에 주석으로 명시해놓았다.

1
2
3
4
5
6
# This system property is deprecated. Use the relaxedPathChars relaxedQueryChars
# attributes of the Connector instead. These attributes permit a wider range of
# characters to be configured as valid.
# Allow for changes to HTTP request validation
# WARNING: Using this option may expose the server to CVE-2016-6816
#tomcat.util.http.parser.HttpParser.requestTargetAllow=|
cs

원래는 catalina.properties 파일에 tomcat.util.http.parser.HttpParser.requestTargetAllow 프로퍼티 주석을 풀고 허용할 특수문자를 넣어줬으면 됐지만 deprecated 되었고 server.xml의 Connector 속성에 relaxedPathChars, relaxedQueryChars 두 옵션을 추가하라는 것이다.

relaxedQueryChars는 GET 메서드로 호출할 때 쿼리 스트링 형식의 파라메터에서 특수문자를 허용하는 옵션이고 나는 파일명 문제이므로 relaxedPathChars 옵션만 추가했다.

1
2
3
4
    <Connector port="8080" protocol="HTTP/1.1" 
               URIEncoding="UTF-8"
               relaxedPathChars="[]"
               ... />
cs

이번 경우는 우선 대괄호가 문제여서 대괄호만 추가해서 되는지 안되는지 테스트해보았고 해보니 잘된다…

파일명에 다른 특수문자도 들어있을지 모르니, 대괄호 외에 다른 특수문자도 허용하고 싶으면 relaxedQueryChars 옵션에 허용할 특수문자를 추가로 넣으면 된다. 사실 보안 처리가 된 것을 뚫는거라 좋은 해결 방법은 아닌 것 같다.

그렇기 때문에 왠만하면 사용자가 파일 업로드 하면 시스템에서 파일명 바꿔서 저장하도록 하고, 어지간하면 웹에 올라갈 이미지에 한글파일명은 쓰지 않는 것이 좋다.

Kotlin, SpringBoot 3, GraalVM 환경에서 Native Image로 컴파일하여 애플리케이션 실행

Spring Boot 3부터, GraalVM Native Image를 공식 지원하여 애플리케이션의 시작 속도와 메모리 사용량을 크게 줄일 수 있다. Native Image란 기존의 JVM 기반 위에서 돌아가는 Java 애플리케이션과는 달리 JVM 없이...