14장. 점진적인 개선

공부하는 감자·2024년 2월 17일
0

클린코드

목록 보기
14/18

이 글에서 분류한 기준은 책의 내용을 바탕으로 주관적인 견해로 재정리해본 것입니다.

이 장의 주제

모듈을 개선하고 정리하는 내용이어서, 모든 내용을 정리하지 않고 필요한 부분만 체크하여 정리할 것이다.

  • 점진적인 개선을 보여주는 사례 연구이다.
  • 출발은 좋았으나 확장성이 부족했던 모듈을 제시하고, 해당 모듈을 개선하고 정리하는 단계를 정리했다.

개선

  • 깨끗한 코드를 짜려면 먼저 지저분한 코드를 짠 뒤에 정리해야 한다.
  • 처음부터 깨끗하고 우아한 코드는 없으므로, 단계적으로 개선해 나가야 한다.

모듈 소개

  • 출발은 좋았으나 확장성이 부족했던 모듈을 소개한다.

Args 유틸리티

  • main 함수로 넘어오는 명령행 인수의 구문을 분석하는 모듈이다.
  • Args 생성자에 입력으로 들어온 인수 문자열과 형식 문자열을 넘겨 Args 인스턴스를 생성한 후, Args 인스턴스에 인수 값을 질의한다.

사용 예

  • Args 생성자로 넘기는 매개변수
    • 첫 번째 매개변수: 형식 또는 스키마를 지정
    • 두 번째 매개변수: main으로 넘어온 명령행 인수 배열
  • 형식 문자열
    • -l : 부울 인수
    • -p : 정수 인수
    • -d : 문자열 인수
public static void main(String[] args) {
        try {
            // (1) Args 생성자에 인수 문자열과 형식 문자열을 넘겨 Args 인스턴스를 생성한다.
            Args arg = new Args("l,p#,d*", args);

            // (2) Args 인스턴스에 인수 값을 질의한다.
            boolean logging = arg.getBoolean('l');
            int port = arg.getInt('p');
            String directory = arg.getString('d');

            // (3) 애플리케이션 실행 코드 (중요하지 않다.)
            executeApplication(logging, port, directory);
            
        } catch (ArgsException e) {
            // 만약 (1)에서 형식 문자열이나 명령행 인수 자체에 문제가 있다면
						// ArgsException이 발생한다. 
            System.out.println("Argument error: %s\n", e.errorMessage());
        }
    }

사전에 보면 좋은 코드

리팩터링을 하면서 추가되는 코드이다.

ArgumentMarshaler 인터페이스

  • 명령행 인자를 저장(set)하는 인터페이스다.
  • 이 인터페이스의 파생 클래스는 public static XXX getValue(ArgumentMarshaler am) 메소드가 추가되어 있다.
public interface ArgumentMarshaler {
    void set(Iterator<String> currentArgument) throws ArgsException;
}

파생 클래스 예

  • boolean 인수를 다루는 클래스
public class BooleanArgumentMarshaler implements ArgumentMarshaler {
    private boolean booleanValue = false;

    // set 함수 구현
    @Override
    public void set(Iterator<String> currentArgument) throws ArgsException {
        booleanValue = true;
    }

    // boolean 인수 반환
    public static boolean getValue(ArgumentMarshaler am) {
        if (am != null && am instanceof BooleanArgumentMarshaler)
            return ((BooleanArgumentMarshaler) am).booleanValue;
        else
            return false;
    }
}
  • String 인수를 다루는 클래스
public class StringArgumentMarshaler implements ArgumentMarshaler {
    private String stringValue = "";

    // set 함수 구현
    @Override
    public void set(Iterator<String> currentArgument) throws ArgsException {
        try {
            stringValue = currentArgument.next();
        } catch (NoSuchElementException e) {
            throw new ArgsException(MISSING_STRING);
        }
    }

    // String 인수 반환
    public static String getValue(ArgumentMarshaler am) {
        if (am != null && am instanceof StringArgumentMarshaler)
            return ((StringArgumentMarshaler) am).stringValue;
        else
            return "";
    }
}

개선 후 모듈의 모습

본격적으로 개선하기 전에, 최종적인 모듈이 어떻게 동작하는 지를 알고 가자.

  • 날짜 인수나 복소수 인수 등 새로운 인수 유형을 추가하는 방법이 명백하고, 고칠 코드도 별로 없다.
  • 새로운 인수 유형을 추가한다면 새 클래스를 파생해 getXXX 함수를 추가한 후, Args 유틸리티에서 해당 클래스를 사용하는 코드를 추가하면 된다.

참고: 자바는 정적 타입 언어

  • 단순히 오류 코드 상수를 정의하는 코드가 길어서, 거기에 대한 부연 설명이다.
  • 자바는 정적 타입 언어여서 타입 시스템을 만족하려면 많은 단어가 필요하다.
    • 루비, 파이썬, 스몰토크 등과 같은 언어를 사용했다면 프로그램은 훨씬 작아질 것이다.

점진적인 개선

Args: 1차 초안

코드

코드를 분석한 뒤, 필요하다 싶은 곳만 남기고 주석을 달아봤다.

  • 명령행 인자를 받아서 처리하는 모든 코드가 Args 클래스 하나로 구현되어 있다.
  • 오류가 발생하면 ParseException 를 던진다.
public class Args {
    // 길다란 변수 목록

    private enum ErrorCode {
        // 오류 코드
    }

    public Args(String schema, String[] args) throws ParseException {
        this.schema = schema;
        this.args = args;
        valid = parse();
    }

    private boolean parse() throws ParseException {
        if (schema.length == 0 && args.length == 0)
            return true;

        parseSchema();

        try {
            parseArguments();
        } catch (ArgsException e) {

        }

        return valid;
    }

    private boolean parseSchema() throws ParseException {
        for (String element : schema.split(",")) {

            if (element.length() > 0) {
                String trimmedElement = element.trim();
                parseSchemaElement(trimmedElement);
            }
        }
        return true;
    }

    private void parseSchemaElement(String element) throws ParseException {
        // 스키마(형식 문자열)의 타입 별로 구분한 뒤,
        // parsXXXSchemaElement(char elementId) 메소드를 호출한다.
    }

    // parsXXXSchemaElement(char elementId) : boolean 타입
    private void parseBooleanSchemaElement(char elementId) {
        booleanArgs.put(elementId, false);
    }

    // parsXXXSchemaElement(char elementId) : int 타입
    private void parseIntegerSchemaElement(char elementId) {
        intArgs.put(elementId, false);
    }

    // ...
    // 명령행 인자를 배열에 저장하는 set함수와 가져오는 get함수
    // 에러 메시지를 출력하는 함수 등
}

문제점

  • ‘돌아가지만’ 엉망인 코드이다.
  • 인수 유형을 추가하면 코드가 더욱 엉망이 된다.

리팩터링의 필요

  • 추가할 인수 유형이 더 있었는데, 그러면 코드가 훨씬 더 나빠지리라는 사실이 자명하다.
  • 억지로 추가해서 프로그램은 어떻게든 완성해도 너무 커서 손대기 어려운 골칫거리가 될 것이다.
  • 따라서 코드 구조를 유지보수하기 좋은 상태로 만들기 위해 리팩터링을 시작한다.

리팩터링의 시작

새 인수 유형을 추가하려면 주요 지점 세 곳에 코드를 추가해야 했다.

  1. 인수 유형에 해당하는 HashMap을 선택하기 위해 스키마 요소의 구문을 분석
  2. 명령행 인수에서 인수 유형을 분석해 진짜 유형으로 변환
  3. getXXX 메서드를 구현해 호출자에게 진짜 유형을 반환
  • 인수 유형은 다양하지만 모두가 유사한 메서드를 제공하므로 클래스 하나가 적합하다. → ArgumentMarshaler 개념 탄생

Args: 점진적으로 개선

테스트 주도 개발 기법 사용

  • 프로그램을 망치는 가장 좋은 방법 중 하나는 개선이라는 이름 아래 구조를 크게 뒤집는 행위다.
    • ‘개선’ 전과 똑같이 프로그램을 돌리기가 아주 어렵기 때문이다.
  • 따라서, 테스트 주도 개발(Test-Driven Development, TDD) 기법을 사용했다.
    • TDD는 언제 어느 때라도 시스템을 돌아가야 한다는 원칙을 따른다.
    • TDD는 시스템을 망가뜨리는 변경을 허용하지 않으므로, 변경을 가한 후에도 시스템이 변경 전과 똑같이 돌아가야 한다.
  • 변경 전후에 시스템이 똑같이 돌아간다는 사실을 확인하려면 언제든 실행이 가능한 자동화된 테스트 슈트가 필요하다.
    • 서적에서는 이미 단위 테스트 슈트와 인수 테스트를 만들어 놓았다.
    • JUnit 프레임워크에서 자바로 작성한 단위 테스트 슈트
    • FitNess에서 위키 페이지로 작성한 인수 테스트

기존 코드 끝에 ArgumentMarshaler 클래스의 골격을 추가

  • 코드를 변경할 때마다 시스템 구조는 조금씩 ArumentMarshaler 개념에 가까워졌다.
  • 변경 후에도 시스템은 변경 전과 다름없이 돌아갔다.
    • 한 번에 하나씩 고치면서 테스트를 돌린다.
    • 테스트 케이스가 하나라도 실패하면 다음 변경으로 넘어가기 전에 오류를 수정한다.
  • 각 인수 유형을 처리하는 코드를 모두 ArgumentMarshaler 클래스에 넣고 나서, 추후 파생 클래스를 만들어 코드를 분리할 예정으로 작업한다.
    • 그러면 프로그램 구조를 조금씩 변경하는 동안에도 시스템의 정상 동작을 유지하기 쉬워진다.

ArgumentMarshaler 의 파생 클래스 생성

  • 모든 논리를 ArgumentMarshaler 로 옮긴 후, 파생 클래스를 만들어 기능을 분산한다.
    • 이때, ArgumentMarshaler 클래스를 abstract class 로 변경해서 파생 클래스를 만들었다.
  • 코드는 테스트를 계속 통과해야 한다.
    • 한 번에 하나씩 고치면서 테스트를 돌린다.
  • 여기서, 테스트 케이스에 문제가 있었어서 수정 작업이 있었다.
    • 인수 테스트 케이스를 FitNess에서 구현하고, 단위 테스트 집합은 FitNess에서 구현하지 않았었다.
    • 지금까지는 단위 테스트만 돌렸었다.
    • 그런데, FitNess 테스트에서만 발생하는 문제를 발견해서 모든 FitNess 테스트를 호출하는 새 단위 테스트를 추가했다.

중간 점검

  • (실망스럽게도) 구조만 조금 나아졌다.
    • 첫머리에 나오는 변수(목록)가 그대로 남아있다.
    • 유형을 일일이 확인하는 set함수가 그대로 남아있다.
    • 오류 처리 코드도 Args 클래스에 남아있다.

코드 다듬기

  • 흉한 코드를 파생 클래스로 내려서 제거
    • 유형을 일일이 확인하여 set함수를 호출하는 setArgument 메서드에서
      • set 함수를 파생 클래스로 내려서 감추고
      • setArgument 는 파생 클래스의 메서드를 호출하게 수정 (책임 전가)
    • setArgument 메서드에서 인수 유형을 일일이 확인하던 코드를 제거
  • 몇 가지 허술한 코드를 고쳐 정리한다.
  • ArgumentMarshaler 를 인터페이스로 변경한다.

테스트하기

  • 시스템이 새로운 인수 유형(double)를 제대로 받아들이는지 확인할 테스트 케이스부터 추가한다.
  • 새로운 인수 유형을 추가한 뒤 테스트해본다.
    • 스키마 구문분석 코드를 정리 후 감지 코드(double은 ## 사용)를 추가
    • DoubleArgumentMarshaler 클래스를 추가
    • 새로운 ErrCode 추가
    • getDouble 함수 추가
  • 정상 작동 외의 동작도 테스트한다.
    • 오류 처리 코드가 제대로 도는지 확인한다.
    • double 인수를 빠트린 경우도 확인한다.

예외/오류 처리 코드 분리

  • 예외 코드는 아주 흉할 뿐더러 사실상 Args 클래스에 속하지도 않는다.
    • 코드에서 던지는 ParseException 는 Args 클래스에 속하지 않는다.
  • 그러므로 모든 예외를 하나로 모아 ArgsException 클래스를 만든 후 독자 모듈로 옮긴다.
    • 독자적인 모듈로 만들면 Args 모듈에서 잡다한 오류 지원 코드를 옮겨올 수 있다.
    • Args 모듈은 깨끗해져 차후 확장도 쉬워진다.

SRP와 절충안

  • Args 클래스가 오류 메시지 형식까지 책임지고 있었으므로, 명백히 SRP 위반이었다.
    • Args 클래스는 인수는 처리하는 클래스지 오류 메시지 형식을 철하는 클래스가 아니기 때문이다.
  • ArgsException 클래스가 오류 메시지 형식을 처리하도록 한 것은 절충안이다.
    • ArgsException 에게 맡겨서는 안 된다고 생각하는 독자라면 새로운 클래스가 필요하다.
    • 하지만 미리 깔끔하게 만들어진 오류 메시지로 얻는 장점은 무시하기 어렵다.

정리

  • Args 클래스에서는 주로 코드만 삭제했다.
  • 소프트웨어 설계는 분할만 잘해도 품질이 크게 높아진다.
    • 적절한 장소를 만들어 코리만 분리해도 설계가 좋아진다.
    • 관심사를 분리하면 코드를 이해하고 보수하기 훨씬 더 쉬워진다.

결론

코드는 언제나 최대한 깔끔하고 단순하게 정리하자

  • 나쁜 코드보다 더 오랫동안 더 심각하게 개발 프로젝트에 악영향을 미치는 요인도 없다.
  • 나쁜 코드를 깨끗한 코드로 개선하려면 비용이 엄청나게 많이 든다.
    • 코드가 썩어가며 모듈을 서로 뒤엉키고, 숨겨진 의존성이 수도 없이 생긴다.
    • 오래된 의존성을 찾아내 깨려면 상당한 시간과 인내심이 필요하다.
  • 반면 처음부터 코드를 깨끗하게 유지하기란 상대적으로 쉽다.

Reference

참고 서적

📔 Clean Code

profile
책을 읽거나 강의를 들으며 공부한 내용을 정리합니다. 가끔 개발하는데 있었던 이슈도 올립니다.

0개의 댓글