티스토리 뷰

반응형

이전 글에 이어서 자바 코드를 리팩터링하는 기본적인 방법과 그 내용을 코드를 통하여 조금 더 이해하기 쉽도록 적어보려고 합니다. 

https://abbo.tistory.com/393

 

[Refactor] 자바 코드 리팩터링하기 - 1부

안녕하세요~ 오늘은 개발자의 숙명과도 같은 코드 리팩터링에 대해 알아보고 제가 진행하는 코드 리팩터링 방법에 대해 기술해보려 합니다. 가장 먼저 리팩터링의 정의를 먼저 알아야 합니다.

abbo.tistory.com

 

5. 고차 함수로 의존성 줄이기

스프링을 사용한 프로젝트에서 종종 다음 2가지의 상황을 마주할 수 있습니다. 첫째, 어노테이션에 의한 의존성 주입 남용입니다. 둘째, 오랜 세월의 흐름으로 의도치 않게 서비스 간의 의존성이 복잡하게 강한 결합(Tight Coupling)으로 묶이면서, 코드를 읽기도 어렵고 단위 테스트를 구성하기도 어려운 상황이 생깁니다.

아래는 카카오톡 Java App Server 서비스의 의존성 그래프입니다. 왼쪽의 그림처럼, 복잡했던 기존의 순환 종속성(Circular Dependencies)을 가지는 의존성 그래프를 오른쪽의 단순한 의존성 그래프로 리팩토링 하여, 라이브 서비스에 반영하였습니다. 이번 장에서는 오랜 세월의 흐름으로 서비스 의존성 그래프가 복잡해진 라이브 서비스를 리팩토링 한 내용을, 작은 예제들을 통해 일반화하여 정리해 보겠습니다.

이번 장을 이해하려면 자바의 함수형 인터페이스에 대한 지식이 필요합니다. (참고: Functional Interfaces in Java 8)

이번 장에서 사용하는 예제는 BeforeRefactoring과 AfterRefactoring, 2개의 프로젝트로 나누어서 구성했고, 전체 코드는 여기에서 확인할 수 있습니다.

 

제가 스프링을 사용하지 않고 예제를 구성하려 해 봤지만, 스프링을 사용하지 않고서는 아래에서 보게 될 순환 종속성을 만들기 쉽지 않았습니다. 심지어, 예제로 만든 BeforeRefactoring 프로젝트는 스프링 부트 2.6 버전 기준에서 순환 종속성을 가진다는 이유로 프로젝트 실행이 거부됩니다. 예제 프로젝트를 실행하려면 application.properties 파일에 아래 설정을 추가합니다. 구체적으로 어떤 에러인지 궁금하시면, 아래 설정을 삭제하고 실행해 보면 확인해 볼 수 있습니다. 참고로, 생성자 주입(Constructor Injection)을 사용하면, 아래 설정을 추가해도 순환 종속성 에러로 실행되지 않습니다. 대신 필드 주입(Field Injection)을 사용하면 리플렉션(Reflection) API를 사용하기 때문에 에러가 발생하지 않습니다. 그래서 이 예제에서는 필드 주입을 사용합니다.

 

spring.main.allow-circular-references=true
 

예제 프로젝트 BeforeRefactoring은 아래와 같은 순환 의존성 그래프를 만들게 됩니다.

 

 

BeforeRefactoring 프로젝트에서 각 서비스의 메서드가 다른 서비스의 메서드를 사용하기 위해서, 다른 서비스를 필드에 참조하도록 해서 서비스 간의 순환 종속성을 만들어내도록 했습니다. 이를 다양한 방법으로 풀어낼 수 있지만 여기서는 자바 8부터 지원하는 함수형 인터페이스를 사용하는 고차 함수로 해결하겠습니다. 서비스 간 의존성을 완전히 제거할 수 있다면 더 좋겠지만, 제거할 수 없다면 의존성을 가능한 한 작게 유지하는 것이 좋겠죠. 자바의 함수형 인터페이스가 다른 함수형 언어의 고차 함수와는 달리 결국 클래스 인터페이스로 구현되어 아쉽습니다. 명시적인 클래스 인터페이스보다는 함수형 인터페이스가 더 작고, 약한 결합이기 때문입니다.

 

6. 리팩토링

여기서는 전체 코드의 수정 과정을 설명하지 않고, 간단하게 1개 서비스의 객체 의존성만 함수 의존성으로 수정해 보겠습니다. 자세하게 설명하자면, BeforeRefactoring의 ServiceA가 가지고 있는 ServiceB에 대한 의존성을, 함수형 인터페이스 의존성으로 수정하는 것입니다.

@Service
public class ServiceA {
    @Resource
    ServiceB serviceB;

    public void methodA(Integer paramFirst) {
        Output.printf("'pass %d to ServiceB and get %s' by ServiceA\n", paramFirst, serviceB.methodB(paramFirst));
    }

    public Integer getValue() {
        return 10;
    }
}
 

위 클래스를 아래와 같이 ServiceA::methodA()의 시그니처를 수정하고, serviceB.methodB() 메서드 호출을 함수형 인터페이스 apply() 호출로 변경합니다. 이제 ServiceA는 ServiceB에 의존하지 않으니 serviceB 필드를 삭제합니다.

 

@Service
public class ServiceA {
    public void methodA(Integer paramFirst, Function<Integer, Integer> methodB) {
        Output.printf("'pass %d to ServiceB and get %s' by ServiceA\n", paramFirst, methodB.apply(paramFirst));
    }

    public Integer getValue() {
        return 10;
    }
}
 

위에서 ServiceA::methodA()의 시그니처를 변경하였으므로, Handler::execute() 메서드에서 컴파일 에러가 발생할 것입니다. 이 Handler 메서드 부분도 아래 코드에서처럼, 오류가 발생하지 않도록 수정해 주도록 합니다.

@Component
public class Handler {
    private final ServiceA serviceA;

    public Handler(final ServiceA serviceA) {
        this.serviceA = serviceA;
    }

    public void execute(long count) {
        for (long cnt = 0; cnt < count; cnt++) {
            serviceA.methodA(2);
        }
    }
}
 

이어서 아래와 같이 ServiceB에 대한 의존성을 Handler로 옮겨오고, 생성자 인젝션(Constructor Injection)으로 Handler 클래스에 주입합니다. 그리고, serviceA.methodA() 메서드 호출부의 2번째 아규먼트로 serviceB::methodB를 고차 함수로 넘기면, 위에서 수정한 ServiceA::methodA() 시그니처를 만족하게 되면서 컴파일 에러가 사라집니다.

@Component
public class Handler {
    private final ServiceA serviceA;
    private final ServiceB serviceB;

    public Handler(final ServiceA serviceA, final ServiceB serviceB) {
        this.serviceA = serviceA;
        this.serviceB = serviceB;
    }

    public void execute(long count) {
        for (long cnt = 0; cnt < count; cnt++) {
            serviceA.methodA(2, serviceB::methodB);
        }
    }
}
 

위와 같은 방식을 반복 적용하여 나머지 ServiceB → ServiceC와 ServiceC → ServiceA에 대한 의존성을 제거할 수 있습니다. 이전과 마찬가지로 작업하면서 ServiceA::methodA()의 시그니처를 추가로 수정해주어야 합니다. 모든 작업이 끝나면 서비스 간의 의존성이 모두 사라지면서, 순환 종속성도 사라지게 됩니다. 이렇게 리팩토링 하면 추가로 얻는 이점으로는 Handler가 동작하는데 필요한 각각의 서비스 ServiceA, ServiceB, ServiceC에 흩어져있던 의존성이 Handler 클래스로 모두 명시적으로 모이게 되고, 흩어져서 가려져있던 의존성이 한눈에 보인다는 점입니다. 이로써 객체 의존성이 함수 의존성으로 변경되고 서비스 간의 의존성 그래프는 아래와 같이 모두 끊어지게 됩니다.

 

7. 단위 테스트 구성

이제 각 서비스는 외부에서 주입받는 함수만 필요할 뿐, 서로의 존재(구현 방식)를 몰라도 됩니다. 이를 만족하는지는 단위 테스트에서 확인할 수 있습니다. 순환 의존성을 가진 BeforeRefactoring을 단위 테스트하려면, 아래와 같이 @SpringBootTest를 사용하고 Bean을 스프링에 의존해 생성할 수 있습니다.

@SpringBootTest
class UsingSpringTests {
    @Resource
    ServiceB serviceB;

    @Resource
    Handler handler;

    @Test
    void testServiceB() {
        // 성공하지만, main()이 실행된 후라서 Output::isPrintable의 상태가 기본값이 아니다.
        Integer result = serviceB.methodB(2);
        assertThat(result, equalTo(12));
    }

    @Test
    void testHandler() {
        // 성공하지만, main()이 실행된 후라서 Output::isPrintable의 상태가 기본값이 아니다.
        handler.execute(1);
    }
}

또는, 아래와 같이 @Mock, @InjectMocks를 사용하고 모키토(Mockito)에 의존하여, 객체를 목킹(Mocking)해서 테스트를 구성해야 합니다.

class UsingMockTests {
    @Mock
    private ServiceC serviceC;

    @InjectMocks
    private ServiceB serviceB;

    @InjectMocks
    private Handler handler;

    @BeforeEach
    public void createServiceB() {
        MockitoAnnotations.openMocks(this);
    }

    @Test
    public void testServiceB() {
        int first = 10;
        when(serviceC.methodC(first)).thenReturn(first + 30);

        Integer result = serviceB.methodB(first);
        assertThat(result, equalTo(40));
    }

    @Test
    public void testHandler() {
        // 실패한다. 성공시키려면, ServerA, ServerB, ServerC 의존성에 대한 Mocking을 처리해야 한다.
        handler.execute(1);
    }
}

 

하지만, 함수 의존성을 사용한 AfterRefactoring을 단위 테스트하는 경우에는 리팩토링 전과 다르게 스프링과 모키토 없이도 아래와 같이 바로 생성해서 테스트를 구성할 수 있게 됩니다.

class SampleUnitTests {
    @Test
    public void testServiceB() {
        ServiceB serviceB = new ServiceB();

        Integer result = serviceB.methodB(2, () -> 10, (a, b) -> a + b.getAsInt());
        assertThat(result, equalTo(12));
    }

    @Test
    public void testHandler() {
        Handler handler = new Handler(new ServiceA(), new ServiceB(), new ServiceC());

        handler.execute(1);
    }
}

이전 글 : https://abbo.tistory.com/393

 

[Refactor] 자바 코드 리팩터링하기 - 1부

안녕하세요~ 오늘은 개발자의 숙명과도 같은 코드 리팩터링에 대해 알아보고 제가 진행하는 코드 리팩터링 방법에 대해 기술해보려 합니다. 가장 먼저 리팩터링의 정의를 먼저 알아야 합니다.

abbo.tistory.com

 

 

출처 : https://tech.kakao.com/2023/01/19/kakaotalk-java-app-server-refactoring/

 

카카오톡 Java App Server Refactoring 후기

안녕하세요, 카카오톡 메시징 파트에서 메시징 서버를 개발하고 있는 Soo입니다. 취미가 직업이 된 지 어느덧 8,000일이 넘어가고 있는 개발자입니다. 2019년 말에 톡 메시징 파트에 합류하여 기술

tech.kakao.com

 

반응형
댓글
공지사항