-
테스트를 쓰면서 만난 좋지 않은 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)); }
테스트 통과!
'운영 중인 서비스 > Coconut.' 카테고리의 다른 글
CloudWatch를 사용해서 Discord에서 EC2 상태 알림 받기 (0) 2024.02.16 내 서버는 얼마나 버틸 수 있는가 (0) 2024.02.15 조회수 변경 시 발생하는 레이스 컨디션과 해결 방법 (1) 2024.02.02 String으로 사용되던 데이터 enum으로 상수처리 (0) 2024.01.30 NumberFormatException (0) 2024.01.23