기존 서비스의 확장 전개 - Rest API

김나쁜 Kimbad·2023년 4월 25일
0

서비스 확장

목록 보기
3/3

ApiFactory?

국내 서비스를 받아 해외 전개를 하는 프로젝트의
Rest API 관련 클래스들이 Factory Method 패턴으로 설계되어 있다.
그러던 중 문제아닌 문제 발생..

추상 팩토리 패턴 - sup2is님 블로그

구조

RestServiceFactory
├─ RestService (추상 클래스)
│  ├─ {서비스명}RestService
│  │  └─ {브랜드명}RestService
│  └─ RestApi (RestApiVO를 필드로 가짐)
│     ├─ {서비스}Api
│     │  └─ {서비스}ApiVO (RestApiVO를 확장)
│     └─ RestApiVO

텍스트로 그려진 관계도라 구조를 완벽히 보여주지는 않지만
대강 이런 모양새다.

기존 설계가 굉장히 복잡하게 되어 있었다.
내가 느끼기로는 이렇게까지 복잡해야할 이유가 있나? 싶을 정도였는데...
어쨌든 이 설계를 기반으로 큰 수정 없이 전개를 해야하니 이해하고 넘어가야 했다.

  • RestServiceFactory
    RestService들을 생성하고 제공하는 단일 지점
    @Autowired된 생성자 파라미터로 List<RestService>를 받기 때문에 RestService 타입의 빈을 찾아 생성자에 주입한다.

  • RestService
    직접적인 API 호출을 담당하는 Abstract Class
    필드로 RestApi 객체를 가지고 있으며
    RestService를 정의하는 ServiceId의 Getter/Setter를 추상 메서드로 가지고 있다.

  • {서비스명}RestService
    {서비스명}과 {브랜드명}은 정의가 모호하긴 한데, 그렇게 만들어져 있어서 모호하게 설명할 수 밖에 없는 것 같다.
    어떤 RestService인지 정의하는 클래스로 RestService를 확장받은 클래스다.
    setService()메서드를 통해 serviceId와 restAPI정보 객체를 부여 받는다.
    RestAPI정보 객체는 각 호출 URL의 정보와 ApiVO를 주입 받아 ApiVO의 하위 정보들을 가지고 있다.

  • {브랜드명}RestService
    {서비스명}RestService의 하위 객체로 {서비스명}RestService를 확장받는다. 고유의 Service ID값과(클래스 내 상수로 선언) ApiVO객체를 필드로 가지고 있다.
    ApiVO의 값을 여기서 세팅하게 되고
    사실상 {서비스명}RestService가 사용할 API를 정의하는 클래스가 된다.

  • RestApi
    RestApiVO를 field로 가진 클래스

  • {서비스명}Api
    RestApi 클래스를 확장받은 클래스, 필드로 {서비스}ApiVO 클래스와 API 호출 URL 정보 등을 가지고 있다.
    Getter로 ApiVO가 가진 Host + URL등을 리턴한다.

  • RestApiVO
    brand, host를 필드로 가진 클래스

  • {서비스}ApiVO
    RestApiVO객체를 확장받은 클래스
    {서비스}의 API Key나 Callback URL 등 해당 API의 직접적인 정보를 가지고 있는 VO

단순화 못해요?

RestServiceFactory
├─ RestService (추상 클래스)
│  └─ {서비스명}RestService
└─ RestApi (RestApiVO를 필드로 가짐)
   └─ RestApiVO

이렇게 바꾸면 클래스 계층이 줄어들어 전체적인 복잡성이 감소하고,
단순화된 구조에서는 각 브랜드의 RestService 클래스에서 서비스별 RestApiVO 객체를 사용하여 API 호출을 처리 가능해진다.
또한 {서비스명}RestService 클래스를 만들 필요가 없으며, 클래스 계층을 줄일 수 있다.

다만 실제 수정을 해보진 않았기 때문에 어떤 추가적인 문제가 발생할 지 모르고, "기존 서비스를 확장 전개" 하는 입장에서는 이 경우 구조 수정이 필수적이므로 단순화는 방법만 생각해보고 넘어갔다.

문제

기존 구조에서는 로직 내에서 ARestService라는 고정된 {서비스}RestService 객체를 선언하게 되어있어 문제가 생겼다.

OAuth2.0에서 기본적인 로그인 과정을 보면, 공통적으로 밟는 단계가 있다.

  1. Authorization URL 요청 (이 경우 로그인 URL 요청)
  2. Authentication Code 요청 (리다이렉트된 로그인 URL에서 로그인 성공시 요청 서버의 redirect 경로로 Code 반환)
  3. 토큰 요청
  4. 리소스 요청 (이 경우 유저 정보 요청)
  5. ...

OAuth를 사용하는 경우 여기까지의 Step은 대부분 동일하다.
서비스에서 자체적으로 각 단계 사이에 어떤 추가적인 비즈니스 로직이 들어갈 수는 있겠지만 OAuth를 사용한다면 기본적으로 각 단계는 API 호출이다.

문제가 되는 부분은 이 API 호출 부분인데,
지금의 코드는 {서비스}RestService 변수 선언이 고정적이다.

// ...
ARestService aRestService = restServiceFactory.getService(brand, Constant.A_SERVICE_ID)
String redirectUrl = aRestService.getAuthorizationUrl(request, "");
response.sendRedirect(redirectUrl);

여기서 만약 해외 전개를 하면서, 유저 정보를 받아오는 OAuth의 리소스 주체가 B로 변경 되었다고 가정한다면?

똑같이 BServiceRestService와 BApi 클래스를 만들어야 하고(이건 문제가 아니다) 저 부분의 코드 수정이 불가피하다.

조건은 프로퍼티에서 설정한 특정 국가 값으로 준다고 해도
If절로 다른 서비스를 분기를 시키면 변수 Scope 문제로 중복 코드가 생길 수 밖에 없다.

// ...
RestService restService;
switch(프로퍼티의 국가값) {
    case A : 
        restService = (ARestService) restServiceFactory.getService(brand, Constant.A_SERVICE_ID);
    case B :  
        restService = (BRestService) restServiceFactory.getService(brand, Constant.B_SERVICE_ID)
}
// RestService는 getAuthorizationUrl 메서드가 없으므로 에러 발생
String redirectUrl = restService.getAuthorizationUrl(request, "");
response.sendRedirect(redirectUrl);

RestService 클래스 내에 OAuth 관련 메서드들을 추상 메서드로 구현한다고 해도 이미 RestService를 상속받은 클래스들이 열댓가지가 되기 때문에 쓰레기 코드가 늘어난다.

ARestService와 BRestService는 동일한 OAuth 단계를 거치니까
그냥 ARestService가 가지는 ARestAPI(API 정보를 가진) 객체를 조건에 따라 BRestAPI로 바꿔줄까도 생각해봤지만 이미 필드에 ARestAPI로 선언이 되어 있어서 그것도 불가능했다.

결론

인터페이스로 바꾸자.
원래도 추상 팩토리 패턴은 인터페이스를 사용하는 것 같은데 왜 추상 클래스로 만들었는지 의문이다.
아마 Api 호출하는 기능을 공통적으로 가지기 때문인 듯 한데.

public interface OAuthInterface {
    public String getAuthorizationUrl(HttpServletRequest request, String scope);

    public TokenVO getAccessToken(String code) throws CommonException;

    public ResponseEntity<String> redirectSignoutUrl(String id, String token, String callback);

    public Map<String, Object> getUserProfile(String accessToken) throws CommonException;

    public Map<String, Object> getUserCert(String accessToken) throws CommonException;
}

OAuth의 각 단계를 인터페이스로 만들고
이후의 OAuth를 SomeRestService 클래스들은 모두 이 인터페이스의 구현체로 만든다.

클래스 시그니쳐가 좀 더러워지겠지만 코드를 크게 수정하지 않아도 되고, 기존 로직과 충돌도 없다.

public static OAuthInterface getRestService(String brand, String region) throws CommonException {
    RestServiceFactory restServiceFactory =
            (RestServiceFactory) SpringContextSingleton.getBean("restServiceFactory");

    switch (region) {
        case A : return (ARestService) restServiceFactory.getService(brand, Constant.A_SERVICE_ID);
        case B : return (BRestService) restServiceFactory.getService(brand, Constant.B_SERVICE_ID);
        // default : return null;
    }
}

공통 유틸리티 클래스에서 서비스를 리턴하는 메서드를 만들어주고

OAuthInterface restService = CommonUtil.getRestService(brand, 프로퍼티의 서비스지역 값);
String redirectUrl = restService.getAuthorizationUrl(request, "");

이렇게 만들어주었다.

profile
Bad Language

0개의 댓글