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(){
...
}
}
이런 방법은 구현이 들어가지만 필요에 따라서는 충분히 생각해 볼 수 있는 상황이다.
테스트를 수행할 때 적절한 종류의 봉합을 선택하는 것은 중요한 일이다.
특히 봉합 개념은 객체지향 언어에서는 강력한 힘을 발휘한다.
봉합이라는 관점에서 코드를 바라본다면 테스트 방법이나 테스트 친화적인 코드를 작성하는데 큰 도움이 될 것이다.
'IT > 책읽는 개발자' 카테고리의 다른 글
[레거시 코드 활용 전략] CH6 고칠 것은 많고 시간은 없고 - 2 (0) | 2020.02.15 |
---|---|
[레거시 코드 활용 전략] CH6 고칠 것은 많고 시간은 없고 - 1 (1) | 2020.02.10 |
[레거시 코드 활용 전략] CH3 감지와 분리 (0) | 2020.02.06 |
[레거시 코드 활용 전략] CH2 피드백 활용 (0) | 2020.02.04 |
[레거시 코드 활용 전략] CH1 소프트웨어 변경 (0) | 2020.02.03 |