MODDO - Redis와 Spring Cache를 활용한 성능 최적화

원지윤·2025년 6월 3일
0

MODDO 프로젝트

목록 보기
3/4
post-thumbnail

🐹 MODDO란?

'모또’는 모임 정산 과정에서 발생하는 정산 지연등의 여러 문제에 대한 총무의 부담을 줄이기 위해 게이미피케이션 요소를 더하여 모임원들의 적극적인 정산 참여를 유도하는 서비스입니다.

✨ 주요 기능

👥 모임을 생성하고 모임에 참여했던 인원을 등록해요!

  • 모임의 이름과 참여자를 입력하면 더 간편하게 지출 내역을 입력할 수 있습니다.
  • 모임 구성원을 한눈에 확인하고 관리할 수 있습니다.

💵 모임에서 사용했던 지출내역을 간편하게 정리해요!

  • 사용했던 지출 내역을 각 차수(위치)별로 구분하여 쉽게 입력할 수 있습니다. - 지출 내역 입력 시 참여자를 자유롭게 추가/제외할 수 있고, 참여자별 사용 금액을 1/N로 구분할 수 있습니다.

🔗 모임의 지출내역을 공유하고 확인해요!

  • 작성한 정산 내역을 링크와 QR코드로 모임 참여자들에게 편리하게 공유할 수 있습니다.
  • 공유된 페이지에서는 참여자별, 차수(위치)별 지출내역을 한눈에 확인할 수 있어 투명한 정산이 가능합니다.
  • 입금 마감 시간과 입금 현황을 프로그레스바로 표시하여 모임의 정산 진행 상태를 쉽게 파악할 수 있습니다.

캐시 도입 이유

기존에는 토큰을 해석하여 바로 그룹 ID를 가져올 수 있었지만 정책 변경으로 코드 기반 조회 방식으로 수정하게 되었습니다.
이로 인해 사용자가 입력한 코드를 통해 그룹 ID를 가져올 때마다 데이터베이스에 매번 접근해야 하는 문제가 발생했습니다.
데이터베이스 접속이 잦아지면 전체 서비스의 성능 저하로 이어질 수 있기 때문에 이러한 문제를 해결하고자 캐시를 도입하기로 결정했습니다.

🤔 캐시 선택 과정

캐시 도입을 결정하면서 로컬 캐시와 글로벌(분산) 캐시 중 어떤 방식을 사용할지 고민했습니다.
현재는 단일 인스턴스 환경이기 때문에 로컬 캐시만으로도 충분히 성능 개선 효과를 볼 수 있습니다.
하지만 로컬 캐시의 경우 무중단 배포 등으로 여러 인스턴스를 동시에 띄워야 하는 상황이 오면 각 인스턴스 간 캐시 데이터의 일관성을 보장하기 어렵다는 한계가 있습니다.
또한 추후 비회원 참여자 선택과 같은 기능에서도 여러 인스턴스 간 데이터 일관성이 중요해질 수 있기 때문에 로컬 캐시보다는 분산 캐시를 활용하는 것이 더 유리하다고 판단했습니다.

🌐 글로벌(분산) 캐시 종류

글로벌(분산) 캐시는 여러 서버나 인스턴스에서 데이터를 공유할 수 있도록 해주는 캐시 시스템입니다. 대표적으로 다음과 같은 솔루션들이 있습니다.

  • Redis: 다양한 데이터 구조(문자열, 해시, 리스트, 셋 등)를 지원하며 높은 성능과 확장성, 데이터 영속성 옵션까지 제공하는 오픈소스 분산 캐시입니다. 복잡한 형태의 데이터를 저장하거나 분산 환경에서의 동기화가 필요한 경우에 적합합니다.

  • Memcached: 단순한 key-value 형태의 데이터를 빠르게 캐싱하는 데 특화된 오픈소스 분산 캐시입니다. 구조가 단순하고 대규모 웹 서비스에서 널리 사용됩니다. 하지만 데이터 영속성이나 복잡한 자료구조는 지원하지 않습니다.

  • Hazelcast: 분산 환경에서의 데이터 그리드, 캐싱, 메시징 등 다양한 기능을 제공하는 인메모리 데이터 그리드입니다. 대용량 데이터 처리와 고가용성이 필요한 엔터프라이즈 환경에 적합합니다.

이 중에서 분산 환경에서의 동기화가 잘 되고 다양한 형태의 key-value 데이터를 효율적으로 저장할 수 있는 Redis를 글로벌 캐시로 선택하였습니다.

🚀 캐시 도입

Spring에서는 Redis를 연동할 때 RedisTemplate을 직접 사용할 수도 있지만 Spring Cache의 추상화 기능을 활용하면 어노테이션만으로 간편하게 캐시를 적용할 수 있어 유지보수와 확장성 측면에서 더욱 유리합니다.
따라서 이번에는 Spring Cache를 함께 사용하기로 결정했습니다.

build.gradle

implementation 'org.springframework.boot:spring-boot-starter-cache:3.4.3'
implementation 'org.springframework.boot:spring-boot-starter-data-redis:3.4.3'

application.yml 설정

spring: 
  data:
    redis:
      host: localhost
      port: 6379

Redis.config

@EnableCaching
@Configuration
public class RedisConfig {

	@Value("${spring.data.redis.host}")
	private String host;

	@Value("${spring.data.redis.port}")
	private int port;

	@Value("${spring.data.redis.password:}")
	private String password;

	@Bean
	public RedisConnectionFactory redisConnectionFactory() {
		RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
		config.setHostName(host);
		config.setPort(port);

		// 비밀번호가 설정되어 있는 경우에만 적용(운영 환경)
		if (!password.isEmpty()) {
			config.setPassword(password);
		}

		return new LettuceConnectionFactory(config);
	}

	@Bean
	public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
		RedisTemplate<String, Object> template = new RedisTemplate<>();
		template.setConnectionFactory(redisConnectionFactory());
		return template;
	}

	@Bean
	public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
		return RedisCacheManager.builder(connectionFactory)
			.cacheDefaults(RedisCacheConfiguration.defaultCacheConfig()
				.entryTtl(Duration.ofMinutes(10)))
			.build();
	}
}

이 설정에서 가장 중요한 부분은 cacheManager() 메서드입니다.
Spring Cache는 내부적으로 CacheManager를 통해 다양한 캐시 구현체(Redis, EhCache, Caffeine 등)를 일관된 방식으로 사용할 수 있도록 추상화합니다.
즉 CacheManager에 Redis 연결을 주입하면 @Cacheable, @CacheEvict 같은 어노테이션만으로 손쉽게 Redis 기반 캐싱을 적용할 수 있습니다.
이 덕분에 코드가 간결해지고 추후 다른 캐시 구현체로의 전환도 설정만 바꾸면 되어 유지보수가 편리합니다.

캐시 적용 예시

@Cacheable(cacheNames = "groups", key = "#code")
public Long findIdByCode(String code) {
    return groupReader.findIdByGroupCode(code);
}

이렇게 하면 그룹 코드를 조회할 때마다 Redis에 캐시된 값을 먼저 확인하고 없으면 DB에서 조회한 뒤 캐시에 저장합니다.

Redis에서 실제 저장된 데이터 예시

127.0.0.1:6379> keys *
1) "groups::1Zdm7Jwl"
2) "groups::57N4J5p9"
3) "groups::5hyOwLnC"
4) "groups::4gBO2oS7"

캐시 적중/미스 통계 확인

127.0.0.1:6379> INFO stats
...
keyspace_hits:11
keyspace_misses:4

이처럼 Redis와 Spring Cache를 연동하면 간단한 설정과 어노테이션만으로도 효율적인 캐시 시스템을 구축할 수 있습니다. 이는 서비스 성능 개선뿐 아니라 유지보수와 확장성 측면에서도 큰 장점을 제공합니다.

✅ 캐시 적용 결과

실제 서버에 약 1만 개의 더미 데이터를 삽입한 뒤 JMeter를 활용해 성능 테스트를 진행했습니다.
그룹 코드는 단일 값이 아닌 CSV 파일로 추출한 약 200개의 서로 다른 코드를 순차적으로 사용하여 캐시 효과가 과도하게 반영되지 않도록 현실적인 조건에서 테스트를 설계했습니다.


테스트 방식

  • 데이터 규모: 약 1만 개 그룹
  • 그룹 코드: 200개 가량을 CSV로 준비, 각 요청마다 다른 코드 사용
  • JMeter 설정: Thread Group을 활용해 동시 요청 및 반복 요청 시나리오 구성

결과 비교

구분요청 수평균(ms)최소최대표준편차에러율TPS
캐시 미적용4,00038251,31856.080%12.3
캐시 적용4,00031242199.670%32.1

📝 성능 테스트 결과 비교

지표캐시 미적용캐시 적용변화폭/의미
평균 응답 시간38ms31ms약 18% 단축
최소 응답 시간25ms24ms
최대 응답 시간1,318ms219ms약 6배 감소
표준편차56.08ms9.67ms응답 일관성 5.8배↑
TPS(Throughput)12.3/sec32.1/sec2.6배 향상
에러율0.00%0.00%동일, 모두 안정적

👍 결론

캐시를 도입한 결과 평균 응답 속도 자체는 38ms에서 31ms로 약간만 개선되어 수치상으로는 큰 차이가 없어 보일 수 있습니다.
하지만 최대 응답 속도가 1,318ms에서 219ms로 대폭 감소하고 표준편차 역시 56.08ms에서 9.67ms로 크게 줄어들면서 전체 서비스의 응답이 훨씬 더 일정해지고 예측 가능해졌습니다.
즉, 사용자는 언제 요청하더라도 비슷한 속도로 결과를 받을 수 있게 되었고 서비스 운영자 입장에서도 트래픽이 몰리는 상황이나 일시적인 부하에도 안정적으로 대응할 수 있게 되었습니다.
이처럼 캐시 도입의 가장 큰 성과는 단순히 평균 속도를 줄이는 데 그치지 않고 서비스 전체의 응답 품질을 일정하게 유지하며 신뢰성을 높인 데 있다고 할 수 있습니다!

Ref

https://www.baeldung.com/spring-data-redis-properties
https://www.baeldung.com/spring-boot-redis-cache

0개의 댓글