본문 바로가기

Programing/Framework

DataIntegrityViolationException

Sentry 로 알림이 오면 팀에서는 습관적으로 ignore 처리를 하는 것을 발견했다.

특히 배포 직후에는 이런 ignore가 빈번했는데 새로운 버전이 배포가 되면 기존의 custom ignore가 무효화가 되는 것 같았다.

 

백로그의 티켓 중 내부 개선 에픽이 붙어있는 묵혀놓은 티켓을 발견했다. 바로 "정상적인 케이스의 알람을 받지 않는다."이다.

위의 알람의 경우 사용자가 입력을 짧은 시간에 여러번 하여 save 명령이 두 번이 된 경우에 주로 발생했다.

 

DB에는 UC(Unique Constraint) 조건이 달려있었기에 나중에 요청이 온 쿼리가 수행이 되지 않는다.

버튼을 클릭 후 disable로 변경시켜 입력을 두 번하는 것을 막는 것이 근본적인 방향이겠지만 해당 UI는 다른 팀이 담당하고 있어서 백엔드를 개발하는 우리 팀에서는 외부 요청 티켓을 만들고 진행 사항에 대한 추적관리를 해야해서 직접적인 통제 범위를 넘어선다.

 

그래서 내부에서는 어떤 작업을 할 수 있을까에 대한 생각을 해보았다.

해당 케이스는 특별히 부작용(데이터가 꼬인다거나, 알람이 두 번 간다거나 등)이 없고 단지 정상적인 알람과 섞여서 주의를 빼앗기게 된다는 점에서 알람이 안가게 하는 조치를 생각해 볼 수 있다.

 

그래서 이전에 어떻게 되어 있는지 분석이 필요했다.

 

DB의 에러코드를 보니 1062라는 코드가 발생한다.

SQL Error: 1062, SQLState: 23000

DB 벤더별로 에러코드는 다르기에 스프링 프레임워크는 코드에 대한 매핑을 별도의 xml로 빼놓았다.

org.springframework.jdbc.support.sql-error-codes.xml

<bean id="MySQL" class="org.springframework.jdbc.support.SQLErrorCodes">
		<property name="duplicateKeyCodes">
			<value>1062</value>
		</property>
		<property name="dataIntegrityViolationCodes">
			<value>630,839,840,893,1169,1215,1216,1217,1364,1451,1452,1557</value>
		</property>

여기에 보면 1062는 duplicateKeyCodes 로 매핑이 되어 있음을 알 수 있다.

이 프로퍼티는 org.springframework.dao.DuplicateKeyException 로 매핑이 될 것이라 생각을 했는데,

알람상 org.springframework.dao.DataIntegrityViolationException 로 왔음을 알 수 있다.

 

클래스의 계층 구조를 살펴보면 DuplicateKeyException의 부모 클래스가 DataIntegrityViolationException 임을 알 수 있다.

정말로 어떤 예외 클래스가 생겼는지는 재현을 해서 찾아보면 된다.

브레이크 포인트로는 SQLErrorCodeSQLExceptionTranslator 를 걸어보면 될 터이다.

package org.springframework.jdbc.support;

public class SQLErrorCodeSQLExceptionTranslator extends AbstractFallbackSQLExceptionTranslator {

	@Override
	@Nullable
	protected DataAccessException doTranslate(String task, @Nullable String sql, SQLException ex) {

		// ..
		// Check SQLErrorCodes with corresponding error code, if available.
		if (this.sqlErrorCodes != null) {
			String errorCode;
			if (this.sqlErrorCodes.isUseSqlStateForTranslation()) {
				errorCode = sqlEx.getSQLState();
			}
			else {
				// Try to find SQLException with actual error code, looping through the causes.
				// E.g. applicable to java.sql.DataTruncation as of JDK 1.6.
				SQLException current = sqlEx;
				while (current.getErrorCode() == 0 && current.getCause() instanceof SQLException) {
					current = (SQLException) current.getCause();
				}
				errorCode = Integer.toString(current.getErrorCode());
			}

			if (errorCode != null) {
				// Look for defined custom translations first.
				CustomSQLErrorCodesTranslation[] customTranslations = this.sqlErrorCodes.getCustomTranslations();
				if (customTranslations != null) {
					for (CustomSQLErrorCodesTranslation customTranslation : customTranslations) {
						if (Arrays.binarySearch(customTranslation.getErrorCodes(), errorCode) >= 0 &&
								customTranslation.getExceptionClass() != null) {
							DataAccessException customException = createCustomException(
									task, sql, sqlEx, customTranslation.getExceptionClass());
							if (customException != null) {
								logTranslation(task, sql, sqlEx, true);
								return customException;
							}
						}
					}
				}
				// Next, look for grouped error codes.
				if (Arrays.binarySearch(this.sqlErrorCodes.getBadSqlGrammarCodes(), errorCode) >= 0) {
					logTranslation(task, sql, sqlEx, false);
					return new BadSqlGrammarException(task, (sql != null ? sql : ""), sqlEx);
				}
				// ..
				else if (Arrays.binarySearch(this.sqlErrorCodes.getDuplicateKeyCodes(), errorCode) >= 0) {
					logTranslation(task, sql, sqlEx, false);
					return new DuplicateKeyException(buildMessage(task, sql, sqlEx), sqlEx);
				}
				else if (Arrays.binarySearch(this.sqlErrorCodes.getDataIntegrityViolationCodes(), errorCode) >= 0) {
					logTranslation(task, sql, sqlEx, false);
					return new DataIntegrityViolationException(buildMessage(task, sql, sqlEx), sqlEx);
				}