[BetterDay] #1. 잡담 - 분리된 모듈에서 @DataJpaTest
BetterDay 백엔드 서비스는 멀티 모듈 프로젝트다.
한 프로젝트에 기능과 레이어를 나눈 모듈로 이뤄져 있다.
MySQL(로컬 - h2)와 통신하는 모듈은 JPA 기반이다.
분리된 JPA 모듈을 테스트하며 느낀 점이다.
본 글은 잡담이다.
출근하며 적고 있다.
자세한 기술 내용은 다음 글을 참조하길.
테스트 목적
테스트 목적은 암호화/복호화다.
BetterDay 서비스는 일기를 저장한다.
일기는 개인적이고 민감한 데이터가 들어간다.
따라서 데이터베이스 저장 시 암호화해야 한다.
암호화 / 복호화 모듈 의존성
암호화/복호화는 보안 모듈에 의존한다.
‘:security:core' 모듈에 정의된 PrivacyEncryptor 인터페이스에 의존한다.
따라서 보안 모듈의 특정 구현체를 참조한다.
현재는 AES 암호화를 구현한 모듈을 참조한다.
( ’:security:aes-security' )
유즈케이스 모듈과 도메인 모듈은, 다른 모듈이 구현해야 할 인터페이스가 있다.
의존성 역전으로 다른 모듈에 직접 의존하지 않는다.
구현할 모듈이 해당 인터페이스만 의존하여 구현한다.
이 때, 다른 모든 내용까지 알 필요는 없다.
인터페이스만 ':usecase:core' 으로 뺀다.
보안 모듈과 어댑터(MySQL) 모듈의 의존 관계는 어때야 할지
고민이었다.
처음엔 유즈케이스처럼 어댑터 안에 암복호화 인터페이스를 정의하려 했다.
다만, 보안 모듈이 어댑터 모듈에 의존하는 게 맞는 걸까?
라는 생각이 들었다.
특정 암호화 기술이 담긴 모듈은 보안 레이어지만 공통 모듈에 속한다.
어느 모듈에서든 독립적으로 접근할 수 있어야 한다.
따라서 보안 모듈이 어댑터 모듈에 의존하는 구조는 적합하지 않다.
어댑터 모듈이 보안 모듈에 의존하되,
암호화 기술 구현체는 자유롭게 선택한다.
암복호화 인터페이스 : ':adapter:mysql' ➡️ ':security:core'
기술 구현체 : ':adapter:mysql' ➡️ ':security:aes-security'
보안 모듈 : ':security:aes-security' ➡️ ':security:core'
JPA 데이터 암호화 / 복호화
JPA는 데이터베이스 데이터와 Java 객체(엔티티)를 동기화해주는 기술이다.
여러 객체와 연결되어도 데이터베이스 데이터와 동기화된다.
데이터베이스에서의 정보가 같은 테이블에 있든, 다른 테이블에 있든 Java 객체 구조는 변하지 않는다.
물론, JPA Annotation 으로 어떻게 데이터베이스와 매핑되어야 하는지 기술해야 한다.
객체 필드 값이 데이터베이스에 저장되거나 불러올 때,
민감한 데이터는 암호화되어야 한다.
애플리케이션에서 복호화된 데이터를 원한다면, 데이터베이스에서 불러올 때 동작해야 편하다.
향상된 보안을 위해 비대칭 암호화를 사용하고
각 모바일 기기마다 개인키로 복호화하는 구조를 생각했다.
다만, 우수한 보안이 도메인 개발보다 당장 중요하지 않았다.
개인키 탈취를 방지하기 위한 대책도 준비해야 하고.
다시 돌아와, JPA에서 엔티티 객체의 특정 필드에 전후 처리를 도와주는 기능이 있다.
AOP 처럼 관심사를 분리했다.
필드값이 데이터베이스에 저장될 때,
데이터베이스에서 필드값으로 불러올 때,
별도 처리가 가능하다.
이를 Converter 이라고 한다.
'ContentConverter' 으로 content 필드의 컨버터라고 명명했다.
보안 컨버터임을 명시하는 이름으로 바꿀까도 고민 중이다.
컨버터에서 보안 모듈의 공통 인터페이스를 주입받는다.
임포트한 보안 구현체 모듈로 암복호화한다.
여기까진 좋았다.
테스트를 만나기 전까지.
JPA 테스트 난관
기쁜 마음으로 테스트 코드를 작성했다.
도메인과 컨트롤러(웹, api), 서비스(유즈케이스) 레이어만 작성해봤다.
JPA를 직접 테스트해보는 건 처음이다.
@DataJpaTest 으로 테스트를 한다더라.
데이터베이스 연결, JPA와 관련한 빈을 설정하고 주입해준다.
스프링 프레임워크의 컨테이너와 빈을 활용한다.
내부 Annotation을 보니 해당 역할이 있음을 알 수 있다.
@BootstrapWith(org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTestContextBootstrapper.class)
@ExtendWith(org.springframework.test.context.junit.jupiter.SpringExtension.class)
@OverrideAutoConfiguration(enabled=false)
@TypeExcludeFilters(DataJpaTypeExcludeFilter.class)
@Transactional
@AutoConfigureCache
@AutoConfigureDataJpa
@AutoConfigureTestDatabase
@AutoConfigureTestEntityManager
@ImportAutoConfiguration
문제는 JPA 모듈은 Spring 프레임워크와 분리된 모듈이라는 점이다.
따라서 Spring 프레임워크의 ApplicationContext 를 찾을 수 없다.
이를 동일한 패키지의 Configuration 을 만들어 임시로 해결했다.
더 좋은 방법이 있는지 고민이 필요하다.
테스트를 위한 코드는 지양하고자 한다.
암호화를 실제로 하는지 검증하기
두 번째 난관이 일어났다.
암호화가 실제로 되는지 어떻게 확인할까?
처음엔 목킹으로 암호화 모듈의 메소드가 실행되는지, 또는 컨버터의 메소드가 호출되는지 확인하려 했다.
컨버터의 유닛 테스트는 그리 했다.
그러나 암호화를 테스트하는 케이스는 유닛 테스트가 아니다.
통합 테스트로 실제로 데이터베이스에 저장하는지 확인해야 한다.
그 동작을 실제로 확인하고자 하는 테스트다.
문제는 JPA 객체에 값을 불러올 때, 복호화가 된다는 점이다.
목킹으로 암호화 메소드를 호출하는 걸 확인한다 해도,
실제 데이터베이스에 암호화되는 지는 모를 일이다.
JpaRepository 말고 EntityManager 으로 하면 되려나 싶었지만... 역시나 되지 않았다.
그야 당연하게도 JpaRepository 구현체도 EntityManager 영속성 컨텍스트 기반이기 때문...
그러다 네이티브 쿼리로 직접 할 수 밖에 없다는 생각이 들었다.
현재는 JPQL 네이티브 쿼리로 암호화한 필드를 불러왔다.
그리고 실제로 암호화 되었는지 검증했다.
마무리
곧 회사에 도착해서 급 마무리를 짓는다.
JPA가 편리하나 스프링 컨텍스트에 영향을 받는 점이 사이드 이펙트인 것 같다.
모듈 의존성과 설계 고민도 재밌다.
협업할 때 이런 패키지, 모듈 설계도 해보고 싶다.
자세한 코드와 분석은 퇴근하고 나서나 주말에 적을 예정이므로 많관부!