(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);
}



댓글남기기