2024. 11. 19. 13:06ㆍ기술 창고/Spring
Spring Boot 프로젝트를 하나의 monolithic 프로젝트로써 MVC 패턴을 적용하여 구현하려고 했습니다.
이 때 View 부분은 Thymeleaf 로 구성하였고 정상적으로 페이지가 구현되고 완성되었습니다.
하지만 운영 서버에 배포할 때, 초기에 배포한 프로젝트 다음에 js나 css와 같은 Resource 를 수정한 version 2 프로젝트를 다시 재배포하게 되었을 때 업데이트한 Resource들이 정상적으로 반영되지 않고 계속 이전 Resource 내용으로 적용되어 문제가 발생되는 이슈가 있었습니다.
이는 Resources들이 갱신되지 않기도 하거니와 서버 캐시 또한 새로고침(?) 되지 않아 발생된 이슈입니다.
따라서 이를 해결해주지 않으면 서비스에 접속한 유저들이 일일히 직접 접속한 브라우저나 앱 상에서 캐시 제거를 해주어야지 갱신이 되게 됩니다.
이는 꽤 큰 이슈라고 생각되어 해결해야할 필요가 있다고 느꼈습니다.
위와 같은 이슈를 방지하기 위해서는 js 나 css 와 같은 Resource 호출할 때 버전 관리를 해주어야 합니다.
서버를 재기동할 때마다 Resources들이 새롭게 버전 관리된 내용으로 실행되게 되며 캐시가 쌓일 일도 없을 것입니다.
오늘은 이와 같은 Resources 들의 버전 관리 방법에 대해 정리해보도록 하겠습니다.
JSP (일반 View 구성일 경우)
타임리프가 아닌 일반적인 페이지 구성일 경우에는 간단합니다.
방법 1 ( : 고정 값으로 버전 관리 )
[script]
(전)
<script src"/folder/dir/test.js" type="text/javascript" />
(후)
<script src"/folder/dir/test.js?ver=20241120" type="text/javascript" />
[css]
(전)
<link rel="stylesheet" type="text/css" href="/folder/dir/style.css" />
(후)
<link rel="stylesheet" type="text/css" href="/folder/dir/style.css?ver=20241120" />
호출하고자 하는 js, css Resources 들의 import 태그에서 src 속성과 href 속성에 파라미터값을 추가로 넣어 호출해주면 됩니다.
위의 케이스들의 경우 ? 기호 뒤에 ver 이라는 관리용 파라미터를 넣어 그 안에 날짜형 데이터를 넣어 버전 관리를 진행해주겠다고 명시하였습니다.
파라미터 명은 ver 도 되고 version 도 됩니다. (딱히 고정된 명칭을 사용해야 하는 것은 아닌 것으로 보입니다.)
방법 2 ( : 동적 값으로 버전 관리)
동적 값으로 버전 관리를 한다는 말은 즉 버전 관리 파라미터를 프로젝트를 재실행할 때마다 자동적으로 값이 바뀌어 파라미터 값으로 들어가 관리된다는 뜻입니다.
이 동적 값으로 버전 관리를 하는 방법에는 여러 가지가 있지만 대표적으로 한 방법에 대해서 정리해보겠습니다.
[controller]
# 세션 데이터를 활용할 경우
@RequestMapping(value = "/page/move.ux", consumes = {MediaType.ALL_VALUE})
public ModelAndView couponBox(HttpServletRequest request, HttpServletResponse response) {
ModelAndView mav = new ModelAndView();
try {
// session 호출
HttpSession session = request.getSession();
// 동적 버전 관리용 현재 시간 데이터 호출
LocalDateTime versionDate = LocalDateTime.now();
// 실제 동적 버전 관리 변수 생성
String version = versionDate.getYear() + String.valueOf(versionDate.getMonthValue()) + versionDate.getDayOfMonth();
// session 에 버전 관리 변수 담기
session.setAttribute("time", version);
// 이동할 페이지
mav.setViewName("/page/main");
return mav;
} catch (Exception ex) {
LogUtil.logException(ex, request);
mav.addObject("errMsg", AppErrorCode.AVAILABLE_COUPON_SELECT_FAIL.getErrMsg()); // 에러 페이지 전달 데이터 세팅
mav.setViewName(errorPage); // 에러 페이지 세팅
return mav;
}
}
# View 에 전달되는 Object 데이터를 활용할 경우
@RequestMapping(value = "/page/move.ux", consumes = {MediaType.ALL_VALUE})
public ModelAndView couponBox(HttpServletRequest request, HttpServletResponse response) {
ModelAndView mav = new ModelAndView();
try {
// 동적 버전 관리용 현재 시간 데이터 호출
LocalDateTime versionDate = LocalDateTime.now();
// 실제 동적 버전 관리 변수 생성
String version = versionDate.getYear() + String.valueOf(versionDate.getMonthValue()) + versionDate.getDayOfMonth();
// 이동할 페이지
mav.setViewName("/page/main");
// 페이지 전달 데이터 Object
mav.addObject("time", version);
return mav;
} catch (Exception ex) {
LogUtil.logException(ex, request);
mav.addObject("errMsg", AppErrorCode.AVAILABLE_COUPON_SELECT_FAIL.getErrMsg()); // 에러 페이지 전달 데이터 세팅
mav.setViewName(errorPage); // 에러 페이지 세팅
return mav;
}
}
[script]
(전)
<script src"/folder/dir/test.js" type="text/javascript" />
(후 : session의 경우)
<script src"/folder/dir/test.js?ver=${session.time}" type="text/javascript" />
- ver 파라미터에 ${} 기호 안에 session 안에 담은 버전 관리 데이터 변수 명을 넣어 호출
(후 : 전달 object의 경우)
<script src"/folder/dir/test.js?ver=${time}" type="text/javascript" />
- ver 파라미터에 ${} 기호 안에 addObject로 페이지에 전달한 버전 관리 데이터 변수 명을 넣어 호출
[css]
(전)
<link rel="stylesheet" type="text/css" href="/folder/dir/style.css" />
(후 : session의 경우)
<link rel="stylesheet" type="text/css" href="/folder/dir/style.css?ver=${session.time}" />
- ver 파라미터에 ${} 기호 안에 session 안에 담은 버전 관리 데이터 변수 명을 넣어 호출
(후 : 전달 object의 경우)
<link rel="stylesheet" type="text/css" href="/folder/dir/style.css?ver=${time}" />
- ver 파라미터에 ${} 기호 안에 addObject로 페이지에 전달한 버전 관리 데이터 변수 명을 넣어 호출
controller 단에서 View 단으로 넘어올 때, session 에 버전 관리 데이터를 담아 넘기거나, ModelAndView를 통해 addObject에 버전 관리 데이터를 담아 넘깁니다.
넘긴 버전 관리 데이터를 각 방식에 따라 버전 관리 파라미터에 넣어 css, js 를 호출하게 되면 동적으로 서버를 실행할 때마다 버전 관리가 동적으로 진행되어 캐시가 쌓이지 않게 될 것입니다.
Thymeleaf
타임리프의 경우에는 위의 일반적인 페이지 구성일 경우의 방식과 더불어 다른 방법들 또한 존재합니다.
또한, 문법 자체가 추가되고 호출하는 코드 형식이 달라 방법이 다른 부분들도 있습니다.
방법 1 ( : 고정 값으로 버전 관리 )
[script]
(전)
<script th:src"@{/folder/dir/test.js}" type="text/javascript" />
(후)
<script th:src"@{/folder/dir/test.js?ver=20241120}" type="text/javascript" />
또는
<script th:src"@{/folder/dir/test.js(ver=20241120)}" type="text/javascript" />
[css]
(전)
<link rel="stylesheet" type="text/css" th:href="@{/folder/dir/style.css}" />
(후)
<link rel="stylesheet" type="text/css" th:href="@{/folder/dir/style.css?ver=20241120}" />
또는
<link rel="stylesheet" type="text/css" th:href="@{/folder/dir/style.css(ver=20241120)}" />
마찬가지로 호출하고자 하는 js, css Resources 들의 import 태그에서 src 속성과 href 속성에 파라미터값을 추가로 넣어 호출해주면 됩니다.
동일하게 ? 기호 뒤에 ver 이라는 관리용 파라미터를 넣어 그 안에 날짜형 데이터를 넣어 버전 관리를 진행해주겠다고 명시하거나 ? 기호 대신에 () 괄호 안에 파라미터 데이터를 넣어 호출하도록 합니다.
방법 2 ( : 동적 값으로 버전 관리)
Thymeleaf 에서 동적 값으로 버전 관리하는 부분도 큰 틀은 일반 jsp 와 다를 바가 없지만 문법 때문에 살짝 다릅니다.
(1) Controller 를 활용한 방법
[controller]
# 세션 데이터를 활용할 경우
@RequestMapping(value = "/page/move.ux", consumes = {MediaType.ALL_VALUE})
public ModelAndView couponBox(HttpServletRequest request, HttpServletResponse response) {
ModelAndView mav = new ModelAndView();
try {
// session 호출
HttpSession session = request.getSession();
// 동적 버전 관리용 현재 시간 데이터 호출
LocalDateTime versionDate = LocalDateTime.now();
// 실제 동적 버전 관리 변수 생성
String version = versionDate.getYear() + String.valueOf(versionDate.getMonthValue()) + versionDate.getDayOfMonth();
// session 에 버전 관리 변수 담기
session.setAttribute("time", version);
// 이동할 페이지
mav.setViewName("/page/main");
return mav;
} catch (Exception ex) {
LogUtil.logException(ex, request);
mav.addObject("errMsg", AppErrorCode.AVAILABLE_COUPON_SELECT_FAIL.getErrMsg()); // 에러 페이지 전달 데이터 세팅
mav.setViewName(errorPage); // 에러 페이지 세팅
return mav;
}
}
# View 에 전달되는 Object 데이터를 활용할 경우
@RequestMapping(value = "/page/move.ux", consumes = {MediaType.ALL_VALUE})
public ModelAndView couponBox(HttpServletRequest request, HttpServletResponse response) {
ModelAndView mav = new ModelAndView();
try {
// 동적 버전 관리용 현재 시간 데이터 호출
LocalDateTime versionDate = LocalDateTime.now();
// 실제 동적 버전 관리 변수 생성
String version = versionDate.getYear() + String.valueOf(versionDate.getMonthValue()) + versionDate.getDayOfMonth();
// 이동할 페이지
mav.setViewName("/page/main");
// 페이지 전달 데이터 Object
mav.addObject("time", version);
return mav;
} catch (Exception ex) {
LogUtil.logException(ex, request);
mav.addObject("errMsg", AppErrorCode.AVAILABLE_COUPON_SELECT_FAIL.getErrMsg()); // 에러 페이지 전달 데이터 세팅
mav.setViewName(errorPage); // 에러 페이지 세팅
return mav;
}
}
[script]
(전)
<script th:src="@{/folder/dir/test.js}" type="text/javascript" />
(후 : session의 경우)
<script th:src"@{/folder/dir/test.js?ver=${session.time}}" type="text/javascript" />
또는
<script th:src"@{/folder/dir/test.js(ver=${session.time})}" type="text/javascript" />
- ? 기호 뒤에 ver 파라미터에 ${} 기호 안에 session 안에 담은 버전 관리 데이터 변수 명을 넣어 호출
- 또는 ? 기호 대신에 () 괄호에 session 안에 담은 버전 관리 데이터 변수 명을 넣어 호출
(후 : 전달 object의 경우)
<script th:src"@{/folder/dir/test.js?ver=${time}}" type="text/javascript" />
또는
<script th:src"@{/folder/dir/test.js(ver=${time})}" type="text/javascript" />
- ? 기호 뒤에 ver 파라미터에 ${} 기호 안에 addObject로 페이지에 전달한 버전 관리 데이터 변수 명을 넣어 호출
- 또는 ? 기호 대신에 () 괄호에 addObject로 페이지에 전달한 버전 관리 데이터 변수 명을 넣어 호출
[css]
(전)
<link rel="stylesheet" type="text/css" th:href="@{/folder/dir/style.css}" />
(후 : session의 경우)
<link rel="stylesheet" type="text/css" th:href="@{/folder/dir/style.css?ver=${session.time}}" />
또는
<link rel="stylesheet" type="text/css" th:href="@{/folder/dir/style.css(ver=${session.time})}" />
- ? 기호 뒤에 ver 파라미터에 ${} 기호 안에 session 안에 담은 버전 관리 데이터 변수 명을 넣어 호출
- 또는 ? 기호 대신에 () 괄호에 session 안에 담은 버전 관리 데이터 변수 명을 넣어 호출
(후 : 전달 object의 경우)
<link rel="stylesheet" type="text/css" th:href="@{/folder/dir/style.css?ver=${time}}" />
또는
<link rel="stylesheet" type="text/css" th:href="@{/folder/dir/style.css(ver=${time})}" />
- ? 기호 뒤에 ver 파라미터에 ${} 기호 안에 addObject로 페이지에 전달한 버전 관리 데이터 변수 명을 넣어 호출
- 또는 ? 기호 대신에 () 괄호에 addObject로 페이지에 전달한 버전 관리 데이터 변수 명을 넣어 호출
controller 단에서 View 단으로 넘어올 때, session 에 버전 관리 데이터를 담아 넘기거나, ModelAndView를 통해 addObject에 버전 관리 데이터를 담아 넘깁니다.
(2) Custom Thymeleaf Dialect 를 활용한 방법
[config]
package kbcp.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.thymeleaf.context.ITemplateContext;
import org.thymeleaf.dialect.AbstractProcessorDialect;
import org.thymeleaf.engine.AttributeName;
import org.thymeleaf.model.IProcessableElementTag;
import org.thymeleaf.processor.IProcessor;
import org.thymeleaf.processor.element.AbstractAttributeTagProcessor;
import org.thymeleaf.processor.element.IElementTagStructureHandler;
import org.thymeleaf.templatemode.TemplateMode;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.LinkedHashSet;
import java.util.Set;
@Slf4j
@Configuration
public class ResourceVersioningDialectConfig {
// 버전 관리 Processor Bean 등록
@Bean
ResourceVersioningDialect getResourceVersioningDialect() {
return new ResourceVersioningDialect();
}
// 버전 관리 Processor
static class ResourceVersioningDialect extends AbstractProcessorDialect {
static final String NAME = "RVDialect";
static final String PREFIX = "th"; // 타임리프
static final int PRECEDENCE = 800;
public ResourceVersioningDialect() {
super(NAME, PREFIX, PRECEDENCE);
}
protected ResourceVersioningDialect(String name, String prefix, int processorPrecedence) {
super(name, prefix, processorPrecedence);
}
@Override
public Set<IProcessor> getProcessors(String dialectPrefix) {
LinkedHashSet<IProcessor> processors = new LinkedHashSet<>();
processors.add( new VSRC(TemplateMode.HTML, dialectPrefix)); // 버전 관리 적용된 src Processor 를 등록
processors.add( new VHREF(TemplateMode.HTML, dialectPrefix));
return processors;
}
}
// Resources 에 접근할 src 속성에 버전 관리를 적용 하기 위한 전역 클래스 생성
static class VSRC extends AbstractAttributeTagProcessor {
public static final int ATTR_PRECEDENCE = 1300;
// 새로 생성할 타임리프 vsrc 속성 명 지정
public static final String ATTR_NAME = "vsrc";
// 버전 관리 파라미터 (날짜 데이터로 기준)
public static final LocalDateTime VERSION_TIME = LocalDateTime.ofInstant(Instant.ofEpochMilli(System.currentTimeMillis()), ZoneOffset.UTC);
public VSRC(TemplateMode templateMode, String dialectPrefix) {
super(templateMode, dialectPrefix, null, false, ATTR_NAME, true, ATTR_PRECEDENCE, true);
}
@Override
protected void doProcess(ITemplateContext context, IProcessableElementTag tag, AttributeName attributeName,
String attributeValue, IElementTagStructureHandler structureHandler) {
// 태그 속성 값 별도 저장
String returnVal = attributeValue;
// 태그 속성 값 존재 확인 (예 : script 태그의 src 속성 값 (/kb/js/script.js))
if(returnVal != null) {
// 추출한 script src 속성의 경로 값에 Resource 호출 시 버전 관리할 파라미터 구분 기호 추가
returnVal += returnVal.contains("?") ? "&" : "?";
// 최종 src 속성 경로 값에 버전 관리 파라미터가 추가된 경로 값 생성
returnVal += "version=" + getVersionParameter(VERSION_TIME);
}
// 사용할 script 태그 structureHandler 에 src 속성에 위에서 최종 반영한 경로 값으로 설정
structureHandler.setAttribute("src", returnVal);
}
}
// Resources 에 접근할 href 속성에 버전 관리를 적용 하기 위한 전역 클래스 생성
static class VHREF extends AbstractAttributeTagProcessor {
public static final int ATTR_PRECEDENCE = 1300;
// 새로 생성할 타임리프 vhref 속성 명 지정
public static final String ATTR_NAME = "vhref";
// 버전 관리 파라미터 (날짜 데이터로 기준)
public static final LocalDateTime VERSION_TIME = LocalDateTime.ofInstant(Instant.ofEpochMilli(System.currentTimeMillis()), ZoneOffset.UTC);
public VHREF(TemplateMode templateMode, String dialectPrefix) {
super(templateMode, dialectPrefix, null, false, ATTR_NAME, true, ATTR_PRECEDENCE, true);
}
@Override
protected void doProcess(ITemplateContext context, IProcessableElementTag tag, AttributeName attributeName,
String attributeValue, IElementTagStructureHandler structureHandler) {
// 태그 속성 값 별도 저장
String returnVal = attributeValue;
// 태그 속성 값 존재 확인 (예 : link 태그의 href 속성 값 (/kb/css/style.css))
if(returnVal != null) {
// 추출한 script href 속성의 경로 값에 Resource 호출 시 버전 관리할 파라미터 구분 기호 추가
returnVal += returnVal.contains("?") ? "&" : "?";
// 최종 href 속성 경로 값에 버전 관리 파라미터가 추가된 경로 값 생성
returnVal += "version=" + getVersionParameter(VERSION_TIME);
}
// 사용할 link 태그 structureHandler 에 href 속성에 위에서 최종 반영한 경로 값으로 설정
structureHandler.setAttribute("href", returnVal);
}
}
// 날짜형 버전 관리 파라미터 생성
static private String getVersionParameter(LocalDateTime versionTime){
// Resources 버전 관리(년도)
String versionYear = String.valueOf(versionTime.getYear());
// Resources 버전 관리(달)
String versionMonth = String.valueOf(versionTime.getMonthValue()).length() == 2 ?
String.valueOf(versionTime.getMonthValue()) : "0" + versionTime.getMonthValue();
// Resources 버전 관리(일자)
String versionDay = String.valueOf(versionTime.getDayOfMonth()).length() == 2 ?
String.valueOf(versionTime.getDayOfMonth()) : "0" + versionTime.getDayOfMonth();
// Resources 버전 관리(시간)
String versionHour = String.valueOf(versionTime.getHour()).length() == 2 ?
String.valueOf(versionTime.getHour()) : "0" + versionTime.getHour();
// Resources 버전 관리(분)
String versionMinute = String.valueOf(versionTime.getMinute()).length() == 2 ?
String.valueOf(versionTime.getMinute()) : "0" + versionTime.getMinute();
// Resources 버전 관리(초)
String versionSecond = String.valueOf(versionTime.getSecond()).length() == 2 ?
String.valueOf(versionTime.getSecond()) : "0" + versionTime.getSecond();
return versionYear + versionMonth + versionDay + versionHour + versionMinute + versionSecond;
}
}
[script]
(전)
<script th:src="@{/folder/dir/test.js}" type="text/javascript" />
(후)
<script th:vsrc"/folder/dir/test.js" type="text/javascript" />
[css]
(전)
<link rel="stylesheet" type="text/css" th:href="@{/folder/dir/style.css}" />
(후)
<link rel="stylesheet" type="text/css" th:vhref="/folder/dir/style.css" />
이 방법은 Thymeleaf 를 사용하는 환경에서 태그 프로세서를 커스텀하여 활용하는 방법입니다.
위의 ResourceVersioningDialectConfig config 클래스 파일을 만드는데, 이 클래스에는 3개의 중점적인 내부 중첩 클래스가 존재합니다.
VSRC, VHREF 내부 클래스들은 각각 태그의 src, href 속성을 커스텀하여 사용하기 위한 중첩 클래스라고 볼 수 있습니다.
이 두 내부 클래스들은 모두 AbstractAttributeTagProcessor 추상 클래스를 상속 받고 있는데, 이 상속받은 AbstractAttributeTagProcessor 클래스를 통해 src 와 href 속성을 사용고자하는 HTML 템플릿 태그를 활용하여 각각 vsrc, vhref 라는 타임리프 전용의 새로운 태그 속성을 만들어 생성해주고,
override 받은 doProcess 함수를 활용하여 기존의 src, href 속성 값으로 넣어진 호출 경로 value(attributeValue) 를 가지고 와서 ? 기호와 함께 (기존에 다른 파라미터가 존재하면 &를 붙이도록) version 이라는 파라미터명과 함께 getVersionParameter() 함수를 통해 만들어진 버전 관리용 변수 데이터를 넣어 붙여줍니다.
# 결과 예시 : /static/js/script.js?version=20241119, /static/css/style.css?version=20241119
버전 관리 변수까지 붙여진 호출 경로 value(attributeValue) 를 각 doProcess함수로 전달된 structureHandler에 setAttribute 함수를 사용해서 넣어주게 되면 vsrc, vhref 속성을 통해 호출할 때마다 방금 만든 버전 관리 value 가 적용되게 됩니다.
# vsrc : structureHandler.setAttribute("src", {버전 관리 value});
# vhref : structureHandler.setAttribute("href", {버전 관리 value});
이렇게 만들어진 VSRC, VHREF 커스텀 Dialect 클래스를 가지고 AbstractProcessorDialect 추상 클래스를 상속받은 내부 중첩 ResourceVersioningDialect 클래스에서 실제로 사용할 수 있도록 Proccessor 에 등록하여 Bean 으로 만들어줍니다.
이제 프로젝트가 실행되어 이 Bean 객체가 Spring Container에 등록 생성될 때마다 날짜 데이터가 매번 바뀌어 들어가게 되어 버전 관리 되게 됩니다.
결과
결과를 확인해보면 각각 import 받아 호출한 commonFooter.js, couponDetail.js 에 version 이라는 버전 관리용 파라미터가 붙어 호출되어 적용되게 되는 것을 확인할 수 있습니다.
(2) properties, yml 을 활용한 방법
# application.properties 의 경우
spring.web.resources.chain.strategy.content.paths= /kb/js/**, /kb/css/**
spring.web.resources.chain.strategy.content.enabled= true
# application.yml 의 경우
spring:
web:
resources:
chain:
strategy:
content:
paths: /kb/js/**, /kb/css/**
enabled: true
[script]
# Thymeleaf 의 경우
<script th:inline="javascript" th:src="@{/kb/js/couponHIstory.js}"></script>
# 일반 jsp 의 경우
<script th:inline="javascript" src="/kb/js/couponHIstory.js"></script>
[css]
# Thymeleaf 의 경우
<link rel="stylesheet" th:href="@{/kb/css/style.css}">
# 일반 jsp 의 경우
<link rel="stylesheet" href="/kb/css/style.css">
이 방법은 위에서 설명했던 여타 다른 버전 관리 적용 방법과 달리 매우 간단합니다.
단지 사용하고 있는 설정 파일 (properties 파일인지, yml 파일인지) 에 따라 설정 내용을 넣기만 하고 호출하는 태그 부분에는 각각 기본적인 호출 방식 그대로 사용하기만 하면 알아서 css, js Resources 들을 import 하여 호출할 때 파일명에 자체 난수화 데이터가 넣어져 버전 관리를 진행하게 됩니다.
(단, 버전 관리 데이터는 고정 난수화 데이터)
설정 파일에 넣게될 paths 부분의 내용은 각자 js, css 파일들이 존재하는 경로를 넣어주면 됩니다.
기본적으로 root 경로가 resources/static 이기 때문에 그것을 제외한 js, css 파일 존재 경로를 넣어주면 됩니다.
저는 resources/static/kb/js, resources/static/kb/css 경로에 파일들이 존재하기 때문에 위와 같이 지정해주었습니다.
결과
확인했을 때 난수화가 적용되어 버전 관리가 진행된 것을 확인할 수 있었습니다.
이로써 Resources 들의 버전 관리를 통해 캐시까지 관리될 수 있게끔 만드는 방법에 대해서 정리해보았습니다.
'기술 창고 > Spring' 카테고리의 다른 글
[Spring Boot] 서버 자체에 ssl 적용이 아닌 내장 Tomcat에 ssl 적용하여 https 활성화 (jks 파일 기준) (0) | 2024.08.14 |
---|---|
[Spring Boot] Spring Profile 을 통한 개발 환경 분산 관리 (1) | 2024.06.21 |
[Spring Boot] 타임리프 문법 - th:href (0) | 2023.11.22 |
[Spring Boot] Thymeleaf (타임리프) (0) | 2023.11.13 |
[Spring Boot] 로컬 환경 + 배포 서버에 파일 업로드 (trasferTo 사용법) (0) | 2023.10.19 |