MyBatis vs postgreSQL 암호화 성능 분석 - rdevnoah/final_shoppingmall GitHub Wiki

PostreSQL pgcrypto Module

posrgresql은 테이블의 각 컬럼을 원하는 알고리즘으로 암호화하는 pgcrypto라는 모듈을 제공한다. postgresql을 소스파일로 받아 압축을 해제하면 contrib 폴더 안에 설치할 수 있는 모듈들이 같이 있다. pgcrypto도 그중 하나이고, 폴더 내부에서

$ make && make install	

작업을 수행하면 postgresql에서 pgcrypto를 설치할 준비가 완료된다.

postgresql 프롬프트 내에서

postgres > create extension pgcrypto

명령어를 통해 설치 가능하다.

참고로 추가 모듈 설치는 superuser의 권한으로 설치 가능하므로, 새로운 DB와 새로운 user를 만들면 그 user에게 잠시 superuser 의 role을 주고 그 db에다가 설치하면 된다.

예) user : mok, DB : mok

postgres > alter user mok with superuser # superuser 권한부여
mok > create extension pgcrypto # mok db에 설치
postgres > alter user mok with nosuperuser  # superuser 권한해제
  • pgcrypto는 설치 시 여러 함수들이 추가되는데, 설치 과정을 살펴보면, 사용자 정의 함수를 선언하는 script가 자동으로 실행되고, 암보호화 및 여러 함수들이 사용자 정의 함수로 추가되는 형식으로 설치된다.

AES-128

pgcrypto의 암복호화는 다음과 같다.

encrypt(OriginText:bytea, Key:bytea, Function) 
decrypt(EncryptedText:bytea, Key:bytea, Function)

AES-128로 암복호화 하고 싶다면 Function 자리에 'aes'를 적어주면 된다.

그리고 Key의 길이에 따라 AES-128, AES-256으로 구분한다.

  • Key length : 16 : AES-128
  • Key length : 32 : AES-256

기본 쿼리문 예시

# Encryption
mok > insert into mok1 values(default, encode(encrypt(convert_to('aaa','utf8'),'abcdefghijklmnop', 'aes'),'hex'), encode(encrypt(convert_to('김영호','utf8'),'abcdefghijklmnop', 'aes'),'hex'));
						
# Decryption						
mok > select no, convert_from(decrypt(decode(id,'hex'),'abcdefghijklmnop','aes'),'utf8') as id, convert_from(decrypt(decode(name,'hex'),'abcdefghijklmnop','aes'),'utf8') as name from mok1;

spring의 코드에서 주의할점

  • 우리의 spring 코드에서는 mybatis - postgresql 로 연동되어 있다. 이 때, mybatis의 mapper 설정에 위의 쿼리문을 그대로 입력하면, 동작하지 않는다.

    mybatis는 프롬프트에서 입력할 때처럼 형변환을 자동으로 해주지 않기 때문이다.

    encrypt(OriginText:bytea, Key:bytea, Function) 
    decrypt(EncryptedText:bytea, Key:bytea, Function)

    의 처럼 모든 데이터의 형을 맞춰줘야 한다. 따라서 위의 쿼리문도 아래처럼 바꿔야 한다.

    # Encryption
    mok > insert into mok1 values(default, encode(encrypt(convert_to('aaa','utf8'),convert_to('abcdefghijklmnop','utf8'), 'aes'),'hex'), encode(encrypt(convert_to('김영호','utf8'),convert_to('abcdefghijklmnop','utf8'), 'aes'),'hex'));
    						
    # Decryption						
    mok > select no, convert_from(decrypt(decode(id,'hex'),convert_to('abcdefghijklmnop','utf8'),'aes'),'utf8') as id, convert_from(decrypt(decode(name,'hex'),convert_to('abcdefghijklmnop','utf8'),'aes'),'utf8') as name from mok1;

    즉, 모든 데이터를 encrypt() decrypt() 함수의 파라메터 형과 정확하게 일치시켜줘야 한다. (명시적으로)


Mybatis TypeHandler

암호화는 DB에서 모듈로 할 수도 있지만, DB로 가기 이전에 거치는 Mybatis Framework에서 보내는 데이터의 Type을 Handling할 수 있는 TypeHandler를 구현함으로써 해결할 수도 있다. 구조는 아래와 같다.

typehandler

위의 구조를 통해 구현한 AbstractCipherTypeHandler의 코드이다.

// org.apache.ibatis.type.TypeHandler 인터페이스를 구현하는 추상화 클래스 AbstractCipherTypeHandler 구현
package com.cafe24.mybatistest.handler;

import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.TypeHandler;


public abstract class AbstractCipherTypeHandler implements TypeHandler<String> {
	
  //test할 key
	private static final String key = "abcdefghijklmnop";
	
	
	public void setParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
		// 암호화 여부 확인
		// 암호화 해야되면 받은 파라메터 인코딩
		if (isCipher()) {
			parameter = encode(parameter);
		}

		ps.setString(i, parameter);
	}

	
	public String getResult(ResultSet rs, String columnName) throws SQLException {
		String value = rs.getString(columnName);

		// 암호화 여부 확인
		// 암호화 되는 컬럼이라면 반환값 디코딩
		if (isCipher()) {
			value = decode(value);
		}

		return value;
	}

	public String getResult(ResultSet rs, int columnIndex) throws SQLException {
		String value = rs.getString(columnIndex);

		// 암호화 여부 확인
		// 암호화 되는 컬럼이라면 반환값 디코딩
		if (isCipher()) {
			value = decode(value);
		}

		return value;
	}

	public String getResult(CallableStatement cs, int columnIndex) throws SQLException {
		String value = cs.getString(columnIndex);

		// 암호화 여부 확인
		// 암호화 되는 컬럼이라면 반환값 디코딩
		if (isCipher()) {
			value = decode(value);
		}

		return value;
	}

	protected abstract boolean isCipher();

	protected String encode(String value) {
		try {
			value = AES128.encrypt(value, key);
		} catch (Exception e) {
			e.printStackTrace();
		}
		return value;
	}

	private String decode(String value) {
		try {
			value = AES128.decrypt(value, key);
		} catch (Exception e) {
			e.printStackTrace();
		}
		return value;
	}

}

그리고 암호화는 isCipher() 의 true/false에 따라 암호화/복호화 여부를 판단한다. isCipher() 는 또한 abstract 이므로 Handling할 데이터들은 다시 AbstractCipherTypeHandler 를 상속받고 isCipher() 메소드를 구현함으로써 가능하다.

encode()와 decode() 속의 AES128.encrypt(value, key)AES128.encrypt(value, key) 는 AES128 클래스를 만들고 직접 구현해야한다. 아래의 코드처럼 Id와 Name을 암복호화 하게 하고싶다면 두 컬럼에 해당하는 TypeHandler를 추가하고 AbstractCipherTypeHandler 를 상속받은 뒤, isCipher() 만 구현하면 된다.

// NameCipherTypeHandler
package com.cafe24.mybatistest.handler;

public class NameCipherTypeHandler extends AbstractCipherTypeHandler {
	@Override
	protected final boolean isCipher() {
		return true;
	}

}

// IdCipherTypeHandler
package com.cafe24.mybatistest.handler;

public class IdCipherTypeHandler extends AbstractCipherTypeHandler {
	@Override
	protected final boolean isCipher() {
		return true;
	}

}

mybatis-conf.xml 설정

그리고 mybatis의 configuration 파일에서 TypeHandler를 추가해야한다.

<typeAliases>
  <typeAlias type="com.cafe24.mybatistest.handler.IdCipherTypeHandler" alias="IdCipherTypeHandler"/>
  <typeAlias type="com.cafe24.mybatistest.handler.NameCipherTypeHandler" alias="NameCipherTypeHandler"/>
  <package name="com.cafe24.mybatistest.vo"/>
</typeAliases>

<typeHandlers>
  <typeHandler handler="com.cafe24.mybatistest.handler.IdCipherTypeHandler"/>
  <typeHandler handler="com.cafe24.mybatistest.handler.NameCipherTypeHandler"/>
</typeHandlers>

mapper 설정

이제 mapper에서 암복호화가 이뤄져야 하는 컬럼의 데이터를 입력하는 쿼리문 변수쪽에 typeHandler를 아래와 같이 추가하면 된다.

#{id, typeHandler=IdCipherTypeHandler }, #{name, typeHandler=NameCipherTypeHandler}

이제 Id와 name은 각각 IdCipherTypeHandler NameCipherHandler 를 꼭 거쳐 데이터를 Encryption, Decryption할 수 있게 된다.


Mybatis TypeHandler VS postgreSQL Module

이제 두 가지의 방안 중에 어떤 것이 더 성능이 좋은지 판단하기 위해 테스트를 진행한다.

  • Test OS : mac osX

  • DB Server : centOS6 -> postgresql

  • test tool : JUnit

  • test table : mok1(postgresql modul 전용), mok2(mybatis handler 전용)

     ## mok1, mok2
       필드명 |          형태          |                     기타 조건                      
    --------+------------------------+----------------------------------------------------
     no     | bigint                 | Null 아님 기본 값 nextval('mok1_no_seq'::regclass)
     id     | character varying(300) | Null 아님
     name   | character varying(300) | Null 아님
     
     
  • test data

    • id : abcdefghijkl
    • name : 김영호
  • Key : 'abcdefghijklmnop'

모두 AES-128로 암복호화 되므로, Key값은 길이 16이어야 한다.

  • Test Count : 1,000,000

테스트 개요

  • 총 100만개의 데이터를 postgresql module, mybatis handler를 통해 insert하고 select 해오고 각각 실행 시간을 측정한다.
  • JUnit의 assert 기능은 제외하고 시간측정으로만 확인해본다. 아래는 코드이다.
package mybatistest;

import java.util.ArrayList;
import java.util.List;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import com.cafe24.mybatistest.vo.MokDao;
import com.cafe24.mybatistest.vo.MokVo;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "classpath:applicationContext.xml" }) // root wac에 저장된 bean들 사용
public class MybatisTest {

	@Autowired
	private MokDao mokDao;

	@Test
	public void test01() {

		List<MokVo> list = new ArrayList<MokVo>();

		for (int i = 0; i < 1000001; i++) {
			list.add(new MokVo(null, "abcdefghijkl", "김영호"));
		}

		Long startP = System.currentTimeMillis();

		for (int i = 0; i < 1000001; i++) {
			mokDao.insertP(list.get(i));
		}

		Long endP = System.currentTimeMillis();

		Long startM = System.currentTimeMillis();
		for (int i = 0; i < 1000001; i++) {
			mokDao.insertM(list.get(i));
		}
		Long endM = System.currentTimeMillis();

		System.out.println("ENCRYPT TEST : PostgreSQL 모듈 : " + (endP - startP) / 1000.0);
		System.out.println("ENCRYPT TEST : Mybatis Handler : " + (endM - startM) / 1000.0);

		startP = System.currentTimeMillis();
		List<MokVo> list2 = mokDao.selectP("abcdefghijklmnop");
		endP = System.currentTimeMillis();

		startM = System.currentTimeMillis();
		List<MokVo> list3 = mokDao.selectM();
		endM = System.currentTimeMillis();

		System.out.println("DECRYPT TEST : PostgreSQL 모듈 : " + (endP - startP) / 1000.0);
		System.out.println("DECRYPT TEST : Mybatis Handler : " + (endM - startM) / 1000.0);
	}
}

결과

ENCRYPT TEST : PostgreSQL 모듈 : 769.313
ENCRYPT TEST : Mybatis Handler : 792.37
DECRYPT TEST : PostgreSQL 모듈 : 8.665
DECRYPT TEST : Mybatis Handler : 11.359
  • Encryption과 Decryption 모두 PostgreSQL 모듈이 조금 더 빠른 결과를 나타낸다. DB 자체의 펑션을 바로 실행시키기 때문인 것으로 판단된다. 하지만 이 시간이 실제로 눈에 띄는 차이일까?

  • 실제로 테스트 코드를 작성하면서 느낀 점

    • pgcrypto의 경우 서버쪽의 코드는 전혀 건드릴 것이 없다는 점이다. 쿼리문의 encrypt(), decrypt() 함수만 입력해주는 편리함이 있다. 하지만 mapper.xml에 명시적으로 타입을 정해줘야 함에 따라 쿼리문의 길이가 매우 길어졌다.

    • Mybatis Handler의 경우 쿼리문은 전혀 건드리지 않고, 서버쪽의 Handler 객체들을 추상화하여 넣는 작업이 추가되었다. 복잡하지는 않지만, 컬럼의 종류마다 Handler를 하나씩 추가해줘야 하는 단점이 있다.

      • 이 단점을 극복하도록 Refactoring 할 수 없을까?

Todo

  • 암복호화 방식 최종 선택
⚠️ **GitHub.com Fallback** ⚠️