반응형

웹 캐시란?

 브라우저가 웹 서버에 접속하여 받아온 정적 컨텐츠 (html, 이미지, js 등)를 메모리 또는 디스크에 저장해 놓는 것을 말한다. 이후 HTTP 요청을 할 경우 해당 리소스가 캐시에 있는지 확인하고 이를 재사용함으로써 응답시간과 네트워크 대역폭을 줄일 수 있다.


웹 캐시의 장점

1. 불필요한 네트워크 통신을 줄인다.

 클라이언트가 서버에게 문서를 요청할 때, 서버는 해당 문서를 클라이언트에게 전송하게 된다. 재차 똑같은 문서를 요청할 경우 똑같이 전송하게 된다.

 캐시를 이용하면, 첫번째 응답은 브라우저 캐시에 보관되고, 클라이언트가 똑같은 문서를 요청할 경우 캐시된 사본이 이에대한 응답으로 사용될 수 있기 때문에, 중복해서 트래픽을 주고받는 네트워크 통신을 줄일 수 있다.

 

2. 네트워크 병목을 줄여준다.

 많은 네트워크가 원격 서버보다 로컬 네트워크에 더 넓은 대역폭을 제공한다. WAN 보다 LAN이 구성이 더 쉽고, 거리도 가까우며, 비용도 적게들기 때문이다. 만약 클라이언트가 빠른 LAN에 있는 캐시로부터 사본을 가져온다면, 캐싱 성능을 대폭 개선할 수 있다.

 

빠른 LAN을 통한 문서 조회

 

 예를들어 샌프란시스코 지사에 있는 사용자는 애틀랜타 본사로부터 문서를 받는데 30초가 걸릴 수 있다. 만약 이 문서가 샌프란시스코의 사무실에 캐시되어 있다면, 로컬 사용자는 같은 문서를 이더넷 접속, 즉 LAN을 통해 1초 미만으로 가져올 수 있을 것이다.

 

 

3. 거리로 인한 네트워크 지연을 줄여준다.

 대역폭이 문제가 되지 않더라도, 거리가 문제될 수 있다. 만약 보스턴과 샌프란시스코 사이에 네트워크 통신을 한다면 그 거리는 4,400 킬로미터이고, 신호의 속도를 빛의 속도(300,000 킬로미터 / 초)로 가정하면 편도는 약 15ms, 왕복은 30ms 가 걸린다.

 만약 웹페이지가 20개의 작은 이미지를 포함하고 있다면, 이 속도에 비례하여 통신 시간이 소요된다. 추가 커넥션에 의한 병렬처리가 이 속도를 줄일 수 있지만, 이보다 더 떨어진 거리거나, 훨씬 더 복잡한 웹페이지일 경우 거리로 인한 속도 지연은 무시할 수 없다.

 캐시는 이러한 거리를 수천 킬로미터에서 수십 미터로 줄일 수 있다.

 

4. 갑작스런 요청 쇄도(Flash Crowds)에 대처 가능하다.

 원 서버로의 요청을 줄이기때문에 갑작스런 요청 쇄도 (Flash Crowds) 에 대처할 수 있다.

 


적중과 부적중 (cache hit, cache miss)

 캐시에 요청이 도착했을 때, 그에 대응하는 사본이 있다면 이를 이용해 요청이 처리될 수 있다. 이를 캐시 적중(cache hit)라고 하고, 대응하는 사본이 없다면 원 서버로 요청이 전달된다. 이를 캐시 부적중(cache miss)라고 한다.


웹 캐시 체감하기

1. 기본 셋팅

 실제로 웹 캐시를 적용했을때와 그렇지 않았을 때의 차이를 비교해보자. 테스트 환경으로는 웹서버인 Apache 2.4 버전을 사용했으며, 보다 확실하게 체감하기 위해 Network 속도를 Slow 3G로 설정하였다. 이는 크롬 개발자 도구에서 설정 가능하다.

 

Network 속도 설정

 

 

 

1. 웹 캐시로부터 읽어오지 않은 응답

 

1) 최초 HTTP 통신

 최초 웹서버 접속 시 HTML 형태의 응답을 받고, HTML 내에 존재하는 정적 리소스를 로딩하기 위해 서버로 요청하고 있다.  이때 해당 리소스의 Size는 5~20 kb, Time은 약 2초 정도 걸렸다.

캐시를 적용하지 않았을 때의 첫번째 HTTP 통신

 

2) 두번째 HTTP 통신

 이후 동일 URI로 재요청 한다. 마찬가지로 HTML 형태의 응답을 받고, HTML 내에 존재하는 정적 리소스를 로딩하기 위해 다시 서버로 요청하고 있다. Size와 Time 모두 이 전 요청과 동일하다. 이를 통해 정적데이터가 필요할 때마다 서버로 요청한다는 것을 알 수 있다.

캐시를 적용하지 않았을 때의 두번째 HTTP 통신

 

 

2. 웹 캐시로부터 읽어온 응답

 

1) 최초 HTTP 통신

 최초 웹서버 접속 시 HTML 형태의 응답을 받고, HTML 내에 존재하는 정적 리소스를 읽어오기 위해 서버로 요청하고 있다. 이때 해당 리소스의 Size는 5~20 kb, Time은 약 2초 정도 걸렸다.

캐시를 적용했을 때의 첫번째 HTTP 통신

2) 두번째 HTTP 통신

 이후 동일 URI로 재요청 한다. 마찬가지로 HTML 형태의 응답을 받고, HTML 내에 존재하는 정적 리소스를 읽고 있다. 그런데 다른점이 있다. Size에 memory cache 가 적혀있고, Time은 0ms이다. 해당 리소스를 서버가 아닌 memory에서 조회했다는 것을 알 수 있다. 이를 통해 정적데이터가 캐시에 저장되어 있을 경우 캐시에서 로드한다는 것을 알 수 있다.

 


캐시 옵션 설정하기

 캐시는 Cache-Control이라는 HTTP Header로 설정할 수 있다. 먼저 해당 옵션을 살펴보자.

 

1. Cache-Control

설정 값 내용
no-store 캐시에 리소스를 저장하지 않는다.
no-cache 캐시 만료기간에 상관하지 않고 항상 원 서버에게 리소스의 재검사를 요청한다.
must-revalidate 캐시 만료기간이 지났을 경우에만 원 서버에게 리소스의 재검사 요청한다.
public 해당 리소스를 캐시 서버에 저장한다.
private 해당 리소스를 캐시 서버에 저장하지 않는다. 개인정보성 리소스이거나 보안이 필요한 리소스의 경우 이 옵션을 사용한다.
max-age 캐시의 만료기간(초단위)을 설정한다. 

 

※ 재검사가 뭔가요?

 재검사(Revalidation)는 신선도 검사라고도 하며, 캐시가 갖고 있는 사본이 최신 데이터인지를 서버를 통해 검사하는 작업을 말한다. 최신 데이터인 경우 304 Not Modifed 응답을 받게 되는데, 이는 '캐시에 있는 사본 데이터가 최신이며, 수정되지 않았다'라는 뜻을 의미한다. 이 경우 클라이언트는 해당 리소스를 캐시로부터 로드하게 된다. 

 

2. Apache httpd.conf 설정

 Apache 웹서버의 httpd.conf를 통해 설정할 수 있다. 필자의 경우 캐시의 만료기간을 10초로 설정하기 위해 아래 구문을 최하단에 넣어주었다. 서버를 재시작하여 설정을 적용하고 서버로 요청을 보내보자.

Header Set Cache-Control "max-age=10"

 

 만료기간 10초가 지나기 전에 다시 요청할 경우 캐시 메모리에 저장된 리소스를 가져옴을 확인할 수 있다. 

max-age=10 에 대한 테스트

 

  

3. HTTP Status 304

 캐시의 만료기간인 10초가 지나자 재검사를 진행했고, 서버의 리소스가 바뀌지 않아 304 상태코드를 리턴받고 있다. 그럼 재검증은 HTTP 메시지의 어떤 값을 통해 확인할 수 있는걸까?

200 > 304 StatusCode

 

 

요청 응답 데이터

 

 

 서버를 통해 리소스를 응답받으면 Response Header에 해당 리소스의 마지막 수정날짜가 들어간다. 아래 이미지를 보면 Last-Modified 헤더에 Sat, 04 May 2013 12:52:00 GMT로 되어있다.

HttpResponse - Last-Modified

 

 이를 우리나라 시간으로 환산하면 2013년 5월 4일 21시 52분인데, 실제 서버에 있는 리소스의 마지막 수정날짜이다.

apache_pb.gif 파일 정보

 

 

 이 후 서버로 리소스 요청을 보낼 때 요청 헤더의 If-Modified-Since 에 리소스의 마지막 수정날짜를 보낸다. 서버는 이 값과 실제 수정 날짜를 비교하여 일치하지 않을 경우, 즉 리소스가 변경된 경우에 200 코드와 함께 해당 리소스를 내려준다.

리소스가 변경되지 않았을 땐 304를, 리소스가 삭제되었다면 404를 응답한다.

 


캐시 포톨로지

 캐시는 한 명의 사용자에게만 할당될 수 있고, 수천 명의 사용자에게 공유될 수도 있다. 한명에게만 할당된 캐시를 전용 캐시, private cache라 하고, 여러 사용자가 공유하는 캐시는 공용 캐시, public cache 라고 한다.

 

 private cache의 대표적인 예는 방금 설명했던 브라우저 캐시이다. 웹 브라우저는 개인 전용 캐시를 내장하고 있으며 컴퓨터의 디스크 및 메모리에 캐시해놓고 사용한다.

Private cache

 

 public cache의 대표적인 예는 프락시 캐시라고 불리는 프락시 서버이다. 각각 다른 사용자들의 요청에 대해 공유된 사본을 제공할 수 있어 private cache보다 네트워크 트래픽을 줄일 수 있다.

 

Public cache

 


캐시 처리 단계

 웹 캐시의 기본적인 동작은 총 일곱 단계로 나뉘어져 있다.

 

캐시 처리 단계

1. 요청 받기

  먼저 캐시는 네트워크 커넥션에서의 활동을 감지하고, 들어오는 데이터를 읽어들인다. 즉, 서버로 요청하기 전 캐시에서 선 작업이 진행된다. (캐시는 HTTP의 응용계층에서 처리된다.)

 

2. 파싱

 캐시는 요청 메시지를 여러 부분으로 파싱하여 헤더 부분을 조작하기 쉬운 자료구조에 담는다. 이는 캐싱 소프트웨어가 헤더 필드를 처리하고 조작하기 쉽게 만들어준다.

 

3. 검색

 캐시는 URL을 알아내고 그에 해당하는 로컬 사본이 있는지 검사한다. 만약 문서를 로컬에서 가져올 수 없다면, 그것을 원 서버를 통해 가져오거나 실패를 반환한다.

 

4. 신선도 검사

 HTTP는 캐시가 일정 기간 동안 서버 문서의 사본을 보유할 수 있도록 해준다. 이 기간동안 문서는 신선하다고 간주되고 캐시는 서버와의 접촉 없이 이 문서를 제공할 수 있다. 하지만 max-age를 넘을 정도로 너무 오래 갖고 있다면, 그 객체는 신선하지 않은 것으로 간주되며, 캐시는 그 문서를 제공하기 전 문서에 어떤 변경이 있었는지 검사하기 위해 서버와 통신하여 재검사 작업을 진행한다.

 

5. 응답 생성

 캐시는 캐시된 응답을 원 서버에서 온 것처럼 보이게 하고 싶기 때문에, 캐시된 서버 응답 헤더를 토대로 응답 헤더를 새로 생성한다.

 

6. 전송

 응답 헤더가 준비되면, 캐시는 응답을 클라이언트에게 돌려준다. 

 

7. 로깅

 대부분의 캐시는 로그 파일과 캐시 사용에 대한 통계를 유지한다. 각 캐시 트랜잭션이 완료된 후, 캐지 적중과 부적중 횟수에 대한 통계를 갱신하고, 로그파일에 요청 종류, URL 그리고 무엇이 일어났는지를 알려주는 항목을 추가한다.

반응형
반응형

1. 개요

 국토 교통부에서 제공하는 법정동 코드를 다운받아 DB 테이블에 밀어 넣고, JPA를 통해 주소를 검색하는 API를 구현하였다. 하지만 데이터의 양이 많아 응답까지 1초 ~ 3초정도가 소요되는 것을 보고 Redis의 캐싱 기능을 도입하게 되었다. 그 과정을 정리한다.

 

 


2. Cache 설정

2.1. RedisCacheConfig.kt

@Configuration
@EnableCaching
class RedisCacheConfig {

    @Bean
    fun redisCacheManager(cf: RedisConnectionFactory?): CacheManager? {
        val redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
            .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(StringRedisSerializer()))
            .serializeValuesWith(
                RedisSerializationContext.SerializationPair.fromSerializer(
                    GenericJackson2JsonRedisSerializer()
                )
            )
            .entryTtl(Duration.ofDays(1))
        return RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(cf!!)
            .cacheDefaults(redisCacheConfiguration).build()
    }
}

 

 TTL은 하루로 설정하였고, 직렬화 방식은 key는 String, value는 GenericJackson2JsonRedisSerializer를 사용했다. 이 형식을 사용한 이유는 캐싱할 value 값이 List<Object> 형태였기 때문이다.

 

2.2. Applicatoin.kt

@SpringBootApplication
@EnableCaching
class Application

fun main(args: Array<String>) {
    runApplication<Application>(*args)
}

 캐시를 사용하기 위해 Application 실행파일에 @EnableCaching을 추가하였다.

 

2.3. Service.kt

@Service
class AddressService (
    private val addressRepository: AddressRepository
){

    @Cacheable(value = ["Address"], key = "#searchWord", cacheManager = "redisCacheManager")
    fun searchAddress(searchWord : String) : List<AddressResponse> {
        return addressRepository.findBySearchWordLike(searchWord)
    }

}

 캐시를 적용하고자 하는 메서드에 @Cacheable 을 설정한다. [value]는 redis에 저장되는 Key의 prefix, [key]는 suffix 이다. 만약 동일한 키를 가진 캐싱 데이터가 있을 경우 cache hit가 발생하여 캐싱된 데이터를 조회하고, 없을 경우 cache miss가 발생하여 DB에서 데이터를 조회한 후 데이터를 캐싱할 것이다.

 cacheManage에는 RedisCacheConfig 에서 생성한 Bean 이름을 넣어준다.

 

2.4. Controller.kt

@Controller
@RequestMapping("/api/address")
class AddressController (
  private val addressService : AddressService
) {
...
    @GetMapping("/search")
    fun searchAddress(@RequestParam("searchWord") searchWord : String) : ResponseEntity<List<AddressResponse>> {

        val list = addressService.searchAddress(searchWord)
        return ResponseEntity.ok(list)
    }
...
}

 Controller에서 캐싱 메서드를 호출한다.


3. 테스트

3.1. 최초 검색

 최초 검색시 cache miss가 발생함에 따라 DB 조회 및 redis 에 데이터를 캐싱하는 과정을 거치게 된다. IDE에 찍힌 로그를 보면 JPA 쿼리 결과가 조회되는데, 이는 DB를 조회하여 데이터를 가져왔다는 사실을 알수있다.

 캐싱된 데이터는 redis-cli 를 실행 후 keys, get 명령어를 통해 확인할 수 있다. 시간은 1400ms가 소요되었다.

postman 테스트 결과 #1
JPA 쿼리 실행 결과 #1

 

3.2. 두번째 요청

postman 테스트 결과 #2

  동일한 값으로 요청하니 cache hit가 발생하여 DB를 조회하지 않고 캐싱된 데이터를 조회하고 있다. DB를 조회했다면 JPA 쿼리 결과가 콘솔에 찍힐테지만, 아무 로그도 찍히지 않고 있다. 시간은 72ms가 소요되었다.

 

3.3. 새로운 키워드로 요청

 캐싱된 값이 없으니 cache miss가 발생하고 3.1. 최초검색과 비슷한 시간이 소요됨을 확인하였다.


4. 문제

4.1. 건당 Redis 메모리 사용량

redis-cli 의 info memory 명령어를 통해 redis 메모리 사용량을 확인할 수 있다. 확인해보니 시, 도 별로 검색할 경우 한 건당 메모리가 약 1M 정도 사용되었다. 결코 적은 양이 아니다. 만약 시, 도별 다른 키워드로 검색을 한다면 cache miss가 발생하여 데이터를 캐싱할 것이고, 1M 정도가 추가로 사용될 것이었다.

 TTL을 하루로 설정하였기에 RAM 용량이 8GB라면 각기 다른 키워드로 8000번 호출 시 redis 서버가 다운될 가능성이 있다.

 

4.2. 주소 검색의 특성

주소 검색 특성상 많은 사람들이 똑같은 키워드보다는 본인이 사는 동이나, 지번으로 검색할 확률이 높다. 새로운 키워드가 들어올 확률이 높다는 것이다. cache miss가 빈번하게 발생할 것이고, 응답 시간은 DB를 단독으로 사용하는 것보다 느린 케이스도 빈번할 것이다. redis 메모리 사용량도 빠르게 늘어날 것이다.

 


5. 개선

 주소 검색 시 선 작업으로 모든 주소를 조회하는 로직을 추가하였다. 그 후 조회한 리스트에서 검색어에 대한 주소 값을 추출하는 방식을 채택했다.

 캐싱은 모든 주소를 조회하는 부분에 적용하였다. cache hit시 리스트에서 필터링하는 시간과 비용만 소비하면 된다. 물론, 첫번째 방식 사용 시보다 응답 속도가 느린 케이스도 있다. TTL이 만료되는 24시간 후 cache miss가 발생할 때 데이터를 가져올때나 동일 키워드로 여러번 검색할때이다. 일단 개선 로직 구현 후 이에 대한 트레이드 오프를 분석할 예정이다.

 

5.1. 개선 AddressController.kt

...
    @GetMapping("/search")
    fun searchAddress(@MemberAuthentication authenticationAttributes: AuthenticationAttributes
                      , @RequestParam("searchWord") searchWord : String) : ResponseEntity<List<AddressResponse>> {
        val allAddressList = addressService.searchAllAddress()
        val findAddressList = addressService.searchAddress(allAddressList, searchWord)

        return ResponseEntity.ok(findAddressList)
    }
...

 addressService.searchAllAddress() 메서드를 통해 캐싱되어있는 모든 주소리스트를 조회하고, searchAddress() 메서드를 통해 키워드를 포함하는 요소를 찾아 리턴한다.

 searchAddress() 메서드 안에서 searchAllAddress()를 호출할 수 도 있지만, AOP를 사용하는 캐싱 메서드의 특성 상 Self-invocation 이슈가 있어 Controller에서 따로 호출하였다.

 

더보기

* Self-invocation 으로 인한 이슈

 AOP 기반으로 호출되는 캐싱 메서드의 특성 상 같은 클래스 내 위치한 특정 메서드에 의해 캐싱 메서드가 호출될 경우 캐싱 기능이 동작하지 않는다. 이에따라 컨트롤러에서 캐싱 메서드를 호출하여 캐싱 처리하고, 응답받은 값을 통해 서치하는 메서드를 호출하는 방식을 채택하였다.

 

5.2. 개선 AddressService.kt

@Service
class AddressService (
    private val addressRepository: AddressRepository
){
    ...

    @Cacheable(value = ["AllAddress"], key = "", cacheManager = "redisCacheManager")
    fun searchAllAddress() : List<AddressResponse> {
        val list = addressRepository.findAll()
        return list.stream()
            .map { e -> AddressResponse(e.id, e.addr) }
            .collect(Collectors.toList())

    }

    fun searchAddress(list : List<AddressResponse>, searchWord: String): List<AddressResponse> {
        return list.stream()
            .filter { address -> address.name.contains(searchWord) }
            .collect(Collectors.toList())
    }
}

 searchAllAddress() 메서드는 DB에서 모든 주소 값을 읽어와 List<AddressResponse> 형태로 리턴하며 캐싱한다. cache miss가 발생 시 시간이 다소 소요된다. searchAddress는 list에 대해 필터링하는 메서드이다.


6. 개선 테스트

 

6.1. 최초 검색

 1차 테스트와 동일한 검색어로 검색한 결과 cache miss 발생 시 응답속도가 1400 ms 에서 2170 ms로 0.67초 느려진 것을 확인하였다. 모든 주소 값을 읽고, 캐싱하는 부분으로 인한 시간이라 생각한다.

개선 로직에 대한 postman 테스트 결과 #1

 

6.2. 두번째 검색

 동일한 키워드로 두번째 검색을 하였다. 전자의 경우 '시'라는 키워드에 대한 결과가 미리 캐싱되어있었기에 소요되는 시간이 조금 더 걸릴것으로 예상했으나, 72ms에서 92ms로 생각보다 시간 차이가 얼마 나지 않음을 알 수 있었다. 몇번 더 테스트해봐도 100ms 안밖이었다.

 cache miss가 발생하지 않았기에 사용되는 메모리는 증가하지 않았다. 참고로 약 9MB 정도를 사용중이다. 이제 다른 키워드로 검색해보자.

개선 로직에 대한 postman 테스트 결과 #2
Redis 메모리 사용량 #1

6.3. 새로운 키워드로 검색

 서울, 강원 등 여러 키워드로 검색해보았다. 그 결과 걸린 시간은 모두 100ms 안팎임을 확인하였다. 메모리 사용량도 변함이 없다.

개선 로직에 대한 postman 테스트 결과 #3
개선 로직에 대한 postman 테스트 결과 #4
Redis 메모리 사용량 #2


7. 장단점

 개선 방식으로의 변경을 통해 확인한 장단점을 정리해보았고, 트레이드 오프를 고려했을 때 개선된 방식이 훨씬 더 효율적이라는 결론을 내렸다.

 

* 장점

 1. Redis 메모리 부족 위험에 대해 완전히 벗어날 수 있다.

 2. 새로운 키워드로 검색해도 cache miss가 발생하지 않아 속도가 빠르다. (1400 ms > 100ms로 개선)

 3. 동일한 키워드로 검색해도 이전 방식과 속도차이가 거의 나지 않는다. (약 20ms 차이)

 

* 단점

 1. cache miss 발생 시 이전보다 더 많은 시간이 소요된다. (1400 ms > 2170 ms 로 증가)

 2. 데이터 정합성 문제 발생 확률이 이전보다는 높다. (하지만 쓰기 작업을 연마다 하는 특성 상 큰 문제는 되지 않을 것이라 판단하였다)


8. 회고

 데이터 수정이 없는 주소 데이터 특성에 의해 무작정 캐싱 도입을 하였으나, 큰 성능향상은 얻지 못했고, 오히려 Redis 메모리에 대한 잠재적인 문제와, cache miss 시 속도문제를 안게 되었다. 이건 캐싱에 대한 이해도가 부족해 발생한 것이라 생각하여 개념, 용어, 방법, 전략들을 공부하였다.

 

 전략을 선택할 때 가장 중요한 건 캐싱할 데이터의 성격을 분석하는 것이라 생각한다. 메모리 용량은 많아봤자 32GB로 제한적이다. 많은 데이터를 캐싱할 수록 메모리의 부담은 커지고, 메모리 부담을 덜기 위해 무작정 TTL을 낮춘다면, 잦은 DB 혹은 API 통신으로 시간이 더 걸릴 수 있다. 만약 특정 키워드로 반복적인 읽기가 많은 작업이었다면 캐싱을 적용하기 전보다 성능이 낮아질수도 있다.

 조회 작업이 많은지, 쓰기 작업이 많은지, 쓰기 작업 없진 않는지, 있다면 그 빈도는 어떤지, 조회만 하는지 등을 분석하여 메모리 사용량을 낮추고, 속도는 비교적 높일 수 있는 캐싱 전략을 세워야 하는데, 이는 캐싱 데이터의 성격에 따라 달라진다.

 주소 검색의 경우 쓰기 전략을 Write Around를, 읽기 전략은 Look Aside로 설정하고 코드를 개선해나갔다. 주로 읽기 작업이고, 쓰기작업은 연 주기로 공공기관을 통해 데이터를 다운받아 DB에 밀어넣는 것 하나이기 때문이다. 또한 쓰기 작업이 되어 새로 등록된 주소는 cache miss가 발생할 때 조회해도 서비스 운영에는 큰 문제가 발생하지 않고, 즉시 조회를 해야 한다고 해도 캐시를 수동으로 만료시키는 방안도 있었다.

 이렇게 데이터의 성격을 파악하고 전략을 수립한 상태에서 리팩토링을 하니 속도와 메모리 효용성을 향상시킬 수 있는 방법을 구상하고 적용할 수 있게 되었다. 캐싱을 적용하는 것은 어렵지 않다. 다만 데이터의 성격을 분석하여 캐싱을 왜 적용하는지, 어느 부분에 적용해야 좋은지, 적용을 통해 얻을 수 있는 장단점은 어떻고, 적절한 트레이드 오프인지를 생각하는 것이 중요하다고 생각한다.

반응형

+ Recent posts