들어가며
올해 2월 말부터 서버 이관(?)을 담당하게 되었는데, 이관해야 할 항목 중에 ETL 배치(앞으론 상황에 따라서 배치 또는 클라이언트라고 할게요)가 약 60개 정도 됐습니다. 배치를 이관하는 중에는 배치를 신규 서버로 이관하고 다음 날 출근하면 배치가 잘 돌았는지 확인하는 루틴을 가지고 있었는데.. 어떤 날은 배치가 모두 잘 돌았고, 어떤 날은 3개, 다른 날은 10개가 실패하는 장애가 발생했습니다.. 심지어 장애가 발생한 배치가 매번 같지 않고 다르다는 사실이 정말 이상하다 생각이 들었습니다.

그렇게 오랜만에 블로그에 글을 쓰게 해 준 에러를 먼저 보여드리겠습니다. 아래 에러는 클라이언트(배치) 단에서 기록된 log입니다.

찾아보니 Connection reset by peer가 뜻하는 바는 이러합니다.
peer : 또래, 동료라는 뜻. 통신을 주고받는 상대방. 즉, 클라이언트 입장에선 서버를 의미합니다.
reset : 강제로 연결을 끊는 것을 뜻합니다.
원인을 알 수 없지만 상대측(DB)에서 강제로 연결을 종료했다는 뜻을 의미했습니다. 이 상황에서 두 가지 가능성을 가정했습니다. 1) 배치는 정상적으로 시작했지만, ETL 작업 중 서버 측에서 연결을 끊었다. 2) 애초에 서버와의 연결 자체가 정상적으로 성립되지 않았다.
우선 첫 번째 가정을 검증하기 위해 확인을 시작했습니다. 배치가 정상적으로 시작되었다면 Oracle에 연결된 후 배치 History 테이블에 기록이 남아있어야 했습니.

6월 2일과 5일 : 등록된 시간에 배치 작업이 실행되지 않아 수동으로 처리한 기록만 존재.
6월 7일(토) : 없음
6월 8일 : 7일에 돌아야 할 배치까지 함께 실행된 기록 존재
이 시점부터 "Oracle에 연결되지 못하고 있다"라고 의심하기 시작했고, 확신하기 위해 시스템 로그를 확인하기로 했습니다.
확인한 로그 파일의 종류는 listener.log와 alert.log로 만약 listener.log에 에러가 남아 있다면 DB에 들어오기 전 네트워크/Listener 쪽 문제, alert.log에 에러가 남아있다면 DB 내부 처리 문제라고 생각했습니다.

$ORACLE_HOME/diag/rdbms/<db명>/<인스턴스명>/trace/alert.log
$ORACLE_HOME/diag/tnslsnr/<hostname>/listener/trace/listener.log
확인 결과 alert.log와 listener.log 두 시스템 로그에서 유의미한 내용을 찾을 수 없었습니다. 이때부터 "클라이언트가 간헐적으로 서버에 도달하지 못하는 상황"이라 생각하고 원인을 찾기 시작했습니다.

1. 신규 서버의 DB에서 허용하는 프로세스/세션 개수가 문제인가 하고 확인하기

총 사용할 수 있는 세션의 6% 정도가 현재 사용되고 있고, 배치가 6시 20 ~ 50분 사이에 30개가 2~5분 단위로 나눠서 실행되며 배치에서 실행되는 쿼리에 PARALLEL(8) Hint가 있다고 가정했을 때 사용률은 8~11%로 예상됐습니다. 이 정도면 나름 쾌적한 환경이기 때문에 원인에서 제외했습니다.
2. HA 솔루션 문제
기존 서버에서는 문제없이 배치가 계속해서 돌고 있고 신규 서버의 DB에 간헐적으로 접속에 실패하는 것이 네트워크 단의 문제이지 않을까 라는 생각을 했습니다. 때문에 신규 서버에는 기존과 다르게 HA 솔루션이 적용되어 그 문제인가? 싶었지만 이것도 원인이 아니었습니다.
그렇게 처음 가정했던 원인 2가지는 모두 원인이 아니었고.. 개발자의 영원한 Google 선생님을 통해 유사 사례를 조사해 보니 비슷한 사례를 발견할 수 있었습니다.

원인
원인을 먼저 말하면 배치 애플리케이션(JAVA로 만들어진)이 DB와 연결을 시도할 때 사용하는 난수(SecureRandom) 생성 방식이 장애를 발생시키고 있었습니다.
아래 사진은 Client(배치)와 DB가 연결을 맺는 과정을 도식화해 본 그림입니다.

보통 JAVA 애플리케이션에서 Oracle과 연결하기 위해서는 JDBC 드라이버라는 라이브러리를 사용하게 됩니다. 이때 JDBC는 네트워크 연결, 인증 처리, SQL 전달 등과 같은 역할을 하게 됩니다.
왜 갑자기 드라이버 이야기를 하느냐! 왜냐하면 드라이버가 SecureRandom 클래스를 사용하는 주체라고 생각되기 때문입니다. 위 도식화에서 보면 애플리케이션과 Oracle이 정상적으로 연결을 맺기 위해 6.DB 인증 과정을 진행하고 이때 데이터 암호화 및 무결성 검사를 위해 SecureRandom을 사용하지 않을까?라는 생각을 하게 됐습니다. (이런 생각을 하게 된 배경 중 하나는 저희 배치 코드 내에서는 명시적으로 SecureRandom을 사용하는 부분은 없기 때문입니다. Oracle 공식 사이트를 보면 JDBC 클라이언트가 Oracle RDBMS로 보안 연결을 한다고 소개되어 있습니다. 그리고 username과 password 같은 정보는 암호화되어야 하는 정보이기 때문에 이때 드라이버 내부 로직에서 SecureRandom을 사용할 것이라 생각했습니다.. Stack Overflow, Oracle Bug 리포트 등과 같은 사이트들에서 저와 같은 오류가 발생한 사람들이 계속해서 존재하고 공통점이 드라이버 사용이라고 생각해 내릴 수 있는 최대한의 결론이었습니다.. 무책임한 말일 수도 있지만 틀릴 수 있습니다..)

그렇다면 대체 왜! SecureRandom을 사용해 난수를 생성하는 방식이 문제가 되느냐. 가 궁금하실 겁니다.
Java의 SecureRandom 클래스는 보안적으로 강력한 난수를 생성하기 위해 사용되는 클래스이고, 리눅스 시스템에서 제공하는 엔트로피를 활용하여 난수를 생성합니다.
이때 엔트로피란 무엇이냐?! 엔트로피는 쉽게 말해 무작위성의 정도 즉, 얼마나 예측 불가능한가를 나타내는 수치를 말합니다.
때문에 서버와 연결을 맺고 암호화 알고리즘의 seed를 초기화할 때와 같이 보안과 신뢰성을 높여야 하는 상황에 예측 불가능한 정보(엔트로피)를 사용하는 것입니다.
리눅스 시스템에서는 마우스의 움직임, 키보드 입력, 디스크 동작 같은 예측 불가능한 정보들을 모아서 이 엔트로피를 만들어내고 SecureRandom은 이렇게 만들어진 엔트로피를 기반으로 랜덤 값을 생성합니다. 그런데 문제가 되는 건 바로 이 정보를 어디서 가져오냐는 점입니다!!!

JAVA는 기본적으로 다음 두 가지 중 하나에서 엔트로피를 가져오고 JVM 설정(java.security 파일)또는 시스템 기본값에 따라 무엇을 사용할지 정해집니다.
1) /dev/random // Linux Default
2) /dev/urandom
차이점은 다음과 같습니다.

결과적으로 기존 서버에서는 잘 작동하는 애플리케이션이 신규 서버에서 간헐적으로 실패한 이유를 정리해 보면 다음과 같습니다. 신규 서버는 2024년 12월 말쯤부터 운영을 시작했고 본격적으로 이관을 시작한 시점은 2월 17일 이후부터였습니다. 때문에 짧은 운영 기간으로 엔트로피가 적게 쌓여있는 상태에서 /dev/random을 사용하여 랜던값을 생성하려 하니 블로킹이 발생해 장애가 발생하는 것이었습니다. 이 상황을 도식화해 보면 아래와 같습니다.(직접 그린 그림이라.. 역시 틀릴 수 있습니다..)

실제로 서버에서 엔트로피가 얼마나 쌓였는지 확인해 보면.. 기존 서버와 신규 서버의 엔트로피가 약 62배 차이가 나는 것을 볼 수 있습니다!!
cat /proc/sys/kernel/random/entropy_avail

해결 방법
저와 같은 비슷한 문제를 겪은 사람들이 꽤 많아서 그런지 Oracle Bug 사이트? 에 솔루션이 있습니다. 정확히 말하면 해결 방법이라기보다는 우회하는 방법으로 /dev/random 말고 /dev/urandom을 사용하는 것입니다.
방법은 두 가지가 있습니다. 1) 서버에서 JVM 설정 파일을 바꾸기 2) 애플리케이션 실행 시 옵션 추가하기
SecureRandom 클래스는 java.security 설정 파일을 기반으로 동작하기 때문에 이 부분을 수정해 주면 해결할 수 있습니다. 매우 간단하죠? 파일의 위치와 수정 방법은 아래와 같습니다.
$JAVA_HOME/conf/security

1번 방법은 시스템 전체 설정을 바꾸는 방법인데 만약 그럴 수 없다면 애플리케이션 실행 시에 개별적으로 옵션을 추가하는 방법이 있습니다.
java -Djava.security.egd=file:/dev/urandom -jar batch-program.jar
마치며
솔직히 서버 이관하는 작업 쉽게 생각했는데 오만한 생각이었다는 걸 이번에 깨달았습니다.. 일부만 말해보자면 기존에 있는 테이블 3개를 신규 DB로 옮긴 후 외부 시스템으로부터 받은 파일을 가지고 데이터를 업데이트하는데 며칠이 소요되고.. 이 테이블들과 관련된 배치가 실패해서 데이터 확인하고 해결하는데 또 며칠 걸리고.. 몇 억 건의 데이터에서 잘못된 데이터를 찾고 연관된 테이블에 영향이 가지 않을지를 파악하는 과정이 정말 쉽지 않았습니다.. 이 외에도 배치를 분석하는 것도 쉽지 않았고.. 후후.. 하지만 이렇게 성장하는 것 아니겠습니까..

혹시라도 틀린 내용이 있다면 댓글로 알려주세요!! 많이 조사해 보고 GPT랑 싸우면서 알아낸 것들이지만 틀린 게 있을 수 있습니다.
Reference
1. Stack Overflow에서 유사 사례
https://stackoverflow.com/questions/2327220/oracle-jdbc-intermittent-connection-issue
Oracle JDBC intermittent Connection Issue
I am experiencing a very strange problem This is a very simple use of JDBC connecting to an Oracle database OS: Ubuntu Java Version: 1.5.0_16-b02 1.6.0_17-b04 Database: Oracle 11g
stackoverflow.com
2. JDBC가 Oracle과 기본적으로 보안 연결을 한다는 내용
https://docs.oracle.com/cd/B19306_01/network.102/b14268/asojbdc.htm?utm_source=chatgpt.com#i1006209
Configuring Network Data Encryption and Integrity for Thin JDBC Clients
14/30 5 Configuring Network Data Encryption and Integrity for Thin JDBC Clients This chapter describes the Java implementation of Oracle Advanced Security, which lets thin Java Database Connectivity (JDBC) clients securely connect to Oracle Databases. This
docs.oracle.com
3. Oracle Java Bug Database 사이트에서 Linux에서 SecureRandom을 사용할 때 중단된다는 내용.
https://bugs.java.com/bugdatabase/view_bug?bug_id=6521844
Bug ID: JDK-6521844 SecureRandom hangs on Linux Systems
bugs.java.com
4. JDBC 클라이언트 측 10가지 보안 기능
https://docs.oracle.com/cd/B19306_01/java.102/b14355/clntsec.htm#EHAEFBJJ
JDBC Client-Side Security Features
19/50 10 JDBC Client-Side Security Features This chapter discusses support in the Oracle Java Database Connectivity (JDBC) Oracle Call Interface (OCI) and JDBC Thin drivers for login authentication, data encryption, and data integrity, particularly, with r
docs.oracle.com
'Develop' 카테고리의 다른 글
| [Error] ORA-01861: literal does not match format string 해결 (0) | 2025.02.09 |
|---|---|
| [Project] 나이스 본인확인 API 적용 (feat.JAVA) (4) | 2024.11.07 |
| [Error] Authentication failed for nexus 401 unauthorized (0) | 2024.06.15 |
| [Error] invalid signature file digest for manifest main attributes 해결 (0) | 2024.06.11 |
| [Dev Environment] 안드로이드 스튜디오 Flutter 환경 세팅 에러 해결 (2) | 2024.06.05 |