MyBatis vs postgreSQL 암호화 성능 분석 - rdevnoah/final_shoppingmall GitHub Wiki
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가 자동으로 실행되고, 암보호화 및 여러 함수들이 사용자 정의 함수로 추가되는 형식으로 설치된다.
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 코드에서는 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()
함수의 파라메터 형과 정확하게 일치시켜줘야 한다. (명시적으로)
암호화는 DB에서 모듈로 할 수도 있지만, DB로 가기 이전에 거치는 Mybatis Framework에서 보내는 데이터의 Type을 Handling할 수 있는 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의 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에서 암복호화가 이뤄져야 하는 컬럼의 데이터를 입력하는 쿼리문 변수쪽에 typeHandler를 아래와 같이 추가하면 된다.
#{id, typeHandler=IdCipherTypeHandler }, #{name, typeHandler=NameCipherTypeHandler}
이제 Id와 name은 각각 IdCipherTypeHandler
NameCipherHandler
를 꼭 거쳐 데이터를 Encryption, Decryption할 수 있게 된다.
이제 두 가지의 방안 중에 어떤 것이 더 성능이 좋은지 판단하기 위해 테스트를 진행한다.
-
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 할 수 없을까?
-
- 암복호화 방식 최종 선택