Hello, Freakin world!

[Java] if문이 안티패턴이 되는 경우에 대해 본문

프로그래밍 언어/Java

[Java] if문이 안티패턴이 되는 경우에 대해

johnna_endure 2020. 4. 9. 15:17

if문은 코딩에서 분기를 처리하는 가장 기본적인 문법입니다.

 

if문의 사용자체가 안티패턴을 유발하진 않습니다만,

하지만 특정 상황에 따라 if문의 사용은 코드를 어지럽힐 수 있습니다.

 

어떤 경우가 있을까요?

 

이런 경우들을 일반화시켜 하나의 문장으로 정리하면 다음과 같습니다.

1:N의 관계에서 N이 상당히 큰 경우거나 커질 가능성이 있는 경우.

특히 아키텍쳐 레벨에서 이런 식의 코드로 흐름을 제어할 경우, 정말 개발/유지하기 힘든 코드가 탄생할 수 있습니다.

상대적으로 개수가 고정적이고 확장의 폭이 좁은 '메뉴'의 핸들러 매핑과 같은 문제에 대해서는 개수가 꽤 되더라도 if문 분기를 이용해서 해결할 수도 있습니다. N이 그리 크지 않다면 if문이 더 편한 경우도 분명히 존재한다고 생각합니다.

 

그렇다면 대안은?

Spring Framework에서 흔히 사용되는 애너테이션과 리플렉션을 이용해 해결하는 방법입니다.

 

switch문은 if문의 대안이 될 수 없습니다. 자바의 switch문은 if문의 또 다른 형태일 뿐입니다.

if문을 이용해 아무리 메서드와 클래스로 책임을 분리하고 깔끔한 코드를 만들려고 해도, 결국 클래스간의 강결합을 피할 수 없습니다. 그리고 분기가 추가될 때마다 else if를 덧붙이면서 코드는 점점 길어집니다.

 

애너테이션과 리플렉션을 이용하면 이 문제를 아주 깔끔하게 해결할 수 있습니다.

 

이제 예제를 살펴볼까요?

 

예제 코드

 

아래는 전체 코드 주소입니다.

https://github.com/johnna-endure/alternative-if-example

 

johnna-endure/alternative-if-example

Contribute to johnna-endure/alternative-if-example development by creating an account on GitHub.

github.com

 

코드 살펴보기

 

Handlers.class

package example;

public class Handlers {
    @RequestMapping(method = HttpMethod.GET, url = "/hello")
    public Response hello_GET() {
        return new Response(200,"hello, client","getBody");
    }

    @RequestMapping(method = HttpMethod.POST, url = "/hello")
    public Response hello_POST() {
        return new Response(200,"hello, client", "postBody");
    }

}

 

위와 같이 @RequestMapping 과 같은 애너테이션으로 메서드를 마킹할 수 있습니다.

마킹한 애너테이션과 자바의 reflection api를 이용해 메서드를 다른 객체에서 선별할 수 있습니다.

 

Dispatcher.class

package example;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Optional;

public class Dispatcher {
    private Handlers handler;

    public void setHandler(Handlers handler) {
        this.handler = handler;
    }

    public Response dispatch(Request request) {
        Optional<Method> handlerOpt = findHandler(request.getHttpMethod(), request.getUrl());
        if(handlerOpt.isEmpty()) {
            return new Response(400,"bad request");
        }
        try {
            return (Response) handlerOpt.get().invoke(handler);
        } catch (IllegalAccessException | InvocationTargetException e) {
            e.printStackTrace();
            return new Response(500, "method invoke error");
        }
    }

    private Optional<Method> findHandler(HttpMethod httpMethod, String url) {
        Method[] methods = handler.getClass().getMethods();
        return Arrays.stream(methods)
                .filter(m -> m.isAnnotationPresent(RequestMapping.class))
                .filter(m -> m.getAnnotation(RequestMapping.class).method().equals(httpMethod))
                .filter(m -> m.getAnnotation(RequestMapping.class).url().equals(url))
                .findFirst();
    }
}

 

 

자바의 리플렉션 api(Class, Method 같은 클래스에서 제공하는 메서드들)와 자바 Stream api를 이용해서 분기를 처리하는 모습입니다.

이제 더이상 핸들러를 추가할 때마다 if else문을 추가하는 것과 같은 일을 할 필요가 없어졌습니다.

Dispatcher는 자동으로 요청정보를 이용해 @RequestMapping에 등록된 정보와 비교해 조건에 맞는 핸들러를 매핑시킵니다.

 

 

Controller.class

package example;
/*
example.Request 받아 처리를 위임하고
Response를 반환합니다.
 */

public class Controller {
    Dispatcher dispatcher;

    public void setDispatcher(Dispatcher dispatcher) {
        this.dispatcher = dispatcher;
    }

    public Response processRequest(Request request) {
        /*
            전처리 코드
         */
        Response response = dispatcher.dispatch(request);
        /*
            후처리 코드
         */
        return response;
    }
}

 

Contoller에서 분기를 처리하는게 아닌, Dispatcher에 그 일을 위임합니다.

 

위의 코드들이 if문 분기보다 나은 점은 클래스들이 가지는 책임의 경계가 명확해졌다는 겁니다.

이렇게 클래스 간의 결합이 약해지면 테스트하기도, 유지보수하기도 한결 수월해집니다.

 

초반에 틀을 잡기 위한 약간의 비용이 들긴 하지만, 충분히 복잡해질 여지가 있는 프로젝트라면 여기서 발생하는 트레이드오프는 충분히 감수할 만하다고 생각합니다.

 

(위의 예제는 상황을 단순화한 예제입니다. 이를 출발점으로 스프링에서 제공하는 핸들러의 인수를 자동 바인딩해주는 기능들을 추가할 수 있습니다. 이와 관련된 내용은 https://javachoi.tistory.com/180 의 카테고리 글들을 참고하세요.)

Comments