[TIL]대용량 데이터 처리

기 원·2025년 5월 8일

[Project] Spring-plus

목록 보기
6/8
post-thumbnail
  • 대용량 데이터 처리 실습을 위해, 테스트 코드로 유저 데이터를 100만 건 생성해주세요.
    • 데이터 생성 시 닉네임은 랜덤으로 지정해주세요.
    • 가급적 동일한 닉네임이 들어가지 않도록 방법을 생각해보세요.
  • 닉네임을 조건으로 유저 목록을 검색하는 API를 만들어주세요.
    • 닉네임은 정확히 일치해야 검색이 가능해요.
  • 여러가지 아이디어로 유저 검색 속도를 줄여주세요.
    • 조회 속도를 개선할 수 있는 여러 방법을 고민하고, 각각의 방법들을 실행해보세요.
    • README.md 에 각 방법별 실행 결과를 비교할 수 있도록 최초 조회 속도와 개선 과정 별 조회 속도를 확인할 수 있는 표 혹은 이미지를 첨부해주세요.

1. 최초 설계

코드

Controller

    @GetMapping("/user/nickname")
    public ResponseEntity<User> getUserByNickname(@RequestParam String nickname) {
        User user = userService.getUserByNickname(nickname);
        return ResponseEntity.ok(user);
    }

Repository

Optional<User> findByNickname(String nickname);

Service

public User getUserByNickname(String nickname) {
        return userRepository.findByNickname(nickname)
            .orElseThrow(() -> new IllegalArgumentException("사용자가 없습니다."));
    }

K6

항목
성공률63.91% (673/1053)
실패률36.08% (연결 거부 등 380건 실패)
평균 요청 응답 시간12.86초(12860ms), 최대 39.8초(39800ms)
동시 사용자 (VU)1000명 동시에 요청
요청 수 (총)1053건, 초당 약 26건 처리
  • 요약
    actively refused 발생: 서버가 감당 못하고 연결 자체를 끊어버림
    평균 응답 시간 12.8초(12860ms), p95 = 36.7초(39800ms)
    부하가 1초에 수백건이 들어 오면 일부만 응답 성공

2. 코드 개선 - DB 인덱싱

조회를 진행할때 인덱싱이 되어 있지 않으면 풀 스캔 -> nickname 인덱싱

UserEntity

@Table(name = "users", indexes = {
    @Index(name = "idx_nickname", columnList = "nickname")
})
  • 위 코드를 추가함으로 nickname 컬럼에 인덱스 지정

주의

  • 이미 생성된 테이블의 경우 적용 안됨
    -> 직접 DDL 실행

직접 SQL로 인덱스 추가 - 이미 생성된 테이블의 경우

CREATE INDEX idx_nickname ON users(nickname);

K6

항목
성공률97.93% (29,272 / 29,888)
실패률2.06% (연결 거부 등 616건 실패)
평균 요청 응답 시간0.331초(331.98ms), 최대 0.711(711.46ms)초
동시 사용자 (VU)1000명 동시에 요청
요청 수 (총)29888건, 초당 약 2901건 처리
  • 요약
    nickname 필드에 인덱스 추가 후, 성능이 비약적으로 향상됨
    실패율이 2% 이하로 감소했고, 응답 시간도 평균 1초 이내로 안정화
    서버가 동시 1000명 × 10초간 요청을 처리 가능함
    평균 응답 시간 0.331초(331.98ms), p95 = 0.405초(405.08ms)
    이전 테스트 대비 성공률 +34%, 평균 응답 시간 -11초 이상 개선됨

3. DB 커넥션 풀 튜닝(HikariCP)

병목 가능성: 다수의 요청이 들어 올 경우 커넥션 부족으로 대기 시간 존재
-> .yml파일 maximumPoolSize, connectionTimeout 조정

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/${MYSQL_NAME}?useSSL=${SSL}&allowPublicKeyRetrieval=${ALLOWPUBLICKEYRETRIEVAL}
    username: ${MYSQL_USERNAME}
    password: ${MYSQL_PASSWORD}
    driver-class-name: com.mysql.cj.jdbc.Driver
    hikari:
      maximum-pool-size: 32 //동시에 사용할 수 있는 최대 커넥션 (서버 CPU의 스레드 * 2)
      minimum-idle: 10 // 최소 대기 커넥션
      idle-timeout: 30000 // 최대 대기 시간
      max-lifetime: 1800000 // 커넥션 하나의 최대 생존시간
      connection-timeout: 30000 // 커넥션 획득까지 기다릴 최대 시간

K6

항목
성공률98.75% (30,378/30,761)
실패률1.24% (383건)
평균 요청 응답 시간0.323초(323.39ms), 최대 0.616초(616.23ms)
동시 사용자 (VU)1000명 동시에 요청
요청 수 (총)30761건, 초당 약 2984건 처리
  • 요약
    응답 시간은 평균 0.323초(323.39ms), p95 = 0.396초(396.79ms)
    실패율 36.08%(최초) -> 2.06%(1차 개선) -> 1.24%(현재)

4. Redis 도입

한번 호출한 유저 정보를 Redis에 10분 간 저장하고, 10분내에 재 호출 할 경우 Redis에서 바로 응답

.yml

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

RedisConfig

@Configuration
@EnableCaching
public class RedisConfig {

	@Bean
	public RedisConnectionFactory redisConnectionFactory() {
		return new LettuceConnectionFactory();
	}

	@Bean
	public RedisTemplate<String, Object> redisTemplate() {
		RedisTemplate<String, Object> template = new RedisTemplate<>();
		template.setConnectionFactory(redisConnectionFactory());
		template.setKeySerializer(new StringRedisSerializer());
		template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
		return template;
	}

	@Bean
	public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
		RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
			.entryTtl(Duration.ofMinutes(10)) // 10분 TTL
			.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
			.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));

		return RedisCacheManager.builder(connectionFactory).cacheDefaults(config).build();
	}
}

UserService

@Cacheable(value = "userByNickname", key = "#nickname", unless = "#result == null")
    public User getUserByNickname(String nickname) {
        return userRepository.findByNickname(nickname)
            .orElseThrow(() -> new IllegalArgumentException("사용자가 없습니다."));
    }

K6

항목
성공률100 (40117)
실패률0 (0)
평균 요청 응답 시간0.250초(250.80ms), 최대 0.404초(404.21ms)
동시 사용자 (VU)1000명 동시에 요청
요청 수 (총)40117건, 초당 약 3919건 처리
  • 요약
    응답 시간은 평균 0.250초(250.80ms), p95 = 0.292초(292.66ms)
    실패율 36.08%(최초) -> 2.06%(1차 개선) -> 1.24%(2차 개선) -> 0%(Only Redis)

최종 요약

  • 응답 시간

  • 실패률

  • 요청 수
profile
노력하고 있다니까요?

0개의 댓글