인프런 커뮤니티 질문&답변

kwsic님의 프로필 이미지
kwsic

작성한 질문수

실전! 스프링 데이터 JPA

강의 자료

AttributeConverter에 대해 질문이 있습니다.

해결된 질문

작성

·

349

0

학습하는 분들께 도움이 되고, 더 좋은 답변을 드릴 수 있도록 질문전에 다음을 꼭 확인해주세요.

1. 강의 내용과 관련된 질문을 남겨주세요.
2. 인프런의 질문 게시판과 자주 하는 질문(링크)을 먼저 확인해주세요.
(자주 하는 질문 링크: https://bit.ly/3fX6ygx)
3. 질문 잘하기 메뉴얼(링크)을 먼저 읽어주세요.
(질문 잘하기 메뉴얼 링크: https://bit.ly/2UfeqCG)

질문 시에는 위 내용은 삭제하고 다음 내용을 남겨주세요.
=========================================
[질문 템플릿]
1. 강의 내용과 관련된 질문인가요? (예/아니오) 네
2. 인프런의 질문 게시판과 자주 하는 질문에 없는 내용인가요? (예/아니오) 네
3. 질문 잘하기 메뉴얼을 읽어보셨나요? (예/아니오) 네

[질문 내용]
안녕하세요 AttributeConverter 관련 질문이 있습니다.

attribute와 dbDate를 암호화 & 복호화하는 사용자 정의 컨버터를 구현했는데 실제 query를 이용하여 조회했을 때 정상적으로 암호화된 데이터가 적재된 것을 확인했습니다.

다만 애플리케이션에서는 converter가 동작하니 실제로 암호화 된 데이터가 적재되었는지를 애플리케이션 레벨에서 할 수 없는데 이를 위한 테스트 작성을 어떻게 해야하는지 궁금합니다.

답변 2

1

kwsic님의 프로필 이미지
kwsic
질문자

네 알겠습니다. 감사합니다. <3

0

안녕하세요. kwsic님, 공식 서포터즈 y2gcoder입니다.

해당 기능이 핵심 비즈니스 기능에 해당한다면 암복호화하는 기능을 따로 클래스로 분리해 위임해서 컨버터에서 사용하는 방식으로 구현한다면 단위테스트가 가능할 것 같습니다.

적재되었는지 파악하는 것이 중요하다면 @SpringBootTest를 이용해서 통합테스트로 만들되, 저장된 엔티티를 조회하여 dbDate가 암호화 되었는지 확인하는 식으로 테스트 코드를 짜면 되지 않을까 조심스레 말씀드려봅니다! (확인하는 방법은 복호화가 가능하다면 조회 후 복호화한 값 확인하는 방식으로 테스트 해볼 것 같습니다. 단방향이라면 아마 암호화 값과 raw 값이 매칭되는지 확인하는 메서드를 만드셨을테니 그걸 이용할 것 같습니다)

감사합니다.

kwsic님의 프로필 이미지
kwsic
질문자

안녕하세요, 답변 감사합니다. 다만 제가 드렸던 질문이 상세하지 않아서 추가로 작성합니다.

 

public interface Crypto {

    String encrypt(String text);

    String decrypt(String cipherText);

}
public class AES256 implements Crypto {

    private final Key key;

    private final AlgorithmParameterSpec algParamSpec;

    public AES256(String key) {
     ...
    }

    @Override
    public String encrypt(String text) {
     ...
    }

     @Override
    public String decrypt(String cipherText) {
    ...
    }
}

위와 같은 암복호화 기능을 제공하는 클래스를 정의합니다.

 


class AES256Test {

    @Nested
    @DisplayName("AES256 객체 생성 테스트")
    class CreateTest {

        @ParameterizedTest
        @ValueSource(strings = {"abc123def4564321", "cba123def4564abc"})
        @DisplayName("16자리 키를 주입 받은 객체는 정상적으로 생성이 된다.")
        void testCreate_success(String value) throws Exception {
            assertThatNoException().isThrownBy(() -> new AES256(value));
        }

        @ParameterizedTest
        @ValueSource(strings = {"1234567abc", "abcdefg"})
        @DisplayName("16자리가 아닌 키를 주입 받은 객체는 정상적인 생성이 이루어지지 않는다.")
        void testCreate_fail(String value) throws Exception {
            assertThatThrownBy(() -> new AES256(value)).isInstanceOf(EncryptException.class);
        }
    }

    @Nested
    @DisplayName("AES256 암호화 & 복호화 테스트")
    class IntegrationTest {

        private final Crypto crypto = new AES256(UUID.randomUUID().toString().substring(0, 16));

        @ParameterizedTest
        @ValueSource(strings = {"ones1k95", "kws", "aaa"})
        @DisplayName("인코딩 여부를 확인한다.")
        void testEncoded(String value) throws Exception {
            String encrypted = crypto.encrypt(value);

            assertThat(encrypted).isNotNull()
                    .isBase64();
        }

        @ParameterizedTest
        @ValueSource(strings = {"ones1k95", "kws", "aaa"})
        @DisplayName("암호화 여부를 확인한다.")
        void testEncrypted(String value) throws Exception {
            String encrypted = crypto.encrypt(value);

            assertThat(encrypted).isNotNull()
                    .isNotEqualTo(value);
        }

        @ParameterizedTest
        @ValueSource(strings = {"ones1k95", "kws", "aaa"})
        @DisplayName("암호화 된 값이 정상적으로 복호화 되는지 확인한다.")
        void testSuccess(String value) throws Exception {
            // given
            String encrypted = crypto.encrypt(value);

            // when
            String decrypted = crypto.decrypt(encrypted);

            // then
            assertThat(decrypted).isNotNull()
                    .isEqualTo(value);
        }

    }

}

테스트 정상 동작 확인했습니다.

 

@RequiredArgsConstructor
@Convert
@Component
public class CryptoConverter implements AttributeConverter<String, String> {

    private final Crypto crypto;

    @Override
    public String convertToDatabaseColumn(String attribute) {
        return crypto.encrypt(attribute);
    }

    @Override
    public String convertToEntityAttribute(String dbData) {
        return crypto.decrypt(dbData);
    }
}

해당 클래스를 통해 AttributeConverter에 등록합니다.

@Embeddable
public class Temp {

    @Column(name = "temp", length = 24)
    @Convert(converter = CryptoConverter.class)
    private String value;
}

컬럼에 대한 Convter 등록 후 @SpirngBootTest 테스트를 진행합니다.

@RequiredArgsConstructor
@ApplicationBootTest
class ServiceTest {

    private final Service service;
    private final EntityManager em;

    @Test
    @DisplayName("...")
    void test() throws Exception {
        // given
        String aaa = "111";
        String bbb = "222";
        String ccc = "333";

        Temp temp = Temp.create(aaa, bbb, cc);

        // when
        Temp temped = service.save(temp);

        // and
        em.clear();

        // and
        long id = temped.getId();
        Temp result = service.findById(id);

        // then
        assertThat(result).isNotNull()
                .extracting(Temp::getAaa, Temp::getBbb, Temp::getCcc)
                .isNotEmpty()
                .map(Object::toString)
                .containsExactly(aaa, bbb, ccc);
    }
}

 

테스트도 정상적으로 동작합니다. 또한 실제 테이블에는 암호화된 값이 적재된 것을 query로 확인했습니다. 다만 여기서 궁금한 것이 실제로 데이터베이스에 암호화된 값이 적재된 다는 것을 어떻게 테스트를 명시적으로 작성할 수 있는지가 궁금합니다.

 

단지 "암호화 모듈이 정상적으로 암복호화되고, 암/복호화 컨버터를 컬럼에 매핑해놓았고 service의 find() 결과와 평문으로 구성된 expected 값이 일치하는 것으로 데이터베이스에는 암호화된 값이 저장된다." 와 같이 다소 불분명한 내용으로 마무리 지어야 하는가가 저는 의문입니다.

 

제가 테스트 코드 작성의 베테랑은 아니라 말씀드리기 조심스럽습니다.

개인적으로 테스트 코드를 작성하는 제일 큰 이유는 자신감을 얻기 위해서라고 생각합니다. 리팩토링을 위한 자신감도 있겠지만, 그보다는 테스트 코드를 작성함으로써 비즈니스 로직 코드에 대한 자신감을 얻을 수 있는 것 같습니다.

그러한 맥락에서 kwsic님께서는 지금 작성하신 테스트 코드로 자신감을 얻지 못하신 것으로 보입니다. 제가 테스트 코드를 봤을 때는 굉장히 꼼꼼하게 테스트 코드를 작성하셨다고 생각합니다. encrypt 한 문자열과 decrypt 이 일치하고 있음을 보여주는 테스트도 작성하고 계십니다. 개인적으로는 암복호화 모듈에 대한 단위 테스트 코드를 작성하셨다면 믿고 사용하셔도 되는 코드라 생각합니다. DB에 암호화된 값이 저장되었는지까지 확인하고 싶으시다면 DB에서 암호화된 값을 그대로 불러오도록 로직을 고치거나 추가적인 로직을 구현해야 할 것 같습니다. 다만 테스트를 위해 이러한 코드를 작성하는 것이 맞는 지는 모르겠습니다.

작성하신 단위 테스트 코드를 믿어보심이 어떨까 합니다 :)

kwsic님의 프로필 이미지
kwsic
질문자

답글 감사합니다. 말씀 주신 내용에 백번 공감합니다. 하지만 개인적으로 테스트 코드는 단위 기능에 대한 신뢰성도 중요하지만 문서로서 역할도 중요하다고 생각합니다. 이런 관점으로 봤을 때 구현 가능하다면 제가 질문 드린 내용을 명시적으로 테스트로 남기고 싶습니다.

위와 같은 이유로 저는 방법에 대해 질문을 드렸지만 서포터즈님께서는 관점에 대해서 설명해주신 것 같아 아쉬운 부분이 다소 남아 있습니다. 예를 들어 특정 컬럼 값을 암호화 후 DB에 저장하는 것을 전사적인 규칙이라고 했을 시 로직을 고치는 것은 힘들어 보입니다.

따라서 추가적인 로직과 같은 방법이 무엇이 있을지 알고 싶습니다.

PS 김영한님의 고견 또한 궁금합니다. 방법이 없다면 서포터즈님의 의견을 수용하겠습니다. 감사합니다.

김영한님의 프로필 이미지
김영한
지식공유자

안녕하세요. kwsic님

저도 y2gcoder님 의견에 동의합니다.

첨언을 드리자면 단위 테스트에서 충분히 검증하고, DB에 입력하는 통합 테스트는 기능의 통합이 잘되는지 그러니까, 암호화 한 값이 잘 저장되고, 잘 조회 되는지 정도를 확인하는 정도면 충분하다 생각합니다. 이때 추가 검증이 필요하다면 JdbcTemplate 같은 것을 사용해서 암호화된 DB의 값을 조회해보시면 됩니다.

감사합니다.

kwsic님의 프로필 이미지
kwsic

작성한 질문수

질문하기