본문 바로가기
가치관 쌓기/개발 돌아보기

[리팩토링] 다형성을 이용한 IF 문 제거하기

by simplify-len 2023. 5. 21.

들어가며

코드를 통해 본격적으로 다형성을 이용한 IF 문 제거하기 를 들어가기 앞서, 왜 IF문을 제거해야 하는지 부터 이해해보자.

IF 문을 제거해야 하는 이유로 2가지를 말할 수 있을 것 같다.

  1. 인지 과부하
  2. OCP(Open closed principle)

 

인지 과부하

IF 라는 키워드를 사용하게 되면, 필연적으로 우리의 두뇌는 IF ELSE 을 고민하게 된다.

if(...) {
  // do Something A
} else {
  // do Something A else...
}

if 가 한 개일때는 우리의 두뇌는 문제 되지 않는다.

2가지의 경우의 수만 고민하면 된다. 여기에 if문을 중복으로 추가하게 되면 어떻게 될까?

if(...) {
    // do Something A
  if (...) {
    // do Something B
  } else {
    // do Something B'
  }
} else {
  // do Something A'
}

4가지다. 하나더 추가해보면 아래 그림과 같다.

// do Something D
if(...) {
  // do Something A
  if (...) {
    // do Something B
    if (...) {
      // do Something C
    } else {
      // do Something C'
    }
    // do Something B'
  } else {
    // do Something A'
  }
} else {
  // do Something D'
}

위 언급된 코드를 보면, do Something n 을 보면 8가지 경우가 있을 수 있다. 이것은 맞을 수도 있고, 맞지 않을 수도 있다.

if 문과 if 문이 만나는/끝나는 교점에 코드가 삽입되는 부분을 고민해보면, 점점 더 복잡해짐을 알 수 있다.

이때부터 우리의 두뇌는 인지과부하 라는 문제를 겪게 된다. 물론 이런 코드를 만들지 않을거라 생각하겠지만, 실제로 종종 이런 코드를 만들어내는 우리 모습을 발견하곤 한다.

개방 폐쇄의 원칙 - OCP(Open Closed Principle)

기존의 코드를 변경하지 않으면서, 기능을 추가할 수 있도록 설계가 되어야 한다는 원칙.

OCP를 확장에 대해서는 개방적(open)이고, 수정에 대해서는 폐쇄적(closed)이어야 한다는 의미로 정의

그렇다면, 확장에 개방적이라는 의미는 어떤 것인가?

수정에 대해서 폐쇄적이라는 것은 어떤 것인가?

여기서 확장이란, 새로운 기능을 추가하는 것을 의미한다. 그러므로, 기능 추가 요청이 오면 클래스를 확장을 통해 손쉽게 구현하면서, 확장에 따른 클래스 수정은 최소화하도록 프로그램을 작성해야 하는 설계 기법을 말한다.

이 또한 코드로 간단히 살펴보면 다음과 같다.

class Main {
  public static void main(String[] args) {
        HelloAnimal hello = new HelloAnimal();
​
        Animal cat = new Animal("Cat");
        Animal dog = new Animal("Dog");
​
        Animal sheep = new Animal("Sheep");
        Animal lion = new Animal("Lion");
​
        hello.hello(cat); // 냐옹
        hello.hello(dog); // 멍멍
        hello.hello(sheep); 
        hello.hello(lion);
    }
}
class HelloAnimal {
  // 기능을 확장하기 위해서는 클래스 내부 구성을 일일히 수정해야 하는 번거로움이 생긴다.
    void hello(Animal animal) {
        if (animal.type.equals("Cat")) {
            System.out.println("냐옹");
        } else if (animal.type.equals("Dog")) {
            System.out.println("멍멍");
        } else if (animal.type.equals("Sheep")) {
            System.out.println("메에에");
        } else if (animal.type.equals("Lion")) {
            System.out.println("어흥");
        }
        // ...
    }
}

위 예제 코드에서 만약 동물의 타입이 추가된다면, hello 라는 메소드 안에서 if 문을 추가해야 한다. 기존의 hello 메소드를 계속적으로 수정해줘야 될 것이다.

이것이 OCP 에서 말하는 '변경에는 닫혀있다'라는 말에 대해서 위배되는 부분이다. 위 코드는 '변경에는 열려 있는 상태를 말한다.'

어떻게 다형성으로 문제를 해결할 수 있을까?

아래 코드는 충분히 우리가 만날 수 있는 코드다.

public ApiRes get(Request request, UserInfo userInfo) {
​
        if (request instanceof QnaCategoriesRequest) {
            return new ApiRes<>(ApiClient.getQnaCategories((QnaCategoriesRequest) request));
        }
​
        if (request instanceof FaqTopListRequest) {
            return new ApiRes<>(ApiClient.getFaqTopList((FaqTopListRequest) request));
        }
​
        if (request instanceof FaqDetailViewRequest) {
            return new ApiRes<>(ApiClient.getFaqDetailView((FaqDetailViewRequest) request));
        }
​
        if (request instanceof FaqListRequest) {
            return new ApiRes<>(ApiClient.getFaqList((FaqListRequest) request));
        }
​
        if (request instanceof FaqSearchListRequest) {
            return new ApiRes<>(ApiClient.getFaqSearchList((FaqSearchListRequest) request));
        }
​
        throw new NotFoundException("적합한 요청을 찾을 수 없습니다. = " + request);
​
    }

위와 같은 코드를 마주칠 때 우리는 대체로 기존의 관성을 따라가려고 할 것이다. if 문 아래에 또다른 if 문을 추가하려고 할 것이다.

그러나 이미 앞에서 살펴본 인지 과부화와 OCP 원칙의 위반을 이해하고 있다면, 우리는 이 코드를 바꿔야 한다는 사실을 인지할 수 있다.

그럼 어떻게 바꿀 수 있을까?

먼저 위 코드에 대한 도식화를 해보면 다음과 같다.

ApiClient과 Request의 방향성이 Request를 향해 강결합을 가지고 있다. 또한 각각의 Instance casting 하는 것에 문제가 있을 수 있다.

다형성을 통해 이 문제를 해결하기 위해서는 앞서 말한 2가지를 해결해야 한다.

  • 강한 의존성
  • Instance casting

이 두가지 문제를 해결하기 위해서는 다형성을 사용할 수 있다.

결론부터 변경된 도식화를 표현하면 다음과 같다.

2가지가 대표적으로 변경되었다.

  1. Request 는 instance casting이 아닌, 인터페이스 구현체로 인스턴스를 생성한다.
  2. 각 인스턴스의 타입에 따른 처리를 RequestHandler 위임한다. 이전에는 직접 ApiClient 사용
public ApiRes get(Request request, UserInfo userInfo) {
​
    Result result = request.handle(requestHandler);
    return new ApiRes<>(result);
}
public interface RequestHandler {
    FaqDetailDto onHandle(FaqDetailViewRequest faqDetailViewRequest);
    FaqListDto onHandle(FaqListRequest faqDetailViewRequest);
    QnaCategoriesDto onHandle(QnaCategoriesRequest qnaCategoriesRequest);
}
// impl
public class RequestHandlerImpl implements RequestHandler {
    @Override
    public FaqDetailDto onHandle(FaqDetailViewRequest faqDetailViewRequest) {
        return ApiClient.getFaqDetailView(faqDetailViewRequest);
    }
​
    @Override
    public FaqListDto onHandle(FaqListRequest faqListRequest) {
        return ApiClient.getFaqList(faqListRequest);
    }
​
    @Override
    public QnaCategoriesDto onHandle(QnaCategoriesRequest qnaCategoriesRequest) {
        return ApiClient.getQnaCategories(qnaCategoriesRequest);
    }
}

이로써 기존의 문제였던 IF문을 제거하게 되고, 코드를 추가하는 형태로 요구사항을 충족시킬 수 있게 된다.

[참고자료]

댓글