본문 바로가기
Backend 개발자/StackOverflow

Rest API 호출시 보일러 플레이트 코드를 리펙토링 하는 방법, 같은 모양의 양을 줄여보시오!!

by by 앵과장 2022. 8. 9.
반응형
내몸에 익숙함을 바꿔야하는데 ...

프로젝트를 진행하면서 Functional 형태의 구현을 해봐야되겠다는 생각을 늘하지만 

갑자기 코딩 스타일을 바꿔나간다는것이 어렵다고 느껴지고 실제로도 익숙해지기까지 시간이 걸립니다.

 

사람은 누구나 익순한것을 좋아하는데 이유는 위험요소도 적고 자연스럽기 때문입니다.

내가 컨트롤 가능한상태를 구성하기 때문에 표현하기도 쉽다는 생각이 드는데 문제는 내가알고있는것보다 

더 좋은 방법들은 계속 생겨나고 발전하고 생태계 전반적으로 적용하기도합니다. 

자칫 잘못하면 서비스들은 성장하는데 내자신만 멈춰있는 느낌을 받을수도 있습니다.

 

머리로는 알고있고 새로운것을 습득해 나가야한다고 인지는 하지만 참 여러가지 이유로 멀리하게되는데 

하나씩 내가 소화가능한 만큼씩 접근해 나가도록 하겠습니다.

 

보일러 플레이트 코드를 리펙토링 하는방법

 

중복되는 양을 하나로 바꿔보세요 !!!!

그래서 이번만큼은 함수형으로 바꿔보자는 마음으로 마침 중복되는 코드도있고 리펙토링도 해볼겸 아래 함수형 전에 코드레벨들을 

함수형태로 바꿔보는 작업을 진행해보려고합니다.

예시)

특정 Domain에서 검색 API를 호출할일이 있어서 
FeignClient를 호출해서 처리하는 방법에 대해서 설명을 해보도록 하겠습니다.

동일한 Object Class Request, Response를 가진 10건 정도 API를 호출해서 처리한다는 가정입니다.
API URI 타입은 3가지정도로 분류가 됩니다.

10건정도의 검색 API를 호출한뒤 Frontend에 제공하는 Response를 전달하는 프로젝트 입니다.

 

Request, Response 과 동일하고 메소드와 서비스 API URI만 달라지는경우
어떻게 하면 하나의 API 로 호출하게 만들수 있을까 ?

아래처럼 이런 API를 제공하는 서비스가 있다고 고려해보겠습니다.
내부에서 컨트롤 가능하다면 해당 코드를 수정하는것이 가장 적합한 방법이지만 

외부에서 제공하는 API라면 이런 코드를 그냥 받아들이고 작업을 들어가야할때도 있습니다.
또는 여러가지 상황으로 당장 이런 API를 사용해야하는 경우도 발생합니다.
@PostMapping("/a")
public UserResponse getA(@RequestBody @Valid User user) {
    return userService.getA(user);
}

@PostMapping("/b")
public UserResponse getB(@RequestBody @Valid User user) {
    return userService.getB(user);
}

@PostMapping("/c")
public UserResponse getC(@RequestBody @Valid User user) {
    return userService.getC(user);
}

@PostMapping("/user/type-a")
public UserResponse getTypeA(@RequestBody @Valid User user) {
    return userService.getTypeA(user);
}


@PostMapping("/user/type-b")
public UserResponse getTypeB(@RequestBody @Valid User user) {
    return userService.getTypeB(user);
}

@PostMapping("/user/type-a/detail-a")
public UserResponse getTypeADetailA(@RequestBody @Valid User user) {
    return userService.getTypeADetailA(user);
}

@PostMapping("/user/type-a/detail-b")
public UserResponse getTypeBDetailB(@RequestBody @Valid User user) {
    return userService.getTypeBDetailB(user);
}

@PostMapping("/user/type-a/detail-c")
public UserResponse getTypeCDetailC(@RequestBody @Valid User user) {
    return userService.getTypeCDetailC(user);
}

 

Request, Response 와 형식도 동일하고 메소드, URI 정도에 차이가 있습니다. 이런것들은 특정한 패턴으로 구성되고 동일합니다.

이런 코드레벨을 보통 보일러 플레이트 코드 라는 표현을 합니다.

 

간단하게 구현 한다면 그냥 10개를 호출하는 인터페이스를 만들고 10개를 각각처리할수 있는 서비스를 구성하면됩니다.

 

하지만 예시에 설명한것 처럼 

동일한 Object Class Request, Response에 메소드만 다른 API라면 

중복되는 Request, Response 그리고 동일한 로직을 가진 method가 10개나 된다는 예기입니다.

 

이런 부분을 보통 보일러플레이트(boilerplate code) 코드 라고 합니다.

 

보일러 플레이트 코드

보일러 플레이트 코드를 검색해보면 아래와같은 내용을 확인할 수 있습니다.

 

1. 최소한의 변경으로 재사용할수 있는것

2. 적은 수정만으로 여러곳에 활용 가능한 코드, 문구

3. 각종 문서에서 반복적으로 인용되는 문서의 한부문

 

기능상 크리티컬한 이슈는 없지만 서비스를 지속하고 운영하다보면 변경사항이 발생하게되는데 의도치 않게 통일성이 깨지게되고

유지보수하기 어려워지게 구성될수 있다는 단점이 있는 코드입니다.

 

특정 수정이 일어날 경우 동일한 로직에 포함된 모든 코드를 변경해야한다는 이슈도 발생하게 됩니다.

의도하고 만들기 시작한건 아니지만 작업하다보면 많은 개발코드에서 생각보다 많은 빈도수로 만나볼수 있는 코드이기도 합니다.

 

보일러플레이트가 발생할수 있는 API형태를 어떻게 하면 코드를 깔끔하게 구성할수 있는지 고민을 해 보도록 하겠습니다.

 

 

URI 형식에 대한 패턴적인 고려

제공하는 Restful API URI 패턴들의 종류입니다.

url 패턴
----------------------------
/a
/b
/c
/d
/user/type-a
/user/type-b
/user/type-c
/user/type-d
/user/type/detail-a
/user/type//detail-b
/user/type//detail-c
/user/type//detail-d
----------------------------

각 URI를 직접 호출할수도 있는 코드레벨로 만들수 있지만 

패턴을 분석해보면 "/" 단위로 URL을 분리하면 3가지정도에 인터페이스로 구성이가능할것 같습니다. 

 

interface를 구성하고 각파라메타를 담을수있게 메소드를 마들어 보겠습니다.

@FeignClient(name = "user-client", primary = false, url = "https://localhost:30301")
public interface UserClient {
    @PostMapping("/user/{url-1}")
    ResponseEntity<DisplayResponse<User>> find(User user
            , @PathVariable("url-1") String firstUrl);

    @PostMapping("/user/{url-1}/{url-2}")
    ResponseEntity<DisplayResponse<User>> find(User user
            , @PathVariable("url-1") String firstUrl
            , @PathVariable("url-2") String secondUrl);

    @PostMapping("/user/{url-1}/{url-2}/{url-3}")
    ResponseEntity<DisplayResponse<User>> find(User user
            , @PathVariable("url-1") String firstUrl
            , @PathVariable("url-2") String secondUrl
            , @PathVariable("url-3") String thirdUrl);
}

이렇게 3가지 정도의 패턴형식의 인터페이스를 만들어 

제공하는  Rest API를 모두 FeignClient로 사용할수 있도록 작업하였습니다. 

 

혹시 오류가 발생할수있는 부분을 위해서 아래처럼 split 를 좀더 오류처리까지 포함하고있는 메소드로 구성하겠습니다.

static List<String> split(String url){
    if(StringUtils.isEmpty(url)){
        return Collections.emptyList();
    }
    return Arrays.asList(url.split("/"));
}

그냥 Split 를 사용해도되지만 url이 null일수도 있기때문에 empty로 null에대한 오류를 벗어나보겠습니다.

List<String> urls = url.split();

if(1 == urls.size()){
    getUrlTypeA()
}else if(2 == urls.size()){
    getUrlTypeA()
}else if(3 == urls.size()){
    getUrlTypeA()
}

public ResponseEntity<UserResponse<User>> getUrlTypeA(){
    return ResponseEntity.ok(userClient.find(User,url.get(0)))
}

public ResponseEntity<UserResponse<User>> getUrlTypeB(){
    return ResponseEntity.ok(userClient.find(User,url.get(0),url.get(1)))
}

public ResponseEntity<UserResponse<User>> getUrlTypeC(){
    return ResponseEntity.ok(userClient.find(User,url.get(0),url.get(1),url.get(2)))
}

URL을 split 로 "/" 분리해서 LIST 자료구조로 담아내고 

if 조건문을 통해서 3가지 형태를 아래 메소드를 호출해서 처리하는 방법으로 작업하였습니다.

 

여기서도 문제점은 발생합니다.

일단 다른 패턴이 발생하게 되면 인터페이스는 추가되지만 증가되는 패턴구조에 대한 

if문 케이스와 처리하는 메소드를 지속적으로 만들어야하는 이슈가 발생합니다.

 

if else 문의 지옥행 코드열차

요새 최대한 if문에 대한 처리를 줄여보고자 노력하는편입니다. 

if(a==1){

}

당연하게도 특정 Case를 처리하기위해서는 필요한 내용입니다. 

 

가끔 무지성 말하는 개발자들 만나면... 어 !!!!!

if else if else 이렇게하는거아니야 라고 예기하시는 분들이 있었는데 .... 어우 다시는 안보고싶은 느낌이네요!!

이런코드가 많아지면 개발자들사이에서는 "아도겐" 이라는표현을 합니다.

 

이걸 나쁘다라고 표현하기보다는 잘못짰다 라는 표현이 맞는거같습니다.

최근 에는 이런코드 자주본적이 없지만 Legacy에서는 종종 볼수 있는 코드입니다. 아무생각없이 일단 다른코드 이해하기 싫다면 

이렇게 Case by Case를 구성하는게 가장 빠른 지름길입니다. 

 

일정 쪼이면서 개발하면 어쩔수 없져 ㅜㅡㅜ

일단 하고 나중에 고치자 보통 나중은 없어요 !! 퇴사가 답이지 

 

IF문 처리방법을 자료구조형을 바꿔서 처리할수 있습니다.
if(1 == urls.size()){
    getUrlTypeA()
}else if(2 == urls.size()){
    getUrlTypeA()
}else if(3 == urls.size()){
    getUrlTypeA()
}

Map<Integer,UrlMapper> mapping = Map.of(
    1, url이 "/a" 라는 패턴에 FeignClient를 호출한다.
    2, url이 "/user/typeA" 라는 패턴에 FeignClient를 호출한다.
    3, url이 "/user/type/detailA" 라는 패턴에 FeignClient를 호출한다.
);

Map 타입으로 자료구조를 변경하면 기존에 if else로 처리하는것 보다 좀더 직관적이고 의미있는 형태의 자료구조로 변경할수있는것 같습니다.

 

이건 어디까지나 지속적으로 증가할거같다는 판으로 작업을한거고 if 문에 대한 코드가 크게 증가하지 않는다면 가볍게 처리해도될것같습니다. 

최근 if문 1개 이상일경우는 자료구조를 바꾸는형태로 많이 변경해서 구성하는편입니다.

 

그렇다면 Map에 value에 정의한 Todo 내용을 코드로 바꿔보도록 하겠습니다.

Map<Integre,UrlMapper> mapping = Map.of(
    1, new UrlMapper() {
                @Override
                public UserClientSignature urlMapping(UserClient userClient, List<String> urls) {
                    return new UserClientSignature() {
                        @Override
                        public ResponseEntity<UserResponse<User>> exchange(User user) {
                            return userClient.find(searchRequest, urls.get(0));
                        }
                    };
                }
            },
    2, new UrlMapper() {
                @Override
                public UserClientSignature urlMapping(UserClient userClient, List<String> urls) {
                    return new UserClientSignature() {
                        @Override
                        public ResponseEntity<UserResponse<Recruitment>> exchange(User user) {
                            return userClient.find(searchRequest, urls.get(0), urls.get(1));
                        }
                    };
                }
            }
    3, new UrlMapper() {
                @Override
                public UserClientSignature urlMapping(UserClient userClient, List<String> urls) {
                    return new UserClientSignature() {
                        @Override
                        public ResponseEntity<UserResponse<Recruitment>> exchange(User user) {
                            return userClient.find(searchRequest, urls.get(0), urls.get(1),urls.get(2));
                        }
                    };
                }
            }        

);

 

우선 UrlMapper 라는 클래스를 만들어서 함수형으로 처리할수있게 인터페이스를 선언합니다.

함수형 인터페이스는 다음에 한번 또 다뤄보도록 하겠습니다. 

궁금하면 구글링으로 검색 해보시면 자세한 내용들을 찾아볼수있습니다.


@FunctionalInterface 를사용해서 

한개의 인터페이스만 제공하는 함수형 인터페이스를 선언합니다.


@FunctionalInterface
함수형 인터페이스를 사용하는 이유는 자바의 람다식은 함수형 인터페이스로만 접근이 되기 때문입니다.

@FunctionalInterface
public interface UrlMapper {
    ResponseEntity<UserResponse<User>> urlMapping(final userClient userClient, Urls urls);
}

Urls 에 처리되는 URL은 별도의 클래스로 좀더 용도에 부합하게 구성하겠습니다.

  @Slf4j
    @AllArgsConstructor
    final class Urls {
        private final List<String> urls;
        private int index;

        static Urls empty(){ return new Urls(Collections.emptyList(), 0); }

        static Urls of(String[] urls) { return new Urls(Arrays.asList(urls), 0); }
        String path(){
            if (isIndexOutOfBounds()){
                throw new ApiException(ServiceErrorType.NOT_FOUND_URL);
            }
            final String url = urls.get(index);
            index++;
            log.info("=========== url : " + url);
            return url;
        }

        private boolean isIndexOutOfBounds() {
            return index < 0 || index >= urls.size();
        }

        int size() {
            return urls.size();
        }
    }

 

 

UrlMapper 에서 정의한 Object 타입은 좀더 의미있게 시그니처 인터페이스를 구성해서 위임하는 구성으로 하나더 만들도록 하겠스니다. 이것도 함수형으로 바꾸기 위한 작업이라고 생각해주시면될것 같습니다.

@FunctionalInterface
public interface UserClientSignature {
    ResponseEntity<UserResponse<User>> exchange(User user);
}

아까전에 Map에 있는 내용들을 함수형태로 변경하면 아래와같은 형태가 됩니다.

   Map<Integer, UrlMapper> mapping = Map.of(
            1, (c, u) -> (s) -> c.find(s, u.path()),
            2, (c, u) -> (s) -> c.find(s, u.path(), u.path()),
            3, (c, u) -> (s) -> c.find(s, u.path(), u.path(), u.path())
    );

 

어떤가요 참 .......쉽져 함수형 프로그래밍이 익숙하지 않다면 아직 이런 구조가 눈에 잘익지는않겠지만 

위임하고 함수형 인터페이스를 사용하고 연습하다보면 Java 도 다른 언어만큼이나 좀더 세련된 느낌으로 개발을 진행해볼수 있게됩니다.

 

추가적인 코드는 
https://github.com/lswteen 에 프로젝트에 구성을 하도록 하겠습니다.