본문 바로가기

IT/책읽는 개발자

[레거시 코드 활용 전략] CH6 고칠 것은 많고 시간은 없고 - 1

반응형

2020/02/03 - [IT/책읽는 개발자] - [레거시 코드 활용 전략] CH1 소프트웨어 변경

2020/02/04 - [IT/책읽는 개발자] - [레거시 코드 활용 전략] CH2 피드백 활용

2020/02/06 - [IT/책읽는 개발자] - [레거시 코드 활용 전략] CH3 감지와 분리

2020/02/07 - [IT/책읽는 개발자] - [레거시 코드 활용 전략] CH4 봉합 모델

CH5 는 단순한 테스트 도구와 관련된 이야기라 다루지 않습니다~

ch 6 - 고칠 것은 많고 시간은 없고 - 1

현실적으로 레거시에서는 잘 하지 않았던 행위들을 공부하고 있을 수 있다.

또한 테스트 코드를 작성 및 코드 변경에 많은 시간이 필요한 작업이다.

그렇기 때문에 정말로 '가치'가 있는지 궁금할 때가 있다.

 

실제로 의존 관계를 제거하고, 테스트 루틴을 작성하는 것은 많은 시간을 쏟아부어야 한다.

그리고 우리는 이런 노력이 '결국' 개발 시간과 시행착오를 줄여준다고 믿는다.

그렇다면 그 '결국' 은 언제일까?

운이 나쁘게도 이것은 항상 우리를 도와주는 것은 아니다. 당연하게도 모든 프로젝트마다 다르다.

 

우선 최상의 경우를 생각해보자

우리는 테스트 코드를 작성하는데 2시간, 코드를 작성하는 데 15분이 걸렸다.

그렇다면 2시간은 낭비한 것인가? 하지만 이러한 작업을 하지 않았다면 이후 코드 변경에 더 많은 시간이 걸리기도 하고, 변경하는 과정에서 실수를 했다면 디버깅에 상당한 시간이 걸렸을 것이다.

테스트 루틴이 준비되어 있었기 때문에 코드 변경에 드는 시간 + 오류를 포착하는 시간을 절약할 수 있었던 것이다.

 

반대로 최악의 경우도 생각해보자.

간단한 작업이지만 변경 지점마다 빠짐없이 테스트를 작성했다고 가정하자.

이때의 테스트 작성은 언제 가치를 발휘할까? 이는 동일 부분 및 연관 부분을 작성하거나 수정할 때 빛을 발휘한다.

역으로 말하면 만약 동일 및 연관 부분에 신규/수정이 발생하지 않았다면 영영 알 수 없다.

 

하지만 대부분의 변경은 시스템의 특정 부분에 집중된다. 오늘 어떤 코드를 변경했다면, 가까운 시일 내에 그 코드의 바로 옆 코드도 변경될 가능성이 높다.

또한 이러한 행위들은 결국 코드 품질 향상을 불러오고 자연스럽게 긍정을 경험하게 될 것이다.

 

불행하게도 우리는 현실이라는 적과 마주치기도 한다.

테스트 코드를 작성할지, 아니면 시간이 지날수록 힘들어지는 현실을 받아들이지 선택해야 한다.

 

당장 변경해야 할 부분이 있다면 해당 클래스를 분석해보자.

완전히 새로운 코드를 작성하여 해당 문제에 대처할 수 있는가? 그렇다면 아래와 같은 기법들을 한번 활용해보자.

다만 지금부터 이야기할 기법들은 신중하게 사용해야 한다.

테스트가 완료된 새로운 코드들을 시스템에 추가할 수 있지만, 이 코드를 호출하는 코드까지 테스트하지 않으면 의도하지 않은 결과가 나올 수도 있기 때문이다.

 

발아 메소드

새로운 기능을 추가해야 하는데 이 기능을 완전히 새로운 코드로 표현할 수 있다면, 새로운 메소드를 구현하여 필요한 위치에서 호출하는 방법을 사용할 수도 있다.

다음 예를 보자.

간단하게 전달받은 숫자를 계속해서 더하는 메소드가 있다.

public class Calculator{
    private int base = 0;
    ...
    public int sum(int num){
        base += num;
        return base;
    }
    ...
}

이때 음수가 들어오면 해당 음수는 절댓값으로 변경한 뒤 더해주는 부분을 추가해야 한다고 하자.

그럼 아래와 같이 작성할 수 있다.

public class Calculator{
    private int base = 0;
    ...
    public int sum(int num){
        if(num < 0) {
            num = -num;
        }

        base += num;
        return base;
    }
    ...
}

이는 정말 단순한 변경이지만 많은 문제가 있다.

우선 기존 코드와 새로 작성된 코드 사이에 구분이 없다.

또한 sum이라는 메소드에 절댓값으로 숫자를 변경하는 관심사까지 더해지면서 코드의 의미가 불분명 해졌다.

 

우리는 새롭게 추가된 항목을 아래와 같이 추출해보자.

public class Calculator{
    private int base = 0;
    ...
    public int abs(int num){
        if(num < 0)
            return -num;
        return num;
    }
    ...
}

이제 해당 메소드는 아주 쉽게 테스트 코드를 작성할 수 있다.

테스트 코드 작성 후, 기존 코드에 새롭게 작성한 메소드를 아래와 같이 추가할 수 있다.

public class Calculator{
    private int base = 0;
    ...
    public int sum(int num){
        base += abs(num);
        return base;
    }
    ...
}

 

지금까지 설명한 것은 발아 메소드(sprout method)이다. 발아 메소드의 작성 순서는 다음과 같다.

  1. 어디에 코드 변경이 필요한지 식별한다.
  2. 메소드 내의 특정 위치에서 한 줄의 명령문 (메소드 호출)으로 구현할 수 있는 변경이라면, 필요한 처리를 수행하는 신규 메소드 호출 코드를 작성 후 주석 처리한다.
  3. 호출되는 메소드내부에서 신규 변경 지점이 되는 변수를 확인하고, 이 변수를 신규 메소드 인수로 전달한다.
  4. 리턴 값이 있다면 적절한 반환 코드 및 호출 영역을 작성한다.
  5. 새롭게 추가된 메소드의 테스트를 작성한다.
  6. 주석 처리했던 신규 메소드 호출 코드 주석을 제거한다.

독립된 한 개의 기능으로서 코드를 추가하거나 기존 메소드의 테스트 루틴이 아직 준비되지 않은 경우에는 발아 메소드의 사용을 생각해 볼 수 있다.

코드를 인라인 형태로 그대로 넣는 것보다 더 바람직한 결과이기 때문이다.

발아 메소드의 장점과 단점

  • 부정적인 면

    1. 원래의 메소드와 그 클래스를 잠시 포기한 것과 같다

      원래의 루틴을 보호하는 것도 아니고, 개선하는 것도 아니기 때문이다. 그저 신규 기능을 추가하는 것뿐이다.

    2. 코드의 이해도를 낮출 수 있다.

      기존 코드 + 발아 메소드 포함 형태이기 때문에 기존 코드의 이해도를 낮출 수 있다.

    3. 기존 코드 테스트 루틴 추가 시 결국 추가적인 작업이 필요하다.

  • 긍정적인 면

    1. 기존 코드와 새로운 코드 구분이 가능해진다.

      새로운 코드는 테스트 코드가 존재한다.

    2. 영향받는 변수들을 인수로 받기 때문에 코드의 정확성도 분석 가능해진다.

    3. 새롭게 작성되는 코드는 테스트 루틴으로 보호가 가능하다.

발아 클래스

만약 의존관계가 복잡하게 얽힌 상황에서는 발아 메소드가 효과적이지는 않다.

어떤 클래스의 객체를 테스트 코드에서 생성할 방법이 없다.

따라서 발아 메소드를 적용해서 테스트 루틴을 작성할 방법도 없다.

이러한 경우 변경에 필요한 기능을 별도의 클래스로 추출한 후, 이 클래스를 기존 클래스에서 이용하는 방법을 사용할 수 있다.

 

아래와 같은 코드가 있다고 가정하자.

아래 코드는 Mail로 전달되는 메시지를 만드는 메소드이다.

public class MessageGenerate {
    ...
    public String generate(){

        String message = "";
        message += "Hello,";

        List<User> users = getTargetUsers();
        for(User user : users){
            message += user.getName() + ",";
        }
        ...
    }
    ...
}

만약 이 코드에 꼬리말을 추가해야 한다고 가정하자.

하지만 위 클래스는 너무나 거대해 테스트 코드를 작성하는 데만 하루가 넘게 걸린다고 가정하자. 그리고 작성할 만한 시간도 없다고 가정하자.

 

이럴 경우 FooterMessageProducer이라는 클래스를 작성해보자.

public class FooterMessageProducer{
    public String makeFooter(){
        return "bye~";
    }
}

그리고 MessageGenerate 내에서 이 클래스를 인스턴스화해서 직접 호출해보자.

public class MessageGenerate {
    ...
    public String generate(){

        String message = "";
        message += "Hello,";

        List<User> users = getTargetUsers();
        for(User user : users){
            message += user.getName() + ",";
        }
        FooterMessageProducer footerMessageProducer = new FooterMessageProducer();
        message += footerMessageProducer.makeFooter();
    }
    ...
}

그렇다면 고작 이 정도 기능을 만들기 위해 해당 클래스를 작성한 걸까?

우리는 더 유연하게 생각해 볼 수 있다. 좀 더 자세히 살펴보자.

FooterMessageProducer 클래스를 FooterMessageGenerate라고 명하고 generate()라는 인터페이스를 구현하게 하면 어떻게 될까?

 

이제 아래와 같이 수정해보자

public interface TextGenerate {
    String generate();
}

public class MessageGenerate implements TextGenerate {
    ...
    public String generate(){
        ...
    }
    ...
}

public class FooterMessageGenerate implements TextGenerate {
    ...
    public String generate(){
        ...
    }
    ...
}

이렇게 된다면 공통의 코드로 더욱 활용도를 높일 수 있을 것이다.

 

발아 클래스를 작성하는 이유는 본질적으로 두 가지다.

  1. 어떤 클래스에 완전히 새로운 역할을 추가하고 싶을 경우다.

    특정 로직에서 A 클래스에 날짜 계산 기능을 추가하려고 한다. 하지만 해당 기능은 A 클래스의 역할이나 관심사와는 너무나 동떨어진 기능이다.

    어쩌면 해당 기능은 새로운 클래스에게 위임하는 것이 더 바람직한 설계 구조라고 할 수 있다.

  2. 기존 클래스에 약간의 기능을 추가하고 싶지만 테스트 코드 내에서 테스트할 수 없는 경우이다.

    만약 클래스를 테스트할 수 있다면 발아 메소드를 사용하는 것도 좋은 방법이다.

다음은 발아 클래스를 작성하는 순서다.

  1. 어느 부분의 코드를 변경해야 하는지 식별한다.
  2. 메소드 내의 특정 위치에서 한 줄의 명령문으로 구현할 수 있는 변경이라면, 변경을 구현할 클래스에 적합한 이름을 생각한다
  3. 이어서 해당 위치에 그 클래스의 객체를 생성하는 코드를 삽입하고, 클래스 내의 메소드를 호출하는 코드를 작성한다. 그리고 그 코드를 주석 처리한다.
  4. 새로운 클래스와 해당 메소드의 테스트 코드를 작성한다.
  5. 앞서 주석 처리했던 주석을 제거하고, 객체 생성과 메소드 호출을 활성화한다.

발아 클래스의 장점과 단점

발아 클래스의 단점은 파악하기가 조금 힘들다는 것이다.

보통 코드 베이스에서 분석을 진행할 때 관련된 클래스들을 기준으로 분석한다.

하지만 발아 클래스 같은 경우에 동작 자체의 의미보다는 기존 동작을 분할해서 가지고 있기 때문에

발아 클래스의 인터페이스와 다른 클래스 내의 처리 부분을 보고 분석하기에 난해한 경우도 생긴다.

이런 경우 원래 하나의 클래스에서 변경을 수행했어야 할 것을 안전한 변경 작업을 위해 여러 개의 클래스로 쪼갠 것으로 봐야 하기 때문이다.

장점은 발아 메소드와 비슷하다.

반응형