새로운 일을 준비하는 가운데 꽤 바쁜 2월을 보내고 있다.
이번 글은 내가 해결할 수 없는 없는 미지의 블랙박스 영역을 라이브러리 업데이트로 해결한 케이스를 추적해가며 해결해나간 과정을 적어보고자 한다. 기술적으로 조치는 간단했지만 간단한 해법에 도달하기까지 과정에서 Oracle JDBC 내부의 블랙박스에 대해 행동도 취할 수 없음에 답답해했던 일이라 공유하고자 한다.
MyBatis 의 useGeneratedKeys=”true” 란?
이번 사건의 가장 중심에 있는, 데이터베이스<-> 애플리케이션 간 미들웨어 역할을 해주는 프레임워크인 MyBatis의 useGeneratedKeys
속성에 대해 우선적으로 간략히 소개를 한다.
데이터베이스에 삽입된 데이터 행을 개별적으로 식별하기 위해 키(key)를 지정하는데, 단순히 속성 하나가 키를 이루는 경우도 있고 여러 속성을 사용해 키를 구성하는 복합키 구성도 있지만 여기에서는 단일 구성만 쓴다고 가정한다. 보통 생각해볼 수 있는 키는 각 행을 유일하게 식별할 수 있어야하기에 식별자(Identifier)로 불리게 되고 보통 줄여서 ID라고 부른다.
ID는 Generator를 사용해 생성 받을 수 있지만 단순히 각 행을 식별하거나 내부 업무용으로 사용되는 아이디를 만드는 목적으로는 DBMS에서 제공해주는 자동 증분값(Auto-increment)를 사용하면 무난한 선택이 된다. 데이터베이스 동작 중에서 키의 생성과 등록이 필요한 쿼리는 INSERT
와 UPDATE
정도이고, 업무에 따라서는 이렇게 생성된 ID 값을 바탕으로 진행 중 트랜잭션 내에서 다른 업무를 추가로 처리한 뒤에 사용자에게 응답을 보내는 경우가 많다.
이에 따라 DB에서 생성된 값을, 쿼리 파라미터로 전달된 POJO 객체에 다시 세팅하여 애플리케이션 단으로 전달해주기 위해 사용하는 옵션이 Mapper에서 사용되는 useGeneratedKeys="true"
이다. 실질적으로는 어떤 칼럼이 Key column인지 지정하는 keyColumn
옵션과 파라미터로 전달된 POJO 객체의 어떤 프로퍼티의 setter를 호출해서 키값을 애플리케이션 단에서 알 수 있게 전달해줄지 설정하는 keyProperty
옵션이 필요하다.[1]
아래와 같은 방식 두 가지로 사용된다. 첫번째는 Java 파일상의 Mapper 에서 사용시 지정하는 방법이고, 두번째는 Java상에서 선언해둔 Mapper 인터페이스에 연결시킬 XML 파일에 선언하는 방법이다.
1 |
|
1 | <insert |
전자의 코드에선 author
객체의 setAuthorId
setter를 호출해서 처리되고, XML파일도 마찬가지로 연결된 Java Mapper Interface를 통하여 파라미터로 전달된 POJO 객체의 setAuthorId
를 MyBatis가 호출하여 생성된 데이터를 전달 해준다.[2]
DBMS가 만들어준 ID를 알아내는 원리
Maria DB/MySQL에서 제공하는 AUTO_INCREMENT
속성이나 MS-SQL에서 제공하는 Identity 속성, Oracle 같은 경우도 12c 버전부터 NUMBER
타입 칼럼에 한하여 IDENTITY
속성을 테이블 생성시 부여하여 혜택을 누릴 수 있다.
해당 정보를 받아오는 메커니즘은 데이터베이스가 쿼리를 수행한 뒤에 반환해주는 메타데이터를 활용하는 방법으로, MyBatis는 JDBC 드라이버를 통하여 수신된 메타데이터를 keyColumn
속성을 통해 주어진 칼럼의 등록값을 확인하고 keyProperty
속성을 통해 파라미터 POJO 객체의 어떤 setter를 호출할지 결정하여 작업을 마친다.
JDBC 드라이버의 구조상 드라이버를 조작하는 인터페이스만 정의하고 있으며[3], 내부의 구현은 각 벤더(vendor)사에 맞게 구현하도록 되어있다. 그리고 드라이버 구현체의 소스를 공개할지 말지는 개별 벤더사의 몫에 달려있다.
Oracle의 경우 적어도 유상 기술지원 없이는 이 드라이버를 블랙박스처럼 이용하는 방법 밖에 없는 것이다. 대부분 애플리케이션 문제는 이 드라이버에서 생기지 않지만 않으면 괜찮은데 꼭 그렇진 않다. 살다보면 밟을 수도 있다.
Oracle JDBC 드라이버 12.1.0.1 메타데이터 처리 버그
버그가 수정되지 않은 드라이버(12.1.0.1)을 사용하여 useGeneratedKeys
가 활성화된 작업을 수행하면, 쿼리나 MyBatis Mapper 설정이나 데이터베이스 이상이 없음에도 다음과 같은 오류를 내면서 데이터베이스 작업이 실패한다. 메시지는 상이할 수 있으나, 콜스택에 initMetaDataColumnIndexes
나 initMetaData
또는 AutoKeyInfo
클래스 관련 오류가 발생되는 공통점이 있다. 실 스택트레이스는 회사 사내 로그라서 오라클 기술문서에서 공개한 문서에서 발췌한 로그로 대신한다.
1 | java.lang.NullPointerException |
대충 구글에 오라클 드라이버 버전과 함께 관련된 키워드를 찾으면 다음 기술 문서가 나온다. (Doc ID 1619185.1)
https://support.oracle.com/knowledge/Middleware/1619185_1.html
Using the 12.1.0.1.0 JDBC driver connected to an Oracle 11.2 database, the following null pointer exception is received when getting metadata from a generated key:
하지만 정말 유감스럽게도 해당 기술문서 페이지에서 확인할 수 있는 사항은 여기까지다. 증상(Symptoms), 원인(Cause), 해법(Solution), 참고문서(References)를 확인하려면 My Oracle Support 계정이 필요한데 이것은 유료이기도 하고 Oracle 제품군 구입시에 받은 고객 지원 ID(SI)를 등록해야 서포트 계정이 활성화 된다.
일반적인 방법으론 정보를 얻을 수 없다. 왠지 드라이버 버전을 올리면 될 것 같지만, 정확한 조치사항을 알고 싶어서 회사의 DBA를 살살 구슬려 문서 확인하고 내용 좀 알려달라고 부탁했다. 그리고 결론은,
해법
Oracle JDBC 드라이버를 12.1.0.1 에서 12.1.0.2로 올리면 된다.[4]
올리고 나니까 데이터를 INSERT
한 후에 DB에서 생성해준 키값이 무사히 POJO 객체의 setter 호출을 통해 전달되어 애플리케이션에 수신되는게 확인된다.
해당 문제는 12.1.0.1에만 있던 문제였고, 12.1.0.2나 그 이후 드라이버 또는 그 이전 드라이버에는 없는 문제였으므로 충분한 테스트 후에 드라이버를 교체하는 것으로 문제는 해결될 수 있다.
충분한 테스트가 필요한 이유는, 오비이락이란 말도 있듯 드라이버 올린 그 날 갑자기 전혀 상관없는 원인으로 장애가 생길 수도 있으니까. 보통의 사람들은 어떤 문제가 생기면 가장 간단히 언급하기 좋고 기억하기 편한 ‘드라이버 업데이트’ 처럼 어마어마하게 생긴 사건을 우선적으로 들먹이고 장애의 원인을 업데이트 하자고 한 사람에게 뒤집어 씌우려고 할 것이다.
만약 Spring 애플리케이션을 디플로이할 때 아카이브 통째로 실행하는 방식으로 디플로이하는게 아니라 별도의 Web Application Server를 통해 가동시키는 방법을 사용하고 있다면, 해당 WAS가 DB Connection Pool을 생성하고 관리하는 DataSource를 생성할 때 쓰이는 드라이버 또한 교체해야한다.
이 경우 드라이버 교체 후 서버를 완전히 내려야하는 다운타임이 필요하므로 사전에 다운타임에 대한 공지나 서비스 우회 계획을 잘 세워두어야한다.
끝마치며, Oracle에 대한 씁쓸함
서비스 안정성 이슈 때문에 아직 드라이버 변경은 수행되지 못했다. 그리고 데이터베이스는 Oracle 12c Release 2(12.1.0.2)를 설치해두고 있었음에도 DBMS 설치시에 제공된 JDBC 드라이버가 아니라 구형 드라이버를 올려둔 부분에 대한 미스테리는 지금도 이해가 가질 않는 상황.
이번 건을 마무리 지으면서 큰 씁쓸함을 느낀건 Oracle에 대한 부분이었다.
물론 이런 부분이 다 비지니스 모델의 요소이겠지만, 적어도 드라이버 업데이트시 ChangeLog를 제공하든지 약식으로라도 수정된 변경사항과 결함을 알려주든지 해야하지 않을까 싶다.
- 1.https://mybatis.org/mybatis-3/sqlmap-xml.html ↩
- 2.여전히 전자정부프레임워크 기반 프로젝트에 Oracle을 쓴다면
<selectKey />
기능을 사용하여 키로 사용할 값을 미리 쿼리로 계산하는 방법도 많이 이용할 것이다. ↩ - 3.https://docs.oracle.com/javase/8/docs/technotes/guides/jdbc/ ↩
- 4.오라클 홈페이지에서 다운로드 받을 수 있는데, 혹시나 망 분리 환경 등으로 인해 드라이버를 구하기 어렵다면 Oracle 12c Release 2가 설치된 서버에 접속해서 Oracle 설치 경로로 찾아가 jdbc 폴더에 들어가면 ojdbc*.jar 파일이 있으니 그걸 쓰면 된다. ↩