본문 바로가기

IT/책읽는 개발자

[레거시 코드 활용 전략] CH4 봉합 모델

반응형

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

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

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

ch 4 - 봉합 모델

봉합

단위 테스트를 위해 개별 클래스를 추출하려고 하면 수많은 의존 관계를 제거할 필요가 있다.

'좋은' 설계를 기반하고 있다 하더라도 많은 작업이 수반된다.

 

많은 작업 중 하나인 '봉합'이라는 개념을 알아보자.

우선 봉합은 코드를 직접 편집하지 않고도 프로그램의 동작을 변경할 수 있는 위치를 말한다.

 

아래 예제를 보자

public class MailSender {

    public void sendMail(String type){
                ...
        if("2차".equals(type)){
            ErrorHandler.sendError("메일 발송 실패!");
        }
                ...
    }
}

public class ErrorHandler {

    public static void sendError(String errorMessage){
            ...
    }
}

MailSender에서 메일을 전송할 때 type을 검사한다.

그리고 만약 타입이 사용자가 지정한 타입과 일치하면 메일 발송 실패가 발생한다.

 

우리는 여기서 ErrorHandler의 sendError를 포함하는 테스트를 제거해야 한다.

그 이유는 테스트의 관심 대상이 아니고, 테스트하기가 너무 어려운 환경이기 때문이라고 가정하자.

또한 실제 운영환경에서는 호출 가능해야 한다.

 

앞에서 말한 것과 같이 여기서 '봉합'이라는 개념을 사용할 수 있다.

위 코드에서 봉합 지점이 있을까? 한번 자세히 살펴보자

ErrorHandler.sendError()는 static 메소드로써 MailSender의 메소드가 아니다.

 

그렇다면 MailSender에 sendError()라는 메소드를 추가하면 어떻게 될까?

public class MailSender {

    public void sendMail(String type){
                ...
        if("2차".equals(type)){
            sendError("메일 발송 실패!");
        }
                ...
    }

        public void sendError(String errorMessage){
            ErrorHandler.sendError(errorMessage);
        }
}

이와 같이 단순히 호출 단계를 하나 더 추가했을 뿐이다.

다음으로 MailSender의 서브 클래스를 작성해 sendError 메소드를 오버라이딩하면 어떻게 될지 살펴보자

public class TestMailSender extends MailSender{

        @Override
        public void sendError(String errorMessage){
            return ;
        }
}

이렇게 변경 후 MailSender를 생성하는 부분에서 TestMailSender를 호출한다면 메일 발송 시 sendError 동작을 실질적으로 무효화시킬 수가 있다.

이 경우 호출하는 코드는 거의 변경하지 않고 호출되는 메소드만 변경할 수 있다.

 

그렇다면 왜 봉합을 사용해야 할까?

레거시 코드를 테스트할 때 가장 큰 문제점은 의존 관계를 제거하는 일이다.

테스트할 때 봉합 지점에서 동작을 다른 것으로 변경할 수 있다면 의존 관계를 일부분 제거할 수 있기 때문이다.

 

봉합의 종류

사용할 수 있는 봉합의 종류는 프로그래밍 언어마다 차이가 있다.

여기서는 Java를 기준으로 링크 봉합, 객체 봉합만 설명한다.

봉합 : 봉합은 코드를 편집하지 않고도 동작을 변화시킬 수 있는 위치를 말한다.

활성화 지점 : 모든 봉합은 활성화 지점을 갖는다. 활성화 지점에서는 어느 동작을 사용할지 선택할 수 있다.

 

링크 봉합

아래는 간단한 Java 클래스이다.

import com.Blog;

public class Writer {
    private String text;
    private Blog blog;

    public void write(String text){
        blog = new Blog();
        blog.posting(text);
    }
}

com.Blog 클래스를 임포트 하고 있다.

컴파일러와 JVM은 이 클래스를 CLASSPATH 환경 변수를 활용해서 찾아온다.

만약 CLASSPATH 위치를 변경하고 그곳에 별도의 com.Blog를 링크할 수 있다.

 

그렇다면 위에서 봉합 지점은 어디일까?

봉합 지점은 write 메소드 내부 new Blog() 호출 부분이다.

활성화 지점은 CLASSPATH 환경 변수가 된다.

객체 봉합

객체 봉합은 객체 지향 언어에서 사용할 수 있는 봉합 기법들 중 가장 유용하다.

아래 코드를 보자

sender.send();

만약 다른 코드의 변경 없이 send() 메소드를 변경할 수 있다면 이 호출 위치를 봉합 지점이라고 부를 수 있다.

다만, 객체 지향 언어에서 메소드 호출이 꼭 봉합 지점이 되는 것은 아니다.

아래는 봉합 지점이 아닌 호출을 보여 준다.

public void sendMessage(){

    Sender sender = new Sender();
    sender.send();
}

이 코드에서는 Sender 을 생성하고 동일 메소드 스코프 안에서 객체를 사용하고 있다.

이 경우 send 메소드 호출은 객체 봉합이 아니다. 여기에는 활성화 지점이 없다.

Sender 변수의 클래스는 객체가 생성될 때 결정되고, 모든 선택권은 sender에게 있다.

우리는 호출될 send 메소드를 변경할 수가 없다.

 

하지만 코드가 다음과 같은 경우는 어떨까?

public void sendMessage(Sender sender){
    sender.send();
}

이때 sendMessage 메소드 내부에 있는 sender.send() 호출 지점은 봉합 지점이 된다.

어떤 종류의 Sender 객체든 인수로서 제공하면서 send를 호출할 수 있기 때문이다.

호출하는 쪽의 코드 변경 없이 우리는 동작을 변경할 수 있다.

그렇다면 활성화 지점은 어딜까?

여기서 활성화 지점은 인수를 전달하는 지점이다.

이때 전달하는 Sender 종류를 결정하고, 테스트에 필요한 send 동작을 변경할 수 있는 지점이기 때문이다.

 

조금은 간접적인 구현이 들어간 구현 방법도 있다.

이전에 보았던 Writer 코드를 보자

public class Writer {
    private String text;
    private Blog blog;

    public void write(String text){
        blog = new Blog();
        blog.posting(text + ", 시간 : " + getNow());
    }

    private static LocalDateTime getNow(){
        LocalDateTime.now();
    }
}

getNow()는 정적 메소드다. 그렇다면 write 내부에 getNow()는 봉합에 해당할까? 그렇다.

write 메소드를 변경하지 않아도 호출 도작을 변경할 수 있다.

getNow 메소드의 static 키워드를 제거하고 private 을 protected로 변경한다면, 해당 클래스의 서브 클래스를 만들어 메소드를 오버라이딩 할 수 있다.

public class Writer {
    private String text;
    private Blog blog;

    public void write(String text){
        blog = new Blog();
        blog.posting(text + ", 시간 : " + getNow());
    }

    protected LocalDateTime getNow(){
        LocalDateTime.now();
    }
}

public class NovelWriter extends Writer{
    protected LocalDateTime getNow(){
        ...
    }
}

이런 방법은 구현이 들어가지만 필요에 따라서는 충분히 생각해 볼 수 있는 상황이다.

 

테스트를 수행할 때 적절한 종류의 봉합을 선택하는 것은 중요한 일이다.

특히 봉합 개념은 객체지향 언어에서는 강력한 힘을 발휘한다.

봉합이라는 관점에서 코드를 바라본다면 테스트 방법이나 테스트 친화적인 코드를 작성하는데 큰 도움이 될 것이다.

반응형