본문 바로가기
가치관 쌓기/개발 돌아보기

개발자는 '다형성을 활용한 리팩토링'을 수련해야 한다.

by simplify-len 2022. 2. 27.

앞선 포스터에서 Interface Segregation Principle (인터페이스 분리 원칙)

에 대해서 이야기했다.

 

이제 본격적으로 인터페이스 분리 원칙을 함으로써 얻을 수 있는 실용적인 이익에 대해서 이야기해보려 한다.

 

또한,

인터페이스 분리 원칙과 테스트 코드를 함께 작성함으로써 다형성을 활용할 수 있는 냄새를 맡을 수 있었다.

 

 

같이 한번 살펴보자.

 

아래 테스트 코드에서는 메소드 하나가 너무 많은 역할을 한다는 냄새를 맡을 수 있다. 

@Test
void execute_with_single_notificationTarget() {
    given(targetProvider.getTargets()).willReturn(List.of(target1));
    given(factory.findBy(any())).willReturn(notificator);
    given(notificator.execute(target1)).willReturn(requestSuccess);

    sut.execute();

    verify(targetProvider).getTargets();
    verify(factory).findBy(any());
    verify(notificator).execute(any(NotificationTarget.class));
    verify(successHandler).onHandle(requestSuccess);
    verifyNoMoreInteractions(failHandler);
}

 

역할

1. Targets 을 가져온다.

2. target은 어떤 factory 로 부터 어떤 notificator 를 가져온다.

3. 가져온 notificator 로부터 execute() 메소드를 호출한다.

4. execute() 메소드가 반환한 값이 어떤 값이냐에 따라 서로 다른 handler 중 하나가 선택되어진다.

5. 선택된 핸들러에 의해 후속 조치가 취해진다.

 

5가지의 역할이 하나의 테스트 코드에 드러나고 있다. 프로덕트 코드를 사용하는 테스트 코드를 통해 이 코드는 리팩토링 대상이 될 수 있다는 냄새를 맡게 된 것이다.

 

우리는 하나의 메소드는 하나의 역할만 수행해야 한다고 말합니다.(SRP) 비록 약간의 트레이드오프도 있지만, 그럼에도 불구하고 최소한의 역할이 주어져야만 한다.

 

리팩토링이 된 후의 결론에 도달한 테스트 코드는 아래와 같다.

@Test
void execute_with_single_notificationTarget() {
    given(targetProvider.getTargets()).willReturn(List.of(target1));
    given(factory.findBy(any())).willReturn(notificator);
    given(notificator.execute(target1)).willReturn(requestSuccess);

    sut.execute();

    verify(targetProvider).getTargets();
    verify(factory).findBy(any());
    verify(notificator).execute(any(NotificationTarget.class));
    verify(requestSuccess).handle(requestResultHandler); // Look At this!!
}

 

여러 역할 중 아래 2가지는 해당 sut 에서 사용되어지지 않아도 된다는 사실을 발견할 수 있다.

4. execute() 메소드가 반환한 값이 어떤 값이냐에 따라 서로 다른 handler 중 하나가 선택되어진다.
5. 선택된 핸들러에 의해 후속 조치가 취해진다.

 

이 부분은 책임을 위임하는 클래스를 만들어 리팩토링을 진행했다.

 

 이야기가 잠시 다른 곳으로 빠졌지만, '다형성을 활용한 리팩토링' 이라는 주제로 다시 돌아가 위와 같은 책임을 위임하게 되면서 어떻게 다형성을 활용한 리팩토링이 진행되었는지 살펴보자.

 

실제 코드는 아래와 같다.

@Service
@RequiredArgsConstructor
public class NotificatorProcessor {

	...
	private final RequestedSuccessHandler successHandler;
    private final RequestedFailureHandler failHandler;

    void execute() {
		// ...

        for (NotificationTarget target : notificationTargets) {

            RequestResult result = notificator.execute(target);
            if (result instanceof RequestSuccess) {
                successHandler.onHandle((RequestSuccess) result);
            } else {
                failHandler.onHandle((RequestFailed) result);
            }
        }
    }
}

 

 

2개의 RequestedXXXHandler 가 result 의 instanceof 를 보고 handler 를 결정한다.

 

'RequestSuccess 의 구현체 타입에 따라 액션이 취하도록 한다.' 를 표현하는 코드이다.

 

위에서 언급했던 2가지 역할

4. execute() 메소드가 반환한 값이 어떤 값이냐에 따라 서로 다른 handler 중 하나가 선택되어진다.
5. 선택된 핸들러에 의해 후속 조치가 취해진다.

 

위 2가지 역할을 해결 하는 ResultHandler 가 등장한다.

@Service
@RequiredArgsConstructor
public class NotificatorProcessor {

	// ...
	private final RequestResultHandler resultHandler;

    void execute() {
        final List<NotificationTarget> notificationTargets = targetProvider.getTargets();

        for (NotificationTarget target : notificationTargets) {

		// ...
        	RequestResult result = notificator.execute(target);
	        result.handle(resultHandler); // Look at this!!
        }
    }
}

 

단순히 If 문의 코드를 ResultHandler 에게 넘기게 되면 문제가 해결되지 않는다.

 위 코드와 같이 책임져야 할 handler 자체를 RequestResult 에게 넘겨줘야 합니다. 그래야만 위 코드의 result 가 어떤 타입에 따라 처리될지 resultHandler 가 알게된다.

 

만약 ResultHandler가 If 문을 가질 경우의 클래스 다이어그램은 아래와 같다.

여전히 ResultHandler 에는 if 문이 남을 수밖에 없다. 어떻게 해야될까?

 

바라보는 방향이 달라져야 한다.(당연히 우리 뇌 사고방식또한 달라져야 한다) RequestResult 에서 Handler 를 알게 되면, ResultHandler 에서 어떤 타입의 RequestResult 따라 처리할 지 결정할 수 있게 된다.

 

바로 아래와 같이 클래스 다이어그램이 변경하게 된다.

 

결론적으로, RequestResult 을 상속받은 어떤 구현체가 추가될 때마다 ResultHandler 의 메소드 +on(RequestXX xx) 가 추가된다.

이로써 마침내 if문을 벗어날 수 있게 되었다.

 

 

댓글