๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
BackEnd๐ŸŒฑ/DB & SQL

์‹ค๋ฌด์—์„œ Redisson์„ ์—ฐ๊ฒฐํ•˜๋ฉด์„œ ๊ฒช์€ ๋ฌธ์ œ์ ๋“ค

by ์•ˆ์ฃผํ˜• 2023. 8. 18.

Redisson์„ ์‹ค๋ฌด์— ์ ์šฉํ•˜๋ฉด์„œ ๊ฒผ์—ˆ๋˜ ์—ฐ๊ฒฐ ๋ฌธ์ œ์™€ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•์„ ํ•œ๋ฒˆ ์ •๋ฆฌํ•ด๋ณด๋ ค๊ณ  ํ•œ๋‹ค. 

์ฒซ ๋ฒˆ์งธ๋กœ, ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ๋‹ค.

Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.redisson.api.RedissonClient]: Factory method 'redissonClient' threw exception; nested exception is org.redisson.client.RedisConnectionException: Unable to connect to Redis server: [Redis ์ฃผ์†Œ]
	at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:185)
	at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:653)
	... 38 common frames omitted
Caused by: org.redisson.client.RedisConnectionException: Unable to connect to Redis server: [addr=redis://Redis ์ฃผ์†Œ:ํฌํŠธ]
	at org.redisson.connection.pool.ConnectionPool.lambda$createConnection$0(ConnectionPool.java:132)
	at java.base/java.util.concurrent.CompletableFuture.uniWhenComplete(CompletableFuture.java:859)
	at java.base/java.util.concurrent.CompletableFuture$UniWhenComplete.tryFire(CompletableFuture.java:837)
	at java.base/java.util.concurrent.CompletableFuture.postComplete(CompletableFuture.java:506)
	at java.base/java.util.concurrent.CompletableFuture.completeExceptionally(CompletableFuture.java:2088)
	at org.redisson.connection.pool.ConnectionPool.promiseFailure(ConnectionPool.java:294)
	at org.redisson.connection.pool.ConnectionPool.lambda$createConnection$6(ConnectionPool.java:253)
	at java.base/java.util.concurrent.CompletableFuture.uniWhenComplete(CompletableFuture.java:859)
	at java.base/java.util.concurrent.CompletableFuture$UniWhenComplete.tryFire(CompletableFuture.java:837)
	at java.base/java.util.concurrent.CompletableFuture.postComplete(CompletableFuture.java:506)
	at java.base/java.util.concurrent.CompletableFuture.completeExceptionally(CompletableFuture.java:2088)
	at org.redisson.client.RedisClient$1$1.run(RedisClient.java:248)
	at io.netty.util.concurrent.AbstractEventExecutor.safeExecute(AbstractEventExecutor.java:164)
	at io.netty.util.concurrent.SingleThreadEventExecutor.runAllTasks(SingleThreadEventExecutor.java:469)
	at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:503)
	at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:986)
	at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
	at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
	at java.base/java.lang.Thread.run(Thread.java:829)
Caused by: java.util.concurrent.CompletionException: org.redisson.client.RedisTimeoutException: Command execution timeout for command: (AUTH), params: (password masked), Redis client: [addr=redis://Redis ์ฃผ์†Œ:ํฌํŠธ]
	at java.base/java.util.concurrent.CompletableFuture.encodeRelay(CompletableFuture.java:367)
	at java.base/java.util.concurrent.CompletableFuture.completeRelay(CompletableFuture.java:376)
	at java.base/java.util.concurrent.CompletableFuture$UniRelay.tryFire(CompletableFuture.java:1019)
	at java.base/java.util.concurrent.CompletableFuture.postComplete(CompletableFuture.java:506)
	at java.base/java.util.concurrent.CompletableFuture.completeExceptionally(CompletableFuture.java:2088)
	at org.redisson.client.RedisConnection.lambda$async$0(RedisConnection.java:257)
	at io.netty.util.HashedWheelTimer$HashedWheelTimeout.run(HashedWheelTimer.java:715)
	at io.netty.util.concurrent.ImmediateExecutor.execute(ImmediateExecutor.java:34)
	at io.netty.util.HashedWheelTimer$HashedWheelTimeout.expire(HashedWheelTimer.java:703)
	at io.netty.util.HashedWheelTimer$HashedWheelBucket.expireTimeouts(HashedWheelTimer.java:790)
	at io.netty.util.HashedWheelTimer$Worker.run(HashedWheelTimer.java:503)
	... 2 common frames omitted
Caused by: org.redisson.client.RedisTimeoutException: Command execution timeout for command: (AUTH), params: (password masked), Redis client: [addr=redis://Redis ์ฃผ์†Œ:ํฌํŠธ]
	at org.redisson.client.RedisConnection.lambda$async$0(RedisConnection.java:256)
	... 7 common frames omitted

์˜ค๋ฅ˜๋ฅผ ๋ณด๋ฉด Redis ์„œ๋ฒ„์— ์—ฐ๊ฒฐํ•  ์ˆ˜ ์—†์–ด์„œ ํƒ€์ž„์•„์›ƒ์ด ๋ฐœ์ƒํ–ˆ๋‹ค๋Š”๋ฐ, ์ด์ƒํ•œ ์ ์€ ๊ธฐ์กด์— ์‚ฌ์šฉํ•˜๋˜ Lettuce์—์„œ๋Š” ์ •์ƒ์ ์œผ๋กœ ์—ฐ๊ฒฐ์ด ๋˜๊ณ , Redisson Client์—์„œ๋งŒ ์—ฐ๊ฒฐ์ด ๊ณ„์†ํ•ด์„œ ์‹คํŒจํ•˜๋Š” ์ ์ด์—ˆ๋‹ค.

Lettuce์—๋Š” ์ •์ƒ์ ์œผ๋กœ ์—ฐ๊ฒฐ์ด ๋˜๊ธฐ์—, password๊ฐ€ ํ‹€๋ฆฐ ๋ฌธ์ œ๋„ ์•„๋‹ˆ๊ณ , Redisson๋„ Lettuce์™€ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ SSL ํ†ต์‹ ์„ ํ•˜๊ธฐ ์œ„ํ•ด setSslEnableEndpointIdentification(true)๋ฅผ ์„ค์ •ํ•ด ์ฃผ์—ˆ๋Š”๋ฐ ๊ณ„์† ์‹คํŒจํ–ˆ๋‹ค. ๋ฌธ์„œ๋Š” Baeldung์˜ Redisson ๊ฐ€์ด๋“œ ์—ฌ๊ธฐ๋ฅผ ์ฐธ๊ณ ํ•ด์„œ ์„ค์ •ํ–ˆ๋Š”๋ฐ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ๋‹ค.

@Configuration
@EnableRedisRepositories(enableKeyspaceEvents = RedisKeyValueAdapter.EnableKeyspaceEvents.ON_STARTUP)
public class RedisConfig {
...
    private static final String REDISSON_HOST_PREFIX = "redis://";
    private final ObjectMapper objectMapper;
    
    @Value("${redis.host}")
    private String redisHost;
    
    @Value("${redis.port}")
    private Integer redisPort;
    
    @Value("${redis.database}")
    private int redisSessionDatabase;
   
    @Value("${redis.password}")
    private String redisPassword;

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        LettuceConnectionFactory connectionFactory =
                new LettuceConnectionFactory(redisStandaloneConfiguration(), lettuceClientConfiguration());
        connectionFactory.setDatabase(this.redisSessionDatabase);
        
        return connectionFactory;
    }

    @Bean
    RedisStandaloneConfiguration redisStandaloneConfiguration() {
        RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
        redisStandaloneConfiguration.setHostName(this.redisHost);
        redisStandaloneConfiguration.setPort(this.redisPort);
        redisStandaloneConfiguration.setPassword(this.redisPassword);
        
        return redisStandaloneConfiguration;
    }

    @Bean
    LettuceClientConfiguration lettuceClientConfiguration() {
        return LettuceClientConfiguration
                .builder()
                .useSsl()
                .build();
    }

    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        config.useSingleServer()
                .setAddress(REDISSON_HOST_PREFIX + this.redisHost + ":" + this.redisPort)
                .setPassword(this.redisPassword)
                .setSslEnableEndpointIdentification(true);
                
        return Redisson.create(config);
    }

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

        Jackson2JsonRedisSerializer<String> stringJackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(String.class);
        stringJackson2JsonRedisSerializer.setObjectMapper(objectMapper);
        redisTemplate.setValueSerializer(stringJackson2JsonRedisSerializer);

        return redisTemplate;
    }
    ...
}

๋ณด์•ˆ์ƒ ๊ฐ€๋ฆด ๊ฑฐ ๋‹ค ๊ฐ€๋ฆฌ๊ณ , ๋ณด์—ฌ์ค„ ์ˆ˜ ์žˆ๋Š” Config๋Š” ์œ„์™€ ๊ฐ™๋‹ค.

ํ˜ผ์ž ํ•œ 3-4์‹œ๊ฐ„ ์‚ฝ์งˆํ•˜๋‹ค๊ฐ€, ํŒ€์›๋ถ„์ด ํ•ด๊ฒฐ์ฑ…์„ ์ฐพ์•„์ฃผ์—ˆ๋Š”๋ฐ, Redisson ์—ฐ๊ฒฐ ์„ค์ •์—์„œ ์‚ฌ์šฉํ•œ REDISSON_HOST_PREFIX๊ฐ€ ์›์ธ์ด์—ˆ๋‹ค. SSL ํ†ต์‹ ์„ ์œ„ํ•ด์„œ๋Š” "redis://"๊ฐ€ ์•„๋‹Œ "rediss://"๋กœ ์„ค์ •ํ•ด์•ผ ํ–ˆ๋˜ ๊ฒƒ์ด๋‹ค.  Lettuce์—์„œ๋Š” LettuceClientConfiguration์—์„œ userSsl()์„ ํ™œ์„ฑํ™”ํ•˜์—ฌ ์ž๋™์œผ๋กœ "rediss://"๋กœ ์„ค์ •์ด ๋˜์–ด์„œ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š์•˜๋˜ ๊ฒƒ์ด์—ˆ๋‹ค.

์‹ค์ œ๋กœ lettuce-core ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜  RedisURI.java ํŒŒ์ผ์„ ํ™•์ธํ•˜๋ฉด 166๋ฒˆ์งธ ์ค„์— URI_SCHEME_REDIS_SECURE๊ฐ€ "rediss"๋กœ ์„ค์ •๋˜์–ด ์žˆ์Œ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.(๋ถ€๊ฐ€์ ์ธ ์„ค์ •์€ ๊ณต์‹ ์œ„ํ‚ค๋ฅผ ํ™•์ธํ•˜์ž.)

์ธํ„ฐ๋„ท์˜ ๋งŽ์€ Redisson Client ์ ์šฉ ์˜ˆ์‹œ๋“ค์€ ๋กœ์ปฌ ํ™˜๊ฒฝ์—์„œ ๋™์ž‘ํ•˜๋Š” ๊ฒƒ์„ ๊ธฐ๋ฐ˜์œผ๋กœ ํ•˜๋ฏ€๋กœ, secure๊นŒ์ง€ ์„ค์ •ํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋“œ๋ฌผ์–ด "redis://"๋กœ ์„ค์ •ํ•œ ์ฝ”๋“œ๋“ค์ด ๋งŽ๋‹ค. ํ•˜์ง€๋งŒ ๋ณด์•ˆ์„ ๊ณ ๋ คํ•œ ์‹ค๋ฌด ํ™˜๊ฒฝ์—์„œ๋Š” ์ด๋Ÿฌํ•œ ์„ธ๋ถ€ ์„ค์ •์ด ์ค‘์š”ํ•˜๋ฏ€๋กœ ๊ผญ ํ™•์ธํ•˜์—ฌ ๋‚˜์ฒ˜๋Ÿผ ์‹œ๊ฐ„์„ ๋‚ญ๋น„ํ•˜๋Š” ์ผ์ด ์—†์—ˆ์œผ๋ฉด ์ข‹๊ฒ ๋‹ค.(ํŠนํžˆ๋‚˜ ์ฐธ๊ณ ํ–ˆ๋˜ ๋ฒจ๋ฉ์˜ ๋ฌธ์„œ์—๋„ ๋‚˜์™€์žˆ์ง€ ์•Š์•˜๋‹ค.)

 

๋‘ ๋ฒˆ์งธ๋Š” ์•„๋ž˜์˜ค๋ฅ˜์ด๋‹ค.

Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'javax.sql.DataSource' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Qualifier(value="securityDataSource")}
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.raiseNoMatchingBeanFound(DefaultListableBeanFactory.java:1799)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1394)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1309)
	at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:887)
	at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:791)
	... 188 common frames omitted

NoSuchBeanDefinitionException ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ๋Š”๋ฐ, securityDataSource ๋นˆ์„ ์ฐพ์ง€ ๋ชปํ•˜๋Š” ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ๋‹ค.

์ฝ”๋“œ๋ฅผ ์‚ดํŽด๋ณด๋ฉด, DataSourceConfig ํด๋ž˜์Šค์— ์—ฌ๋Ÿฌ DataSource ๋นˆ๋“ค์ด ์ •์˜๋˜์–ด ์žˆ๋Š”๋ฐ, ๊ทธ์ค‘ securityDataSource ๋นˆ์ด ์ฃผ์ž…๋˜๋ ค๊ณ  ํ•  ๋•Œ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ๋‹ค.

@Slf4j
@Configuration
@RequiredArgsConstructor
public class DataSourceConfig {
   // ... ๋‹ค๋ฅธ ๋นˆ ์ •์˜๋“ค


    @Bean
    public DataSource securityDataSource() {
        return getDataSource();
    }

    private DataSource getDataSource() {
        JndiObjectFactoryBean jndi = new JndiObjectFactoryBean();
        jndi.setProxyInterface(DataSource.class);
        jndi.setJndiName(JndiResource.JNDI_NAME);
        jndi.setResourceRef(true);
        try {
            jndi.afterPropertiesSet();
            return (DataSource) jndi.getObject();
        } catch (NamingException ne) {
            log.error(JndiResource.JNDI_NAME, ne);
            return null;
        }
    }
    
    // ... ๋‹ค๋ฅธ ๋ฉ”์„œ๋“œ๋“ค
}

ํŠน์ดํ•œ ์ ์€, ์ €๊ธฐ์„œ JNDI๋กœ DataSource ์ฃผ์ž…ํ•˜๋Š” ๊ฒŒ ์•„๋‹ˆ๋ผ ์•„๋ž˜์ฒ˜๋Ÿผ HikariCP๋ฅผ ์‚ฌ์šฉํ•ด์„œ ์ฃผ์ž…ํ•˜๋ฉด ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š๋Š”๋‹ค๋Š” ์ ์ด๋‹ค. 

@Slf4j
@Configuration
@RequiredArgsConstructor
public class DataSourceConfig {
   // ... ์œ„์™€ ๋™์ผ
   
    public DataSource securityDataSource() {
        return getDataSource();
    }
    
    private DataSource getDataSource() {
        HikariConfig config = new HikariConfig();
        config.setDriverClassName(Driver.class.getName());
        config.setJdbcUrl("url");
        config.setUsername("username");
        config.setPassword("password");
        config.setMaximumPoolSize(size);

        return new HikariDataSource(config);
    }
}

์š”๊ฒƒ๋„ ํ˜ผ์ž ๋ณ„์ง“ ๋‹คํ•ด๋ณด๋‹ค๊ฐ€ ๊ฒฐ๊ตญ ์…€์žฅ๋‹˜์ด ์›์ธ์„ ์ฐพ์•„์ฃผ์…จ๋Š”๋ฐ, ๋ฌธ์ œ์˜ ๊ทผ์›์€ RedisConfig์—์„œ ObjectMapper๋ฅผ ์ฃผ์ž…๋ฐ›๋Š” ๊ณผ์ •์—์„œ ๋ฐœ์ƒํ–ˆ๋‹ค.

ServletConfig์—๋Š” ObjectMapper๊ฐ€ ์ •์˜๋˜์–ด ์žˆ์—ˆ๊ณ , PublicAuthInterceptor์—๋Š” DB ์ ‘๊ทผ์ด ํ•„์š”ํ•œ PrincipalRepository๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ์—ˆ๋Š”๋ฐ, ์ด๋กœ ์ธํ•ด securityDataSource๊ฐ€ ์•„์ง ์ƒ์„ฑ๋˜๊ธฐ ์ „์— ์ ‘๊ทผํ•˜๋ ค ํ•ด์„œ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ๋˜ ๊ฒƒ์ด๋‹ค.

@Configuration
@RequiredArgsConstructor
public class ServletConfig implements WebMvcConfigurer {
    // ... ๋‹ค๋ฅธ ์ •์˜๋“ค
   
    private final PublicAuthInterceptor publicAuthInterceptor;

    @Bean
    public ObjectMapper objectMapper() {
      // ... objectMapper ์„ค์ •
    }

    // ...
}
@Slf4j
@Component
@RequiredArgsConstructor
public class PublicAuthInterceptor implements HandlerInterceptor {

 private final PrincipalRepository principalRepository;
    // ...

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        // principalRepository ์‚ฌ์šฉ ์ฝ”๋“œ
    }
    
    // ...
}

ํ•ด๊ฒฐ์ฑ…์€ ๊ฐ„๋‹จํ–ˆ๋Š”๋ฐ, ObjectMapper๋ฅผ ServletConfig์—์„œ ๋ถ„๋ฆฌํ•˜์—ฌ ์•„๋ž˜์ฒ˜๋Ÿผ ๋ณ„๋„์˜ ํด๋ž˜์Šค์—์„œ ์ฃผ์ž…ํ•ด ์ฃผ๋ฉด ํ•ด๊ฒฐ๋˜์—ˆ๋‹ค.

@Configuration
public class ObjectMapperConfig {
    @Bean
    public ObjectMapper objectMapper() {
        // ... objectMapper ์„ค์ •
    }
}

 

์ดํ•ด๋˜์ง€ ์•Š๋Š” ์ ๋“ค

๊ฒฐ๊ตญ... ํ•ด๊ฒฐ์€ ํ–ˆ์ง€๋งŒ, ์•„์ง๊นŒ์ง€ ์™œ RedissonClient ๋นˆ์„ ์ถ”๊ฐ€ํ•˜๋ฉด JNDI ์ฃผ์ž…์—์„œ๋Š” ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•˜๊ณ , HikariCP ์ฃผ์ž…์—์„œ๋Š” ๋ฐœ์ƒํ•˜์ง€ ์•Š๋Š”์ง€๋ฅผ ์ •ํ™•ํžˆ ๋ชจ๋ฅด๊ฒ ๋‹ค. RedisConfig์—์„œ Redisson ์—ฐ๊ฒฐ์„ ์œ„ํ•œ ์•„๋ž˜ RedissClient ๋นˆ๋งŒ ๋นผ๋ฉด JNDI ์ฃผ์ž…์œผ๋กœ๋„ ์ „ํ˜€ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. 

    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        config.useSingleServer()
                .setAddress(REDISSON_HOST_PREFIX + this.redisHost + ":" + this.redisPort)
                .setPassword(this.redisPassword)
                .setSslEnableEndpointIdentification(true);
                
        return Redisson.create(config);
    }

๊ทธ๋ฆฌ๊ณ  RedissonClient ์—†์ด, ๊ธฐ์กด์ฒ˜๋Ÿผ LettuceClient๋งŒ ์žˆ์œผ๋ฉด HikariCP, JNDI ์ฃผ์ž… ์ „๋ถ€ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š๊ธฐ์— ๋”๋”์šฑ ์ดํ•ด๊ฐ€ ๊ฐ€์ง€ ์•Š๋Š”๋‹ค. RedissonClient๋ž‘ Datasource๋ž‘ ๊ด€๊ณ„๊ฐ€ ์žˆ๋Š” ๊ฑธ๊นŒ..?

๋Œ“๊ธ€