1. 개요
1.1. 배경 및 적용 목적
admin 마이그레이션 도중, DB의 특정 테이블들에 개인정보가 평문으로 저장되어있음을 파악하였습니다. 해당 개인정보들 중에는 동아리 멤버들의 정보 뿐만 아니라, 동아리에 지원서를 낸 모든 사람의 정보가 저장되어 있었습니다. 현재 구조로는 새로운 유저를 추가하는 등 DB 데이터를 임의로 바꿔야 하는 일이 생기고, 필연적으로 DB에 접근해 작업(INSERT로 데이터 추가)해야 하는 일이 있다고 파악했습니다. 이때 DB에 접근하게 되면 다른 유저들의 정보를 평문값으로 보는 것이 문제가 될 수 있으리라 생각이 되어 해당 컬럼들의 암호화를 생각해보게 되었습니다.
암호화를 적용하게 되면, 추후에 마이그레이션 등 DB에 관한 작업을 진행할 때 타인의 개인정보를 평문으로 읽지 못하게 되는 마스킹 효과를 기대하였습니다.
1.2. 적용 범위
암호화 대상 컬럼들은 다음과 같습니다.
MEMBER 테이블 : STUDENT_ID, MEMBER_NAME, DEPARTMENT, MEMBER_EMAIL
EMAIL_RECEIVER 테이블 : RECEIVER_EMAIL, RECEIVER_NAME
2. 기술 검토
2.1. 암호화 기법 선정
AES-GCM과 AES-SIV 방식이 존재합니다. 두 방식의 특징은 다음과 같습니다.
AES-GCM (Galois/Counter Mode): 대칭키 암호화 방식으로, 빠른 성능과 짧은 인증 태그로 데이터 기밀성과 무결성을 동시에 보장하는 널리 사용되는 모드입니다.
AES-SIV (Synthetic IV): 비결정적 난수 대신 입력 데이터에서 유도된 IV를 사용해, 같은 키·같은 메시지라도 재사용 안전성을 높이고 nonce 오용에도 안전한 인증 암호 방식입니다.
두 가지 방식 중 AES-SIV 암호화를 선택하였습니다. 이유는 다음과 같습니다.
- Database 검색 및 인덱스 활용 불가 문제
- AES-GCM을 사용할 경우 동일한 평문과 키를 사용하더라도 매번 암호화된 결과가 달라지므로, Database에서 동일한 값 검색(WHERE 절)이나 인덱스 활용이 불가능합니다.
- 테이블 구조 변경 리스크
- AES-GCM은 암호문 외에도 Nonce(IV)와 Authentication Tag가 추가로 필요합니다. 따라서 단순 암호문만 저장하는 기존 구조로는 부족하며, 이를 위해 테이블 구조 변경이 필연적이고 경우에 따라 서비스 로직 수정까지 요구될 수 있습니다.
이에 따라, 현재 상황에서는 결정적 암호화 방식(같은 평문 → 같은 암호문)을 사용하는 것이 현실적이라고 판단하였습니다. AES-SIV를 적용하면 데이터는 안전하게 암호화되면서도 고유성이 보장되므로, 추후 마이그레이션 시에도 기존 방식 그대로 진행할 수 있습니다. 또한 테이블 구조를 유지한 채 암호화를 적용할 수 있다는 점에서 운영상 유리하다고 판단했습니다.
2.2. 암호화 성능 검증
Spring의 Convert를 활용해서 애플리케이션 레이어에서는 평문을 사용하고, DB 레이어에서는 암호화된 텍스트가 저장되는 방식이 되도록 구성하였습니다. 이 부분에 대하여 JMH(Java Microbenchmark Harness)를 적용하였습니다.
컨버터를 거친 이후에 스프링 애플리케이션에서는 평문으로 모든 작업을 진행하기 때문에, 컨버터에 대한 성능 테스트를 진행하였습니다. 아래 2가지 테스트를 진행하였습니다.
공통 환경
JMH 버전: 1.37
JDK: OpenJDK 21.0.6 (Server VM)
2.2.1. 순수 암·복호화 오버헤드
목적 : 다양한 조건에서 전체적인 오버헤드 성향을 보기 위한 것
- 벤치마크 모드: Throughput (ops/s, 초당 연산 수)
- Warmup/Measurement 설정:
- 첫 번째 실행: 워밍업 3회(각 1초) → 측정 5회(각 2초) → Fork 1회
- 두 번째 실행: 워밍업 3회(각 1초) → 측정 5회(각 2초) → Fork 3회
- 세 번째 실행: 워밍업 5회(각 10초) → 측정 5회(각 10초) → Fork 5회
벤치마크 결과
| 실행 | Decrypt 성능 | Encrypt 성능 |
|---|---|---|
| 첫 번째 실행 (fork 1) | 1490 ops/ms (≈ 1.49M ops/s) | 3025 ops/ms (≈ 3.02M ops/s) |
| 두 번째 실행 (fork 3, 안정적) | 1500 ops/ms (≈ 1.50M ops/s) | 2995 ops/ms (≈ 3.00M ops/s) |
| 세 번째 실행 (측정시간 확장) | 1,390,524 ops/s (≈ 1390 ops/ms) | 2,891,110 ops/s (≈ 2891 ops/ms) |
3M ops/s: 초당 약 3백만 건의 암호화 연산 처리 → 암호화 1회당 약 0.33 μs
1.4M ops/s: 초당 약 1백40만 건의 복호화 연산 처리 → 복호화 1회당 약 0.71 μs
2.2.2. JPA 경로에서 “컨버터 유/무” 비교
목적 : 일관된 조건에서 상대적인 성능 차이를 보기 위한 것
- 벤치마크 모드: Throughput (ops/s, 초당 연산 수)
- Warmup/Measurement 설정:
- 워밍업: 5회(각 2초)
- 측정: 10회(각 2초)
- Fork: 3회
벤치마크 결과
| Benchmark | 평균 처리량 (ops/s) | 표준편차 (stdev) | 신뢰구간 (99.9%) |
|---|---|---|---|
| saveConvertedEntity (암호화된 엔티티 저장) | 50,917 ops/s | ± 3,135 | [48,823 ~ 53,012] |
| savePlainTextEntity (평문 엔티티 저장) | 61,973 ops/s | ± 2,678 | [60,184 ~ 63,762] |
평문 저장(savePlainTextEntity)이 암호화 저장(saveConvertedEntity)보다 약 22% 빠릅니다. 즉, JPA AttributeConverter에서 암호화를 추가하면 처리량이 약 20% 감소하는 것으로 보입니다.
벤치마크 수치상 순수 암호화 오버헤드는 미미하고, JPA 컨버터 적용 시 약 20% 처리량 감소가 있지만 실서비스 여유가 있다면 수용 가능한 수준이므로, 민감 데이터 한정 적용은 문제없다고 보입니다.
3. 적용 계획
3.1. 적용 및 마이그레이션 전략
결정 암호화 방식이기 때문에, 암호화 대상 테이블에 접근하여 UPDATE를 진행하면 될 것이라고 판단했고 테스트를 통해 정상적으로 수행됨을 확인했습니다.
암호화용 코드를 생성하여 같은 라이브러리(google tink)를 사용해 데이터베이스에 접근→테이블 업데이트를 진행하는 마이그레이션용 코드를 미리 작성해뒀습니다.
3.2. 적용 이후 운용 방안
데이터베이스에 접근해서 값을 읽게되면 암호문으로 보여서 디버깅에 어려움을 겪을 수 있다고 생각했습니다. 이 문제를 해결하기 위해, 디버깅용 간단한 타임리프+스프링 서버(암호화 테이블을 평문으로 조회 + 단문 암호화/복호화 기능)를 준비했습니다.

4. 철회 사유
현재 DB를 직접 접근해 데이터를 수정·삽입하는 경우는 새로운 멤버 추가나 기수 변경과 같은 특수한 작업에 한정되어 있습니다. 이에 대한 기능이 구현된다면, 평상시에 DB에 직접 접근해 데이터를 추가·수정·삭제할 필요는 없다고 판단했습니다.
또한 암호화 적용 시 얻을 수 있는 이점에 비해, 문제 발생 시 처리해야 할 오버헤드가 과도하게 커질 수 있다는 우려가 제기되었습니다. 특히 로그에 평문과 암호문 중 어떤 값을 기록해야 하는지, 그리고 해당 데이터가 정말 암호화가 필요한 정보인지에 대한 논의가 있었습니다.
종합적으로 검토했을 때, 이번 암호화 적용은 이점보다 결점이 클 수 있으며 오히려 오버엔지니어링이 될 가능성이 있다고 결론지었습니다.
DB에 접근하면 개인정보들이 평문으로 보이는 문제 에 대해서, DB 데이터를 암호화하기보다는 DB에 접근해서 데이터를 추가·수정·삭제해야 하는 상황들을 근본적으로 줄여나아가는 방향으로 선회하는게 옳다고 생각했습니다.
'Backend' 카테고리의 다른 글
| 헥사고날 아키텍처 (0) | 2026.02.18 |
|---|---|
| [객체지향][OOAD] Lotto 미션을 통한 객체지향 연습 (0) | 2025.01.07 |