IT/Live Coding

Spring Boot + Redis๋กœ ์š”์ฒญ ์ œํ•œ (ํ…Œ์ŠคํŠธ ์˜์ƒ & ์†Œ์Šค ์ฝ”๋“œ ํฌํ•จ)

์–ดํฅ๊ผฌ๋น„ 2025. 3. 9.

Redis๋ฅผ ํ™œ์šฉํ•ด ์•„์ดํ”ผ ๊ธฐ๋ฐ˜ ์š”์ฒญ์ œํ•œ ๊ธฐ๋Šฅ ๊ตฌํ˜„(์งง๊ณ  ๊ฐ„๋‹จํ•œ ์˜์ƒ)

์—ญ์‹œ๋‚˜ ๋ ˆ๋””์Šค ๊ณต๋ถ€ํ•œ ๊ฒƒ ๊นŒ๋จน๊ธฐ ์‹ซ์–ด ๊ธฐ๋ก์œผ๋กœ ๋‚จ๊ธด๋‹ค.

๋™์˜์ƒ

์†Œ์Šค

application.yml

server:
  port: 9090
  servlet:
    context-path: /
    encoding:
      charset: UTF-8
      enabled: true
      force: true

spring:
  cache:
    type: redis
  devtools:
    livereload.enabled: true
    restart.enabled: true
  datasource:
    driver-class-name: net.sf.log4jdbc.sql.jdbcapi.DriverSpy
    url: jdbc:log4jdbc:mysql://localhost:3309/db๋ช…
    hikari:
      username: ์‚ฌ์šฉ์ž
      password: ๋น„๋ฐ€๋ฒˆํ˜ธ
      connectionTimeout: 10000
      maximum-pool-size: 15
      max-lifetime: 600000
      readOnly: false
      connection-test-query: SELECT 1
  jpa:
    hibernate:
      ddl-auto: update #create update none
      naming:
        physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
    show-sql: true
# Redis ์„ค์ • ์ถ”๊ฐ€
data:
  redis:
    host: localhost   # Redis ์„œ๋ฒ„ ์ฃผ์†Œ
    port: 6379        # Redis ํฌํŠธ
    password:         # (ํ•„์š”ํ•˜๋ฉด ๋น„๋ฐ€๋ฒˆํ˜ธ ์ถ”๊ฐ€)
    timeout: 6000     # ์—ฐ๊ฒฐ ํƒ€์ž„์•„์›ƒ (ms)
    lettuce:
      pool:
        max-active: 8  # ์ตœ๋Œ€ ํ™œ์„ฑ ์—ฐ๊ฒฐ ์ˆ˜
        max-idle: 8    # ์ตœ๋Œ€ ์œ ํœด ์—ฐ๊ฒฐ ์ˆ˜
        min-idle: 0    # ์ตœ์†Œ ์œ ํœด ์—ฐ๊ฒฐ ์ˆ˜
        max-wait: -1   # ์ตœ๋Œ€ ๋Œ€๊ธฐ ์‹œ๊ฐ„ (๋ฌด์ œํ•œ)

build.gradle

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.3.9'
    id 'io.spring.dependency-management' version '1.1.7'
}

group = 'com.lsy'
version = '0.0.1-SNAPSHOT'

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(21)
    }
}

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    // Logging
    implementation 'org.bgee.log4jdbc-log4j2:log4jdbc-log4j2-jdbc4.1:1.16'
    // swagger
    implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2'
    // redis
    implementation 'org.springframework.boot:spring-boot-starter-data-redis'
    compileOnly 'org.projectlombok:lombok'
    developmentOnly 'org.springframework.boot:spring-boot-devtools'
    runtimeOnly 'com.mysql:mysql-connector-j'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

tasks.named('test') {
    useJUnitPlatform()
}

RedisConfig

package com.lsy.requestlimittest1.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
@EnableCaching
public class RedisConfig {

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

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

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

        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());

        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());

        return redisTemplate;
    }
}

Filter

package com.lsy.requestlimittest1.config.filter;

import com.lsy.requestlimittest1.service.LimitService;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.GenericFilterBean;

import java.io.IOException;

@Component
@RequiredArgsConstructor
@Slf4j
public class LimitFilter extends GenericFilterBean {

    private final LimitService service;

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {

        HttpServletRequest httpRequest = (HttpServletRequest) request;
        String clientIp = httpRequest.getRemoteAddr();
        //isAllow()์˜ ๊ฐ’์ด false์ธ ๊ฒฝ์šฐ์—” 429 Too Manny Request๋ฅผ ๋ฐ˜ํ™˜
        if(!service.isAllowed(clientIp)) {
            HttpServletResponse httpResponse = (HttpServletResponse) response;
            httpResponse.setStatus(429);
            httpResponse.getWriter().write("Too Many Requests!!!!!");
            return;
        }
        //isAllow()๊ฐ€ true์ธ ๊ฒฝ์šฐ ๋‹ค์Œ ํ๋ฆ„์œผ๋กœ ๋„˜๊น€
        chain.doFilter(request, response);
    }
}

controller

package com.lsy.requestlimittest1.controller;

import com.lsy.requestlimittest1.service.LimitService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
@Slf4j
public class LimitController {

    private final LimitService service;

    @GetMapping("/hello")
    public String hello() {
        return "Hello, World!";
    }
}

service

package com.lsy.requestlimittest1.service;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.time.Duration;

@Service
@RequiredArgsConstructor
@Slf4j
public class LimitService {

    private final StringRedisTemplate redisTemplate;
    private static final int LIMIT = 30;           // ์ตœ๋Œ€ ์š”์ฒญ ํšŸ์ˆ˜
    private static final int WINDOW_SECONDS = 120; // ์ œํ•œ ์‹œ๊ฐ„ (์ดˆ)

    public boolean isAllowed(String clientIp) {
        String key = "rate_limit:" + clientIp;
        Long currentCount = redisTemplate.opsForValue().increment(key);

        log.info("isAllowed Request~~~~");

        if (currentCount == 1) {
            Long hasTTL = redisTemplate.getExpire(key);
            if (hasTTL == null || hasTTL < 0) {
                redisTemplate.expire(key, Duration.ofSeconds(WINDOW_SECONDS));
            }
        }
        return currentCount <= LIMIT;
    }
}

๋™์˜์ƒ์— ์ž˜๋ชป๋œ ์ฝ”๋“œ๊ฐ€ ์žˆ์–ด์„œ ์ •์ •ํ•œ๋‹ค.

๋™์˜์ƒ์—์„œ๋Š” ์•„๋ž˜ ์ฝ”๋“œ๋ฅผ blooeanํƒ€์ž…์œผ๋กœ ๋ฐ›์•„ ์‚ฌ์šฉํ–ˆ๋Š”๋ฐ ์ž˜๋ชป๋˜์—ˆ๋‹ค.

Long hasTTL = redisTemplate.getExpire(key);

getExpire()์˜ ๋ฐ˜ํ™˜ํƒ€์ž…์€ Long์ด๋ผ์„œ Long์œผ๋กœ ๋ฐ›์•„์•ผ ํ•œ๋‹ค.


๊ฐœ์ธ ์Šคํ„ฐ๋”” ๊ธฐ๋ก์„ ๋ฉ”๋ชจํ•˜๋Š” ๊ณต๊ฐ„์ด๋ผ ํ‹€๋ฆฐ์ ์ด ์žˆ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ํ‹€๋ฆฐ ์  ์žˆ์„ ๊ฒฝ์šฐ ๋Œ“๊ธ€ ๋ถ€ํƒ๋“œ๋ฆฝ๋‹ˆ๋‹ค.

 

Spring Boot + Redis๋กœ ์„ธ์…˜ ๊ณต์œ ํ•˜๊ธฐ (ํ…Œ์ŠคํŠธ ์˜์ƒ & ์†Œ์Šค ์ฝ”๋“œ ํฌํ•จ)

Redis๋ฅผ ํ™œ์šฉํ•œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๊ฐ„ ์„ธ์…˜ ๊ณต์œ  ์‹คํ—˜! (์งง๊ณ  ๊ฐ„๋‹จํ•œ ์˜์ƒ)๋ ˆ๋””์Šค๋ฅผ ๊ณต๋ถ€ํ•œ์ง€ ์–ผ๋งˆ ์•ˆ๋˜์–ด์„œ ๊นŒ๋จน๊ธฐ ์‹ซ์–ด ๊ธฐ๋ก์œผ๋กœ ๋‚จ๊ธด๋‹ค.๋™์˜์ƒ์†Œ์Šคapplication.ymlserver: port: 9090 servlet: context-path: / encodin

yaga.tistory.com

 

Spring Boot + Redis ์บ์‹ฑ ๊ตฌํ˜„ํ•˜๊ธฐ (ํ…Œ์ŠคํŠธ ์˜์ƒ & ์†Œ์Šค ์ฝ”๋“œ ํฌํ•จ)

Redis๋ฅผ ํ™œ์šฉํ•œ ์บ์‹ฑ ๊ตฌํ˜„! (์งง๊ณ  ๊ฐ„๋‹จํ•œ ์˜์ƒ)์—ญ์‹œ๋‚˜ ๋ ˆ๋””์Šค ๊ณต๋ถ€ํ•œ ๊ฒƒ ๊นŒ๋จน๊ธฐ ์‹ซ์–ด ๊ธฐ๋ก์œผ๋กœ ๋‚จ๊ธด๋‹ค.๋™์˜์ƒ์†Œ์Šคapplication.ymlserver: port: 9090 servlet: context-path: / encoding: charset: UTF-8 enabled: true force: tru

yaga.tistory.com

 

Spring Boot + Redis๋กœ ์‹ค์‹œ๊ฐ„ ๋žญํ‚น ๊ตฌํ˜„ (ํ…Œ์ŠคํŠธ ์˜์ƒ & ์†Œ์Šค ์ฝ”๋“œ ํฌํ•จ)

Redis๋ฅผ ํ™œ์šฉํ•œ ์‹ค์‹œ๊ฐ„ ๋žญํ‚น ๊ตฌํ˜„! (์งง๊ณ  ๊ฐ„๋‹จํ•œ ์˜์ƒ)์—ญ์‹œ๋‚˜ ๋ ˆ๋””์Šค ๊ณต๋ถ€ํ•œ ๊ฒƒ ๊นŒ๋จน๊ธฐ ์‹ซ์–ด ๊ธฐ๋ก์œผ๋กœ ๋‚จ๊ธด๋‹ค.๋™์˜์ƒ์†Œ์Šคapplication.ymlserver: port: 8081 servlet: context-path: / encoding: charset: UTF-8 enabled: true

yaga.tistory.com

 

๋Œ“๊ธ€