if문은 특정 조건에 따라 프로그램을 제어하기 위해 사용한다. 그렇기 때문에 요구사항(조건)이 변경되면 영향도가 가장 많이 발생하는 코드 중 하나이다. 일반적으로 우리는 서비스를 분기할때 다형성을 통해 클래스를 디자인하여 (ex. strategy, policy pattern 등) if문을 없애곤 한다. 하지만 이렇게 나뉘어진 서비스들도 각 함수내에 목적 코드를 실행하기 전 제어를 위한 중복된 if문들이 생기게 된다. 경험상 모두가 알다시피 이러한 중복된 if 조건들은 변경사항이 발생하면 프로젝트내에 모두 찾기하여 일일이 수정해야 하기 때문에 분명 좋은 코드가 아니다.

 

  아래 몇가지 패턴은 내가 자주 쓰는 패턴 중 하나다. if 조건들을 오브젝트화 하여 핵심관심(Core Concerns)과 횡단관심(Crosscutting Concerns)을 분리(Computer Science, SoC)하고 중복코드를 없애 재사용이 가능한 상태로 만드는 디자인이다. (물론, AOP를 적용하면 좀 더 우아하게 처리 할 수도 있다.) 

Interceptor Filter Pattern

  코드에서 대부분의 if문들은 아래와 같은 형태를 띄고 있다. [그림 1]은 함수내에서 원하는 목적 코드를 수행하기 전 인증, 권한, 데이터 유효성 확인 등과 같은 전처리를 수행하는 코드를 예로 든다. 보통 이러한 전처리 코드들은 경험상 중복되는 경우가 많으며 변경 가능성이 높은 코드들이다. 

[그림 1] if 조건에 따른 필터 처리

  [그림 1]에서 글쓰기 권한을 '준회원'에서 '일반회원'으로 변경되면 우리는 해당 if문들을 찾아 일일이 수정해야한다. 예시가 간단하기 때문에 쉽게 수정할 수 있다고 생각하겠지만, if문이 많은 그리고 복잡도가 높은 실제 프로젝트내에서 중복 코드의 변경은 개발자의 실수가 충분히 발생할 수 있다. 그리고 이러한 횡단관심 코드들은 해당 함수의 핵심관심을 읽기 어렵게 만든다. 즉, 한 마디로 코드가 더러워 진다. 

 

  위와 같은 경우에는 Interceptor Fileter Pattern을 사용하여 조건들을 오브젝트화 하여  중복코드를 없애면 코드를 좀 더 깔끔하게 만들 수 있다.

[그림 2] Interceptor filter class diagram

  Interfceptor Filter pattern은 조건들을 오브젝트화 하여 중복 코드를 없애고 여러 조건들을 조합하여 재사용할 수 있도록 한다. 또한, 조건 코드와 목적 코드의 디팬던시가 제거됨으로 변경에 따른 영향도를 최소화 할 수 있다.

[그림 3] Interceptor filter sequence diagram

  시퀀스는 간단하다. Client는 FilterChain에 target과 filter들을 추가하고 FilterChain.doFilter()를 호출한다. FilterChain은 첫 번째 Filter를 호출하고 호출된 해당 Filter는 주어진 조건을 확인하고 판단하고 다시 FilterChain..doFilter를 호출하면 다음 Filter를 호출하게 되고 FilterChain.stopFilter를 호출하면 중단되며 target은 실행되지 않는다. 모든 filter들이 실행되어 통과되면 target은 그 때 실행될 수 있다.  각 요구사항에 따라 디테일한 동작은  변형하여 디자인 하면 된다. 수도코드를 보면 아래와 같다.

 

1. FilterChain

2. Filter

3. Target

4. Client에서 사용할 때

 

Chain of Responcibility Pattern

  [그림 1]처럼 하나의 목적코드를 실행하기 위해 전처리 조건들을 사용할때도 있지만, [그림 4] 다음과 같이 조건에 따른 여러 분기를 하여 그에 알맞은 목적 코드를 실행 할 때도 있다. 이럴때는 Chain of Responsibility Pattern를 사용하여 조건들을 오브젝트화할 수 있다. 

[그림 4] if 조건에 따라 목적코드를 분기하는 경우

  [그림 4]는 특정 조건에 따라 목적 코드를 분기하는 경우다. Chain of Responsibility Pattern의 컨셉은 자신의 조건이 맞으면 수행하고 그렇지 않으면 패스한다. else if로 만들수도 있고 if, if, 조건으로 만드는 등 각 요구사항에 맞게  Varient하여 디자인 하면 된다. [그림 5] 아래는 log level에 따라 각 Logger들이 로깅을 할지 말지를 판단하는 체인을 구현한 예시다. 

[그림 5] Logger class diagram

 

[그림 6] Logger sequence diagram

1. CommonLogger

2. concrete Logger 

3. Client 사용할 때

5. 호출 결과

consoleLogger.logMessage(logLevel: .info, message: "info message!!")

 -> console logger info message!!

 

consoleLogger.logMessage(logLevel: .error, message: "error message!!")

 -> console logger error message!! 

      file logger error message!! 

      error logger error message!!

 

  Application에서 Persistent Data를 다루는 방법엔 여러가지가 있다. RDBMS를 사용하거나 단순히 txt로 저장할 수도 있고 네트워크를 통해 외부 시스템을 사용할 수도 있다. 그리고 각 저장 방식에는 메커니즘이 다르기 때문에 구현방법도 모두 다르다. 그렇기 때문에 Persistent Logic은 외부 환경과 Dependency가 매우 강하다. 따라서 Application Logic과 Persistent Logic은 섞이면 안되며 서로 독립된 레이어에 존재해야 한다. 만약 그렇지 않고 로직이 섞인 상태에서 Data Source가 바뀌게 된다면 Application Logic까지 영향도(Force)가 전달되어 변경 전파(Change Propagation)가 발생하게 된다. 

 

[그림 1] DAO Class Diagram

  [그림 1] Data Access Object Pattern은 특정 Data Source에 접근하는 로직을 추상화하고 캡슐화 한다. DAO는 DataSource의 메커니즘을 구현하고 단순한 API만을 Client에게 제공한다. (이때, DataSource에서 사용하는 ErrorCode나 Exception같은 것도 역시 상위 레이어로 전달하면 안되며, 모두 wrapping하여 상위로 전달 해야한다.) 이제 Client에서는 DAO를 생성하고 저장해야할 Value Object를 생성하여 DAO에 전달하기만 하면 된다. 이렇게 Layer를 분리하면 DataSource가 Oracle에서 외부 네트워크 시스템으로 변경되어도 Client의 수정없이 DAO만 변경하면 된다.

 

[그림 2] DAO Sequence Diagram

  [그림 2]는 Client가 DAO를 통해 데이터를 읽을때의 Sequence Diagram을 나타낸다. DAO는 Data Source를 통해 데이터를 읽어와서 Value Object로 변환하여 반환하고 Client를 Value Object를 통해 실제 데이터를 사용한다.

 

  수도 코드로 예를 들면, 회원 정보를 저장하는 DAO는 아래와 같이 구현될 수 있다. 

  만약 Data Source와 관련된 로직을 추상화 할 수 있다면, 아래와 같이 Generic으로 추상화하여 CommonDAO를 만들 수 있다. 그렇게 되면 앞으로 생성되는 DAO class들은 정의만 하면 되고 중복 구현은 추상화 되어 없앨 수 있다. 그리고 특정 서비스에서만 사용되는 로직들만 확장하여 사용하면 된다.

 

 

개발을 하다보면 boolean 타입의 파라미터를 넘기는 코드들을 자주 접할 수 있다. 간혹 이러한 코드들은 가독성을 떨어뜨리는데, 괄호 안에 존재하는 boolean의 의미를 알기 위해서는 선언부까지 확인해야 되기 때문이다.


read(key, true);


심한 경우에는 아래와 같이 boolean 타입이 여러개가 오는 코드도 본적이 있다. 이런 경우에는 코드의 가독성도 문제가 되지만, 개발자의 실수로 파라미터의 순서가 바뀌게 된다면 원인을 찾기 힘든 버그를 만들어 낼 것이다. 


read(key, true, false);


아래처럼 enum과 같은 타입을 정의하여 사용하면 가독성은 물론이고, 만약 개발자의 실수로 파라미터의 순서가 바뀌기라도 한다면 컴파일러는 이 잘못된 부분을 정확히 짚어낼 것이다. 


enum class CACHE { YES, NO };
enum class SORT { ASC, DESC };

read(key, CACHE::YES, SORT::DESC);


'CleanCode' 카테고리의 다른 글

Error handling - 우아하게 실패하는 방법  (0) 2019.07.20
javascript AOP  (0) 2016.10.18

+ Recent posts