0. 들어가기 전
현재 프로젝트에서 자주 호출하는 '동선 기간 조회' API의 조회 성능을 높이기 위해 캐싱을 도입하기로 했습니다.
위의 페이지에서 다음과 같이 '동선 기간 조회' API 호출이 발생합니다.
- 달력 페이지에서 동선 기간 조회 1달 간격으로 조회
- 동선 보관함에서 월별 필터로 조회
해당 페이지들은 서비스에서 핵심적인 페이지들로 사용자들이 자주 사용할 것이라고 판단됐기 때문에,
해당 페이지에서 호출되는 '동선 기간 조회' API의 조회 성능을 높이기 위해 '캐싱'을 도입했습니다.
이번 포스팅에서는 프로젝트에 캐싱을 적용한 과정을 담아보겠습니다!
1. 캐싱이란?
일반적인 CS 관점에서의 캐싱은 다음과 같이 말할 수 있습니다.
캐시는 CPU와 RAM과 같이 느린 장치와 빠른 장치의 속도 차이로 인한 병목 현상을 줄이기 위한 중간 계층으로,
캐싱은 자주 사용하는 데이터를 미리 캐시(임시 저장소)에 저장해놓는 것이다.
CS 관점에서는 '느린 장치와 빠른 장치의 속도 차이로 인한 병목 현상을 줄이기 위한 중간 계층'의 의미도 중요하지만,
애플리케이션 서비스에 적용되는 관점에서 캐싱은 캐시 자체의 의미보다는 마지막 의미가 더 중요합니다.
캐싱은 자주 사용하는 데이터를 미리 캐시(임시 저장소)에 저장해놓는 것이다.
이러한 의미를 더 애플리케이션 서버 관점에 맞게 수정해보면, 다음과 같이 말할 수 있습니다.
캐싱은 DB로 접근하여 가져오는 자주 사용하는 데이터를 캐시 저장소에 임시로 저장을 해두고,
다음에 동일한 데이터 요청이 왔을 때 DB에 접근하지 않고 캐시 저장소에서 데이터를 주는 방식이다.
위의 의미를 이해하면, 캐싱을 사용했을 때 왜 조회 성능이 향상되는지 이해할 수 있습니다.
캐싱을 사용하면, 다음과 같은 효과를 얻을 수 있습니다.
- DB 부하 감소
- 응답 시간 향상
아래 그림은 캐싱을 도식화한 그림입니다.
(일반적인 캐싱 읽기 전략인 Look Aside 전략으로 나타냈습니다!)
- 첫 데이터 요청
- 처음 데이터 요청 시에는 캐시 저장소에 데이터가 저장되어 있지 않기 때문에 원래대로 DB에 접근하여 데이터를 가져옵니다.
- 이때, 가져온 데이터를 캐시 저장소에 저장 후에 응답합니다.
- 이후의 동일한 데이터 요청
- 동일한 데이터가 요청될 때, 캐시 저장소에 데이터가 존재하기 때문에 DB에 접근하지 않습니다.
- 따라서 바로 캐시 저장소에서 데이터를 가져옵니다.
결론적으로, DB를 거치지 않고 미리 저장된 데이터를 가져오기 때문에 조회 성능이 향상됩니다.
2. 캐싱 사용 시 고려할 점
그렇다면, 캐싱을 사용할 때 고려할 점은 어떤 것들이 있을까요?
제가 생각했을 때 다음과 같이 크게 3가지를 고려해서 캐싱을 적용하게 되었습니다.
- 어떤 데이터를 캐싱해야할까?
- Local Cache vs Global Cache
- 캐시 저장소로 무엇을 사용할까?
2-1. 어떤 데이터를 캐싱해야 할까?
이전에 캐싱의 정의, 원리 등을 살펴봤는데 그렇다면 데이터를 캐싱하는 것이 무조건 좋을까요?
앞서 살펴본 캐싱 원리를 다시 분석해봅시다.
앞서 설명할 때는 해당 원리를 살펴보며 캐싱의 장점들만 언급했었습니다.
여기서는 해당 원리에서 캐싱이 좋지 않은 상황들을 생각해보겠습니다.
- 대부분의 데이터가 처음 요청되는 데이터여서 동일한 데이터 요청이 자주 발생하지 않는 경우
- 위의 원리를 살펴보면 결국 동일한 데이터 요청이 왔을 때 캐싱의 효과가 발생합니다.
- 만약, 요청 시마다 새로운 데이터를 요청하게 된다면 캐싱하는 의미가 사라집니다.
- 이렇게 되면 리소스를 들여서 캐시 저장소를 사용할 이유가 없게 됩니다.
- 데이터가 자주 변경되는 경우
- 데이터가 자주 변경된다면, 캐시 저장소의 데이터와 DB 데이터 사이에 데이터 정합성 문제가 발생할 수 있습니다.
- 이를 방지하기 위해 데이터가 변경되는 경우 캐시의 데이터를 지우는 '캐시 무효화'를 수행합니다.
- 데이터가 자주 변경된다면 캐시 무효화 비용이 캐싱으로 얻는 절감 비용보다 커질 수 있습니다.
그래서 프로젝트에서 캐싱할 데이터를 생각했을 때, 다음과 같은 2가지 기준으로 고려하게 되었습니다.
- 동일한 데이터의 요청이 자주 발생하는가?
- 해당 데이터의 변경보다 조회가 빈번한가?
처음에 언급했듯이 제가 캐싱하기로 결정한 데이터는 '1달 간격의 동선 데이터'입니다.
해당 데이터의 특성은 제가 생각한 기준을 다음과 같이 만족하기 때문에 캐싱 데이터로 선택하게 되었습니다.
- 사용자들이 자주 드나드는 페이지에서 '1달 간격의 동선 데이터'를 요청
- 오늘 기준으로 이전의 날짜들은 동선을 생성하는 일이 거의 없으므로 변경보다 조회가 빈번하다고 판단
2-2. Local Cache vs Global Cache
캐시를 어디에 저장하는지에 따라서 Local Cache와 Global Cache가 나뉘게 됩니다.
Local Cache와 Global Cache에 대해서 정의와 장단점을 나열해보겠습니다.
- Local Cache
- 각 애플리케이션 서버의 로컬에 캐시 저장소를 두고 데이터 처리
- 각각의 애플리케이션 서버 내에 캐시 저장소가 존재하기 때문에 데이터 처리 속도가 빠르다.
- 애플리케이션 서버가 Scale-out 되어 있을 때 캐시 저장소 간의 데이터 정합성 일치를 위해 데이터를 전파해야한다.
- Global Cache
- 각 애플리케이션 서버와 분리되는 캐시 서버를 따로 두고, 각 애플리케이션 서버에서 캐시 서버에 접근하여 데이터 처리
- 캐시 서버로 접근하는 네트워크 비용이 존재하기 때문에 상대적으로 데이터 처리 속도가 느리다.
- 애플리케이션 서버가 Scale-out 되어 있어도 공통된 캐시 저장소를 사용하므로 데이터 정합성 문제가 없다.
위의 장단점을 살펴보면, 데이터 처리 속도와 데이터 정합성에 trade-off가 있음을 알 수 있습니다.
결론적으로 저는 다음과 같은 이유로 Global Cache를 선택하게 되었습니다.
- 일반적으로 서비스가 확장됨에 따라서 Scale-out 환경이 구축된다.
- 이때, 캐싱할 데이터는 서비스에서 중요한 도메인인 '동선' 데이터이므로 데이터 정합성이 중요하다.
- 이 상황에서 Local Cache를 사용한다면 데이터 정합성을 지키기 위해 데이터 전파 시간을 고려해야한다.
- 따라서, Global Cache와 속도가 크게 차이나지 않을 것 같다.
2-3. 캐시 저장소로 무엇을 사용할까?
캐싱을 사용하면 DB에 접근하지 않고 캐시 저장소에서 데이터를 가져오는 것을 확인했었습니다.
이때, 캐시 저장소로 어떤 저장소를 사용하는 것이 좋을까요?
일반적인 DB의 데이터 조회와 비교했을 때 훨씬 더 빠르게 데이터를 가져올 수 있는 저장소를 선택하는 것이
캐싱의 효과를 극대화할 것입니다.
이러한 이유로 보통 캐시 저장소로 In-Memory DB인 Redis와 Memcached를 대부분 캐시 저장소로 선택합니다.
저도 일반적으로 많이 사용하는 Redis와 Memcached 중에 고민을 하게 되었습니다.
결론적으로 다음과 같은 이유로 Redis를 사용하게 되었습니다.
- 이미 서비스에서 멱등키 저장소로 Redis를 사용 중 (관련 포스팅 : https://ksh-coding.tistory.com/149)
- Redis가 다양한 자료구조를 제공하여 추후 서비스 확장 시에도 유리
- 서버 프레임워크로 사용한 Spring에서 Redis를 추상화하여 쉽게 사용 가능한 라이브러리 제공
3. 프로젝트에 Redis 캐싱 적용하기 (feat. Spring Data Redis)
Spring에서 Redis를 쉽게 사용할 수 있도록 해주는 라이브러리인 Spring Data Redis를 통해 프로젝트에 적용해봅시다.
Spring Data Redis에 관한 이론적인 내용은 이전에 Session 저장소로 Redis를 이용한 포스팅의 2번 챕터에서 언급했었습니다.
따라서 이에 관한 부분은 링크만 남겨두고 넘어가도록 하겠습니다!
https://ksh-coding.tistory.com/128#%E2%80%BB%20spring-boot-starter-data-redis-1
3-1. 캐싱 키 설계
본격적으로 캐싱을 적용해보기 전에 어떻게 적용할지 프로젝트 맥락을 파악하면서 캐싱 키를 설계한 후에 적용해보겠습니다.
캐싱 데이터를 가져오는 것과 캐시 무효화를 위한 '캐싱 키'를 설계하는 작업을 진행해보겠습니다.
캐싱 키를 정하기 위한 현재 캐싱 데이터 및 서비스 컨텍스트는 다음과 같습니다.
- 캐싱해야 하는 데이터 : '1달 간격의 사용자별 동선 데이터'
- 캐싱 적용 메소드 : 동선 기간 조회 메소드
- 파라미터 : 사용자 ID, 조회할 시작 날짜, 조회할 끝 날짜
- 캐싱 무효화 메소드 : 동선 생성, 편집, 삭제 메소드
- 생성, 편집, 삭제 : Request Dto에 해당 동선 날짜 포함
이러한 상황에서, '1달 간격의 사용자별 동선 데이터'를 나타내는 캐싱 키는 조회 메소드의 파라미터를 합쳐서 다음과 같이 구성하였습니다.
- 사용자 ID + 조회할 날짜의 연월
- EX : 1:2024-06
3-2. Spring Data Redis 설정
이제 의존성을 추가하고 기본 Spring Data Redis 설정을 진행해보겠습니다.
build.gradle - Spring Data Redis 의존성 추가
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
RedisConfiguration - Cache 설정 추가
Redis Connection이나 RedisTemplate 등 다른 설정은 되어 있다고 가정하고 Cache 부분만 설정해보겠습니다.
@Configuration
public class RedisConfiguration {
// Redis Connection, Redis Template 등 설정
...
@Bean
public RedisCacheManager redisCacheManager() {
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
.entryTtl(Duration.ofDays(1L));
return RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(redisConnectionFactory())
.cacheDefaults(redisCacheConfiguration).build();
}
}
- 기본적으로 RedisCacheConfiguration을 통해 CacheManger의 설정을 구성합니다.
- serializeKeysWith(...) : 캐싱 Key의 직렬화 객체를 등록, 캐싱 Key는 문자열로 구성하므로 String 직렬화 객체 등록
- serializeValuesWith(...) : 캐싱된 데이터의 직렬화 객체를 등록, 캐싱 값들은 Json 형태로 직렬화하기 위해 관련 객체 등록
- entryTtl(Duration.ofDays(1L)) : 캐시 데이터의 유효 시간 설정
- 캐싱되는 데이터는 '1달 간격의 사용자별 동선 데이터'인데, 기본적으로 동선 생성, 수정, 편집 시에 캐시 무효화 진행
- 따라서 생성, 수정, 편집이 없이 조회만 있을 때만 고려했을 때 캐싱 효과를 최대한 받기 위해 긴 시간으로 캐싱 데이터의 유효 기간 설정
- 너무 길면 Side-Effect가 발생할 수 있으니 최대 하루로 설정
SpringBootApplication - @EnableCaching 설정 추가
@EnableCaching
public class XxxApplication {
public static void main(String[] args) {
SpringApplication.run(XxxApplcation.class, args);
}
}
@EnableCaching을 추가하여 자동으로 Spring 캐싱과 관련한 Spring 컴포넌트들이 등록되게 해줘야합니다.
3-3. 서비스 로직에 캐싱 적용
본격적으로 동선 관련 CRUD 메소드에 캐싱 & 무효화를 적용해보겠습니다.
RouteService - 동선 기간 조회 메소드 (캐싱 적용)
@Transactional(readOnly = true)
@Cacheable(
value = "routes",
key = "T(String).valueOf(#memberId).concat(':').concat(T(String).valueOf(T(java.time.YearMonth).of(#startDate.getYear(), #startDate.getMonthValue())))",
condition = "#startDate.plusMonths(1).minusDays(1).isEqual(#endDate)"
)
public RoutesResponse findRouteByPeriod(final Long memberId, final LocalDate startDate, final LocalDate endDate) {
...
}
- @Cacheable : Bean으로 등록된 CacheManager가 AOP를 통해 캐시를 생성하거나 반환합니다.
- 해당하는 캐시 데이터가 존재하지 않으면 메소드의 Return 값을 캐시 저장소에 저장합니다.
- 해당하는 캐시 데이터가 존재하면 캐시의 데이터를 그대로 반환합니다.
- value 옵션 : Cache의 이름을 지정합니다. (동선 데이터이므로 간단하게 'routes'를 지정했습니다.)
- key 옵션 : 캐시 데이터를 식별할 캐시 키를 지정합니다.
- 앞서 언급했듯이 '1달 간격의 사용자별 동선 데이터'를 나타내기 위해 '사용자 ID + 조회할 날짜의 연월'로 구성했습니다.
- SpEL을 통해 key를 구성할 수 있습니다.
- 타입을 나타내는 'T'와 파라미터를 가져오는 '#'을 통해 '사용자 ID:조회할 날짜의 연월'을 구성했습니다.
- EX) 1:2024-06
- condition 옵션 : 캐시 조건을 설정하여 조건을 만족할 때만 캐싱이 동작하도록 설정합니다.
- 해당 기간 조회 메소드는 월별 조회 이외의 자유롭게 시작 날짜, 끝 날짜를 조절하여 조회가 가능합니다.
- 예를 들면, 현재 서비스의 주 단위 달력에는 1주 간격으로 조회하여 해당 메소드를 사용하고 있습니다.
- '1달 간격의 사용자별 동선 데이터'만 캐싱할 것이기 때문에, 월별 조회(달의 첫날-달의 끝날)만 캐싱하도록 설정했습니다.
- 마찬가지로 SpEL을 통해 시작 날짜에 1달을 더해서 하루를 뺀 날짜가 파라미터의 끝 날짜면 캐싱이 동작하도록 설정했습니다. (한 달 조회인지 판별)
실제로 메소드를 실행시켰을 때, 다음과 같이 value(cacheName)과 key가 합쳐져서 캐싱되는 것을 알 수 있습니다.
(조건도 월별 조회 시에만 데이터가 저장되도록 잘 설정되었습니다.)
RouteService - 동선 생성 메소드 (캐시 무효화)
동선 생성, 편집, 삭제 시에 모두 캐시를 무효화하는 로직을 추가했습니다.
3개의 메소드 모두 캐시 무효화 로직이 동일하므로 '동선 생성' 메소드 1개를 대표로 기록하겠습니다.
@CacheEvict(
value = "routes",
key = "T(String).valueOf(#memberId).concat(':').concat(T(String).valueOf(T(java.time.YearMonth).of(#request.date().getYear(), #request.date().getMonthValue())))"
)
public Long addRoute(final Long memberId, final RouteAddRequest request) {
...
}
- @CacheEvict : Bean으로 등록된 CacheManager가 AOP를 통해 캐시를 무효화합니다.
- 해당하는 캐시 데이터를 캐시 저장소에서 삭제합니다.
- value 옵션 : 무효화할 Cache의 이름을 지정합니다. (이전에 캐시를 저장한 'routes'를 지정했습니다.)
- key 옵션 : 무효화할 캐시 데이터를 식별할 캐시 키를 지정합니다.
- 이전에 저장한 캐시 키와 같이 '사용자 ID + 조회할 날짜의 연월'로 구성했습니다.
- 마찬가지로 SpEL을 통해 key를 구성할 수 있습니다.
- 타입을 나타내는 'T'와 파라미터를 가져오는 '#'을 통해 '사용자 ID:조회할 날짜의 연월'을 구성했습니다.
- EX) 1:2024-06
4. 성능 테스트를 통해 캐싱 성능 알아보기
위에서 애플리케이션에 캐싱을 성공적으로 적용해보았습니다.
캐싱을 적용한 후에 다음과 같은 테스트 조건으로 성능 테스트를 진행해보았습니다.
- 테스트 데이터 : 각 사용자 당 6월 1달의 동선 20개
- 사용자 수 : 약 100명
- 테스트 시간 : 1분
- 환경 : 로컬 환경 대신 실제 AWS dev 환경에서 테스트 (실제 환경 + WAS와 RDS의 네트워크 영향을 고려하기 위해)
- 기본 k6 수치 비교 & grafana와 연동하여 시각적 그래프로 비교
프로젝트는 아직 배포하지 않은 개발 단계였기 때문에 사용자 수를 100명으로 작게 설정했습니다.
해당 100명의 사용자가 1분동안 다음과 같은 달력의 동선들을 1분동안 조회한다고 가정해보았습니다.
본격적으로 결과 수치 및 그래프를 살펴보기 전에 어떤 성능 지표를 기준으로 성능 개선을 판단할까요?
k6 테스트에서는 아래 그림과 같이 테스트 결과에서 다양한 지표들을 제공합니다.
(아래 테스트는 테스트 지표를 보기 위해 k6 공식 문서의 예제 스크립트를 실행한 것으로, 본 포스팅의 캐싱 테스트와는 관련이 없습니다.)
이처럼 다양한 지표들이 존재하지만 그 중에서 제가 성능 측정 기준으로 잡은 지표는 다음과 같습니다.
- cheks : 스크립트에서 정의한 결과(응답 코드)가 잘 도출됐는지를 체크, 즉 요청 성공 여부
- http_req_duration 중 p(95) 지표
- http_req_duration은 'http_req_sending + http_req_waiting + http_req_receiving'을 합한 지표로, 요청부터 응답까지 총 걸린 시간을 의미합니다.
- 캐싱의 목적이 조회 성능을 높여 사용자의 경험을 개선하는 것이므로, 응답 시간 지표가 가장 중요하다고 생각했습니다.
- 그 중 avg보다 p(95) 지표를 선택한 이유는, '평균의 함정'에 빠지지 않기 위함입니다.
- avg 평균 지표는 극히 소수의 요청이 오래 걸리게 된다면 해당 요청으로 인해서 평균 지표의 걸린 시간이 상승합니다.
- 예를 들어서, 1000개의 요청 중 995개의 요청이 1초 이내에 성공하고, 5개의 요청이 10초가 걸린다면 평균 지표는 요청 대다수가 1초 이내에 성공했음에도 1초 이상을 나타내게 될 것입니다.
- 성능 테스트 측면에서 소수의 요청은 무시해도 될 정도이기 때문에 이를 무시한 p(95) 지표를 사용했습니다.
- p(95)는 총 요청 중 성공한 95% 이상의 요청의 총 걸린 시간을 의미합니다.
- 그래서 앞의 예시에서 p(95) 지표는 1초 이내의 값을 나타낼 것입니다.
이러한 성능 지표를 기준 성능 테스트를 진행해봤습니다.
4-1. 캐싱 적용 전 결과
캐싱 적용 전에 요청이 모두 DB를 거쳐 동선 데이터를 가져오는 조회 성능 결과를 보면 다음과 같습니다.
- 요청은 총 2886개가 성공했습니다.
- 총 걸린 시간은 p(95) 지표를 보면 2.37s가 소요된 것을 알 수 있습니다.
4-2. 캐싱 적용 후 결과
이번에는 캐싱을 적용한 후에 성능 테스트를 진행했습니다.
캐싱 적용 후의 상황은 다음과 같이 2가지 상황으로 나눌 수 있습니다.
- 캐시 저장소에 동선 데이터가 없는 케이스 : 첫 조회 요청 or 캐시 무효화(동선 생성, 편집, 삭제) 후 조회 요청
- 캐시 저장소에 동선 데이터가 존재하는 케이스 : 첫 조회 요청 이후 조회 요청
따라서, 위의 2가지 상황을 나눠서 테스트를 진행했습니다.
1. 캐시 저장소에 동선 데이터가 없는 케이스 테스트
첫 조회 요청 또는 캐시 무효화 이후의 요청을 가정하고 Redis의 동선 관련 데이터를 초기화하고 테스트를 진행했습니다.
- 요청은 총 4990개가 성공했습니다.
- 총 걸린 시간은 p(95) 지표를 보면 734.6ms가 소요된 것을 알 수 있습니다.
- 그래프를 보면, 테스트 초반 부에 조회 성능이 나머지 조회 성능보다 크게 차이나는 것을 알 수 있습니다.
- 이는, 캐시 저장소에 동선 데이터가 존재하지 않아서 DB에서 직접 조회하기 때문에 소요 시간이 큰 것으로 추정됩니다.
- 캐시 저장소에 저장된 이후에는 조회 성능이 향상되는 추이를 볼 수 있습니다.
2. 캐시 저장소에 동선 데이터가 존재하는 케이스 테스트
이번에는 캐시 저장소에 동선 데이터가 존재하는 케이스를 테스트해보겠습니다.
캐시 저장소에 테스트할 조회 동선 데이터들을 미리 저장한 후에 테스트를 진행했습니다.
- 요청은 총 5900개가 성공했습니다.
- 총 걸린 시간은 p(95) 지표를 보면 50.36ms가 소요된 것을 알 수 있습니다.
5. 테스트 결과 요약 및 결론
캐싱 적용 전과 후 테스트 결과를 요약해서 개선을 수치로 나타내면 다음과 같습니다.
- 100명의 사용자의 1분 동안 동선 기간 조회 요청 개수
- 캐싱 전 : 2886개 성공
- 캐싱 후
- 캐시 저장소에 데이터 없는 케이스 : 4990개 -> 요청 개수 약 73% 증가
- 캐시 저장소에 데이터 있는 케이스 : 5900개 -> 요청 개수 약 104% 증가
- 100명의 사용자의 1분 동안 동선 기간 조회 요청 시 총 걸린 시간
- 캐싱 전 : 2.37s
- 캐싱 후
- 캐시 저장소에 데이터 없는 케이스 : 734.6ms -> 총 걸린 시간 약 3배 감소
- 캐시 저장소에 데이터 있는 케이스 : 50.36ms -> 총 걸린 시간 약 47배 감소
이렇듯, 결과적으로 총 요청 걸린 시간이 줄어듦에 따라서 단위 시간동안 수행하는 요청의 수도 늘어난 것을 알 수 있습니다.
특히, 캐싱 전과 캐싱 후 캐시 저장소에 데이터가 있을 때 사용자의 요청 시간을 비교해보면
2.37초와 0.05초로 엄청나게 큰 차이를 보이는 것을 알 수 있습니다.
UX 측면에서 사용자가 어떠한 기능을 사용할 때 단 1초로 인해서 사용자가 앱을 떠난다는 이야기를 많이 듣곤 했습니다.
따라서 이렇게 2초 넘게 차이를 줄이는 개선은 상당히 유의미한 개선이 될 수 있을 것 같습니다.
동선 생성, 편집, 삭제로 인해 캐시 무효화가 발생하더라도 약 3배의 응답 시간 감소를 보여주므로
캐싱을 적용하여 사용자의 응답 시간 감소를 통한 UX 개선, 성능 개선이 유의미한 결과를 가져온다는 것을 확인할 수 있었습니다.
Reference
https://yozm.wishket.com/magazine/detail/2296/
https://kingofbackend.tistory.com/289
'이슈 & 트러블 슈팅' 카테고리의 다른 글
[이슈] JPA N+1 문제 해결 및 성능 비교 (feat. Batch Size) (1) | 2024.07.11 |
---|---|
[이슈 & 트러블 슈팅] 동시성 보장하기 (feat. 비관적 락, Redis 분산 락) (3) | 2024.06.11 |
[이슈] 사용자의 중복 요청 방지하기(feat. 멱등키, Redis) (1) | 2024.06.07 |
[트러블 슈팅] 멀티 스레드 테스트 시 초기 데이터 오류(feat. 트랜잭션 격리 수준) (0) | 2024.06.06 |
[트러블 슈팅] SpringBoot에서 application.yml 이외의 yml 프로퍼티 적용하기 (0) | 2022.08.26 |