포스트

프로젝트:샐로그 / 최적화 - 조회 성능 개선 2 (캐싱)


개요

지난 포스트에서는 조회 성능 개선을 위해 비동기 처리를 적용했었다.

하지만 결과를 보니 성능 개선은 없었고, 오히려 메모리 사용량이 늘어 성능이 저하된 모습을 볼 수 있었다.

리소스가 제한된 상황에서 병렬적으로 처리하려다 보니 오히려 추가적인 오버헤드 발생을 제어하지 못해 그런 것으로 보인다.


그래서 이번 포스트에서는 두 번째 솔루션인 캐싱을 적용하고 결과를 살펴볼 것이다.


이 캐싱은 조회 요청이 많고 반복되는 경우 효과적으로 생각할 수 있다.

캐싱이라는 것 자체가 자주 요청되는 데이터를 DB에서 미리 로드하여 메모리에 저장하고 요청 시 DB 접근 없이 데이터를 빠르게 응답한다.

이로써 캐싱은 DB 접근을 줄여서 성능을 향상 시키고 응답 시간을 단축시킬 수 있다.


DB에 많은 데이터가 있지 않고 대용량 트래픽을 발생시키는 현재 상황에 캐싱이 적합하다고 생각했다.

만약 DB에 데이터가 많은 경우 인덱싱과 동시에 적용해보는 방향도 괜찮을 것이라 생각한다.




캐싱 적용

캐싱을 적용하기 앞서 의존성을 추가해야 한다.

1
	implementation 'org.springframework.boot:spring-boot-starter-cache'

빌드 그래들에 위와 같은 의존성을 추가하여 스프링 부트에서 캐싱 기능을 사용할 수 있다.


이후 애플리케이션의 메인 클래스에

1
2
3
4
5
6
7
8
9
@SpringBootApplication
@EnableCaching // DB 조회 성능 향상을 위해 캐싱 적용
public class SalogApplication {

	public static void main(String[] args) {
		SpringApplication.run(SalogApplication.class, args);
	}

}

위와 같이 @EnableCaching 어노테이션을 추가해 이 애플리케이션에서 캐싱 기능을 사용한다고 명시를 해야한다.

이로써 캐싱 기능을 사용하기 위한 준비는 되었고, 이제 캐싱을 적용할 타겟 메서드에 캐싱 옵션을 추가하여 사용한다.


캐싱 기능을 적용하기 위한 타겟 메서드는 서비스 레이어의 서비스 로직이다.

1
2
3
4
5
    @Cacheable(value = "getIncomes", key = "#memberId + '_' + #page + '_' + #size + '_' + #incomeTag + '_' + #date")
    public MultiResponseDto<IncomeDto.Response> getIncomes(String token, int page, int size, String incomeTag, String date) {
                ...
            }
        }

이와 같이 @Cacheable 어노테이션을 적용해 캐싱을 설정한다.

여기서 value 값은 “getIncomes”라는 이름의 캐시를 정의한 것이다.

이 캐시 이름을 통해 Spring 캐시 매니저가 관리할 수 있도록 한 것이고, 이를 통해 캐시에 저장된 데이터에 접근하거나 사용할 수 있게 된다.


두 번째로 key 값은 캐시의 키를 정의하는 부분이다.

이 키는 메서드의 파라미터를 조합하여 생성되는데 캐시에서 데이터를 저장하고, 조회할 때 사용하는 고유한 식별자를 생성하는 데 도움을 준다.

즉, 이 키들을 조합하여 캐시에 저장된 값을 식별하고, 결과를 캐시할 수 있게 된다.


캐시의 키를 파라미터 기준으로 지정하는 이유는

메서드의 입력 파라미터에 따라 결과가 달라질 수 있기 때문에 각 파라미터 조합에 대해 고유한 키를 생성함으로, 다양한 결과를 개별적으로 캐시할 수 있기 때문이다.

또한, 동일한 파라미터로 메서드를 호출하는 경우 캐시된 결과를 재사용할 수도 있다.

추후 메서드의 유연성을 높이고 어떤 파라미터를 입력해 캐시되었는지를 쉽게 파악할 수 있어, 디버깅이나 캐시 관리가 용이해진다.


이런 식으로 캐시의 이름과 파라미터를 기반으로 키 값을 설정하고 캐싱을 적용할 수 있다.




캐시 미스와 캐시 히트

추후 이 캐시를 활용하기 위해 알아가야 할 부분이 몇 가지 더 있다.

캐시 데이터가 메모리에 저장되는 시점인데, 이 시점을 “캐시 미스”라고 한다.

캐시 미스

  • 캐시 미스는 클라이언트가 데이터를 요청했을 때, 해당 데이터가 캐시에 존재하지 않으면 “캐시 미스”가 발생한다. 이 경우에는 메서드가 실제로 실행되어 데이터가 조회되고, 이 결과가 캐시에 저장된다.


캐시 미스가 발생하여 데이터가 캐시 메모리에 저장된 이후 요청 시 이미 데이터가 캐시에 존재한다면 “캐시 히트”가 발생한다.

캐시 히트

  • 캐시 히트는 클라이언트가 요청한 데이터가 이미 캐시에 존재하면 발생하고, 메서드는 실행되지 않고 캐시에서 데이터를 반환한다. 이를 통해 DB에 접근하지 않아도 응답이 가능한 것이다.




캐싱 적용 주의사항

캐싱을 적용하는 것은 높은 트래픽을 처리할 때 유용하고 DB의 부하를 줄이는 데 크게 기여할 수 있는 기술이지만 주의할 점이 있다.

바로 메모리에 미리 DB의 데이터를 저장하는 기술이기 때문에 추후 해당 데이터가 변환되면 데이터의 일관성을 해칠 수 있다는 것이다.

이를 위해 어느정도 핸들링이 필요하다.

그래서 캐싱 처리하는 데이터들에 대해 이 데이터를 변환 시킬 수 있는 메서드는 아래와 같은 어노테이션을 적용하여 변환 시 마다 캐시를 초기화 해야한다.

1
@CacheEvict(value = "getIncomes", allEntries = true)

이 @CacheEvict 어노테이션은 스프링 프레임워크에서 캐시를 관리하는 데 사용된다.

캐시에서 특정 데이터를 제거하는 역할을 한다.

value 옵션에는 삭제할 캐시 이름을 지정한다.

allEntries 옵션은 캐시의 모든 엔트리를 의미한다.

즉, 특정 키에 해당하는 엔트리를 삭제할 지 여부를 정하는 옵션인데, 현재는 모든 데이터를 삭제하도록 true 지정했다.

이외에도

key 옵션을 활용하여 특정 엔트리만을 제거할 수 있으며

beforeInvocation 옵션을 활용해 메서드 실행 전 캐시를 비울 지 여부를 결정할 수 있다.


이 방법을 통해 데이터가 변경되었을 때 캐시를 갱신하여 데이터의 일관성을 유지하자.




캐시 적용 모니터링 결과

이제 캐시를 적용했으니 동일한 모니터링 결과를 살펴보고 성능이 개선되었는 지 살펴보아야 한다.

가장 중점적으로 볼 것은

DB에 대한 접근 수를 줄였기 때문에 메모리 사용량을 확인할 것이고 최대 HTTP 요청 지연 시간을 살펴보고 응답 속도가 얼마나 개선되었는 지 확인할 것이다.

비교를 위해 아무 작업도 하지 않은 기본 상태를 먼저 살펴보자.

메모리 사용량은 요청 시 크게 올랐다가 Gc 실행 후 큰 폭으로 하락한다.

이후 점진적으로 증가하는 모습을 보인다.

또한 HTTP 요청 지연 최대 시간이 0.5에 긴 시간 머무른다.


이제 캐싱을 적용한 모니터링 결과를 살펴보자

앞서 말한 매트릭이 큰 폭으로 하락한 것을 볼 수 있다.

우선 메모리 사용량의 경우 눈에 띄게 큰 폭으로 줄어들었고 이후 메모리 누수로 인해 값이 증가해도 이전 상태에 비해 아주 양호한 모습을 보인다.

다음으로 HTTP 응답 지연 최대 시간의 경우 0.5 초대에서 요청 초기 시 0.2초, 이후 0.1초 대로 줄어든 모습을 보인다.

이러한 모습은, 메모리에 데이터를 저장하고 DB 접근 없이 해당 데이터를 바탕으로 응답을 보내기 때문에 접근 시간이 줄어들었기 때문으로 보인다.

즉, 처음에만 데이터를 읽어오고 이후에는 DB에 접근하지 않는다는 의미이다.


캐싱을 적용하고 시스템이 전반적으로 향상된 것으로 보인다.

미미하지만 CPU 사용량도 줄었고 DB에 접근을 자주 할 필요가 없기 때문에 메모리 사용량, 요청 지연 최대 시간이 큰 폭으로 줄었다.

이를 통해 샐로그 애플리케이션의 조회 성능을 크게 개선 시켰다.




후기

여기까지 긴 여정이었다.

이후에도 기회가 된다면 제한된 리소스를 어느정도 해소할 수 있도록 메모리 누수를 핸들링할 생각이다.

하지만 아직 샐로그에 대해 처리해야할 내용이 조금 더 남아 있기 때문에 다른 이슈를 다 처리하고 나면 진행해보려 한다.


이번 여정에서 가장 힘들었던 것은 “방법”보다는 “목표”였다.

목적이야 샐로그 애플리케이션의 성능 개선이 목적인데, 이번 모니터링 후 계속해서 목표가 달라지고, 모니터링 시각화를 했음에도 어떻게 읽어야 할지가 아주 어려웠다.

방법이야 찾아보면 다 나오기 때문에 큰 문제가 되지 않았으며, 에러가 발생하거나 한 경우도 거의 없었기 때문에 내가 실수한 것이거나 지식이 부족한 것이 아니면 대부분 해결되는 문제였다.

그러나 이 매트릭 데이터들을 어떤 식으로 분석하고 어떻게 생각할 것이냐에 따라 개선을 위해 어떻게 목표를 가지고 진행해야할 지가 가장 어려웠던 것 같다.

아직도 내가 잘 한 건지 모르겠고, 누가 훈수를 두어줬음 좋겠다…


여튼 데이터 분석과 어떤 해결법을 적용하는 것이 적합할 지 생각하는게 가장 어려웠고 그럼에도 성장한 느낌이 들어서 나쁘지 않은 경험이었다.

이 블로그는 저작권자의 CC BY 4.0 라이센스를 따릅니다.