토이 프로젝트 스터디 #3

appti·2022년 6월 2일
0

토이 프로젝트 스터디 #3

  • 스터디 진행 날짜 : 6/2
  • 스터디 작업 날짜 : 5/31 ~ 6/2

토이 프로젝트 진행 사항

  • 회원 이메일 인증 로직 수정
    • 기존 RDB에 저장하던 방식을 Redis로 변경
  • 테스트 코드 작성
    • jacoco 도입

내용

회원 이메일 인증 로직

  • 기존 로직의 경우 회원 가입 -> RDB 저장(인증 전) -> 이메일 인증 -> 인증 전 데이터를 인증 후로 변경의 순서로 동작
  • 팀원이 인증 전 회원의 데이터를 RDB에 저장하는 것이 아니라 Redis에 저장하는 것이 어떻겠냐는 의견 제시
    • 이를 토대로 quartz를 사용해 인증 전 회원을 삭제하는 것은 그대로 유지하고, 저장하는 주체만 RDB에서 Redis로 수정
public interface AuthUtilsHelper {

    void saveAccountWithUUID(Account account, String uuid);

    Optional<Map<String, Object>> getAccount(String email);
    
    ...
}
  • 기존 토큰을 Redis에 저장하는 역할을 하는 AuthUtilsHelper에 추가적으로 인증 전 회원 정보와 인증 키 uuid를 저장하는 로직 추가
// AccountService.class
private void validateSignInInfo(SignIn signIn, Account account) {
    // 삭제
    if (account.getVerify().equals(VerifyStatus.BEFORE)) {
        throw new AccountNotValidateException();
    }
    
    if (account.isDeleted()) {
        throw new AccountWithdrawalException();
    }

    if (!passwordEncoder.matches(signIn.getPassword(), account.getPassword())) {
        throw new SignInFailureException();
    }
}
  • 인증 전 회원의 정보를 RDB에 저장하지 않고 Redis에 저장했기 때문에 RDB에 없는 회원이라면 인증되지 않은 회원
    • 따로 Account의 속성을 가져와 인증된 회원인지 검증할 필요가 없어짐
    • validateSignInInfo() 메소드에서 인증된 회원인지 검증하는 로직 삭제

ObjectMapper LinkedHashMap

Map<String, Object> saveMap = new HashMap<>();

saveMap.put("account", account);
saveMap.put("uuid", uuid);
  • ObjectMapper를 통해 RedisString형식으로 저장하고자 함
// AuthRedisUtilsHelper.class
@Override
public Optional<Map<String, Object>> getAccountMap(String email) {
    try {
        return objectMapper.readValue((String) redisTemplate.opsForValue().get(email), Map.class);
    } catch (Exception e) {
        return Optional.empty();
    }
}

// AuthUtils.class
public Optional<Account> getAccount(String email) {
    return authUtilsHelper.getAccount(email)
            .map(map -> Optional.ofNullable((Account) map.get("account")))
            .orElse(Optional.empty());
}

// 예외 발생
java.lang.ClassCastException: 
java.util.LinkedHashMap cannot be cast to com.project.board.application.account.domain.Account
  • Redis에서 저장한 Map을 꺼낸 뒤 그 Map에서 Account을 꺼내려고 할 때 ClassCastException 예외 발생
변환 전 타입변환 후 타입
ObjectLinkedHashMap<String, Object>
ArrayArrayList<Object>
StringString
number (no fraction)Integer, Long or BigInteger (smallest applicable)
number (fraction)Double (configurable to use BigDecimal)
booleanBoolean
nullnull
  • ObjectMapper에서 Object를 변환 시 LinkedHashMap<String, Object>으로 변환되기 때문
// AuthRedisUtilsHelper.class
@Override
public Optional<Map<String, Object>> getAccountMap(String email) {
    Map<String, Object> accountMap = new HashMap<>();
    try {

        Map<String, Object> redisMap =
                objectMapper.readValue((String) redisTemplate.opsForValue().get(email), Map.class);

        accountMap.put("uuid", redisMap.get("uuid"));
        accountMap.put("account", objectMapper.convertValue(redisMap.get("account"), Account.class));

        return Optional.ofNullable(accountMap);
    } catch (Exception e) {
        return Optional.empty();
    }
}

// AuthUtils.class
public Optional<Account> getAccount(String email) {
        return authUtilsHelper.getAccountMap(email)
                .map(map -> Optional.ofNullable((Account) map.get("account")))
                .orElse(Optional.empty());
    }
  • ObjectMapper.convertValue()를 통해 LinkedHashMap 타입을 다른 타입으로 변환 가능
  • 이미 ObjectMapperDI받는 AuthRedisUtilsHelper에서 형변환을 해서 Map으로 반환

jacoco 도입

  • jacoco를 통해 테스트 커버리지를 확인하면서 테스크 코드 작성
@Test
void signUp_email_중복_실패_테스트() {
    // given
    SignUp signUp = SignFactory.createSignUp("중복 이메일", "nickname");

    given(accountRepository.existsByEmail(anyString())).willThrow(new AccountEmailAlreadyExistsException());

    // when, then
    assertThrows(AccountEmailAlreadyExistsException.class, () -> accountService.signUp(signUp));
}

@Test
void signUp_nickname_중복_실패_테스트() {
    // given
    SignUp signUp = SignFactory.createSignUp("test@email.com", "중복 닉네임");

    given(accountRepository.existsByEmail(anyString())).willReturn(false);
    given(accountRepository.existsByNickname(anyString())).willThrow(AccountNicknameAlreadyExistsException.class);

    // when, then
    assertThrows(AccountNicknameAlreadyExistsException.class, () -> accountService.signUp(signUp));
}
  • 기존에 테스트를 작성할 때에는 직접 예외를 발생하도록 함

  • jacoco를 확인해보니 의도한대로 테스트가 동작하지 않았음
    • 직접 예외를 발생시키는 것이 아닌, 작성한 분기문에 따라 예외가 발생하도록 작성했어야 함
@Test
void signUp_nickname_중복_실패_테스트() {
    // given
    SignUp signUp = SignFactory.createSignUp("test@email.com", "중복 닉네임");

    given(accountRepository.existsByEmail(anyString())).willReturn(false);
    given(accountRepository.existsByNickname(anyString())).willReturn(true);

    // when, then
    assertThrows(AccountNicknameAlreadyExistsException.class, () -> accountService.signUp(signUp));
}

@Test
void signUp_authUtils_이메일_중복_실패_테스트() {
    // given
    SignUp signUp = SignFactory.createSignUp("authUtils 중복 이메일", "nickname");

    given(accountRepository.existsByEmail(anyString())).willReturn(false);
    given(authUtils.existsAccountByEmail(anyString())).willReturn(true);

    // when, then
    assertThrows(AccountEmailAlreadyExistsException.class, () -> accountService.signUp(signUp));
}

  • 테스트 시 직접 예외를 발생시키는 것이 아닌 제대로 분기문이 실행되도록 수정
profile
안녕하세요

0개의 댓글