본문 바로가기

JAVA

유연한 솔루션보다 단순한 솔루션을 선택하자

들어가며

이 글은 thinkingsideways.net 에 올라온 글을 번역한 글입니다. 원 저작자의 번역 동의를 받지는 않았으며, 문제시 삭제 될 수 있습니다. 또한, 이 글의 내용에 대해 옳고 그름을 판단하지 않습니다.

오역이 충분히 발생할 수 있으니, 될수 있으면 원문을 보시기 바랍니다. 혹시, 오역된 부분을 알려 주시면 수정하도록 하겠습니다. 미리 감사합니다.

본문

내 경험상, 소프트웨어 개발은 단순성의 가치에 보다는 유연성에 중점을 두고 있습니다.

많은 개발자가 앞으로 발생할 수 도 있는 문제를 처리할 수 있도록 코드를 작성하려고합니다. 그들은 문제에 대한 해결책을 찾기 위해 점을 치는 것입니다. 예측이 옳다면 잘 해결 될 수 있습니다. 그러나 대부분의 경우 이러한 유연성은 코드에 불필요한 복잡성만 초래하고 실제로 발생한 문제를 해결하지는 못합니다.

사업의 미래가 복잡하다는 것은 놀라운 일이 아닙니다. 그럼에도 불구하고, 유연성에 중점을 두고 개발해야 한다는 생각은 대학 및 많은 기업 환경에서 권장되고 있습니다만, 제 의견으로는 매우 위험합니다.

예를 들어, 몇 년 전, 동료에게 내가 방금 작성한 if-else-statement를 switch-statement로 변경하라는 요청을 받았습니다. 나는 코드에 처리할 두 가지 다른 사례만 있었기에 switch-statement를 사용하는 것은 지나치다고 생각하였기에 혼란 스러웠습니다.(나는 스위치 케이스를 싫어합니다.) 그래서 왜 그것을 바꿔야하는지 물었고 장래에 switch-statement에 더 많은 경우를 추가 할 것으로 예상되기 때문에 코드가 그것을 준비하기를 원한다고 답변을 받았습니다. 그는 또한 지금은 if-statement를 유지하고 필요하게 되었을때 switch-statement로 변경하자는 나의 제안을 기각했습니다. 그는 누군가 if-statement 를 변경해야 할 필요가 있고, 변경할 것이라고 확신 할 수 없다고 말했습니다. 결국, 나는 포기하고 코드를 수정했습니다. 스위치 케이스문은 현재까지 추가 되지 않았습니다.

내 동료는 유연성이 공짜로 제공되지 않는다는 사실을 깨닫지 못했습니다. - 코드 복잡성이 증가하면서(무의미한 switch-statement로) 비용을 지불해야 합니다.

유연성은 일반적으로 코드에 더 많은 추상화를 의미합니다. 추상적인 표현이 많을수록 코드 기반에서 어떤 일이(그리고 언제) 일어나는지 알아내기가 더 어려워집니다. 만약 당신이 그것을 과도하게 한다면, 당신은 특정한 코드를 찾기 위해 많은 시간을 보낼 것입니다. 예를 들어 UI의 숫자 값이 잘못된 이유를 궁금해 할 수 있습니다. 백엔드에서 제공하는 데이터를 프론트엔드에서 읽는 메소드를 빠르게 찾을 수는 있습니다. 그러나 실제로 이 값을 계산하는 코드를 찾는 것은 훨씬 어려울 수 있습니다. 나는 그런 로직을 찾기위해 10개의 클래스를 탐색해야 했고, 그것은 즐거운 경험이 아니었습니다.

이런 과한 복잡성은 너무 많은 객체(깊은 상속 계층 구조, 많은 오버라이드된 메소드들)를 사용하면 빠르게 늘어납니다. 또한, visitor 패턴과 같은 일부 디자인 패턴은 매우 강력하지만, 코드를 식별하고 이해하기가 어렵습니다.

될 수 있으면, 유연성을 추구하기 보다는 가장 간단한 솔루션을 사용해야 합니다. 코드로 예를 들어 보겠습니다. 이 예제는 엔터프라이즈 클라우드 소프트웨어에서 가져온 실제 코드를 기반으로 합니다. 무고한 사람들을 보호하고 이해를 쉽게 하기 위해 코드를 변형했습니다. 이 코드는 Java7 기반입니다. CitrusFruit 인터페이스와 잘 알려진 구현체인 Lemon 과 Lime 이 있습니다. 인터페이스와 두가지 구현체를 살펴 보겠습니다.

  List<CitrusFruit> citrusFruits = getFruits();

  fruitStore.stockFruit(Lemon.class, new ArrayList<>(

          (Collection<? extends CitrusFruit>) Collections2

              .filter(citrusFruits, new CitrusFruitClassPredicate(Lemon.class))));

  fruitStore.stockFruit(Lime.class, new ArrayList<>(

          (Collection<? extends CitrusFruit>) Collections2

              .filter(citrusFruits, new CitrusFruitClassPredicate(Lime.class))));

이 코드는 구글의 Guava API 를 사용하여 functional 프로그래밍 스타일을 java 7 형태로 작성되었습니다. Collections2 는 predicate 를 사용하여 목록을 필터링합니다. predicate 코드는 아래와 같습니다.

public class CitrusFruitClassPredicate implements Predicate<CitrusFruit> {

  private Class<?> clazz;

  public <T extends CitrusFruit> CitrusFruitClassPredicate(Class<T> clazz) {

    this.clazz = clazz;

  }

  @Override

  public boolean apply(CitrusFruit citrusFruit) {

    if (clazz == null || citrusFruit == null) {

      return false;

    }

    return clazz.isInstance(citrusFruit);

  }

}

FruitStore 는 다음과 같은 메소드를 선언하고 있으며, 구체적인 구현만 허용됩니다.

  public <T> void stockFruit(Class<T> type, List<T> fruits)

그래서, 여기서 무슨 일이 벌어지고 있을까요? CitrusFruits 목록을 가지고 있는데, CirtusFruit 인터페이스를 구현하기 때문에 Lime 과 Lemon 이 조합된 목록입니다. 하지만, 우리는 매장에서 구체적인 구현이 필요하기 때문에 Lime을 Lemon과 분리해야 합니다. predicate / filter 조합은 이 문제를 해결하고, 매우 유현하게 CitrusFruit 의 구현체를 통과할 수 있습니다. 그러나, 이것은 javac 를 통해 컴파일될 때 발생하는 컴파일 오류(또는 warning들)를 이해하기 어렵게 합니다.

이 문제를 해결하는 간단한 방법이 있습니다.

  List<CitrusFruit> citrusFruits = getFruits();

  List<Lime> limes = new ArrayList<>();

  List<Lemon> lemons = new ArrayList<>();

  for (CitrusFruit citrusFruit : citrusFruits) {

    if (citrusFruit instanceof Lime) {

      limes.add((Lime) citrusFruit);

    }

    if (citrusFruit instanceof Lemon) {

      lemons.add((Lemon) citrusFruit);

    }

  }

  fruitStore.stockFruit(Limes.class, limes);

  fruitStore.stockFruit(Lemon.class, lemons);

이 코드는 오류없이 컴파일 되며, 짧고 이해하기 쉽습니다. 이 코드는 유연성은 떨어집니다. 루프는 기본적으로 CirtusFruit의 새로운 구현을 처리 할 수 없습니다. 만약 목록에서 지원하는 유형이 많아진다면 다른 방법을 사용해야 할 수도 있습니다. 하지만, 지금은 이 해결책이 이전 해결책 보다 훨씬 좋습니다. 왜 더 유연한 해결책이 선택되었는지 모르겠지만, 코드를 작성한 사람에게는 아마 좋은 이유가 있었을 것입니다. 그럼에도 나는 지난 1.5년 동안 CitrusFruit 의 하위 클래스가 도입되지 않았다는 것을 알고 있습니다. 따라서, 내 해결책이 가까운 미래에 유연성이 낮아 문제가 되지는 않을 것이라고 생각합니다.

결론

유연성은 더 복잡하다는 것을 의미하기 때문에 유연성과 항상 싸워야 합니다. 좋은 디자인은 불완전한 디자인이며, 필요한 경우 좋은 코드 기반은 쉽게 리팩토링 할 수 있다는 것을 명심하십시오. 그리고, 미래에 대해 말하고 싶다면, 주식이나 비트코인의 가격에 대해 말해보세요.