타입핸들러

----------------------------- DECODE( aaa, 'N', '99991231', NVL(birth_enc, birth) ) AS birth_date ----------------------------- 대답 : 변경 SQL: NVL(aaa, '#NULL#') || '^|^' || NVL(birth_enc, '#NULL#') || '^|^' || NVL(birth, '#NULL#') AS birth_date SELECT person_id, person_name, NVL(aaa, '#NULL#') || '^|^' || NVL(birth_enc, '#NULL#') || '^|^' || NVL(birth, '#NULL#') AS birth_date FROM person Y^|^암호문데이터^|^19800101 N^|^#NULL#^|^19800101 ################ 복호화 TypeHandler ################ package com.example.mybatis.handler; import java.sql.CallableStatement; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.regex.Pattern; import org.apache.ibatis.type.BaseTypeHandler; import org.apache.ibatis.type.JdbcType; import org.apache.ibatis.type.MappedJdbcTypes; import com.example.crypto.CryptoService; @MappedJdbcTypes(JdbcType.VARCHAR) public class BirthDateDecryptTypeHandler extends BaseTypeHandler { private static final String DELIMITER = "^|^"; private static final String NULL_VALUE = "#NULL#"; private static final String PRIVATE_BIRTH_DATE = "99991231"; private final CryptoService cryptoService; public BirthDateDecryptTypeHandler(CryptoService cryptoService) { this.cryptoService = cryptoService; } /** * 조회 전용 TypeHandler라면 입력 파라미터에는 사용하지 않는다. */ @Override public void setNonNullParameter( PreparedStatement ps, int parameterIndex, String parameter, JdbcType jdbcType) throws SQLException { ps.setString(parameterIndex, parameter); } @Override public String getNullableResult( ResultSet rs, String columnName) throws SQLException { return parseAndDecrypt(rs.getString(columnName)); } @Override public String getNullableResult( ResultSet rs, int columnIndex) throws SQLException { return parseAndDecrypt(rs.getString(columnIndex)); } @Override public String getNullableResult( CallableStatement cs, int columnIndex) throws SQLException { return parseAndDecrypt(cs.getString(columnIndex)); } private String parseAndDecrypt(String combinedValue) throws SQLException { if (combinedValue == null) { return null; } /* * "^|^"에는 정규식 특수문자가 포함되어 있으므로 * Pattern.quote()로 일반 문자열 구분자로 처리한다. * * split(..., -1)은 빈 문자열 필드도 보존한다. */ String[] values = combinedValue.split( Pattern.quote(DELIMITER), -1 ); if (values.length != 3) { throw new SQLException( "생년월일 복호화 데이터 형식이 올바르지 않습니다. " + "expected=3, actual=" + values.length + ", value=" + maskForLog(combinedValue) ); } String aaa = restoreNull(values[0]); String birthEnc = restoreNull(values[1]); String birth = restoreNull(values[2]); /* * Oracle: * * DECODE( * aaa, * 'N', '99991231', * NVL(birth_enc, birth) * ) */ if ("N".equals(aaa)) { return PRIVATE_BIRTH_DATE; } if (birthEnc != null) { try { return cryptoService.decrypt(birthEnc); } catch (Exception e) { throw new SQLException( "BIRTH_ENC 복호화에 실패했습니다.", e ); } } return birth; } private String restoreNull(String value) { if (value == null || NULL_VALUE.equals(value)) { return null; } return value; } /** * 암호문이나 개인정보 전체가 예외 로그에 기록되지 않게 한다. */ private String maskForLog(String value) { if (value == null || value.isEmpty()) { return value; } int visibleLength = Math.min(10, value.length()); return value.substring(0, visibleLength) + "..."; } } ----------------------------- ----------------------------- where 조건에 암호화대상컬럼 in ('000234234','023424','34535345',... ) 라는 조건이 있다. {암호화대상컬럼}_hash in ('위의하드코딩값의해시값1','해시값',,,) 으로 조회되도록 타입핸들러를 제작해줘 ㅎㅏ드코딩된 값을 목록으로 받게 package com.example.mybatis.handler; import java.sql.CallableStatement; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import org.apache.ibatis.type.BaseTypeHandler; import org.apache.ibatis.type.JdbcType; import com.example.crypto.HashService; public class SearchHashTypeHandler extends BaseTypeHandler { private final HashService hashService; public SearchHashTypeHandler(HashService hashService) { this.hashService = hashService; } @Override public void setNonNullParameter( PreparedStatement ps, int parameterIndex, String parameter, JdbcType jdbcType) throws SQLException { if (parameter == null) { ps.setNull(parameterIndex, java.sql.Types.VARCHAR); return; } try { /* * 저장할 때 사용한 것과 반드시 동일한 전처리 규칙을 사용해야 한다. * * 예: * - trim 여부 * - 대소문자 변환 * - 하이픈 제거 * - 문자 인코딩 * - HMAC 키 */ String normalizedValue = normalize(parameter); String hashValue = hashService.hash(normalizedValue); ps.setString(parameterIndex, hashValue); } catch (Exception e) { throw new SQLException( "검색 조건 해시 변환에 실패했습니다.", e ); } } private String normalize(String value) { return value.trim(); } /* * 이 TypeHandler는 검색조건 입력용이므로 * 조회 결과 변환에는 사용하지 않는다. */ @Override public String getNullableResult( ResultSet rs, String columnName) throws SQLException { return rs.getString(columnName); } @Override public String getNullableResult( ResultSet rs, int columnIndex) throws SQLException { return rs.getString(columnIndex); } @Override public String getNullableResult( CallableStatement cs, int columnIndex) throws SQLException { return cs.getString(columnIndex); } } package com.example.crypto; public interface HashService { String hash(String plainText); } package com.example.mybatis.handler; import java.sql.CallableStatement; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Types; import java.time.Clock; import java.time.DateTimeException; import java.time.LocalDate; import java.time.Period; import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.time.format.ResolverStyle; import org.apache.ibatis.type.BaseTypeHandler; import org.apache.ibatis.type.JdbcType; public class AgeGroupTypeHandler extends BaseTypeHandler { private static final DateTimeFormatter BIRTH_DATE_FORMATTER = DateTimeFormatter.ofPattern("uuuuMMdd") .withResolverStyle(ResolverStyle.STRICT); /* * Oracle DB와 애플리케이션 서버의 날짜 기준이 달라지지 않도록 * 운영 환경의 기준 시간대를 명시한다. */ private static final ZoneId DEFAULT_ZONE_ID = ZoneId.of("Asia/Seoul"); private final Clock clock; public AgeGroupTypeHandler() { this(Clock.system(DEFAULT_ZONE_ID)); } /* * 단위 테스트에서 기준 날짜를 고정하기 위한 생성자 */ AgeGroupTypeHandler(Clock clock) { this.clock = clock; } @Override public void setNonNullParameter( PreparedStatement ps, int parameterIndex, String parameter, JdbcType jdbcType) throws SQLException { if (parameter == null) { ps.setNull(parameterIndex, Types.VARCHAR); return; } ps.setString(parameterIndex, parameter); } @Override public String getNullableResult( ResultSet rs, String columnName) throws SQLException { return calculateAgeGroup(rs.getString(columnName)); } @Override public String getNullableResult( ResultSet rs, int columnIndex) throws SQLException { return calculateAgeGroup(rs.getString(columnIndex)); } @Override public String getNullableResult( CallableStatement cs, int columnIndex) throws SQLException { return calculateAgeGroup(cs.getString(columnIndex)); } private String calculateAgeGroup(String birthDateValue) throws SQLException { if (birthDateValue == null || birthDateValue.isBlank()) { return null; } String normalizedBirthDate = birthDateValue.trim(); /* * 개인정보 보호를 위해 사용하는 특수 생년월일인 경우 * 실제 연령 계산을 하지 않는다. * * 업무 규칙에 따라 "60", null, "99" 등으로 변경 가능하다. */ if ("99991231".equals(normalizedBirthDate)) { return null; } try { LocalDate birthDate = LocalDate.parse( normalizedBirthDate, BIRTH_DATE_FORMATTER ); LocalDate today = LocalDate.now(clock); if (birthDate.isAfter(today)) { throw new SQLException( "생년월일이 현재 날짜보다 이후입니다." ); } int age = Period.between(birthDate, today).getYears(); return convertToAgeGroup(age); } catch (DateTimeException e) { throw new SQLException( "생년월일 형식이 올바르지 않습니다. expected=YYYYMMDD", e ); } } private String convertToAgeGroup(int age) { /* * 기존 CASE 문처럼 60세 이상은 모두 60으로 묶는다. */ if (age >= 60) { return "60"; } if (age >= 50) { return "50"; } if (age >= 40) { return "40"; } if (age >= 30) { return "30"; } if (age >= 20) { return "20"; } if (age >= 10) { return "10"; } return "00"; } } ----------------------------- ubstr( nvl( a.birth_date_hash , a.birth_date ), 5,8 ) = to_char( sysdate, 'MMDD') 에 대ㅐ서도 파터미터를 넘겨받게 해서 타입핸들러를 적용해줘 sysdate 를 사용하지 말고 오늘날짜를 타입핸들러를 통해서 해시값으로 파라미터로 받게 package com.example.mybatis.handler; import java.sql.CallableStatement; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import org.apache.ibatis.type.BaseTypeHandler; import org.apache.ibatis.type.JdbcType; import com.example.crypto.SearchHashService; public class BirthdayMmddHashTypeHandler extends BaseTypeHandler { private static final DateTimeFormatter MMDD_FORMATTER = DateTimeFormatter.ofPattern("MMdd"); private final SearchHashService searchHashService; public BirthdayMmddHashTypeHandler( SearchHashService searchHashService) { this.searchHashService = searchHashService; } @Override public void setNonNullParameter( PreparedStatement ps, int parameterIndex, LocalDate parameter, JdbcType jdbcType) throws SQLException { try { /* * 예: 2026-08-05 → "0805" */ String mmdd = parameter.format(MMDD_FORMATTER); /* * BIRTH_MMDD_HASH 저장 시 사용한 것과 * 반드시 동일한 해시/HMAC 함수를 사용한다. */ String hashedMmdd = searchHashService.hashBirthdayMmdd(mmdd); ps.setString(parameterIndex, hashedMmdd); } catch (Exception e) { throw new SQLException( "오늘 날짜의 MMDD 검색 해시 생성에 실패했습니다.", e ); } } /* * 이 TypeHandler는 WHERE 입력 파라미터 전용이다. */ @Override public LocalDate getNullableResult( ResultSet rs, String columnName) throws SQLException { throw new UnsupportedOperationException( "BirthdayMmddHashTypeHandler는 조회 조건 전용입니다." ); } @Override public LocalDate getNullableResult( ResultSet rs, int columnIndex) throws SQLException { throw new UnsupportedOperationException( "BirthdayMmddHashTypeHandler는 조회 조건 전용입니다." ); } @Override public LocalDate getNullableResult( CallableStatement cs, int columnIndex) throws SQLException { throw new UnsupportedOperationException( "BirthdayMmddHashTypeHandler는 조회 조건 전용입니다." ); } }

댓글

이 블로그의 인기 게시물

food eff privacy

판다 스픽 , 개인정보 처리방침 , Panda Speak Privacy Term

판다 수학 개인정보 처리방침 , Privacy , Panda Math