로컬 환경 정적 리소스 로딩 성능 개선¶
개요¶
로컬 개발 환경에서 페이지 로딩 지연 문제를 분석하고 개선한 내용을 정리한다.
증상¶
- 로컬(
localhost:8000) 접속 시 페이지 로딩 15초 이상 소요 - 브라우저 네트워크 탭에서 2.8KB 짜리 JS 파일이 12초 걸리는 등 파일 크기와 무관하게 모든 정적 리소스가 느림
- Timing 탭 확인 결과: 서버 응답 대기(TTFB) = 11.45초, 다운로드·연결은 정상
원인 분석¶
1. 정적 리소스가 Spring Security 필터 체인을 전부 통과¶
SecurityConfig에서 정적 리소스를 아래와 같이 처리하고 있었다.
.authorizeHttpRequests(authorizeRequests -> authorizeRequests
.requestMatchers(IGNORE_RESOURCES).permitAll() // /js/**, /css/**, /assets/** 등
...
)
permitAll()은 인가 단계에서 통과시킬 뿐, Spring Security 필터 체인 자체는 그대로 통과한다. 필터 체인 안에는 SecurityContextHolderFilter → HttpSessionSecurityContextRepository 가 포함되어 있어, 매 요청마다 HTTP 세션을 저장소에서 조회·갱신한다.
2. 세션 저장소가 원격 Redis¶
로컬 프로파일의 세션 저장소 설정:
# application-local.yml (gist-webapp / gist-mobile 공통)
spring:
data:
redis:
host: 123.214.170.232 # 원격 서버 (로컬 PC가 아님)
port: 31010
Spring Session + SpringSessionBackedSessionRegistry 구성으로 인해 모든 HTTP 세션 조회가 이 원격 Redis로 향한다.
3. 인과 체인¶
정적 리소스 요청 (예: /js/app.js, /assets/plugins/filepond/filepond.js)
→ permitAll() 이므로 Spring Security 필터 체인 전체 통과
→ HttpSessionSecurityContextRepository.loadDeferredContext()
→ Spring Session → 원격 Redis (123.214.170.232:31010) 왕복
→ 매 요청마다 원격 Redis 조회 발생
한 페이지에 86개 정적 리소스 요청이 있고, HTTP/2로 동시에 몰리면 Lettuce 커넥션 풀(소수)에서 줄서기 현상이 발생하여 TTFB가 6~12초로 누적된다.
4. 왜 로컬에서만 느린가¶
| 환경 | Redis 위치 | 왕복 지연 | 체감 |
|---|---|---|---|
| 로컬(local) | 원격 서버(인터넷/VPN 경유) | 수십~수백 ms | 정적 리소스 86개 × 지연 = 수십 초 |
| dev/prod 서버 | 동일 망 내 Redis | 1ms 미만 | 사실상 무시 가능 |
해결 방법¶
1. ignoring() — 정적 리소스를 보안 체인에서 완전 제외 (local 전용)¶
ignoring()은 permitAll()과 달리 요청이 Spring Security 필터 체인 자체에 진입하지 않는다. 결과적으로 원격 Redis 세션 조회가 발생하지 않는다.
// SecurityConfig.java (gist-webapp)
@Bean
@Profile("local")
public WebSecurityCustomizer staticResourceSecurityCustomizer() {
return web -> web.ignoring().requestMatchers(IGNORE_RESOURCES);
}
@Profile("local")로 로컬 환경에서만 활성화- dev/prod 는 기존
permitAll()유지 (동일 망 Redis라 영향 미미, 보안 헤더 그대로)
| 방식 | 필터 체인 통과 | 세션 조회 | 용도 |
|---|---|---|---|
permitAll() | 통과함 | 발생함 | dev/prod |
ignoring() | 통과 안 함 | 발생 안 함 | local |
2. 벤더 리소스 캐싱 — 반복 다운로드 제거 (local 전용)¶
application-local.yml 은 개발자 코드 즉시 반영을 위해 모든 정적 리소스에 no-cache 를 강제한다. 하지만 /assets/** 하위 벤더 라이브러리(filepond, select2, pq-grid, tus, jszip, 아이콘폰트 등)는 개발자가 직접 수정하지 않으므로 매 요청마다 재다운로드할 이유가 없다.
LocalStaticResourceConfig 를 신규 추가하여 벤더 영역에만 장기 캐싱을 적용한다.
// LocalStaticResourceConfig.java (gist-webapp / gist-mobile 공통)
@Configuration
@Profile("local")
public class LocalStaticResourceConfig implements WebMvcConfigurer {
private static final CacheControl CACHE_DURATION =
CacheControl.maxAge(Duration.ofDays(365)).cachePublic();
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/assets/**")
.addResourceLocations("classpath:/static/assets/")
.setCacheControl(CACHE_DURATION);
registry.addResourceHandler("/images/**")
.addResourceLocations("classpath:/static/images/")
.setCacheControl(CACHE_DURATION);
}
}
동작 원리: /assets/**, /images/** 가 Spring Boot 기본 /** 핸들러보다 구체적 패턴이므로 해당 경로에 한해 이 핸들러가 우선 적용된다.
적용 범위 및 결과¶
| 경로 | 처리 방식 | 캐시 |
|---|---|---|
/assets/** (벤더 전체) | ignoring() + 커스텀 핸들러 | 365일 |
/images/** | ignoring() + 커스텀 핸들러 | 365일 |
/js/**, /css/** (개발자 코드) | ignoring() | no-cache (즉시 반영) |
API (/api/**) | 기존 보안 체인 그대로 | — |
- 로컬 재기동 후 첫 로딩:
ignoring()효과로 TTFB가 ms 단위로 단축 - 두 번째 로딩부터: 벤더 리소스 브라우저 캐시 적용으로 네트워크 요청 자체가 사라짐
주의사항¶
- 벤더 파일 교체 시: 365일 캐시 때문에 브라우저가 즉시 반영하지 않는다. 교체 후
Ctrl+F5(강력 새로고침) 또는 DevTools →캐시 사용 중지체크 후 새로고침으로 우회한다. - dev/prod 영향 없음:
@Profile("local")로 격리되어 있으므로 운영 환경에는 영향을 주지 않는다. application-local.yml의no-cache블록 유지 필수: 개발자 코드(/js,/css)의 즉시 반영을 보장한다. 제거 시 브라우저 휴리스틱 캐싱으로 수정 내용이 반영되지 않는 문제가 생긴다.
관련 파일¶
| 파일 | 변경 내용 |
|---|---|
gist-webapp/.../config/SecurityConfig.java | @Profile("local") WebSecurityCustomizer 빈 추가 |
gist-webapp/.../config/LocalStaticResourceConfig.java | 신규 — 벤더 리소스 365일 캐싱 |
gist-mobile/.../config/SecurityConfig.java | @Profile("local") WebSecurityCustomizer 빈 추가 |
gist-mobile/.../config/LocalStaticResourceConfig.java | 신규 — 벤더 리소스 365일 캐싱 |