Projects/[Final] Shopping Mall Project

[트러블슈팅] TLS 비활성화 상태에서 환경별로 연결 설정 분리하기

montmer27 2026. 4. 22. 17:40
Moral of the Story
다 프로필을 환경별로 분리하지 않아서 발생한 일이다... 

상황

팀원이 배포 환경에서 ElastiCache 사용을 위해 Redis 연결 설정을 TLS로 강제했다.

코드에선 아래와 같이 적용됐다.

.setAddress의 시작 부분이 redis가 아닌 rediss인데, 바로 이것이 Redisson을 TCP가 아닌 TLS로 연결을 강제하는 기능이다.

하지만 로컬에서는 Redis의 TLS가 비활성화되어 있어, 이 설정으로 로컬에서 실행시키면 아래와 같은 에러가 발생한다.

(생략)
Caused by: java.util.concurrent.ExecutionException: org.redisson.client.RedisConnectionException: Unable to connect to Redis server: localhost/127.0.0.1:6379
	at java.base/java.util.concurrent.CompletableFuture.reportGet(CompletableFuture.java:396) ~[na:na]
	at java.base/java.util.concurrent.CompletableFuture.get(CompletableFuture.java:2096) ~[na:na]
	at org.redisson.connection.MasterSlaveConnectionManager.doConnect(MasterSlaveConnectionManager.java:247) ~[redisson-4.3.0.jar:4.3.0]
	... 93 common frames omitted
Caused by: org.redisson.client.RedisConnectionException: Unable to connect to Redis server: localhost/127.0.0.1:6379
	at org.redisson.connection.ConnectionsHolder.lambda$createConnection$2(ConnectionsHolder.java:169) ~[redisson-4.3.0.jar:4.3.0]
	at java.base/java.util.concurrent.CompletableFuture.uniHandle(CompletableFuture.java:934) ~[na:na]
	at java.base/java.util.concurrent.CompletableFuture$UniHandle.tryFire(CompletableFuture.java:911) ~[na:na]
	at java.base/java.util.concurrent.CompletableFuture.postComplete(CompletableFuture.java:510) ~[na:na]
	at java.base/java.util.concurrent.CompletableFuture.completeExceptionally(CompletableFuture.java:2194) ~[na:na]
	at org.redisson.connection.ConnectionsHolder.lambda$createConnection$5(ConnectionsHolder.java:183) ~[redisson-4.3.0.jar:4.3.0]
	at java.base/java.util.concurrent.CompletableFuture.uniWhenComplete(CompletableFuture.java:863) ~[na:na]
	at java.base/java.util.concurrent.CompletableFuture$UniWhenComplete.tryFire(CompletableFuture.java:841) ~[na:na]
	at java.base/java.util.concurrent.CompletableFuture.postComplete(CompletableFuture.java:510) ~[na:na]
	at java.base/java.util.concurrent.CompletableFuture.completeExceptionally(CompletableFuture.java:2194) ~[na:na]
	at org.redisson.client.RedisClient$2$1.run(RedisClient.java:316) ~[redisson-4.3.0.jar:4.3.0]
	at io.netty.util.concurrent.AbstractEventExecutor.runTask(AbstractEventExecutor.java:148) ~[netty-common-4.2.12.Final.jar:4.2.12.Final]
	at io.netty.util.concurrent.AbstractEventExecutor.safeExecute(AbstractEventExecutor.java:141) ~[netty-common-4.2.12.Final.jar:4.2.12.Final]
	at io.netty.util.concurrent.SingleThreadEventExecutor.runAllTasks(SingleThreadEventExecutor.java:535) ~[netty-common-4.2.12.Final.jar:4.2.12.Final]
	at io.netty.channel.SingleThreadIoEventLoop.run(SingleThreadIoEventLoop.java:201) ~[netty-transport-4.2.12.Final.jar:4.2.12.Final]
	at io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:1195) ~[netty-common-4.2.12.Final.jar:4.2.12.Final]
	at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74) ~[netty-common-4.2.12.Final.jar:4.2.12.Final]
	at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) ~[netty-common-4.2.12.Final.jar:4.2.12.Final]
	at java.base/java.lang.Thread.run(Thread.java:1583) ~[na:na]
Caused by: java.util.concurrent.CompletionException: io.netty.handler.ssl.SslHandshakeTimeoutException: handshake timed out after 10000ms
	at java.base/java.util.concurrent.CompletableFuture.encodeRelay(CompletableFuture.java:368) ~[na:na]
	at java.base/java.util.concurrent.CompletableFuture.completeRelay(CompletableFuture.java:377) ~[na:na]
	at java.base/java.util.concurrent.CompletableFuture$UniRelay.tryFire(CompletableFuture.java:1097) ~[na:na]
	... 11 common frames omitted
Caused by: io.netty.handler.ssl.SslHandshakeTimeoutException: handshake timed out after 10000ms
	at io.netty.handler.ssl.SslHandler.lambda$applyHandshakeTimeout$6(SslHandler.java:2257) ~[netty-handler-4.2.12.Final.jar:4.2.12.Final]
	at io.netty.util.concurrent.PromiseTask.runTask(PromiseTask.java:98) ~[netty-common-4.2.12.Final.jar:4.2.12.Final]
	at io.netty.util.concurrent.ScheduledFutureTask.run(ScheduledFutureTask.java:160) ~[netty-common-4.2.12.Final.jar:4.2.12.Final]
	... 8 common frames omitted

TCP 3-way handshake는 완료돼서 소켓이 열린 상태인데, 그 위에서 TLS 협상을 시도하다 TLS 핸드셰이크가 10초 안에 완료되지 않아서 SslHandshakeTimeOutException이 발생했다.

TCP 연결도 실패했다면, SslHandshakeTimeOutException 자체가 발생하지 않았을 것이다.

TCP조차 실패했다면?
TCP도 안 됐으면 Netty가 SSL 핸드셰이크 단계 자체에 진입하지 못한다. 즉 예외 타입이 ConnectException이 아니라 SslHandshakeTimeoutException이라는 것 자체가 TCP는 성공했다는 증거다.

# TCP 자체 실패 시 에러 메시지
Caused by: java.net.ConnectException: Connection refused
Caused by: java.net.ConnectException: Connection timed out

해결 방안

해결 방안은 단순하다. 로컬 프로파일에서는 TCP 연결을, 배포 환경에서는 TLS 연결을 하도록 수정해주면 된다.

문제가 됐던 "rediss" 부분을 scheme이라는 변수로 바꾸어 프로파일별로 다른 scheme을 가져가도록 하자.

@Value 안의 값은 프로파일에 따라 true(prod)와 false(local)로 나뉘도록 했다. 이렇게 하면 로컬 프로파일에서는 sslEnabled가 false가 되어 .setAddress의 scheme 자리에 "redis"가 들어갈 것이고, 배포 프로파일에서는 "rediss"가 들어가게 될 것이다.

우선 로컬 프로파일에서 실행되는지부터 보자.

잘 실행된다.

 

(추가) 기본값을 명시해주면 해당 키가 없는 프로필에서 placeholder 해석 실패로 Bean 생성이 중단될 수 있어, 기본값을 지정하는 게 좋다고 한다. 우선 로컬 기준으로 기본값을 false로 설정하였다. 

 

수정 후 PR이 열려 있는 상태에서 동일 브랜치에 push하게 되면 코드래빗이 이를 확인하고 이슈를 resolved 처리한다.