Jedis 연결 풀(JedisPool) 구성 및 Redis 캐시를 활용한 데이터 조회 최적화

1. Jedis 연결 풀(JedisPool) 기본 활용

Jedis를 통해 Redis와 통신할 때, 매번 새로운 연결을 생성하고 종료하는 것은 성능 저하의 원인이 됩니다. 이를 해결하기 위해 JedisPool을 사용하여 연결을 재사용합니다.

1.1 기본 연결 풀 생성 및 사용

가장 간단한 형태로 JedisPool을 초기화하고 연결을 가져오는 방법입니다.


package com.example.redis.pool;

import org.junit.jupiter.api.Test;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

public class BasicJedisPoolTest {

    @Test
    public void testBasicPoolUsage() {
        // 기본 설정으로 JedisPool 인스턴스 생성 (localhost:6379)
        JedisPool connectionPool = new JedisPool();

        // 풀에서 Jedis 연결 객체 획득
        Jedis jedisClient = connectionPool.getResource();

        try {
            // Redis 명령 실행
            jedisClient.set("testKey", "testValue");
            System.out.println("저장된 값: " + jedisClient.get("testKey"));
        } finally {
            // 사용 후 반드시 close()를 호출하여 풀로 반환
            if (jedisClient != null) {
                jedisClient.close();
            }
        }
    }
}

1.2 커스텀 설정을 통한 연결 풀 생성

JedisPoolConfig를 사용하여 최대 연결 수, 유휴 연결 수 등의 파라미터를 세부적으로 조정할 수 있습니다.


package com.example.redis.pool;

import org.junit.jupiter.api.Test;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

public class CustomJedisPoolTest {

    @Test
    public void testCustomPoolConfig() {
        // 연결 풀 설정 객체 생성 및 속성 정의
        JedisPoolConfig poolConfig = new JedisPoolConfig();
        poolConfig.setMaxTotal(30);       // 최대 활성 연결 수
        poolConfig.setMaxIdle(10);        // 최대 유휴 연결 수
        poolConfig.setMinIdle(5);         // 최소 유휴 연결 수

        // 설정과 서버 정보를 바탕으로 풀 생성
        JedisPool customPool = new JedisPool(poolConfig, "127.0.0.1", 6379);

        Jedis jedisConn = customPool.getResource();
        try {
            jedisConn.set("regionCode", "SEOUL");
        } finally {
            jedisConn.close();
        }
    }
}

1.3 주요 연결 풀 설정 파라미터

운영 환경에 맞게 연결 풀을 튜닝하기 위해 다음과 같은 설정 값을 활용할 수 있습니다.


# 최대 활성 연결 수
redis.pool.maxTotal=200
# 최대 유휴 연결 수
redis.pool.maxIdle=50
# 최소 유휴 연결 수
redis.pool.minIdle=20
# 연결 획득 시 최대 대기 시간 (ms)
redis.pool.maxWaitMillis=5000
# 연결 대여 시 유효성 검사 여부
redis.pool.testOnBorrow=true
# 연결 반납 시 유휴성 검사 여부
redis.pool.testOnReturn=false
# 유휴 연결 검사 스레드 실행 주기 (ms)
redis.pool.timeBetweenEvictionRunsMillis=60000
# 유휴 상태일 때 유효성 검사 여부
redis.pool.testWhileIdle=true
# Redis 서버 호스트
redis.server.host=192.168.0.100
# Redis 서버 포트
redis.server.port=6379

2. 연결 풀 유틸리티 클래스 구현

설정 파일을 읽어와 JedisPool을 싱글톤으로 관리하는 유틸리티 클래스를 구현하면 코드 재사용성과 유지보수성이 향상됩니다.


package com.example.redis.util;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;

public class RedisConnectionManager {

    private static final JedisPool jedisPool;

    static {
        Properties props = new Properties();
        try (InputStream inputStream = RedisConnectionManager.class.getClassLoader()
                .getResourceAsStream("redis-config.properties")) {
            props.load(inputStream);
        } catch (IOException e) {
            throw new ExceptionInInitializerError(e);
        }

        JedisPoolConfig config = new JedisPoolConfig();
        config.setMaxTotal(Integer.parseInt(props.getProperty("maxTotal")));
        config.setMaxIdle(Integer.parseInt(props.getProperty("maxIdle")));
        config.setTestOnBorrow(Boolean.parseBoolean(props.getProperty("testOnBorrow", "true")));

        String host = props.getProperty("host");
        int port = Integer.parseInt(props.getProperty("port"));

        jedisPool = new JedisPool(config, host, port);
    }

    public static Jedis getConnection() {
        return jedisPool.getResource();
    }
}

위 클래스에서 참조하는 redis-config.properties 파일의 내용입니다.


host=127.0.0.1
port=6379
maxTotal=50
maxIdle=15
testOnBorrow=true

3. Redis 캐시를 활용한 지역 정보 조회 사례

데이터베이스의 부하를 줄이고 응답 속도를 높이기 위해, 변경 빈도가 낮은 데이터는 Redis에 캐싱하는 것이 효과적입니다. 다음은 지역(Region) 목록을 조회할 때 Redis 캐시를 적용한 예제입니다.

3.1 데이터베이스 및 테이블 구성


CREATE DATABASE IF NOT EXISTS app_db;
USE app_db;

CREATE TABLE region (
    region_id INT AUTO_INCREMENT PRIMARY KEY,
    region_name VARCHAR(50) NOT NULL
);

INSERT INTO region (region_name) VALUES ('Seoul'), ('Busan'), ('Incheon'), ('Daegu');

3.2 데이터베이스 연결 풀(Druid) 설정


driverClassName=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/app_db
username=root
password=secret
initialSize=5
maxActive=20

3.3 JDBC 및 Redis 유틸리티 클래스


package com.example.db.util;

import com.alibaba.druid.pool.DruidDataSourceFactory;
import javax.sql.DataSource;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Properties;

public class DatabasePoolManager {
    private static final DataSource dataSource;

    static {
        try {
            Properties dbProps = new Properties();
            InputStream is = DatabasePoolManager.class.getClassLoader().getResourceAsStream("db-config.properties");
            dbProps.load(is);
            dataSource = DruidDataSourceFactory.createDataSource(dbProps);
        } catch (Exception e) {
            throw new RuntimeException("DB 연결 풀 초기화 실패", e);
        }
    }

    public static DataSource getDataSource() { return dataSource; }
    public static Connection getConnection() throws SQLException { return dataSource.getConnection(); }
}

3.4 도메인, DAO, Service 계층 구현


package com.example.domain;

public class Region {
    private int id;
    private String name;

    public int getId() { return id; }
    public void setId(int id) { this.id = id; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
}

package com.example.dao;

import com.example.domain.Region;
import com.example.db.util.DatabasePoolManager;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import java.util.List;

public class RegionDao {
    private final JdbcTemplate jdbcTemplate = new JdbcTemplate(DatabasePoolManager.getDataSource());

    public List<Region> getAllRegions() {
        String sql = "SELECT region_id AS id, region_name AS name FROM region";
        return jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(Region.class));
    }
}

package com.example.service;

import com.example.domain.Region;
import com.example.dao.RegionDao;
import com.example.redis.util.RedisConnectionManager;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import redis.clients.jedis.Jedis;
import java.util.List;

public class RegionService {

    private final RegionDao regionDao = new RegionDao();
    private final ObjectMapper objectMapper = new ObjectMapper();
    private static final String CACHE_KEY = "region_list_cache";

    public String getRegionsAsJson() {
        Jedis jedis = RedisConnectionManager.getConnection();
        String cachedJson = null;

        try {
            cachedJson = jedis.get(CACHE_KEY);

            if (cachedJson == null || cachedJson.isEmpty()) {
                List<Region> regions = regionDao.getAllRegions();
                cachedJson = objectMapper.writeValueAsString(regions);
                jedis.set(CACHE_KEY, cachedJson);
            }
        } catch (JsonProcessingException e) {
            throw new RuntimeException("JSON 변환 중 오류 발생", e);
        } finally {
            if (jedis != null) jedis.close();
        }

        return cachedJson;
    }

    public void invalidateCache() {
        try (Jedis jedis = RedisConnectionManager.getConnection()) {
            jedis.del(CACHE_KEY);
        }
    }
}

3.5 Servlet 컨트롤러 및 프론트엔드 연동


package com.example.web;

import com.example.service.RegionService;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebServlet("/api/regions")
public class RegionServlet extends HttpServlet {
    private final RegionService regionService = new RegionService();

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        resp.setContentType("application/json;charset=UTF-8");
        String jsonResponse = regionService.getRegionsAsJson();
        resp.getWriter().write(jsonResponse);
    }
}


<html lang="ko">
<head>
    <meta charset="UTF-8">
    <title>지역 선택</title>
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</head>
<body>
    <h2>지역 목록 조회</h2>
    <select id="regionSelect">
        <option value="">-- 지역을 선택하세요 --</option>
    </select>

    <script>
        $(document).ready(function() {
            $.ajax({
                url: '/api/regions',
                method: 'GET',
                dataType: 'json',
                success: function(regions) {
                    const selectBox = $('#regionSelect');
                    $.each(regions, function(index, region) {
                        selectBox.append(
                            $('<option></option>').val(region.id).text(region.name)
                        );
                    });
                },
                error: function() {
                    alert('데이터를 불러오는 중 오류가 발생했습니다.');
                }
            });
        });
    </script>
</body>
</html>

태그: jedis JedisPool Redis SpringJdbcTemplate Druid

8월 5일 20:49에 게시됨