본문 바로가기
개발/Java

Springboot, Oracle UDT 사용시 HikariProxyConnection cannot be cast to oracle.jdbc.OracleConnection 문제 해결.

by 이청춘아 2021. 2. 24.

1. 환경

  • SpringBoot, (아래 스크립트 참조)
  • Oracle 12c
  • Oracle JDK 1.8.0_221
buildscript {
    ext {
        springBootVersion = '2.3.5.RELEASE'
    }
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
        classpath "io.spring.gradle:dependency-management-plugin:1.0.10.RELEASE"
    }
}

sourceCompatibility = 1.8

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
    //	oracle
    implementation 'org.springframework.data:spring-data-oracle:1.2.1.RELEASE'
    implementation 'com.oracle.database.jdbc:ojdbc8:21.1.0.0'
}

 

2. 현상

 Springboot에서 오라클 UDT를 사용하여 프로시저를 호출할 때 파라미터 값에 "new SqlArrayValue(value)"와 같이 값을 지정하니 아래와 같이 "HikariProxyConnection cannot be cast to oracle.jdbc.OracleConnection" 에러가 발생하였다. "SqlArrayValue" 외 일반적인 기본형으로 값을 바인딩하는 경우 발생하지 않는다. 파라미터 유형에 따라 수행하기 위한 셋업과정에 차이가 는것 같다.

<에러>

java.lang.ClassCastException: com.zaxxer.hikari.pool.HikariProxyConnection cannot be cast to oracle.jdbc.OracleConnection

	at oracle.sql.TypeDescriptor.setPhysicalConnectionOf(TypeDescriptor.java:823)
	at oracle.sql.TypeDescriptor.<init>(TypeDescriptor.java:605)
	at oracle.sql.ArrayDescriptor.<init>(ArrayDescriptor.java:311)
	at oracle.sql.ArrayDescriptor.<init>(ArrayDescriptor.java:290)
	at org.springframework.data.jdbc.support.oracle.SqlArrayValue.createTypeValue(SqlArrayValue.java:90)
	at org.springframework.jdbc.core.support.AbstractSqlTypeValue.setTypeValue(AbstractSqlTypeValue.java:60)
	at org.springframework.jdbc.core.StatementCreatorUtils.setValue(StatementCreatorUtils.java:292)
	at org.springframework.jdbc.core.StatementCreatorUtils.setParameterValueInternal(StatementCreatorUtils.java:231)
	at org.springframework.jdbc.core.StatementCreatorUtils.setParameterValue(StatementCreatorUtils.java:146)
	at org.springframework.jdbc.core.CallableStatementCreatorFactory$CallableStatementCreatorImpl.createCallableStatement(CallableStatementCreatorFactory.java:209)
	at org.springframework.jdbc.core.JdbcTemplate.execute(JdbcTemplate.java:1090)
	at org.springframework.jdbc.core.JdbcTemplate.call(JdbcTemplate.java:1147)
	at org.springframework.jdbc.core.simple.AbstractJdbcCall.executeCallInternal(AbstractJdbcCall.java:412)
	at org.springframework.jdbc.core.simple.AbstractJdbcCall.doExecute(AbstractJdbcCall.java:372)
	at org.springframework.jdbc.core.simple.SimpleJdbcCall.execute(SimpleJdbcCall.java:198)
    ...
    ...
    ...

 

3. 원인

 Springboot JDBC에서 제공되는 "org.springframework.jdbc.core.JdbcTemplate"를 사용하였는데, 파라미터의 유형과 값을 지정하는 과정에서 오라클 전용 타입을 생성하면서 발생한 오류였다.

at org.springframework.jdbc.core.CallableStatementCreatorFactory$CallableStatementCreatorImpl.createCallableStatement(CallableStatementCreatorFactory.java:209)

 createCallableStatment에서 오라클 전용 타입을 생성하기 위해서는 Connection 유형이 OracleConnection.class여야 하지만, HikariCP를 통해서 Wrapping된 HikariProxyConnection.class 인자로 넘어오면서 발생한다. HikariProxyConnection.class가 넘어오면 왜 문제가 되었는지 살펴보자.

/*
	예제에서 사용된 oracle JDK 1.80._221 기준
*/

//	org.springframework.jdbc.core
JdbcTemplate.execute()
	-> CallableStatementCreatorFactory$CallableStatementCreatorImpl.createCallableStatement()
	-> StatementCreatorUtils.setParameterValue()
    
//	org.springframework.data.jdbc
	-> support.oracle.SqlArrayValue.createTypeValue()
    
//	oracle.sql (JDK에 요딴게 들어있는지 처음 알았다.ㅠㅠ)
	-> ArrayDescriptor.<init>
	-> TypeDescriptor.setPhysicalConnectionOf(Connection var1)
       (Connection = HikariCP를 사용할 경우 기본설정으로는 HikariProxyConnection이 넘겨지게 된다.)

 오라클 전용 유형을 사용하였을 때 JdbcTemplate.execute 메서드가 동작하는 절차 일부를 나열하였다. 정확히 문제가 되는 곳은 "oracle.sql.TypeDescriptor.setPhysicalConnectionOf(Connection var1)"로 Connection을 OracleConnection.class로 캐스팅하면서 발생한다. 아래와 같은 방법으로 HikariProxyConnection.class을 캐스팅하면서 오라클에서만 사용하는 비표준 메서드가 노출되지 않게되어 OralceConnection.class로 캐스팅하지 못하는 현상이다.

//	oracle JDK 1.80._221
//	oracle.sql.TypeDescriptor

package oracle.sql;

@DefaultLogger("oracle.sql")
@Supports({Feature.OBJECT_METADATA})
public class TypeDescriptor implements OracleTypeMetaData, Serializable, ORAData, OracleData {
  
  ...생략
  
  public void setPhysicalConnectionOf(Connection var1) {
      this.connection = ((oracle.jdbc.OracleConnection)var1).physicalConnectionWithin();
  }
  
  ...생략
  
}

 

 

4. 해결방법

4.1 DataSource 생성시 HikariDataSource.class가 아닌 OracleDataSource.class 사용하기

이 경우에는 HikariCP를 사용하지 않게 된다.

/*
	Spring에서 Datasource 생성시 OracleDataSource.class를 사용하여 생성하기.
*/

@Bean
@Override
public DataSource dataSource(DataSourceProperties  properties) throws SQLException {
  OracleDataSource dataSource = new OracleDataSource();
  dataSource.setUser(properties.getUsername());
  dataSource.setPassword(properties.getPassword());
  dataSource.setURL(properties.getUrl());
  dataSource.setImplicitCachingEnabled(true);
  dataSource.setFastConnectionFailoverEnabled(true);
  //...
  return dataSource;
}

4.2 JdbcTemplate 상속받아 Connection 캐스팅 하기

HikariCP를 사용하면서 위 문제를 해결할 수 있는 가장 쉬운 방법인것 같다.

 JdbcTemplate가 작업을 수행하기 위해 Connection을 취득하는 단계에서 정상적으로 타입 캐스팅이 이뤄진 OracleConnection을 뒤로 흘려주면 될것 같았다. 관련내용으로 구글링 해보니 비표준 또는 프록시에 노출되지 않은 메서드를 엑세스할 수 있도록 객체를 반환할 수있는 방법이 있어 적용해보았다.

JdbcTemplate을 상속받아 권장되는 방법(isWrapperFor, unwrap)으로 Connection을 캐스팅하여 전달되도록 수정하였다.

public class JdbcTemplateA extends JdbcTemplate {

    public JdbcTemplateA() {
    }

    public JdbcTemplateA(DataSource dataSource) {
        super(dataSource);
    }

    public JdbcTemplateA(DataSource dataSource, boolean lazyInit) {
        super(dataSource, lazyInit);
    }

    @SneakyThrows
    @Override
    public <T> T execute(CallableStatementCreator csc, CallableStatementCallback<T> action)
            throws DataAccessException {

        Assert.notNull(csc, "CallableStatementCreator must not be null");
        Assert.notNull(action, "Callback object must not be null");
  
        if (logger.isDebugEnabled()) {
            String sql = getSql(csc);
            logger.debug("Calling stored procedure" + (sql != null ? " [" + sql  + "]" : ""));
        }

        //	수정 전
        //	Connection con = DataSourceUtils.getConnection(obtainDataSource());

        //	수정 후 - 시작
        Connection wrap = DataSourceUtils.getConnection(obtainDataSource());
        OracleConnection con = null;

        if(wrap.isWrapperFor(OracleConnection.class)){
            con = wrap.unwrap(OracleConnection.class);
        }
        //	수정 후 - 끝

        CallableStatement cs = null;
        try {
            cs = csc.createCallableStatement(con);
            applyStatementSettings(cs);
            T result = action.doInCallableStatement(cs);
            handleWarnings(cs);
            return result;
        }
        catch (SQLException ex) {
            // Release Connection early, to avoid potential connection pool deadlock
            // in the case when the exception translator hasn't been initialized yet.
            if (csc instanceof ParameterDisposer) {
                ((ParameterDisposer) csc).cleanupParameters();
            }
            String sql = getSql(csc);
            csc = null;
            JdbcUtils.closeStatement(cs);
            cs = null;
            DataSourceUtils.releaseConnection(con, getDataSource());
            con = null;
            throw translateException("CallableStatementCallback", sql, ex);
        }
        finally {
            if (csc instanceof ParameterDisposer) {
                ((ParameterDisposer) csc).cleanupParameters();
            }
            JdbcUtils.closeStatement(cs);
            
            //	수정 전
            // DataSourceUtils.releaseConnection(con, getDataSource());
            
            // 수정 후
            DataSourceUtils.releaseConnection(wrap, getDataSource());
        }
    }

    private static String getSql(Object sqlProvider) {
        if (sqlProvider instanceof SqlProvider) {
            return ((SqlProvider) sqlProvider).getSql();
        }
        else {
            return null;
        }
    }

}

 

boolean isWrapperFor(java.lang.Class<?> iface) throws java.sql.SQLException;

Returns true if this either implements the interface argument or is directly or indirectly a wrapper for an object that does. Returns false otherwise. If this implements the interface then return true, else if this is a wrapper then return the result of recursively calling isWrapperFor on the wrapped object. If this does not implement the interface and is not a wrapper, return false. This method should be implemented as a low-cost operation compared to unwrap so that callers can use this method to avoid expensive unwrap calls that may fail. If this method returns true then calling unwrap with the same argument should succeed.

Params: iface – a Class defining an interface.

Returns: true if this implements the interface or directly or indirectly wraps an object that does.

Throws: SQLException – if an error occurs while determining whether this is a wrapper for an object with the given interface.

Since: 1.6

 

<T> T unwrap(java.lang.Class<T> iface) throws java.sql.SQLException;

Returns an object that implements the given interface to allow access to non-standard methods, or standard methods not exposed by the proxy. If the receiver implements the interface then the result is the receiver or a proxy for the receiver. If the receiver is a wrapper and the wrapped object implements the interface then the result is the wrapped object or a proxy for the wrapped object. Otherwise return the the result of calling unwrap recursively on the wrapped object or a proxy for that result. If the receiver is not a wrapper and does not implement the interface, then an SQLException is thrown.

Params: iface – A Class defining an interface that the result must implement.

Type parameters: <T> – the type of the class modeled by this Class object

Returns: an object that implements the interface. May be a proxy for the actual implementing object.

Throws: SQLException – If no object found that implements the interface

Since: 1.6

댓글0