콘텐츠로 이동

HERA-V4 개인정보 컬럼 암호화 가이드

목차

  1. 개요
  2. 빠른 시작
  3. 단계별 적용 가이드
  4. 핵심 패턴 & 안티패턴
  5. 트러블슈팅
  6. 레퍼런스 링크
  7. 로드맵

1. 개요

무엇인가

HERA-V4는 MyBatis-Flex @Column(typeHandler = ...) 기반의 CryptoStringTypeHandler를 제공한다. 이 TypeHandler를 Entity 필드에 붙이면 DB 저장 시 자동으로 암호화하고, 조회 시 자동으로 복호화한다. 업무 코드는 평문 문자열을 기존과 동일하게 다루면 된다.

암호화 방식은 AES-CBC이며, 암호문은 ENC:v1: prefix로 시작한다. prefix가 없는 기존 평문 데이터는 조회 시 그대로 반환되어 단계적 전환이 가능하다.

현재 구현은 동등 검색(eq) 지원을 위해 같은 키와 같은 평문이면 같은 암호문이 생성되는 deterministic 암호화를 전제로 한다. 이 전제 때문에 검색 조건값을 같은 방식으로 암호화하면 WHERE email = ENC:v1:... 형태의 동등 검색이 가능하다. 단, 부분 검색(like)은 지원하지 않는다.

┌───────────────────────────────────────────────────────────────┐
│                      애플리케이션 코드                          │
│    entity.getEmail() → "hong@example.com" (평문)              │
└───────────────────────────┬───────────────────────────────────┘
                            │ @Column(typeHandler = CryptoStringTypeHandler.class)
                ┌───────────┴───────────┐
                │  INSERT               │  SELECT
                │  평문 → AES 암호화    │  ENC:v1:... → AES 복호화
                └───────────┬───────────┘
┌───────────────────────────┴───────────────────────────────────┐
│                           DB                                   │
│    email = "ENC:v1:Vd3kJ8Qp..." (암호문)                      │
└───────────────────────────────────────────────────────────────┘

언제 사용하는가

사용 O 사용 X
이메일, 전화번호 등 개인정보 문자열 컬럼 비밀번호 (해시 정책 별도)
MyBatis-Flex Entity 매핑 컬럼 검색 LIKE 조건이 필요한 컬럼 (추가 설계 필요)
저장값 자체를 암호화해야 하는 컬럼 숫자/날짜 타입 컬럼
현재 검색 조건에 사용되지 않는 컬럼 HRM 모듈 (현재 미적용)

검색 조건에 이미 사용 중인 컬럼도 적용은 가능하지만, 반드시 Step 4Step 5를 함께 확인한다.


2. 빠른 시작

신규 도메인의 email 컬럼 하나에 암호화를 적용하는 최소 예시다. 이 절은 Entity CRUD 저장/조회만 다룬다. XML JVO 조회나 검색 조건까지 사용하는 도메인은 단계별 적용 가이드를 끝까지 확인한다.

Step 1. 설정 키 확인

crypto.aes.secret-key가 프로파일 설정에 존재하는지 확인한다. 이 키는 CryptoConfig가 읽어 TypeHandler에 전달한다.

# bootstrap-system/src/main/resources/application-local.yml
crypto:
  aes:
    secret-key: 여기에-32자-이상의-키를-넣는다

주의: secret-key는 Git에 평문으로 커밋하지 않는다. 로컬 개발 환경 외에는 Vault 또는 환경변수로 주입한다.

Step 2. Entity 필드에 TypeHandler 지정

// hera-system/src/main/java/kr/co/dandisoft/hera/domain/sys/user/details/UserDetails.java

import com.mybatisflex.annotation.Column;
import com.mybatisflex.annotation.Table;
import kr.co.dandisoft.hera.mapper.handler.type.CryptoStringTypeHandler;

@Table(value = "sys_user_details")
public class UserDetails extends AuditModel<String> {

    @Column(value = "email", typeHandler = CryptoStringTypeHandler.class)
    private String email;
}

이것만으로 MyBatis-Flex 기반 INSERT/UPDATE/SELECT에서 암복호화가 자동 적용된다.

Step 3. 컴파일 확인

./gradlew :hera-system:compileJava

BUILD SUCCESSFUL이 나오면 기본 적용 완료다.


3. 단계별 적용 가이드

새로운 도메인에 개인정보 컬럼 암호화를 처음부터 적용하는 전체 절차다.

전체 작업 체크리스트

작업자는 아래 순서대로 확인한다. 하나라도 애매하면 바로 코드를 적용하지 말고 기존 참조 구현인 UserDetails를 먼저 비교한다.

  • 대상 컬럼이 개인정보 문자열 컬럼인지 확인한다.
  • 대상 컬럼이 VARCHAR 또는 TEXT인지 확인한다.
  • 암호문 저장 후에도 컬럼 길이가 충분한지 확인한다.
  • 해당 컬럼이 WHERE 조건에 쓰이는지 확인한다.
  • 해당 컬럼이 LIKE 검색에 쓰이면 이번 가이드만으로 처리하지 않는다.
  • Entity 필드에 @Column(typeHandler = CryptoStringTypeHandler.class)를 적용한다.
  • XML Mapper의 JVO 조회가 resultType을 쓰면 resultMap으로 전환한다.
  • API 검색 파라미터로 들어오는 SP 필드에는 @EncryptedField를 붙인다.
  • 서비스/배치/직접 호출 경로의 검색 조건은 encryptSearchValue() 방식으로 보완한다.
  • 컴파일 후 저장, 조회, 검색을 각각 확인한다.

적용 경로 판단표

경로 필요한 작업 이유
Entity CRUD 저장/조회 Entity 필드에 TypeHandler 적용 MyBatis-Flex Entity 매핑에서 TypeHandler가 동작
XML Mapper + JVO 조회 resultTyperesultMap으로 변경하고 암호화 컬럼에 TypeHandler 지정 resultType 자동 매핑은 Entity의 @Column 설정을 읽지 않음
webapp *ApiController 검색 SP 필드에 @EncryptedField 적용 AOP가 컨트롤러 진입 전 검색값을 암호화
서비스, 배치, AppRunner 직접 호출 검색 서비스 검색 조건에서 CryptoStringTypeHandler.encryptValue() 호출 webapp AOP가 실행되지 않는 경로
LIKE 검색 별도 설계 필요 암호문은 평문 일부 문자열과 매칭되지 않음

빠른 코드 검색 명령

대상 도메인을 정한 뒤 아래 명령으로 누락 지점을 찾는다.

# JVO XML 조회가 resultType을 쓰는지 확인
rg 'resultType=".*JVO"' {모듈}/src/main/resources/mybatis

# 대상 컬럼이 검색 조건에 쓰이는지 확인
rg 'email|phoneNo|대상필드명' {모듈}/src/main/java hera-domain-data/src/main/java hera-webapp/src/main/java

# 이미 적용된 참조 구현 확인
rg 'CryptoStringTypeHandler|EncryptedField|userDetailsJVOResultMap' hera-system hera-domain-data hera-webapp

Step 1. 암호화 대상 컬럼 결정

적용 전 다음을 먼저 확인한다.

왜 확인해야 하는가? DB에는 평문이 아니라 ENC:v1:... 암호문이 저장된다. 그래서 WHERE email = 'hong@example.com'처럼 평문으로 검색하면 일치하는 행이 없어 결과가 0건이 된다. 검색에 사용 중인 컬럼은 TypeHandler 적용 외에 검색 조건값 암호화가 필요하다.

사전 확인 사항 (어떤 컬럼이든 공통)

  • 컬럼 타입이 VARCHAR/TEXT인가? (다른 타입이면 이 가이드 적용 불가)
  • 컬럼 길이가 충분한가? AES-CBC 암호문 + ENC:v1: prefix로 길이가 늘어난다. (암호문 길이 표 참조)
  • LIKE 검색이 필요한가? 필요하다면 이 가이드의 TypeHandler 적용만으로는 처리할 수 없다.

해당 컬럼을 WHERE 조건 검색에 사용하는 코드가 있는가?

  • 없다 → Entity 필드에 @Column(typeHandler = CryptoStringTypeHandler.class) 한 줄만 추가하면 끝이다. (Step 2로 이동)
  • 있다 → 검색 조건값도 암호화해야 하므로 추가 작업이 필요하다. (Step 4 참조)

암호문 길이 참고:

평문 길이 암호문 길이 (ENC:v1: 포함)
20자 (이메일) 약 60~70자
13자 (전화번호) 약 55~65자

컬럼이 VARCHAR(50)이라면 VARCHAR(100) 이상으로 늘려야 할 수 있다.

Step 2. Entity 필드 적용

대상 파일: {모듈}/src/main/java/kr/co/dandisoft/hera/domain/{도메인}/{Entity}.java

// 예시: hera-system/src/main/java/kr/co/dandisoft/hera/domain/sys/user/details/UserDetails.java
import kr.co.dandisoft.hera.mapper.handler.type.CryptoStringTypeHandler;

@Column(value = "email", typeHandler = CryptoStringTypeHandler.class)
private String email;

@Column(value = "phone_no", typeHandler = CryptoStringTypeHandler.class)
private String phoneNo;

규칙: - @Column(value = "컬럼명", typeHandler = CryptoStringTypeHandler.class) 형태로 컬럼명을 명시한다. - VO, JVO, SP 등 결과 매핑 객체에는 붙이지 않는다.

Step 3. XML Mapper의 JVO 조회 보완

도메인에 XML Mapper(*-mapper.xml)가 있고, resultType="...JVO" 조회가 있다면 반드시 확인한다.

resultType 자동 매핑은 Entity의 @Column(typeHandler=...) 설정을 읽지 않는다. 암호문이 그대로 반환된다.

해결 방법: autoMapping="true" resultMap을 추가하고 암호화 컬럼만 명시한다.

<!-- hera-system/src/main/resources/mybatis/sys/user/details/user-details-mapper.xml -->

<resultMap id="userDetailsJVOResultMap" type="userDetailsJVO" autoMapping="true">
    <result property="email"
            column="email"
            typeHandler="kr.co.dandisoft.hera.mapper.handler.type.CryptoStringTypeHandler"/>
</resultMap>

<!-- resultType → resultMap 으로 변경 -->
<select id="findOne4JVO" resultMap="userDetailsJVOResultMap">
    <include refid="findSql4JVO"></include>
</select>

<select id="findAll4JVO" resultMap="userDetailsJVOResultMap">
    <include refid="findSql4JVO"></include>
</select>

autoMapping="true"를 설정하면 나머지 컬럼은 기존과 동일하게 자동 매핑되고, 명시한 컬럼만 TypeHandler를 거친다.

JVO SELECT에 암호화 컬럼이 여러 개 포함되면 resultMap에도 모두 등록한다. 예를 들어 email, phone_no를 둘 다 SELECT한다면 <result property="email" .../>, <result property="phoneNo" column="phone_no" .../>를 모두 작성한다.

: /convert-to-resultmap 커스텀 커맨드를 사용하면 이 변환을 자동화할 수 있다.

/convert-to-resultmap {mapper-xml-파일명} {resultType명} {암호화-컬럼명들}

Step 4. 검색 조건에 암호화 컬럼이 사용되는 경우

DB에는 평문이 아니라 ENC:v1:... 암호문이 저장된다. 따라서 검색 조건도 같은 방식으로 암호화해야 eq 검색이 가능하다.

해결 방법: 서비스, 배치, AppRunner처럼 webapp AOP를 거치지 않는 경로에서는 검색 조건에 넣는 값을 CryptoStringTypeHandler.encryptValue()로 미리 암호화한다.

// hera-system/src/main/java/kr/co/dandisoft/hera/domain/sys/user/details/UserDetailsServiceImpl.java

import kr.co.dandisoft.hera.mapper.handler.type.CryptoStringTypeHandler;
import java.sql.SQLException;

@Override
protected QueryWrapper searchCondition4JVO(UserDetailsSP searchParams) {

    String id    = searchParams.getId();
    String email = encryptSearchValue(searchParams.getEmail());
    String phoneNo = encryptSearchValue(searchParams.getPhoneNo());

    return QueryWrapper.create()
        .eq("ud.id", id, If::hasText)
        .eq("ud.email", email, If::hasText)
        .eq("ud.phone_no", phoneNo, If::hasText);
}

private String encryptSearchValue(String value) {
    try {
        return CryptoStringTypeHandler.encryptValue(value);
    } catch (SQLException e) {
        throw new IllegalStateException("Failed to encrypt search parameter.", e);
    }
}

encryptValue()는 null/blank면 그대로 반환하고, 이미 암호화된 값도 재암호화하지 않는다. 조건값이 없으면 조건 자체가 무시되므로 안전하다.

주의: 위 방식은 평문과 암호문이 1:1로 대응될 때만 작동한다. CryptoUtils.encryptByAes()는 동일 키 + 동일 평문이면 동일 암호문을 생성한다는 전제에서 동등 검색이 가능하다. 키 변경 이후에는 기존 암호문으로 검색이 불가하므로 키 관리에 주의한다.

중요: LIKE 검색은 지원하지 않는다. email LIKE '%hong%'처럼 일부 문자열로 검색해야 하는 요구가 있으면 별도 검색 컬럼, 해시 컬럼, 마스킹 인덱스 등 추가 설계를 먼저 진행한다.

Step 5. webapp API 컨트롤러 검색 파라미터 자동 처리

webapp의 API 컨트롤러(*ApiController)로 들어오는 검색 파라미터(SP)에서 암호화 컬럼을 자동 처리하려면 SP 필드에 @EncryptedField를 붙인다.

5-1. SP 필드에 어노테이션 추가

// 해당 도메인의 SP 파일
// 예: hera-domain-data/src/main/java/kr/co/dandisoft/hera/domain/sys/user/details/UserDetailsSP.java

import kr.co.dandisoft.hera.annotation.EncryptedField;
import kr.co.dandisoft.minu.base.BaseSP;

public class UserDetailsSP extends BaseSP {

    @EncryptedField
    private String email;

    @EncryptedField
    private String phoneNo;
}

@EncryptedField는 해당 String 필드의 값만 암호화한다. conditionType, conditionValue처럼 공통 검색 필드에 사용자가 선택한 컬럼명과 검색어를 넣는 구조라면 자동으로 판단하지 못한다. 이 경우 서비스 검색 조건에서 conditionType이 암호화 대상 컬럼인지 확인하고 conditionValue를 직접 암호화해야 한다.

5-2. 동작 방식

EncryptedFieldAspect*ApiController의 모든 메서드 진입 전에 실행된다.

HTTP 요청 → *ApiController 메서드 진입
    ↓ @Before AOP
    EncryptedFieldAspect.encryptSearchParams()
    EncryptedFieldProcessor.encrypt(arg)
        → @EncryptedField 필드를 찾아 CryptoStringTypeHandler.encryptValue() 호출
    컨트롤러 → 서비스 → Mapper (이미 암호화된 값으로 조회)

주의: AOP는 kr.co.dandisoft.hera.web.controller.api..*ApiController 패턴에만 적용된다. AppRunner, 배치, 직접 서비스 호출 경로는 AOP 대상이 아니므로 Step 4의 서비스 직접 암호화를 병행한다.

webapp API 경로와 서비스 직접 암호화가 둘 다 적용되어도 encryptValue()가 이미 암호화된 값은 그대로 반환하므로 이중 암호화되지 않는다.


4. 핵심 패턴 & 안티패턴

패턴 1. Entity CRUD 경로 — TypeHandler 한 줄 적용

// Entity 필드에 typeHandler 지정만 하면 된다.
// 서비스/매퍼/컨트롤러 코드는 일절 수정하지 않아도 된다.

@Column(value = "phone_no", typeHandler = CryptoStringTypeHandler.class)
private String phoneNo;

저장:

UserDetails user = new UserDetails();
user.setPhoneNo("010-1234-5678");  // 평문으로 set
userDetailsMapper.insert(user);    // DB에는 ENC:v1:... 암호문 저장

조회:

UserDetails user = userDetailsMapper.selectOneById("JTW");
user.getPhoneNo();  // "010-1234-5678" 평문 반환

패턴 2. JVO XML 조회 — resultMap에 암호화 컬럼만 명시

<!-- autoMapping="true"로 기존 컬럼은 그대로 매핑되고,
     암호화 컬럼만 typeHandler를 통해 복호화된다. -->
<resultMap id="myEntityJVOResultMap" type="myEntityJVO" autoMapping="true">
    <result property="email"
            column="email"
            typeHandler="kr.co.dandisoft.hera.mapper.handler.type.CryptoStringTypeHandler"/>
    <result property="phoneNo"
            column="phone_no"
            typeHandler="kr.co.dandisoft.hera.mapper.handler.type.CryptoStringTypeHandler"/>
</resultMap>

안티패턴 1. VO/JVO/SP에 @Column(typeHandler=...) 붙이기

// ❌ 틀린 사용 — JVO는 DB 매핑 Entity가 아니므로 효과 없음
public class UserDetailsJVO {
    @Column(typeHandler = CryptoStringTypeHandler.class)  // 무효
    private String email;
}

MyBatis resultType 자동 매핑은 Java 객체의 @Column 어노테이션을 읽지 않는다. JVO/VO 복호화는 반드시 Step 3resultMap 방식을 사용한다.


안티패턴 2. CryptoStringTypeHandler를 Spring Bean으로 등록하기

// ❌ 틀린 사용 — @Component, @MappedTypes, @MappedJdbcTypes 사용 금지
@Component
@MappedTypes(String.class)
@MappedJdbcTypes(JdbcType.VARCHAR)
public class CryptoStringTypeHandler extends BaseTypeHandler<String> { ... }

@MappedTypes(String.class) + @MappedJdbcTypes(JdbcType.VARCHAR) 조합으로 전역 등록하면 MyBatis가 모든 String/VARCHAR 컬럼에 TypeHandler를 적용한다. 일반 문자열 조건 파라미터까지 암호화되어 다음과 같은 문제가 발생한다.

-- 의도한 쿼리
WHERE ud.id = 'JTW'

-- 전역 등록 시 실제 쿼리 (id도 암호화됨)
WHERE ud.id = 'ENC:v1:Vd3kJ8Qp...'

CryptoStringTypeHandler는 Spring Bean으로 등록하지 않는다. 설정 주입은 CryptoConfig가 전담한다.


안티패턴 3. TypeHandler에서 @Value 직접 주입

// ❌ 틀린 사용
@Component
public class CryptoStringTypeHandler extends BaseTypeHandler<String> {
    @Value("${crypto.aes.secret-key}")
    private String secretKey;  // TypeHandler 인스턴스가 Spring 외부에서 생성되면 주입 안 됨
}

MyBatis는 TypeHandler를 직접 new로 생성할 수 있어 Spring의 @Value 주입이 보장되지 않는다. HERA-V4는 CryptoConfig.@PostConstruct에서 CryptoStringTypeHandler.configureSecretKey()를 호출하는 방식으로 이 문제를 해결했다.


안티패턴 4. XML resultType을 그대로 두고 복호화 기대하기

<!-- ❌ 틀린 사용 — resultType은 Entity @Column을 무시함 -->
<select id="findAll4JVO" resultType="userDetailsJVO">
    SELECT ud.email FROM sys_user_details ud ...
</select>

이 경우 emailENC:v1:Vd3kJ8Qp... 암호문 그대로 반환된다. 반드시 resultMap으로 교체한다.


안티패턴 5. 검색 조건 암호화 없이 암호화 컬럼으로 동등 검색

// 틀린 사용: 평문 조건으로 암호화 컬럼 검색
QueryWrapper.create().eq("ud.email", "hong@example.com");  // 결과 0건

DB에는 ENC:v1:... 암호문이 저장되어 있으므로 평문 조건으로는 절대 매칭되지 않는다. 반드시 CryptoStringTypeHandler.encryptValue("hong@example.com")로 암호화한 값을 조건에 넣어야 한다.


5. 트러블슈팅

작업 검증

코드 수정 후에는 컴파일만 보지 말고 저장, 조회, 검색을 나눠서 확인한다.

5-1. 컴파일 확인

수정한 모듈 기준으로 최소 범위 컴파일을 실행한다.

./gradlew :hera-system:compileJava
./gradlew :hera-domain-data:compileJava
./gradlew :hera-webapp:compileJava

모듈을 여러 개 수정했다면 수정한 모듈만 골라 실행한다. 공통 모듈인 hera-commons를 수정한 경우에는 해당 모듈을 의존하는 실행 모듈 컴파일도 함께 확인한다.

5-2. 저장 확인

Entity CRUD 경로로 값을 저장한 뒤 DB 저장값을 확인한다.

SELECT email, phone_no
FROM sys_user_details
WHERE id = '테스트ID';

기대 결과:

  • 암호화 대상 컬럼은 ENC:v1:로 시작한다.
  • null 또는 blank 값은 그대로 유지된다.
  • 이미 ENC:v1:로 시작하는 값은 다시 암호화되지 않는다.

5-3. 조회 확인

애플리케이션 조회 결과를 확인한다.

  • Entity 조회 결과는 평문이어야 한다.
  • JVO API 응답도 평문이어야 한다.
  • JVO 응답에서 ENC:v1:가 보이면 XML resultMap 적용이 누락된 것이다.

5-4. 검색 확인

암호화 컬럼을 검색 조건으로 쓰는 경우 아래를 확인한다.

  • email/phoneNo 동등 검색이 결과를 반환한다.
  • webapp API 요청 경로는 SP 필드에 @EncryptedField가 붙어 있다.
  • 서비스, 배치, AppRunner 직접 호출 경로는 encryptSearchValue() 같은 직접 암호화 처리가 있다.
  • LIKE 검색 요구가 있으면 이번 작업 범위에서 완료 처리하지 않는다.

5-5. 리뷰 전 자체 점검

PR 또는 코드 리뷰 요청 전 아래 항목을 다시 확인한다.

  • Entity 외 VO/JVO/SP에 @Column(typeHandler=...)를 붙이지 않았다.
  • CryptoStringTypeHandler@Component, @MappedTypes, @MappedJdbcTypes를 추가하지 않았다.
  • XML Mapper에서 resultType="...JVO"가 남아 있지 않은지 확인했다.
  • phoneNo처럼 JVO SELECT에 포함되는 암호화 컬럼은 resultMap에도 모두 등록했다.
  • 설정 파일에 crypto.aes.secret-key가 있고, 운영 키를 평문으로 커밋하지 않았다.

자주 발생하는 오류

증상 원인 해결 방법
JVO 조회 결과에서 email이 ENC:v1:...로 시작하는 문자열 XML resultType 사용 중. TypeHandler 미적용 resultMap으로 교체 (Step 3)
일반 id, name 조건도 ENC:v1:...로 암호화됨 TypeHandler 전역 등록 (@MappedTypes/@MappedJdbcTypes) @Component, @MappedTypes, @MappedJdbcTypes 제거
Column crypto secret-key is required. SQLException crypto.aes.secret-key 미설정 또는 CryptoConfig Bean 미로드 설정 파일에 crypto.aes.secret-key 추가. bootstrap-systemhera-commons를 의존하는지 확인
Failed to encrypt column value. SQLException CryptoUtils.encryptByAes() 내부 오류 (키 오류 또는 라이브러리 문제) 키 길이 및 형식 확인. minu-core 버전 확인
검색해도 결과가 0건 검색 조건이 평문이지만 DB 컬럼은 암호문 encryptSearchValue() 또는 EncryptedField 적용 누락 (Step 4, Step 5)
Failed to decrypt column value. SQLException 잘못된 키로 복호화 시도, 또는 데이터 손상 저장 시 사용한 키와 현재 키가 동일한지 확인
기존 평문 데이터 조회 시 깨지거나 오류 CryptoStringTypeHandlerENC:v1: prefix가 없는 값은 그대로 반환한다. 별도 처리 불필요
@EncryptedField가 붙어 있는데 암호화 안 됨 API 컨트롤러가 *ApiController 명명 규칙을 따르지 않음 EncryptedFieldAspect 포인트컷(*ApiController) 확인

로그 위치

_temp/logs/bootstrap-system.log
_temp/logs/hera-webapp.log

6. 레퍼런스 링크

핵심 코드

파일 역할
CryptoStringTypeHandler.java TypeHandler 본체. encrypt/decrypt 로직
CryptoConfig.java crypto.aes.secret-key 읽어 TypeHandler에 전달
EncryptedField.java SP 검색 필드 마커 어노테이션
EncryptedFieldProcessor.java @EncryptedField 필드를 리플렉션으로 암호화
EncryptedFieldAspect.java API 컨트롤러 진입 전 자동 암호화 AOP
UserDetails.java TypeHandler 적용 참조 Entity 예시
user-details-mapper.xml resultMap typeHandler 적용 XML 예시
UserDetailsServiceImpl.java searchCondition4JVO 암호화 검색 예시

설정 키 요약

설정 키 위치 설명
crypto.aes.secret-key application-{profile}.yml 또는 Vault AES 암복호화 공통 키. TypeHandler가 이 값을 사용

PDCA Archive

자세한 설계 배경, 결정 기록, 알고리즘 선택 근거는 아래를 참조한다.

변경 이력

날짜 버전 변경 요약 작성자
2026-05-15 1.2 로드맵 (Blind Index) 섹션 추가 장태욱
2026-05-15 1.1 주니어 작업용 체크리스트, 적용 경로 판단표, 검색/검증 절차 보강 장태욱
2026-05-15 1.0 초안 작성 (파일럿: UserDetails email/phoneNo 기준) 장태욱

7. 로드맵

Blind Index — AES-GCM 전환 + HMAC 검색 인덱스

현재 방식의 한계

현재 구현은 AES-CBC deterministic 방식이다. 같은 키와 같은 평문이면 항상 같은 암호문이 나온다. 이 덕분에 DB 스키마 변경 없이 단일 컬럼으로 동등 검색(eq)이 가능하지만, DB에서 암호문을 분석하면 동일 값 여부를 추론할 수 있다는 보안 취약점이 있다.

Blind Index 방식

AES-GCM(비결정적)으로 전환하면서 검색 기능을 유지하려면 Blind Index 패턴을 사용한다.

  • 암호화 컬럼: AES-256-GCM으로 저장한다. 매번 랜덤 nonce를 사용하므로 같은 평문이어도 암호문이 다르다. 패턴 추론이 불가능하다.
  • 인덱스 컬럼: HMAC-SHA256(평문, 별도 인덱스 키) 값을 별도 컬럼(email_blind_idx VARCHAR(64))에 함께 저장한다.
  • 검색: 검색어에 동일하게 HMAC을 계산하여 인덱스 컬럼과 비교한다. 평문이나 암호문이 DB에 노출되지 않고도 동등 검색이 가능하다.
[저장]  email           → AES-256-GCM(랜덤 nonce) → ENC:v2:랜덤암호문 (매번 다름)
        email_blind_idx → HMAC-SHA256(평문, idx_key) → 고정 해시 (항상 동일)

[검색]  WHERE email_blind_idx = HMAC-SHA256('검색어', idx_key)

AES-GCM nonce는 SecureRandom으로 12바이트를 매번 생성하므로, 같은 평문이어도 저장 암호문과 검색 시 재암호화 암호문이 일치하지 않는다. Blind Index 없이는 AES-GCM으로 동등 검색이 불가능하다.

검색 방식 비교

조건 방식 판단
검색 불필요 AES-256-GCM 저장 암호화에 적합
검색 필요 + 컬럼 추가 가능 AES-256-GCM + Blind Index (HMAC) 보안·실무 모두 권장
검색 필요 + 컬럼 추가 불가 AES-CBC deterministic (현재) 단일 컬럼 EQ 가능, 동일값 패턴 노출
LIKE 부분검색 지원 불가 암호화와 LIKE는 충돌. 앱 필터링 등 별도 설계 필요

현재 방식과 Blind Index 비교

항목 현재 (AES-CBC deterministic) Blind Index (AES-GCM + HMAC)
암호화 결정적 (same in → same out) 비결정적 (same in → 매번 다른 out)
동일값 패턴 암호문으로 추론 가능 추론 불가
EQ 검색 암호문 직접 비교 HMAC 인덱스 컬럼 비교
LIKE 검색 미지원 동일하게 미지원
스키마 변경 불필요 {col}_blind_idx VARCHAR(64) 추가
기존 데이터 처리 불필요 재암호화 + 인덱스 생성 배치 필요

예정 작업 항목

  1. CryptoStringTypeHandler — AES-CBC에서 AES-256-GCM으로 전환 (ENC:v2: prefix 도입)
  2. HMAC 유틸리티 추가 — CryptoUtils.hmacSha256(value, indexKey)
  3. DB 스키마 마이그레이션 — 암호화 대상 컬럼마다 {col}_blind_idx VARCHAR(64) 추가
  4. EncryptedFieldProcessor, searchCondition4JVO() — 검색 조건을 HMAC 인덱스 컬럼 기반으로 변경
  5. 기존 ENC:v1: 데이터 재처리 배치 — 전환 기간에는 구버전 복호화 경로 병행 유지

이 주제의 상세 분석은 GitHub Issue #221 댓글을 참조한다.