타입핸들러
-----------------------------
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는 조회 조건 전용입니다."
);
}
}
댓글
댓글 쓰기