본문 바로가기
카테고리 없음

MyBatis 암호화 컬럼 추가: Annotation vs TypeHandler 무엇이 정답일까?

by backend 개발자 지망생 2026. 3. 6.

시스템 운영 중 개인정보 보호나 보안 강화를 위해 기존 컬럼의 암호화 버전 컬럼을 추가해야 하는 상황이 발생했다. 공통 클래스로 관리하되, 개발 공수를 줄이면서도 성능을 놓치지 않기 위해 두 가지 방식을 두고 고민한 과정을 기록한다.

1. 고민의 배경

기존 테이블에 phone_num_enc와 같은 암호화 컬럼을 추가하고, Java 애플리케이션에서 DB로 들어갈 때는 암호화, 나올 때는 복호화를 수행해야 한다.

  • 환경: Java, Spring Boot, MyBatis
  • 핵심 가치: 성능, 유지보수성, 코드의 간결함

2. 두 방식의 비교 (Annotation vs TypeHandler)

처음에는 Entity/DTO에 어노테이션을 붙여 처리하는 방식이 세련되어 보였으나, 기술적인 깊이를 따져보니 차이가 명확했다.

비교 항목 Annotation + AOP/Reflection  MyBatis TypeHandler
작동 위치 Java 애플리케이션 (객체 레벨) MyBatis 영속성 계층 (DB 통신 레벨)
성능 오버헤드 높음 (매번 리플렉션으로 필드 스캔) 매우 낮음 (Direct 바인딩)
데이터 일관성 객체를 통하지 않는 Map 등은 누락 위험 높음 (데이터가 나가는 최종 관문)
구현 난이도 AOP, 커스텀 어노테이션 등 설정 복잡 핸들러 클래스 하나로 끝

3. 왜 TypeHandler를 선택했는가?

🚀 1: 리플렉션(Reflection)의 비용을 무시하지 마라

 

어노테이션 방식은 편리해 보이지만, 런타임에 객체의 모든 필드를 뒤져서 어노테이션이 붙었는지 확인해야 한다. 데이터 건수가 많아질수록 CPU 사용량은 정직하게 올라간다. 반면, TypeHandler는 MyBatis 파이프라인의 일부로 동작하므로 일반적인 데이터 처리와 속도 차이가 거의 없다.

 

🛡️ 2: "최종 관문"을 지키는 보안이 가장 안전하다

 

AOP 방식은 DTO나 Entity 객체를 사용할 때는 잘 작동하지만, Map을 쓰거나 특정 필드만 넘기는 쿼리에서는 동작하지 않을 위험이 있다. TypeHandler는 데이터가 DB로 나가는 마지막 통로를 지키기 때문에 어떤 형태의 파라미터가 들어와도 해당 타입이라면 무조건 암호화가 적용된다.

 

🏗️ 3: 관심사의 분리 (Separation of Concerns)

 

암호화는 비즈니스 로직이라기보다 데이터의 저장 규격에 가깝다. 따라서 서비스 로직이 담긴 Entity보다는 영속성 계층(Persistence Layer)인 MyBatis 설정에서 처리하는 것이 아키텍처적으로 훨씬 깔끔하다.

4. 실전 구현 예시 (TypeHandler)

1) 공통 TypeHandler 작성

@MappedTypes(String.class)
public class AESEncryptHandler extends BaseTypeHandler<String> {

    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
        // DB에 넣을 때: 암호화
        ps.setString(i, CryptoUtils.encrypt(parameter));
    }

    @Override
    public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
        // DB에서 꺼낼 때: 복호화
        return CryptoUtils.decrypt(rs.getString(columnName));
    }

    // ... 나머지 메소드 구현 (columnIndex, callableStatement 등)
}

2) MyBatis XML 적용

INSERT INTO users (user_id, phone_num_enc)
VALUES (#{userId}, #{phoneNum, typeHandler=com.example.handler.AESEncryptHandler})

<resultMap id="UserMap" type="UserDTO">
    <result column="phone_num_enc" property="phoneNum" 
            typeHandler="com.example.handler.AESEncryptHandler"/>
</resultMap>

5. 결론 및 회고

"공수가 비슷하다면 성능이 좋고 확실한 길을 가야 한다."
어노테이션 방식이 "Java스러운" 해결책처럼 보일 수 있지만, MyBatis 환경에서는 프레임워크가 제공하는 로우 레벨 기능을 활용하는 것이 가장 효율적이라는 결론을 내렸다. 이번 작업을 통해 기술을 선택할 때 '트렌디함'보다는 '동작 원리의 효율성'을 먼저 따져보는 습관의 중요성을 다시금 느꼈다.