프로젝트/Petogram

[Petogram] Oauth Login refactoring 여행기 - 2

2023. 6. 17. 03:25
목차
  1. 들어가며
  2. 내가 제어할 수 없는 코드 테스트
  3. Map 데이터 타입으로 Mock객체 주입
728x90

들어가며

이전에 Oauth 로그인을 전략 패턴을 활용해서 로직을 분리했다. (https://brightstarit.tistory.com/58)

결과적으로 공통 로직과 provider별 다른 로직을 분리해 낼 수 있었고, 추가적으로 내가 제어할 수 없는 로직을 분리했다.

이제 테스트를 진행하려는데 맞닥들인 문제가 있었다.

  1. Mockmvc 테스트를 진행하면서 내가 제어할 수 없는 부분은 어떻게 처리해야 할까?
  2. Map으로 동적으로 사용할 빈을 결정하는 경우 Stub을 어떻게 해야 할까?

이 두 문제를 해결함으로써 좀 더 테스트를 진행할 때 문제에 더 유연하게 대처가 가능해질 것 같다.


내가 제어할 수 없는 코드 테스트

다음은 우리 서비스의 /api/login을 호출했을 때의 코드이다.

  @Override
  public Authentication attemptAuthentication(HttpServletRequest request,
      HttpServletResponse response)
      throws AuthenticationException {
    try {
      LoginReqDto loginReqDto = om.readValue(request.getInputStream(), LoginReqDto.class); //1

      //provider validation
      if (Arrays.stream(PROVIDERS)
          .noneMatch((provider) -> provider.equals(loginReqDto.getProvider().toUpperCase()))) {
        throw new RuntimeException("유효하지않은 provider 입니다.");
      } //2

      // oauth login
      User user = oauthLoginService.attemptLogin(loginReqDto); //제어할 수 없는 코드 //3

      // 강제 로그인
      UsernamePasswordAuthenticationToken authenticationToken =
          new UsernamePasswordAuthenticationToken(user.getEmail(), "DEFAULT_PASSWORD");

      // UserDetailsService의 loadUserByUsername 호출
      // JWT를 쓴다 하더라도, 컨트롤러 진입을 하면 시큐리티의 권한체크, 인증체크의 도움을 받을 수 있게 세션을 만든다.
      return authenticationManager.authenticate(authenticationToken); //4
    } catch (Exception e) {
      throw new InternalAuthenticationServiceException(e.getMessage());
    }
  }

 

과정은 다음과 같다.

  1. HttpServletRequest로부터 loginReqDto를 받는다.
  2. 해당 DTO에서 provider의 정보를 받고 우리 서비스가 제공하는 provider인지 유효성 검사를 한다.
  3. DTO에 있는 토큰을 통해 토큰을 검증하고 검증된 사용자를 가져온다. (제어할 수 없는 코드)
  4. 사용자를 강제 로그인시킨다.

이 중 3번 과정이 문제다.

위의 로직이 정상적으로 작동하면 successfulAuthentication  함수가 실행되어 Response에 Httponly Cookie로 refresh Token과 Header로 access Token, 그리고 사용자 정보가 담기게 된다.

Mockmvc 테스트에서는 이 로그인이 성공했을 때 나온 Response를 검증하고 싶다.

하지만 Request로 받는 토큰은 내가 임의로 생성할 수 없기에 oauthLoginService.attemptLogin(loginReqDto) 해당 코드는 무조건 실패하게 된다.

 

Mockito를 활용하여 Mock 객체를 임의로 넣을 수도 없고 막막했었다.

한참을 고민하다가 진행하던 스터디원분의 도움을 얻어 @Profile을 활용하는 방안을 생각했다.

 

3번 과정의 OauthLoginService를 인터페이스로 구성하고 Test용 구현체를 따로 만들어 User객체를 적용하도록 했다.

 

Test용 구현체

@Service
@Profile("test")
public class TestOauthLoginService implements OauthLoginService {

  @Override
  public User attemptLogin(LoginReqDto loginReqDto) {
    return User
        .builder()
        .email("ssar@naver.com")
        .password("DEFAULT_PASSWORD")
        .build();
  }
}

 

실제 구현체

import java.util.Map;

@Service
@Slf4j
@RequiredArgsConstructor
@Profile("!test")
@Transactional
public class OauthLoginServiceImpl implements OauthLoginService {

  //....
}

실제 구현체는 로직이 길어 생략했다.

여기서 @Profile을 적용해 test에는 위의 로직을, test가 아닌 경우에는 아래 로직을 적용하도록 했다.

 

이제 테스트를 실행해 보자.

결과적으로 cookie와 header가 정상적으로 response 되는 것을 확인할 수 있었다.


Map 데이터 타입으로 Mock객체 주입

다음은 제어하지 못하는 함수인 oauthLoginService.attemptLogin(loginReqDto) 로직을 Mockito를 통해 테스트해 보자.

@Service
@Slf4j
@RequiredArgsConstructor
@Profile("!test")
@Transactional
public class OauthLoginServiceImpl implements OauthLoginService {

  //동적으로 빈 사용
  private final Map<String, OauthService> oauthServiceMap;
  private final UserRepository userRepository;
  private final BCryptPasswordEncoder bCryptPasswordEncoder;

  public User attemptLogin(LoginReqDto loginReqDto) {
    OauthService oauthService = oauthServiceMap.get(loginReqDto.getProvider().toUpperCase()); //1
    Map<String, String> payload = oauthService.getPayload(loginReqDto.getToken()); //2
    String email = payload.get("email");
    String name = payload.get("name");

    try {
      // 이미 가입된 사용자가 있을 경우에
      return getJoinedUser(email); //3
    } catch (Exception e) {
      // 가입된 사용자가 없을 경우에 가입한다.
      log.debug("이메일에 해당하는 사용자가 없습니다.");
      return joinUser(loginReqDto, email, name); //4
    }
  }

  private User joinUser(LoginReqDto loginReqDto, String email, String name) {
	//...
    return user;
  }

  private User getJoinedUser(String email) {
	//...
}

해당 로직은 다음과 같다.

  1. LoginReqDto를 통해 provider를 가져와 OauthService의 구현체를 결정한다.
  2. LoginReqDto를 통해 token을 가져와 토큰 검증을 하고 payload를 가져온다.
  3. 가져온 payload를 통해 이미 가입된 사용자라면 사용자 정보를 가져온다.
  4. 가져온 payload를 통해 가입이 안된 사용자라면 새로 가입시킨다.

여기서 부딪혔던 문제는 1번 과정에서 OauthServiceMap을 Mock 객체로 주입하고,  OauthServiceMap.get()으로 return 된 OauthService도 Mock객체로 만들어줘야 한다는 것이다.

 

해결 과정은 다음과 같다.

  1. OauthServiceMap을 Mock객체로 주입을 한다.
  2. mock() 함수를 통해 OauthService의 구현체의 Mock객체를 생성한다.
  3. OauthServiceMap의 return 값을 2번 과정에서 생성된 Mock객체를 return 하도록 한다.
  4. 2번 과정에서 생성된 OauthService의 getPayload()는 임의의 Map데이터를 return 하도록 한다.

위와 같은 과정의 테스트는 다음과 같이 작성했다.

결과적으로 사용자가 가입이 된 상태와 안된 상태일 때 각각 정상적으로 동작하는지 테스트해 볼 수 있었다.

그리고 mock()을 어떤 경우에 사용하면 좋을지 감이 잡힌 과정이었다.

'프로젝트 > Petogram' 카테고리의 다른 글

[Petogram] Oauth Login refactoring 여행기 - 1  (0) 2023.06.16
  1. 들어가며
  2. 내가 제어할 수 없는 코드 테스트
  3. Map 데이터 타입으로 Mock객체 주입
'프로젝트/Petogram' 카테고리의 다른 글
  • [Petogram] Oauth Login refactoring 여행기 - 1
밝은별 개발자
밝은별 개발자
호기심 가득한 밤을 하나씩 밝히는 밝은별입니다.
밝은별 개발 블로그호기심 가득한 밤을 하나씩 밝히는 밝은별입니다.
밝은별 개발자
밝은별 개발 블로그
밝은별 개발자
전체
오늘
어제
  • 전체 글 (59)
    • 자소서 (0)
    • 백엔드 (55)
      • Java (15)
      • 네트워크 (5)
      • JPA (11)
      • Spring (6)
      • 운영체제 (8)
      • MSA (4)
      • etc (4)
      • 데이터베이스 (2)
    • 프로젝트 (3)
      • Petogram (2)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • 쓰기 지연
  • ORM
  • 디자인 패턴
  • pojo
  • 네트워크
  • 캐시
  • java8
  • 상속
  • 클린 아키텍처 #spring
  • 변경 감지
  • 영속성컨텍스트
  • db
  • MSA
  • JDK 동적프록시
  • Spring
  • 객체지향
  • 이펙티브자바
  • Redis
  • Java
  • 런타임데이터영역
  • EUREKA
  • HTTP
  • 데이터베이스
  • 스트림
  • JPA
  • 운영체제
  • Petogram
  • OS
  • 메모리
  • Service discovery

최근 댓글

최근 글

hELLO · Designed By 정상우.
밝은별 개발자
[Petogram] Oauth Login refactoring 여행기 - 2
상단으로

티스토리툴바

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.