본문 바로가기
Experience

Instagram Api Refresh Token batch 리펙토링

by I move forward every day. 2023. 6. 24.

출처 : https://nesoy.github.io/articles/2018-05/Refactoring

개발도중 Instagram API를 사용해야 할 일이 있었다. 해당 API를 사용하기 위해서는 Token을 발급받아 사용해야 하는데 토큰에 기한이 60일이라 주기적으로 새 토큰을 발급받아야 했다. 그래서 해당 기능을 하는 batch 서비스를 하나 만들게 되었고, 정말 짧은 코드지만 해당 코드를 리펙토링 하는 과정을 메모해 둔다.
 

초기 작성 코드


@Service
public class TokenService {

    // 선언부 생략...

    public void callInstaRefreshTokenApi(){
        TokenVO.Request tokenRequestData = this.getAccessToken();

        webClientUtil.getBaseUrl(properties.getInstagramApiOptions().getRefreshTokenUrl())
                .get()
                .uri(uriBuilder -> uriBuilder
                        .queryParam("grant_type", properties.getInstagramApiOptions().getGrantType())
                        .queryParam("access_token", tokenRequestData.getToken())
                        .build())
                .accept(MediaType.APPLICATION_JSON)
                .retrieve()
                .bodyToMono(TokenVO.Response.class)
                .doOnSuccess(refresh -> this.saveInstaApiToken(refresh))
                .block();
    }

    private TokenVO.Request getAccessToken(){
        return tokenRepository.findFirstByOrderByTokenIdDesc().toApiTokenVO();
    }

    private void saveInstaApiToken(TokenVO.Response refreshToken){
        TokenEntity entity = new TokenEntity();
        entity.setToken(refreshToken.getAccess_token());
        entity.setTokenType(refreshToken.getToken_type());
        entity.setExpiresIn(refreshToken.getExpires_in());

        tokenRepository.save(entity);
    }

}

초기에 작성한 코드는 위와 같았다. 서비스레이어에 모든걸 몰아넣었고, 위와 같이 작성한 코드에는 4가지 문제점이 있었다.

  • 첫 번째: 코드만 보고 흐름이 한눈에 읽히지 않는다.
  • 두 번째: 테스트 코드를 작성하기에 적합하지 않다.
  • 세 번째: 서비스 레이어가 복잡하다.

callInstaRefreshTokenApi 메서드는 세 가지의 일을 처리하고 있다. 그러나 세 가지의 일을 처리하고 있다는 것이 한눈에 들어오지 않으며,  각각의 작업 단위로 테스트 해볼수가 없다. 또한 서비스 레이어가 상당히 복잡하다. 이것외에도 더 문제가 있을수 있지만 일단 필자의 짧은 지식으로는 총 3개의 문제점을 발견할 수 있었다. 문제를 해결해보자.
 

리팩토링


callInstaRefreshTokenApi 는 3가지 일을 한다고했다. 그 일은 아래와 같다.

  1. 토큰 재발급을 위해 DB에 저장되어있는 기존의 Token을 조회해온다.
  2. Instagram API에 토큰 재발급을 요청한다.
  3. 새로 발급받은 토큰을 저장한다.

위에서 말했던 모든 문제들은 메소드가 적절히 분리되지 않아서 발생되는 문제라고 생각된다. 첫 번째 리펙토링때에는 작업들을 메소드별로 한 가지의 작업 단위로 분리하여 메소드들이 무엇을 하는지 표현하는 것을 목표로하였다.

1차 리팩토링 코드

@Service
public class TokenService {

    // 선언부 생략...

    public void refreshInstaApiToken(){
        String accessToken = this.getAccessToken().getToken();

        TokenVO.Response resultApiData = this.callInstaRefrashTokenApi(accessToken);

        this.saveInstaApiToken(resultApiData);
    }

    private TokenVO.Request getAccessToken(){
        return tokenRepository.findFirstByOrderByTokenIdDesc().toApiTokenVO();
    }

    private TokenVO.Response callInstaRefrashTokenApi(String accessToken){
        TokenVO.Response apiResponse = new TokenVO.Response();

        webClientUtil.getBaseUrl(cProperties.getInstagramApiOptions().getRefreshTokenUrl())
                .get()
                .uri(uriBuilder -> uriBuilder
                        .queryParam("grant_type", cProperties.getInstagramApiOptions().getGrantType())
                        .queryParam("access_token", accessToken)
                        .build())
                .accept(MediaType.APPLICATION_JSON)
                .retrieve()
                .bodyToMono(TokenVO.Response.class)
                .doOnSuccess(refresh -> {
                    apiResponse.setAccess_token(refresh.getAccess_token());
                    apiResponse.setToken_type(refresh.getToken_type());
                    apiResponse.setExpires_in(refresh.getExpires_in());
                })
                .block();

        return apiResponse;
    }

    private boolean saveInstaApiToken(TokenVO.Response resultApiData){
        TokenEntity entity = new TokenEntity();
        entity.setToken(resultApiData.getAccess_token());
        entity.setTokenType(resultApiData.getToken_type());
        entity.setExpiresIn(resultApiData.getExpires_in());

        entity = tokenRepository.save(entity);

        return entity.getTokenId() != null ? true : false;
    }
}
@Slf4j
@SpringBootTest
public class TokenServiceTest {

    // 선언부 생략....

    @Test
    @DisplayName("인스타그램 Refresh Token API 호출 및 데이터저장")
    public void refreshInstaApiToken(){
        // given
        TokenVO.Request tokenRequestData = tokenDomain.getAccessToken();

        // when
        TokenVO.Response resultApiResponseData = tokenDomain.callInstaRefreshTokenApi(tokenRequestData.getToken());

        // then
        boolean result = tokenDomain.saveInstaApiToken(resultApiResponseData);
        assertThat(result).isEqualTo(true);
    }
}

세 가지의 작업을 각각의 메소드로 분리하였다. 메인 서비스 로직인 callInstaRefreshTokenApi 에서 세 가지 일을 처리한다는 흐름이 보이기 시작한다. 테스트 코드도 더 명확히 작성할 수 있게 되었다. 이전에는 작업이 제대로 분리되어있지 않아 각각의 기능들을 테스트할수가 없었다. 그러나 위의 코드는 작업 단위로 메소드를 작성하였기에 작업 단위로 테스트가 가능해졌다. 또한 작업의 단위로 분리함으로 인해 메소드의 이름을 통해 해당 메소드가 무슨 작업을 하는지 표현할 수 있게 되었다.

 

1차 리펙토링 이후 아쉬운것은 서비스 레이어에 메소드가 많아 복잡하고 작업의 흐름에만 집중하기 힘들다. 그래서 위의 코드를 한번 더 개선하였다.
 

2차 리팩토링 코드

@Service
public class TokenService {

    @Autowired
    private TokenDomain tokenDomain;

    public void refreshInstaApiToken(){
        TokenVO.Request tokenRequestData = tokenDomain.getAccessToken();

        TokenVO.Response resultApiResponseData = tokenDomain.callInstaRefreshTokenApi(tokenRequestData.getToken());

        tokenDomain.saveInstaApiToken(resultApiResponseData);
    }
}

2차 개선때는 token 관련으로 일을하는 메소드들을 tokenDomain이라는 클래스를 만들어 서비스 레이어와 분리하였다. 서비스 레이어에서는 서비스의 흐름에만 집중할 수 있도록 상세한 로직들은 tokenDomain 클래스에 작성하였다. 테스트코드는 1차 리펙토링때와 같다.
 

마무리


리펙토링을 통하여 위에서 말했던 세 가지의 문제들이 많이 개선되었다고 생각한다.

  • 첫 번째: 코드만 보고 흐름이 한눈에 읽히지 않는다.
    • 각각의 작업 단위, 즉 한가지의 일만 하도록 메소드를 분리하였고, 메소드의 이름으로 무슨 작업을 하는지 명확히 표현할 수 있게 되었다.
  • 두 번째: 테스트 코드를 작성하기에 적합하지 않다.
    • 작업 단위로 메소드를 분리하였기에 각 기능별로 테스트를 할 수 있게 되었다.
  • 세 번째: 서비스 레이어가 복잡하다.
    • 상세 로직들을 구현해놓은 domain클래스를 만들어 서비스레이어와 분리함으로써 서비스 레이어는 서비스 로직의 흐름에만 집중할 수 있게 되었다.

 
주니어 개발자인 필자가 작성한 코드는 많은 선배 개발자들에게 보이기에 너무나도 부끄러운 코드임이 분명하다. 그러나 이 글을 읽고 누군가 조언을 해준다면 좋겠다. 메소드명, 변수명도 많은 고민을 하였지만 더 생각이 나질않고 개선점도 지금에 얕은 지식으로는 더 보이지 않는다.
 
긴 글 읽어주셔서 감사드립니다.