본문 바로가기

프로그래밍/Spring

Spring-Mybatis 연동시 오류를 일으키는 SQL 쿼리문을 받아보자(2)

지난 글에서는 Spring-Mybatis 연동 작업에서 SQL문이 실행시 오류가 발생할 경우 이를 예외에 포함시켜 던지기 위해 사전적으로 이해해야 할 내용인 SqlSessionTemplate 클래스에서 예외가 발생시 처리되는 과정에 대한 설명을 진행했다. 이번에는 이러한 이해를 기반으로 우리가 나아갈 방향인 오류가 발생한 SQL문을 넣어 이를 예외에 던지는 구체적인 작업에 대해 진행하도록 하겠다.

 

저번글에서도 잠깐 예기했지만 Spring-Mybatis 연동 작업에서 SQL 관련 오류로 인해 예외가 발생하면 DataAccessException 클래스 객체(정확하게는 DataAccessException 클래스를 상속받은 PersistenceException 클래스 객체이다)가 생성되어 던져지게 된다. 이 구조를 건드리지 않고 유지하면서 작업을 진행하겠다. 우리가 만들어야 할 예외 클래스는 사실 별거 없다. SQL 문을 저장할 수 있는 DataAccessException 클래스를 상속받은 새로운 예외 클래스를 만들면 된다. 다음의 코드를 보자

 

import org.springframework.dao.DataAccessException;

public class CustomDataAccessException extends DataAccessException {
    String query;
    public CustomDataAccessException(String msg){
        super(msg);
    }
    public CustomDataAccessException(String msg, String query){
        this(msg);
        this.query = query;
    }
    public CustomDataAccessException(String msg, Throwable cause){
        super(msg,cause);
    }
    public CustomDataAccessException(String msg, Throwable cause, String query){
        this(msg,cause);
        this.query = query;
    }
    public String getQuery() {
        return query;
    }
    public void setQuery(String query) {
        this.query = query;
    }
}

위에서 보여주는 코드는 DataAccessException 클래스를 상속받은 CustomDataAccessException 클래스 소스이다. 이 클래스의 멤버변수로 String 타입 변수인 query가 있다. 이 변수에 우리가 저장하고자 하는 SQL문이 들어가게 된다. 생성자는 DataAccessException 클래스의 생성자를 그대로 사용한것과 여기에 추가로 SQL문을 생성자에서 바로 저장할 수 있게끔 SQL문을 파라미터로 전달하는 생성자를 추가로 설정했으며, query 변수의 getter, setter 메소드를 추가했다. 코드를 보면 알 수 있겠지만, 외부에서 SQL문을 받았다고 가정할 경우 이 SQL문을 CustomDataAccessException 클래스의 생성자나 setter 메소드를 통해 SQL문을 넣을수 있고 getter 메소드를 통해 SQL문을 가져올 수 있다.

 

그러면 이 예외 객체를 어떻게 생성할까? 이전 글에서 SqlSessionTemplate 클래스를 설명할때 Proxy를 설명하는 과정에서 SqlSession 인터페이스가 구현된 클래스인 DefaultSqlSession 클래스 객체를 대신 실행하는 SqlSessionInterceptor 클래스에 대해 설명했었다. 이 클래스가 DefaultSqlSession 클래스 객체를 대신 실행할때 이 과정에서 예외가 발생하면 DataAccessException 클래스 객체를 상속받은 클래스 객체가 생성되어 예외가 던져지게 된다. 이 DataAccessException 클래스 객체를 상속받은 클래스 객체를 위에서 우리가 만든 CustomDataAccessException 클래스 객체로 바꿔서 생성한뒤 여기에 오류가 발생한 SQL문을 넣어서 예외를 던지면 되는 것이다. 다음의 코드를 보자.

 

public class CustomSqlSessionTemplate extends SqlSessionTemplate {

    public CustomSqlSessionTemplate(SqlSessionFactory sqlSessionFactory) throws IllegalAccessException{
        this(sqlSessionFactory, sqlSessionFactory.getConfiguration().getDefaultExecutorType());
    }

    public CustomSqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType) throws IllegalAccessException{
        this(sqlSessionFactory, executorType,
                new MyBatisExceptionTranslator(
                        sqlSessionFactory.getConfiguration().getEnvironment().getDataSource(), true));
    }

    public CustomSqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
                                    PersistenceExceptionTranslator exceptionTranslator) throws IllegalAccessException{

        super(sqlSessionFactory, executorType, exceptionTranslator);

        Field field = ReflectionUtils.findField(this.getClass().getSuperclass(), "sqlSessionProxy");
        field.setAccessible(true);
        field.set(this, (SqlSession) newProxyInstance(
                SqlSessionFactory.class.getClassLoader(),
                new Class[] { SqlSession.class },
                new CustomSqlSessionInterceptor()));
        field.setAccessible(false);

    }

    private class CustomSqlSessionInterceptor implements InvocationHandler {
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
            ExecutorType executorType = getExecutorType();
            PersistenceExceptionTranslator exceptionTranslator = getPersistenceExceptionTranslator();

            SqlSession sqlSession = getSqlSession(
                    sqlSessionFactory,
                    executorType,
                    exceptionTranslator);
            String sqlQuery = "";
            try {
                Object result = method.invoke(sqlSession, args);
                if (!isSqlSessionTransactional(sqlSession, sqlSessionFactory)) {
                    sqlSession.commit(true);
                }
                return result;
            } catch (Throwable t) {
                sqlQuery = getQuery(sqlSession, (String)args[0], args[1]);
                Throwable unwrapped = unwrapThrowable(t);
                if (exceptionTranslator != null && unwrapped instanceof PersistenceException) {
                    // release the connection to avoid a deadlock if the translator is no loaded. See issue #22
                    closeSqlSession(sqlSession, sqlSessionFactory);
                    sqlSession = null;
                    Throwable translated = exceptionTranslator.translateExceptionIfPossible((PersistenceException) unwrapped);
                    if (translated != null) {
                        unwrapped = translated;
                    }
                }
                CustomDataAccessException cdae = new CustomDataAccessException(unwrapped.getMessage(), unwrapped.getCause(), sqlQuery);
                throw cdae;
            } finally {
                if (sqlSession != null) {
                    closeSqlSession(sqlSession, sqlSessionFactory);
                }
            }
        }
    }

    /**
     * Mybatis Query를 파라미터가 Bounding 된 상태의 쿼리를 보고자 할때 사용
     * @param sqlSession
     * @param queryId
     * @param sqlParam
     * @return
     */
    private String getQuery(SqlSession sqlSession, String queryId , Object sqlParam) throws NoSuchFieldException, IllegalAccessException{

        BoundSql boundSql = sqlSession.getConfiguration().getMappedStatement(queryId).getBoundSql(sqlParam);
        String sql = boundSql.getSql();

        if(sqlParam == null){
            sql = sql.replaceFirst("\\?", "''");
        }else{
            if(sqlParam instanceof Number){ // Integer, Long, Float, Double 등의 숫자형 데이터 클래스는 Number 클래스를 상속받기 때문에 Number로 체크한다
                sql = sql.replaceFirst("\\?", sqlParam.toString());
            }else if(sqlParam instanceof String){	// 해당 파라미터의 클래스가 String 일 경우(이 경우는 앞뒤에 '(홑따옴표)를 붙여야해서 별도 처리
                sql = sql.replaceFirst("\\?", "'" + sqlParam + "'");
            }else if(sqlParam instanceof Map) {        // 해당 파라미터가 Map 일 경우

        	/*
        	 * 쿼리의 ?와 매핑되는 실제 값들의 정보가 들어있는 ParameterMapping 객체가 들어간 List 객체로 return이 된다.
        	 * 이때 List 객체의 0번째 순서에 있는 ParameterMapping 객체가 쿼리의 첫번째 ?와 매핑이 된다
        	 * 이런 식으로 쿼리의 ?과 ParameterMapping 객체들을 Mapping 한다
        	 */
                List paramMapping = boundSql.getParameterMappings();

                for (ParameterMapping mapping : paramMapping) {
                    String propValue = mapping.getProperty();        // 파라미터로 넘긴 Map의 key 값이 들어오게 된다
                    Object value = ((Map) sqlParam).get(propValue);    // 넘겨받은 key 값을 이용해 실제 값을 꺼낸다
                    if (value instanceof String) {            // SQL의 ? 대신에 실제 값을 넣는다. 이때 String 일 경우는 '를 붙여야 하기땜에 별도 처리
                        sql = sql.replaceFirst("\\?", "'" + value + "'");
                    } else {
                        sql = sql.replaceFirst("\\?", value.toString());
                    }

                }
            }else{					// 해당 파라미터가 사용자 정의 클래스일 경우

        	/*
        	 * 쿼리의 ?와 매핑되는 실제 값들이 List 객체로 return이 된다.
        	 * 이때 List 객체의 0번째 순서에 있는 ParameterMapping 객체가 쿼리의 첫번째 ?와 매핑이 된다
        	 * 이런 식으로 쿼리의 ?과 ParameterMapping 객체들을 Mapping 한다
        	 */
                List paramMapping = boundSql.getParameterMappings();

                Class paramClass = sqlParam.getClass();
                // logger.debug("paramClass.getName() : {}", paramClass.getName());
                for(ParameterMapping mapping : paramMapping){

                    String propValue = mapping.getProperty();                   // 해당 파라미터로 넘겨받은 사용자 정의 클래스 객체의 멤버변수명
                    Field field = doDeclaredField(paramClass, propValue);       // 관련 멤버변수 Field 객체 얻어옴
                    if(field == null) {                                         // 최상위 클래스까지 갔는데도 필드를 찾지 못할경우엔 null이 return 되기 때문에 null을 return 하게 되면 NoSuchFieldException 예외를 던진다
                        throw new NoSuchFieldException();
                    }
                    field.setAccessible(true);                    // 멤버변수의 접근자가 private일 경우 reflection을 이용하여 값을 해당 멤버변수의 값을 가져오기 위해 별도로 셋팅
                    Class javaType = mapping.getJavaType();            // 해당 파라미터로 넘겨받은 사용자 정의 클래스 객체의 멤버변수의 타입

                    if (String.class == javaType) {                // SQL의 ? 대신에 실제 값을 넣는다. 이때 String 일 경우는 '를 붙여야 하기땜에 별도 처리
                        sql = sql.replaceFirst("\\?", "'" + field.get(sqlParam) + "'");
                    } else {
                        sql = sql.replaceFirst("\\?", field.get(sqlParam).toString());
                    }
                }
            }
        }
        return sql;
    }

    /**
     * 클래스 필드 검색 재귀함수
     * @param paramClass
     * @param propValue
     * @return
     */
    private Field doDeclaredField(Class paramClass, String propValue){
        Field field = null;
        try {
            /*
            * 해당 객체의 필드를 검색 한다.
            * 존재 하지 않을 경우 NoSuchFieldException 발생
            */
            field = paramClass.getDeclaredField(propValue);

        } catch ( NoSuchFieldException e ){
            // NoSuchFieldException 발생 할경우 상위 클래스를 검색 한다.
            field = doDeclaredField(paramClass.getSuperclass(), propValue);
        }

        return field;
    }

}

 

위에서 보여주는 코드는 SqlSessionTemplate 클래스를 상속받은 클래스인 CustomSqlSessionTemplate 클래스 소스이다. 이 코드에 있는 메소드인 getQuery 메소드와 doDeclareField 메소드는 Mybatis의 XML에 저장되어 있는 SQL문에 사용자가 넘긴 객체의 값을 바인딩하는데 사용되는 메소드이다. 이 메소드에 대해서는 메소드 소스 안에 주석이 있기 때문에(블로그에 있는 소스가 보기 힘들면 github에 올려놓은 소스를 봐도 된다. 거기에도 주석이 있다) 주석을 보면 이해하는데 큰 문제가 없어서 이 메소드들에 대해서는 설명하지 않도록 하겠다. 여기서는 이 두 메소드를 통해 SQL문을 구했다는 가정을 갖고 이 SQL문을 넣은 예외를 생성하는 방법에 대해 설명하도록 하겠다. 

 

위에서 설명했다시피 CustomSqlSessionTemplate은 SqlSessionTemplate 클래스를 상속받은 클래스라고 얘기했다. 그렇기때문에 CustomSqlSessionTemplate은 SqlSessionTemplate 클래스가 제공하는 생성자를 사용해야 한다. 이전 글에서 SqlSessionTemplate 클래스의 생성자에 대해 설명한 것을 보면 알 수 있겠지만 SqlSessionTemplate 클래스에서의 첫번째 생성자는 두번째 생성자에 특정 값을 파라미터로 주어 두번째 생성자를 재활용하고 두번째 생성자는 세번째 생성자에 특정 값을 파라미터로 주어 세번째 생성자를 재활용 하는 방식으로 구현하고 있다. 여기에서도 이 방식을 그대로 따라간다. 그래서 CustomSqlSessionTemplate 클래스의 첫번째와 두번째 생성자도 각각 두번째 생성자와 세번째 생성자에 특정 값을 파라미터로 주어 재활용 하고 있다.

 

public CustomSqlSessionTemplate(SqlSessionFactory sqlSessionFactory) throws IllegalAccessException{
	this(sqlSessionFactory, sqlSessionFactory.getConfiguration().getDefaultExecutorType());
}

public CustomSqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType) throws IllegalAccessException{
	this(sqlSessionFactory
		, executorType
		, new MyBatisExceptionTranslator(sqlSessionFactory.getConfiguration().getEnvironment().getDataSource(), true)
	);
}

 

이러한 구조이기 때문에 결국 세번째 생성자가 실질적인 작업을 하는 코드라 보면 된다. 그러면 CustomSqlSessionTemplate 또한 SqlSessionTemplate 클래스의 세번째 생성자를 그대로 사용하면 되지 않을까? 이렇게 생각할 수 있는데 이렇게 진행 할 수가 없다. 이해를 돕기 위해 기존 SqlSessionTemplate 클래스의 세번째 생성자의 소스 부분과 여기에 사용되는 멤버변수들을 소스로 보여주겠다.

 

private final SqlSessionFactory sqlSessionFactory;
private final ExecutorType executorType;
private final SqlSession sqlSessionProxy;
private final PersistenceExceptionTranslator exceptionTranslator;
  
public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
      PersistenceExceptionTranslator exceptionTranslator) {

    notNull(sqlSessionFactory, "Property 'sqlSessionFactory' is required");
    notNull(executorType, "Property 'executorType' is required");

    this.sqlSessionFactory = sqlSessionFactory;
    this.executorType = executorType;
    this.exceptionTranslator = exceptionTranslator;
    this.sqlSessionProxy = (SqlSession) newProxyInstance(
        SqlSessionFactory.class.getClassLoader(),
        new Class[] { SqlSession.class },
        new SqlSessionInterceptor());
}

 

이 생성자는 SqlSessionTemplate에 정의된 4개의 멤버변수에 대해 초기화하고 있다. 4개의 멤버변수중 sqlSessionFactory, executorType, exceptionTranslator 변수는 파라미터로 전달된 값을 설정하기 때문에 SqlSessionTemplate 클래스를 상속받은 CustomSqlSessionTemplate 클래스에서도 생성자에 파파라미터로 넘긴뒤 super를 이용해서 넘겨주는 방식으로 설정할 수 있다. 그러면 남은 것은 SqlSession 타입  멤버변수인 sqlSessionProxy 변수 이거 하나이다. 문제는 이 멤버변수는 외부에서 파라미터로 값을 넘겨받아 설정하는 것이 아니라 생성자 안에서 초기화를 하고 있기 때문에 CustomSqlSessionTemplate 클래스 생성자에서도 이 멤버변수를 생성자 안에서 초기화 해야 한다.  그러면 우리는 CustomSqlSessionTemplate 클래스 생성자 안에서 부모클래스인 SqlSessionTemplate 클래스 멤버변수인 sqlSessionProxy를 초기화 할 수 있을까? 그럴수가 없다. 이유는 sqlSessionProxy가 private final 로 정의되어 있기 때문이다. final 이기 때문에 생성자 외에는 값을 설정할 수가 없다. 이때문에 sqlSessionProxy 변수에 대한 setter 메소드가 존재하지 않는다. setter 메소드가 존재한다면 super를 사용한 setter 메소드를 이용해서 설정할 수 있지만 setter 메소드가 존재하지 않기 때문에 이 방법을 사용할 수 없다. 그러면 이 sqlSessionProxy 변수를 자식클래스인 CustomSqlSessionTemplate 클래스에서 어떻게 초기화 시킬수 있을까? Java의 Reflection을 이용해서 이 멤버변수에 값을 설정할 수 있다. 다음의 CustomSqlSessionTemplate 클래스 생성자 소스를 보자. 이 생성자 소스를 보면 Reflection을 이용해서 부모 클래스의 멤버변수를 초기화 하고 있다.

 

public CustomSqlSessionTemplate(SqlSessionFactory sqlSessionFactory
								, ExecutorType executorType
								, PersistenceExceptionTranslator exceptionTranslator
								) throws IllegalAccessException {

	super(sqlSessionFactory, executorType, exceptionTranslator);

	Field field = ReflectionUtils.findField(this.getClass().getSuperclass(), "sqlSessionProxy");
    field.setAccessible(true);
    field.set(this
			, (SqlSession) newProxyInstance(
						SqlSessionFactory.class.getClassLoader(),
						new Class[] { SqlSession.class },
						new CustomSqlSessionInterceptor()
			  )
			);
    field.setAccessible(false);
}

 

코드를 살펴보자. 처음엔 super를 이용해서 파라미터 3개를 사용하는 부모클래스 생성자를 사용한다. 이 생성자를 사용하면 부모클래스인 SqlSessionTemplate 클래스의 sqlSessionProxy 변수가 초기화가 된다. 어 초기화 된다고? 그럼 이걸 이대로 사용하면 되자너? 이렇게 생각할 수 있다. 사실 맞다. sqlSessionProxy 변수가 초기화가 이루어진다. 근데 이 초기화된 값을 사용하면 안된다. SqlSessionTemplate 클래스 소스에서 이 변수가 초기화될 때 코드를 살펴보면 Proxy 객체를 실행하는 주체의 역할을 하는 클래스인 SqlSessionInterceptor 클래스 객체를 설정하는 부분이 있다. 이 클래스가 하는 역할은 이전 글에서도 설명했지만 예외가 발생할 경우 Spring 에서 제공하는 예외로 변환해서 이를 던지는 기능이 있다. 그러나 우리는 이 기능을 곧이 곧대로 사용하면 안된다. 왜냐면 위에서 만든 예외인 CustomDataAccessException 클래스 객체가 던져지게끔 설정해야 하기 때문이다. 그래서 SqlSessionTemplate 클래스의 내부 클래스인 SqlSessionInterceptor 클래스를 사용하면 안된다(실제로 이 클래스는 SqlSessionTemplate 클래스에서 private으로 설정되어 있기 때문에 참조도 할 수가 없다) 그래서 이러한 역할을 하는 클래스를 새로이 만들어야 한다. 그것이 CustomSqlSessionInterceptor 클래스이다. 부모클래스인 SqlSessionTemplate 클래스의 멤버변수인 sqlSessionProxy에 CustomSqlSessionInterceptor 클래스 객체를 설정해서 CustomDataAccessException 클래스 객체를 예외로 던질수 있게끔 설정하는 것이다. 그래서 Java의 Reflection 기능을 이용해서 이미 한번은 초기화되어 있는 sqlSessionProxy 멤버변수를 받아서 이를 다시 재설정하고 있다. 말이 나온 김에 CustomSqlSessionInterceptor 클래스 소스만 따로 살펴보자.

 

private class CustomSqlSessionInterceptor implements InvocationHandler {
	@Override
	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
		SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
        ExecutorType executorType = getExecutorType();
        PersistenceExceptionTranslator exceptionTranslator = getPersistenceExceptionTranslator();

        SqlSession sqlSession = getSqlSession(
                sqlSessionFactory,
                executorType,
                exceptionTranslator);
        String sqlQuery = "";
        try {
            Object result = method.invoke(sqlSession, args);
            if (!isSqlSessionTransactional(sqlSession, sqlSessionFactory)) {
                sqlSession.commit(true);
            }
            return result;
        } catch (Throwable t) {
            sqlQuery = getQuery(sqlSession, (String)args[0], args[1]);
            Throwable unwrapped = unwrapThrowable(t);
            if (exceptionTranslator != null && unwrapped instanceof PersistenceException) {
                // release the connection to avoid a deadlock if the translator is no loaded. See issue #22
                closeSqlSession(sqlSession, sqlSessionFactory);
                sqlSession = null;
                Throwable translated = exceptionTranslator.translateExceptionIfPossible((PersistenceException) unwrapped);
                if (translated != null) {
                    unwrapped = translated;
                }
            }
            CustomDataAccessException cdae = new CustomDataAccessException(unwrapped.getMessage(), unwrapped.getCause(), sqlQuery);
            throw cdae;
        } finally {
            if (sqlSession != null) {
                closeSqlSession(sqlSession, sqlSessionFactory);
            }
        }
    }
}

 

CustomSqlSessionInterceptor 클래스 또한 CustomSqlSessionTemplate 클래스 안에서 private 으로 선언한 내부 클래스이다. 기존 SqlSessionTemplate 클래스에 선언된 SqlSessionInterceptor 클래스와 비교해보면 큰 차이는 없다. 다만 catch 부분에서 차이가 발생한다. catch 부분을 진행한다는건 예외가 던져지는 상황이 발생한 것이므로 이 시점에 예외가 발생한 SQL문을 구해야 한다. 그 역할을 하는것이 getQuery 메소드이다. invoke 메소드의 두번째 파라미터로 Object 클래스 배열이 넘어오게 되는데 getQuery 매소드에서는 이 배열의 첫번째 값과 두번째 값을 사용하고 있다. 첫번째 값으로 넘어오는 것은 Mybatis의 SQL문 id이다. 우리가 XML로 Mybatis에서 사용할 SQL문을 정의할때 해당 SQL문에 대한 id를 설정하게 되는데 이 id 값이 넘어온다는 의미이다. 배열의 두번째 값으로 넘어오는 것은 그 SQL문에서 사용될 바인딩되는 값이 넘어오게 된다. 그러나 대부분 특정 클래스의 객체이거나 Map 또는 List 객체가 넘어가게 될 것이다. 아무튼 SqlSession 인터페이스가 구현된 객체와 이 2개의 파라미터를 같이 넘겨줌으로써 실제 사용된 값이 바인딩된 SQL문을 얻어오게 된다. 그 후로 진행되는 것은 기존 SqlSessionInterceptor 클래스와 동일하게 진행되다가 우리가 목표로 한 CustomDataAccessException 클래스 객체를 생성해서 이를 예외로 던지게 된다. 이 객체를 생성할때 Spring에서 사용되는 예외로 변환된 예외 클래스 객체부분과 SQL문을 같이 설정함으로써 이 예외를 받는 쪽에서는 해당 예외에 대한 내용과 관련 SQL문을 가져올 수 있게 된다.

 

이제 이 글의 마무리를 하는 시점이 왔다. 이렇게 기능을 구현했는데 이 기능이 동작을 하는지 확인을 할 시간인 것이다. github에 올린 Source에서 controller package 에 속한 클래스로 TestController와 TestControllerAdvice 이렇게 2개의 클래스가 있다. 먼저 TestController는 다음과 같다

 

@RestController
public class TestController {

    @Autowired
    BoardService service;

    @RequestMapping(value="/errortest")
    public Map<String, String> sendError(){
        Map<String, String> result = new HashMap<String,String>();
        Board board = new Board(0, "title123456789012345678901234567890", "contents1", 0);
        service.insertBoard(board);
        return result;
    }
}

 

이 Controller는 Rest API를 제공하는 목적으로 설계한 클래스이다. 즉 작업을 마치고 작업에 대한 결과나 내용을 Map에 담아 return 하는 방식으로 설계했다. 기본적인 컨셉은 이렇게 설계했지만 동작하는 과정에서는 일부러 에러가 발생하도록 구현했다. Source를 보면 Board 클래스 객체를 만들어서 거기에 테이블에 넣고자하는 값들을 생성자를 통해 설정한뒤에 insertBoard 메소드를 실행시켜 테이블에 값을 insert 하는 작업을 진행한다. 이 과정에서 에러가 발생하게 되는데 제목으로 입력되고 있는 값인 title123456789012345678901234567890 이 테이블에 설정되어 있는 크기보다 크게 입력되어서 에러가 발생한다. 에러가 발생되면 예외가 던져지게 되고 그 예외를 이제 받아서 처리해야 한다. 그 역할을 하는 클래스가 TestControllerAdvice 이다.

 

@RestControllerAdvice
public class TestControllerAdvice {
    @ExceptionHandler(CustomDataAccessException.class)
    public String processCustomDataAccessException(CustomDataAccessException cdae){
        return cdae.getQuery();
    }
}

 

Spring에서는 Controller가 예외를 던질경우 이를 처리하는 클래스에 대한 정의를 할 수 있는데, @Controller 일 경우엔 @ControllerAdvice 어노테이션이 붙은 클래스가, @RestController일 경우엔 @RestControllerAdvice 어노테이션이 붙은 클래스가 해당 작업을 하게 된다. 메소드에 @ExceptionHandler 어노테이션을 붙이고 거기에 해당 예외 클래스를 지정함으로써 그 예외 클래스를 처리하는 메소드로 지정하는 방식이다. SQL문 처리시 문제가 발생하면 관련 SQL문이 들어있는 CustomDataAccessException 클래스 객체가 던져지게 되므로 @ExceptionHandler 어노테이션에 CustomDataAccessException 클래스를 설정해서 이 예외를 처리하게끔 했다. 메소드엔 파라미터로 CustomDataAccessException 클래스 객체를 받게끔 되어 있고, 이 객체에서 getQuery 메소드를 호출해서 관련 SQL문을 받게 된뒤 이 SQL문을 return 하게 된다. 웹브라우저에서 http://localhost:8080/errortest 로 접속하면 이 SQL문을 볼 수 있는것은 TestController와 TestControllerAdvice 클래스에서 이러한 동작을 하기 때문에 볼 수 있는 것이다.