ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 테스트를 쓰면서 만난 좋지 않은 Util 클래스와 의존성관리
    운영 중인 서비스/Coconut. 2024. 1. 25. 22:58

     

    첫 번째 배포 주기에는 로그인 없이 게시판 하나를 만드는 걸 목표로 진행을 하고 있습니다.

    그래서 로그인 구현 전까지 게시글 수정, 삭제를 관리하기 위해서 비밀번호를 사용하기로 결정하였습니다.

     

    비밀번호를 디비에 넣기 전에 암호화를 위해서 간단하게 아래와 같이 클래스를 만들어보았습니다.

    public class SimpleEncrypt {
    
      public static boolean match(String rawString, String hashedString) {
        String hashedPassword = encrypt(rawString);
        return hashedString.equals(hashedPassword);
      }
    
      public static String encrypt(String rawString) {
        try {
          MessageDigest md = MessageDigest.getInstance("SHA-256");
          byte[] hashedPassword = md.digest(rawString.getBytes());
    
          return HexFormat.of().formatHex(hashedPassword);
        } catch (NoSuchAlgorithmException e) {
          throw new RuntimeException("암호화 알고리즘이 존재하지 않음", e);
        }
      }
    
      private static byte[] createSalt(int length) {
        byte[] salt = new byte[length];
        SecureRandom random = new SecureRandom();
        random.nextBytes(salt);
    
        return salt;
      }
    }

     

     

    한 곳에서만 간단하게 사용 될 것이고 굳이 의존성 주입해서 사용하기 보다는 바로 import 해서 쓸 생각으로 static으로 메소드를 만들어서 사용을 했습니다. ( 암호화 로직 자체는 같은 입력에 대해 같은 hash값이 나와서 안전하지 않은 상태 입니다 )

      @Override
      public String generateToken(PostAuthReqDto dto) {
        Optional<Post> optPost = postRepository.findById(dto.getPostId());
        Post post = optPost.orElseThrow(() -> new NotFoundException("[postId: " + dto.getPostId() + "] 게시글이 존재하지 않습니다"));
        boolean isValidPassword = SimpleEncrypt.match(dto.getPassword(), post.getPassword());
    
        if (isValidPassword) {
          String token = SimpleEncrypt.encrypt(dto.getPostId() + dto.getPassword());
          inMemoryDBProvider.setTemperarily(String.valueOf(dto.getPostId()), token, 3 * 60);
          return token;
        } else {
          throw new IllegalArgumentException("잘못된 비밀번호입니다. 비밀번호를 확인해주세요");
        }
      }

     

     

    간단하게 만들어서 그런대로 기능구현을 쉽게 했다고 생각하고 있었는데,, 테스트를 쓰려고 보니 Mock이 쉽게 안되는걸 알게되었습니다.

     @Test
        public void 비밀번호가_잘못된경우_IllegalArgumentException발생() {
          PostAuthReqDto dummyDto = new PostAuthReqDto(1000L, "1234");
    
          when(postRepository.findById(dummyDto.getPostId())).thenReturn(Optional.ofNullable(any(Post.class)));
    	  // 비밀번호 검사 로직이 처리가 안되네...?
    
    	  assertThrows(NotFoundException.class, () -> postService.generateToken(dummyDto));
        }
      }

     

     

    어차피 스프링에서 @Component 달린 객체를 싱글톤으로 사용하면 그냥 ioC에 맡기는게 스프링답게 사용하는 것이고 무엇보다 테스트하기에 쉽다는 생각이 듭니다.

    @Component
    public class SimpleEncrypt {
    
      public boolean match(String rawString, String hashedString) {
        String hashedPassword = encrypt(rawString);
        return hashedString.equals(hashedPassword);
      }
    
      public String encrypt(String rawString) {
        try {
          MessageDigest md = MessageDigest.getInstance("SHA-256");
          byte[] hashedPassword = md.digest(rawString.getBytes());
    
          return HexFormat.of().formatHex(hashedPassword);
        } catch (NoSuchAlgorithmException e) {
          throw new RuntimeException("암호화 알고리즘이 존재하지 않음", e);
        }
      }
    
      private byte[] createSalt(int length) {
        byte[] salt = new byte[length];
        SecureRandom random = new SecureRandom();
        random.nextBytes(salt);
    
        return salt;
      }
    }

     

    아마 MapperStruct 수정하기 번거로워서 그냥 static으로 만들었던 것 같습니다 ㅎㅎ 처음에 올바른 선택을 하지 않으면 나중에 댓가를 치룬다는 생각이 듭니다. ( 공식문서 )

    @Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
    public interface PostMapper {
      PostMapper INSTANCE = Mappers.getMapper(PostMapper.class);
    
      @Mapping(source = "content", target = "textContent")
      Post toEntity(CreateReqDto dto, @Context SimpleEncrypt simpleEncrypt);
    
      @AfterMapping
      default void customMapping(@MappingTarget Post post, CreateReqDto dto, @Context SimpleEncrypt simpleEncrypt) {
        String hashedPassword = simpleEncrypt.encrypt(dto.getPassword());
        post.setPassword(hashedPassword);
      }
    
      @Mapping(source = "textContent", target = "content")
      PostResDto toPostResDto(Post entity);
    }

     

    에러 뜨는 부분 다 수정하고 나서 테스트까지 마무리 해줍니다

        @Test
        public void 비밀번호가_일치하는경우_token반환() {
          PostAuthReqDto dummyDto = new PostAuthReqDto(1000, "1234");
          Post dummyPost = new Post("제목", "내용", "작성자", "카테고리", "1234");
          String dummyToken = "Dummy Token";
    
          when(postRepository.findById(dummyDto.getPostId())).thenReturn(Optional.ofNullable(dummyPost));
          when(simpleEncrypt.match(dummyDto.getPassword(), dummyPost.getPassword())).thenReturn(true);
          when(simpleEncrypt.encrypt(dummyDto.getPostId() + dummyDto.getPassword())).thenReturn(dummyToken);
    
          String token = postService.generateToken(dummyDto);
    
          assertEquals(token, dummyToken);
          verify(inMemoryDBProvider, times(1)).setTemperarily(eq(String.valueOf(dummyDto.getPostId())), eq(token), eq((long) 3 * 60));
        }

     

     

    테스트 통과!

     

     

     

     

     

Designed by Tistory.