JPA @PostLoad를 활용한 개인정보 마스킹

JPA @PostLoad를 활용한 개인정보 마스킹

1. 개인정보 마스킹 요구사항

관리자 화면의 사용자 정보를 조회할 때 이름, 전화번호, 이메일 등 민감한 개인정보를 조회 화면에서 마스킹 처리해야 하는 보안 요건을 마주했습니다. DB에는 원본 데이터를 유지하되, 화면과 API 응답에서는 비식별 조치가 즉시 이루어져야 했습니다.

초기에는 서비스 레이어에서의 DTO 변환, 컨트롤러 응답 처리, QueryDSL Projection 단계에서의 처리, 또는 AOP 기반의 마스킹 등을 고려해 보았습니다. 하지만 이러한 방식들은 로직의 중복 발생, 신규 API 추가 시 누락 위험, DTO 증가에 따른 유지보수 비용 상승 등의 구조적인 한계가 있었습니다. 특히 비즈니스 로직과 보안 로직이 한데 섞여 가독성을 해치는 점이 가장 큰 고민이었습니다.

이 문제를 해결하기 위해 JPA의 엔티티 생명주기 콜백(Entity Lifecycle Callback) 중 @PostLoad를 활용하는 전략을 선택했습니다.

2. 왜 @PostLoad를 선택했는가?

JPA는 엔티티의 생명주기 이벤트를 제공하며 특정 시점에 자동으로 로직을 실행할 수 있는 강력한 기능을 갖추고 있습니다.

구분 실행 시점
@PrePersist 데이터 저장 전
@PreUpdate 데이터 수정 전
@PostPersist 데이터 저장 후
@PostUpdate 데이터 수정 후
@PostLoad 데이터 조회 직후(Entity가 영속성 컨텍스트에 로드된 후)

여러 콜백 중 조회 직후에 실행되는 @PostLoad가 마스킹 처리에 가장 적합하다고 판단한 이유는 다음과 같습니다.

2.1 조회 후 자동 적용

DB 조회 → Entity 생성 → @PostLoad 실행 → 서비스 로직 수행 순으로 흐름이 이어집니다. 이 덕분에 개발자가 수동으로 마스킹 로직을 호출하지 않아도 조회되는 모든 엔티티에 대해 자동으로 보안 정책을 적용할 수 있습니다.

2.2 비즈니스 로직과의 완전 분리

마스킹은 비즈니스 로직이 아닌 '보안 및 출력 정책'에 가깝습니다. @PostLoad를 사용하면 이러한 보안 정책을 엔티티 레벨에서 선언적으로 강제할 수 있어 서비스 코드가 훨씬 깔끔해집니다.

2.3 조회 경로 독립성

JPA를 통한 모든 조회 경로에서 예외 없이 동작합니다.

  • findById(), findAll()
  • JPQL을 통한 엔티티 조회
  • QueryDSL을 통한 엔티티 조회

3. 적용 방식

3.1 엔티티에 @PostLoad 구현

엔티티 클래스 내부에 마스킹 로직을 담은 메서드를 정의하고 @PostLoad 어노테이션을 부여합니다.

3.2 별도 로직 없는 자동 마스킹

이제 서비스 레이어에서는 별도의 마스킹 처리 없이 평소처럼 조회만 수행하면 됩니다.
//서비스 로직 예시


// users 내부의 데이터는 이미 마스킹된 상태입니다.

API 응답 예시:


4. Dirty Checking 이슈와 해결 방법

@PostLoad에서 엔티티의 필드를 직접 수정하면 한 가지 주의할 점이 있습니다. 바로 JPA의 변경 감지(Dirty Checking) 기능입니다. 영속성 컨텍스트가 관리하는 엔티티의 값이 변경되었기 때문에 트랜잭션 종료 시 마스킹된 값이 다시 DB에 업데이트될 위험이 있습니다. 이를 방지하기 위해 다음 두 가지 전략을 사용합니다.

4.1 readOnly 트랜잭션 사용

조회 전용 서비스 메서드에 @Transactional(readOnly = true)를 적용합니다. 이 설정이 있으면 하이버네이트는 스냅샷 비교를 생략하고 변경 감지를 수행하지 않아 성능과 보안을 모두 챙길 수 있습니다.

4.2 @Transient 필드 사용(권장 방식)

가장 안전한 방법은 원본 필드는 건드리지 않고, 마스킹된 데이터를 담을 별도의 가상 필드를 사용하는 것입니다.


5. 적용 결과 및 성과

이 방식을 적용한 후 프로젝트 팀 내에서 느낀 긍정적인 변화는 뚜렷했습니다.

  1. 보안 누락 제거: 새로운 조회 API를 추가할 때 마스킹 처리를 깜빡하더라도 엔티티가 로드되는 순간 자동으로 적용되므로 개인정보 노출 사고를 완벽히 예방할 수 있었습니다.
  2. 코드의 단순화: 컨트롤러와 서비스 레이어에 흩어져 있던 변환 로직이 사라져 핵심 비즈니스 로직에만 집중할 수 있게 되었습니다.
  3. 유지보수성 향상: 마스킹 규칙이 변경될 경우(예: 별표 개수 변경 등), 엔티티 콜백 메서드 한 곳만 수정하면 시스템 전체에 즉시 반영됩니다.
  4. 생산성 증가: 공통 보안 정책이 중앙화되어 개발자가 보안 이슈를 신경 쓰지 않고 기능을 개발할 수 있게 되었습니다.

6. 적용 시 주의사항

6.1 DTO Projection에서의 미동작

@PostLoad는 JPA가 '엔티티'를 생성할 때만 실행됩니다. 따라서 다음 경우에는 실행되지 않습니다.

  • JPQL이나 QueryDSL에서 Projections.constructor 등을 사용해 DTO로 직접 조회할 때
  • Native Query를 사용하여 결과를 받을 때

6.2 대량 조회 성능

@PostLoad는 조회된 엔티티 개수만큼 실행됩니다. 하지만 단순 문자열 마스킹 연산 비용은 매우 낮습니다. 수천 건 정도의 일반적인 조회 상황에서는 성능 이슈가 거의 체감되지 않았습니다.

7. 결론

JPA의 @PostLoad를 활용하면 데이터 조회 시점에 개인정보 마스킹을 깔끔하고 강력하게 적용할 수 있습니다. 보안 정책을 도메인 모델에 강제함으로써 코드의 중복을 줄이고 누락 없는 보안 환경을 구축할 수 있다는 점이 가장 큰 매력이었습니다.

민감한 데이터를 다루는 백엔드 개발 과정에서 흩어져 있는 마스킹 로직으로 고생하고 계신다면 엔티티 생명주기 기반의 처리 방식을 적극적으로 고려해 보시기를 추천드립니다.

dwmoon

Site footer