화요일 오전 9시 10분 쯤 서비스 중인 B2C LMS에서 대체인증이 정상적으로 처리되지 않는다는 문의가 들어왔다.
학습을 위한 대체인증(휴대폰 본인인증)시 사용자 화면에 아래와 같은 오류가 노출되고 있다는 것이었다.
java.sql.SQLException: ORA-00257: archiver error.
Connect internal only, until freed.
먼저 ORA-00257 코드가 어떤 의미인지 확인해보았다.
ORA-로 시작하는 이 오류 코드는 Oracle Database에서 발생하는 오류로,
데이터를 새로 넣거나 바꾼 기록(히스토리)을 저장하는 '아카이브 로그' 스토리지가 꽉 차서, 더 이상 새로운 기록을 만들지 못해 발생하는 오류였다. 그리고 기록을 남겨야만 다음 작업을 진행하는 오라클의 특성 때문에 DB의 모든 입력과 수정 기능이 멈춰버린 상태였던 것이다. (지금 다시 생각해보면 생각만해도 아찔한 상황이다.)
오류 메시지를 확인하고나니 한 가지는 바로 판단할 수 있었다.
우리 서비스의 DB에서 발생한 오류는 아니었다.
그 다음으로 휴대폰 본인인증 모듈부터 확인했다
자사 DB 문제가 아니라는 것이 확실해지고 난 뒤, 오류가 발생된 대체인증 기능에 포커스를 맞추었다.
그래서 먼저 대체인증에서 사용하는 휴대폰 본인인증 모듈 자체에 문제가 있는지 확인했다.
다행히 같은 휴대폰 본인인증 모듈을 사용하는 기능이 하나 더 있었다.
바로 회원가입 본인인증이었다.
같은 환경에서 회원가입 휴대폰 본인인증을 직접 테스트해보니 인증 성공 결과가 정상적으로 처리되고 있었다.
[회원가입 본인인증]
휴대폰 본인인증
↓
인증 성공
↓
회원가입 처리
↓
정상 완료
동일한 본인인증 모듈을 사용하는 회원가입에서는 문제가 발생하지 않았다.
이를 통해 휴대폰 본인인증 모듈 자체가 장애의 원인일 가능성은 낮다고 판단했다.
그렇다면 다음으로 확인해야 할 것은 두 기능의 차이였다.
회원가입 본인인증은 인증 성공 이후 내부 처리를 진행하는 반면, 대체인증은 인증 성공 후 외부 서버로 인증 결과를 추가 전송하는 과정이 있었다.
[대체인증]
휴대폰 본인인증
↓
인증 성공
↓
우리 서버
↓
외부 서버 API 호출
↓
인증 결과 전송
정리하면 상황은 이랬다.
[동일한 휴대폰 본인인증 모듈]
회원가입 본인인증(NICE 휴대폰 본인인증) → 정상
mOTP대체인증(NICE 휴대폰 본인인증) → 실패
↓
외부 서버 호출 존재
같은 본인인증 모듈을 사용하면서도 외부 서버 호출이 추가되는 대체인증에서만 문제가 발생하고 있었다.
여기에 처음 확인한 오류가 Oracle 계열의 ORA-00257이라는 점까지 고려하면, 본인인증 모듈보다는 대체인증 성공 이후 호출되는 외부 시스템과 그 응답 처리 로직을 우선 확인하는 것이 합리적이었다.
그래서 다음 단계에서는 대체인증 이후 외부 API가 어떻게 호출되고, 그 응답을 우리 애플리케이션에서 어떻게 처리하는지 코드 흐름을 따라가기 시작했다.
대체인증 이후 어디까지 갔다 오는가
코드를 따라가 보니 전체 흐름은 대략 이랬다.
휴대폰 본인인증
↓
인증 성공 Callback
↓
우리 서버 Controller
↓
외부 기관 API 호출
↓
외부 응답 수신
↓
XML / JSON 응답 파싱
↓
사용자 메시지 반환
그리고 해당 외부 시스템은 Oracle 기반이었다.
이제 처음에 봤던 ORA-00257과 흐름이 맞아떨어졌다.
외부 시스템 Oracle 장애
↓
외부 애플리케이션에서 SQLException 발생
↓
오류 메시지가 응답에 포함됨
↓
우리 서버가 그대로 수신
↓
외부 응답 메시지를 그대로 사용
↓
사용자 화면에 출력
외부 시스템에서 발생한 Oracle 오류가 응답을 타고 우리 사용자 화면까지 그대로 넘어온 것이었다.
여기까지만 보면 단순히 “외부 시스템 장애였네” 하고 끝낼 수도 있다.
그런데 코드를 조금 더 살펴보니 오히려 우리 쪽에서 신경 써야 할 부분이 더 많이 보였다.
외부 시스템이 장애 나는 것 자체는 우리가 통제할 수 없다.
하지만 외부 시스템의 장애가 우리 서비스 안으로 어떤 형태로 들어오는지는 우리가 통제할 수 있다.
문제 1. HTTP 통신은 잡는데 응답 파싱 오류는 못 잡고 있었다
기존 코드는 HTTP 요청 자체에는 try/catch가 적용되어 있었다.
try {
// HTTP 요청 및 응답 읽기
} catch (Exception e) {
resultMsg = e.getMessage();
result = -1;
} finally {
if (connection != null) {
connection.disconnect();
}
}