(JPA+Boot)브라우저 캐시와 메모리 캐시
스프링 프레임워크에서 메모리 캐시와 브라우저 캐시를 효과적으로 구현하고 gzip 압축을 통해 웹 애플리케이션의 성능을 최적화하는 다양한 전략과 구현 방법을 상세히 설명하는 가이드입니다.
캐시(메모리, 브라우저) + gzip 압축
reload: 타임리프는 properties에 캐시 사용X 하여 reload가 가능한데, JSP는 reloadable=”true”가 기본값이라 톰캣이 자동 컴파일 하면서 reload가 이미 제공!
XML에서 정적리소스 핸들러매핑
<mvc:resources mapping="/**" location="classpath:/static/" />
가능그러나, setCacheControl 같은건 Java 코드로 해야해서 여기선 xml그냥 사용하지 말 것.
gzip 압축은 보통 이미지나 동영상은 이미 압축되어 있는 상태라 HTML,CSS,JS 만 압축!!
- 하는법은 구글링 ㄱㄱㄹ
브라우저 캐시는?
-
WebMvcConfigurer
를 상속받아서 구현하면 스프링 빈에 자동 등록과 기능확장! -
캐시 뿐만 아니라 인터셉터, ArgumentResolver 등 도 새로 구현한 후에는 이곳에 등록해서 확장(추가)해야 한다.
정적이미지경로 핸들링과 "브라우저 캐시" 예시 코드
+참고) interceptor 추가, argumentresolver 추가, CORS 해결
정적이미지 경로 핸들링 + 브라우저 캐시(클라 쪽 메모리 활용) 추가 ->addResoucreHandler()
CORS(Cross-Origin Resource Sharing) 해결 ->addCorsMappings()
@Configuration // 설정 파일임을 알림 @Slf4j public class ApiConfig implements WebMvcConfigurer { private final MyDataSource source; // // 정적이미지 경로 핸들링 + 브라우저 캐시 // 경로 매핑 작업을 하는 오버라이딩이며 특정 경로(=static/) 에 브라우저 캐시까지 추가한 로직!! @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { CacheControl cacheControl = CacheControl.maxAge(Duration.ofDays(365)); registry.addResourceHandler("/image/**") //.addResourceLocations("file:///C:/images-spring/"); //.addResourceLocations("file:///var/www/images-spring/"); .addResourceLocations("file:///"+source.getImgPath()); // registry.addResourceHandler("/**") // **/*.*, /resources/** .addResourceLocations("classpath:/static/") .setCacheControl(cacheControl); // 정적 리소스들 캐시 추가 } // @Override public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) { resolvers.add(new LoginMemberArgumentResolver()); } // @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new MemberCheckInterceptor()) .order(2) .addPathPatterns("/**") // 모든 경로 접근 .excludePathPatterns("/", "/api/v1/members/login", "/api/v1/members/register", "/api/v1/members/logout", "/api/v1/members/*", "/image/**", "/css/**", "/*.ico", "/error"); // 제외 경로! } // // CORS @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**").allowedOrigins("http://localhost:9000"); } }
메모리캐시는?
-
spring-boot-starter-cache, @EnableCaching 설정은 기본임!
-
서비스단에서
@Cacheable(value = "users", key = "#memberId", cacheNames = "users", cacheManager = "cacheManager2")
이런식으로 사용!- 참고로 조건부도 지정 가능:
@Cacheable(value = "items", key = "#searchItem.searchKeyword + '_' + #searchItem.pageIndex", condition = "#searchItem.searchKeyword != null && !#searchItem.searchKeyword.isEmpty()")
-
주의: key를 Object로 접근할 경우 @EqualsAndHashCode로 값이 구분되게 꼭 설정해야 함.
@EqualsAndHashCode는 이름 그대로 해당 메소드 자동 생성해주는 롬복
- 참고로 조건부도 지정 가능:
-
수동 cacheManager 빈 등록 설정으로 “캐시 메모리 용량”까지 설정하는걸 추천!
-
예시 코드
서버 메모리 캐시 위해 CachingConfigurerSupport 상속 및 구현(기능확장)
CacheManager를 오버라이딩!! 물론, 간단히 설정파일(yaml)에서 설정도 지원 중
main 함수있는 클래스에서@EnableCaching
필수 선언!
=> 그러나 직접 빈 등록방식을 사용한다면 해당 설정 파일에서만@EnableCaching
선언해도 됨!
코드는 사용법과 만드는 법을 간단히 소개// 사용법: 서비스단 메소드에 이런식으로 적용 @Cacheable(value = "users", key = "#memberId", cacheNames = "users", cacheManager = "cacheManager2") // // 만드는 법: Caffeine 활용 하여 간단히 설정! CaffeineCacheManager cacheManager = new CaffeineCacheManager("users"); cacheManager.setCaffeine(Caffeine.newBuilder() .initialCapacity(1) // 내부 해시 테이블의 최소한의 크기 (캐릭터 어차피 1개만 기록) .maximumSize(200) // 캐시에 포함할 수 있는 최대 엔트리 수 (멤버 200명 정도한테 적용하자) // .weakKeys() // 직접 키를 설정하므로 주석처리 .recordStats());
아래 방식을 추천 -> 직접 빈 등록 설정 파일
@Configuration @EnableCaching public class CacheConfig extends CachingConfigurerSupport { @Override @Bean public CacheManager cacheManager() { CaffeineCacheManager cacheManager = new CaffeineCacheManager("members"); cacheManager.setCaffeine(Caffeine.newBuilder() .initialCapacity(50) // 내부 해시 테이블의 최소한의 크기 .maximumSize(50) // 캐시에 포함할 수 있는 최대 엔트리 수 // .weakKeys() // 직접 키를 설정하므로 주석처리 .recordStats()); return cacheManager; } // @Bean public CacheManager cacheManager2() { CaffeineCacheManager cacheManager = new CaffeineCacheManager("users"); cacheManager.setCaffeine(Caffeine.newBuilder() .initialCapacity(1) // 내부 해시 테이블의 최소한의 크기 (캐릭터 어차피 1개만 기록) .maximumSize(200) // 캐시에 포함할 수 있는 최대 엔트리 수 (멤버 200명 정도한테 적용하자) // .weakKeys() // 직접 키를 설정하므로 주석처리 .recordStats()); return cacheManager; } }
-
-
기존 타임리프 플젝 웹에서 캐시는 “페이징 게시물 조회 캐시” 흐름이 좀 복잡 -> eGov적용 해본 웹 플젝에서 개선 (eGov꺼로 여기서 정리하겠음)
eGov 플젝에선 하필 URL을 studio/page/itemId 이런식으로 안해서 그냥 URL param으로 넘김 (이건 좀 아쉬움. 이건 JPA로 했던 플젝 방식이 더 좋은듯)- 게시물 페이지별 조회, 게시물 전체 개수: @Cacheable -> 캐시 없으면 기록 및 조회
- 게시물 페이지별 조회(게시물 수정): @CachePut -> 캐시 있어도 기록 및 조회
- 정정: 그냥 수정도 @CacheEvict(key=해당 페이지) 로 해당 페이지만 캐시 초기화 하자.
- 게시물 추가, 게시물 삭제: @CacheEvict -> 캐시 초기화
테스트 방법:
- 웹은 F12-네트워크 체크,
- 메모리는 db쿼리 유무 체크(properties에서 쿼리 로그레벨 설정하여 쿼리체크)
캐시 적용 코드 참고: JSP플젝임
pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
application.properties
# MyBatis 관련 로깅 설정
logging.level.org.mybatis=DEBUG
logging.level.org.apache.ibatis=DEBUG
메인함수.java
@EnableCaching // Spring Boot Cache 사용을 선언
public class EgovBootApplication {...}
ItemDefault.java -> ItemServiceImpl에서 사용하는 파라미터
@EqualsAndHashCode
public class ItemDefault {...}
ItemServiceImpl.java
//CRUD
@Override
@Transactional // 쓰기모드
@CacheEvict(value = {"items", "totalCount"}, allEntries = true) //캐시 초기화
public Long save(Item item) throws Exception {
return itemMapper.save(item);
}
@Override
@Transactional // 쓰기모드
@CacheEvict(value = {"items", "totalCount"}, allEntries = true)
public Long delete(Item item) throws Exception {
// TODO Auto-generated method stub
return itemMapper.delete(item);
}
@Override
@Transactional // 쓰기모드
@CacheEvict(value = "items", key = "#item.pageIndex") //totalCount는 그대로
public Long update(UpdateItemDto item) throws Exception {
// TODO Auto-generated method stub
return itemMapper.update(item);
}
//추가메소드
@Override
@Cacheable(value = "items", key = "#searchItem.pageIndex") //value 로 꼭 캐시 영역을 지정하여 구분
public List<Item> findAllWithPage(ItemDefault searchItem) throws Exception {
// TODO Auto-generated method stub
return itemMapper.findAllWithPage(searchItem);
}
@Override
@Cacheable(value = "totalCount") //totalCount는 공통으로 사용하니 key로 구분 필요 없지
public int findTotalCount(ItemDefault searchItem) throws Exception {
// TODO Auto-generated method stub
return itemMapper.findTotalCount(searchItem);
}
댓글남기기