리팩토링

아래 코드는 while 문의 사용법을 보여주기 위한 예제 코드입니다.

var i = 0;
while(i < 10) {
    document.write('coding everybody');
    i++;
}

이 코드의 가장 큰 문제점은 요구사항과 실제 코드 사이의 괴리입니다. 요구사항은 무척 간단합니다. “coding everybody”라는 문자열을 화면에 10번 출력하는 것이죠. 그런데 코드는 안 간단합니다. 변수 i를 0으로 초기화하고, while 문에서 10보다 작은지 조건을 검사하고, 참이면 화면에 문자열을 한 번 출력하고, 변수 i의 값을 1 증가시킨 다음에 다시 while 루프 처음으로 돌아갑니다.

또한 아주 작은 코드이지만, 이 코드에는 관심사(concern)가 2가지가 존재합니다. 반복과 출력입니다. 그리고 두 가지 관심사를 한 번에 처리하기 때문에 “실제”보다 복잡한 코드가 되었습니다. 두 가지 관심사를 각각의 함수로 분리해 보겠습니다.

function times(n, f) {
    for (var i = 0; i < n; i++) {
        f();
    }
}

function print() {
    document.write('coding everybody');
}

times(10, print);

간단한 분리처럼 보이지만, 이제 출력에 대한 관심사는 print() 함수로, 반복은 times() 함수로 나눠서 이해할 수 있고, times() 함수의 정의를 보지 않고서도 직관적으로 이 코드가 어떤 일을 수행하는지 바로 알 수가 있습니다. 더 이상 변수 선언이나 갱신을 신경쓸 필요 없으므로 코드만 봐도 요구사항이 한 눈에 보입니다.

관심사의 분리(separation of concerns)는 소프트웨어 엔지니어링의 가장 기본적인 원리 중 하나입니다. 하지만 소프트웨어 엔지니어링이란 말이 뭔가 크고 거창한 것이라는 생각에, 많은 개발자들이 이 정도로 작은 코드 수준에서 관심사의 분리 원리가 적용되어야 한다고 생각하지 않는 경향이 있습니다.

부가적으로 times() 함수를 다른 곳에서 재활용할 수도 있습니다. 하지만 times() 함수가 재활용되지 않더라도 한 함수가 한 번에 하나의 일만 해야 한다는 건 객체지향 프로그래밍의 원리인 SOLID의 단일 책임의 원칙(single responsibility principle)과도 일맥상통합니다.

이렇게 내부 동작의 변경 없이 이렇게 코드 가독성을 높이고 유지 보수를 더 쉽게 할 수 있게 수정하는 행위를 리팩토링이라고 부릅니다.

일단 분리하고 나면 필요에 따라 times() 함수의 구현을 바꿀 수도 있습니다. 예를 들어, (JavaScript VM에 꼬리 재귀 최적화(tail call optimization)가 추가되어 재귀 함수의 성능이 루프와 다르지 않는 상황이 온다면) for 문이 아니라 재귀 함수(recursion)를 이용하도록 구현을 바꿀 수도 있을 겁니다. 이 과정에서 만약 times()라는 함수가 별도로 분리되어 있지 않았다면, 코드 변경 시 다른 관심사(출력) 코드를 실수로 건드려 버그를 만들 수도 있었을 겁니다.

function times(n, f) {
    f();
    if (n > 1)
        times(n - 1, f);
}

이렇게 리팩토링된 코드는 테스트하기도 더 편합니다. document.write() 함수는 DOM이라는 전역 공유 상태(global shared state)를 건드리는 함수이기 때문에 테스트하기가 쉽지 않은 반면에 times() 함수는 함수 인자에만 결과값이 의존하는 순수 함수(pure function) 혹은 참조 투명(referential transparent)한 함수이기 때문에 별도의 환경 셋업 없이 다음과 같이 쉽게 테스트가 가능합니다. 심지어 브라우저가 아니어도 테스트가 가능합니다.

function test_times() {
    var x = 0;
    times(10, function () { x++; });
    assert(10, x);
}

그리고 보니 times라는 함수는 워낙 일반적인 함수라서 이미 underscore.jslodash가 제공을 하고 있습니다. 우리가 작성한 코드와 달리 에러 처리 코드도 포함되어 있고, 추가로 context 인자를 넘길 방법을 제공하므로 상용 코드에서는 해당 라이브러리를 쓰는 것이 더 좋은 방법일 수 있습니다.

_.times(10, print);
Advertisements

2 thoughts on “리팩토링

  1. 좋은 내용입니다.
    그런데 요즈음 드는 생각은 모든 인프라가 동일한 것도 아니고 각 상황이 동일한 것이 아니라서 확실한 정답은 없는 거 같다는 생각이 드네요. 각 인프라나 환경에 맞게 그 때 그 때 개발방법론이나 코딩 스타일도 달라져야 할 것 같기도 하네요. 결국 모든 가능한 코딩스타일을 두루 잘 이해한다면 그 때 그 때 맞게 적용 가능하지 않을까 싶네요.
    Powerful한 server환경과 아주 Minimal resource만 이용가능한 환경에서 과연 같은 코딩 스타일을 고수할 수 있을까 계속 고민하게 됩니다. OOP 기반으로 class랑 function을 사용해서 잘 디자인해서 열심히 개발했지만, 결국 많은 function call로 인한 performance degradation이 온다면 결국 코딩을 바꿀 수 밖에 없는 상황으로 여러 좌절하는 사람도 많기도 하네요.

    Context-aware Software Development 를 한번쯤 생각해보고 싶은데, 이것도 뭐 좋은 방법은 아닌 것 같기도 하네요.

    좋아하기

    • 네. 성능 요구사항에 맞춰 적절한 추상화 수준을 찾는 건 엔지니어링에 반드시 필요한 트레이드오프라고 생각합니다.

      10년 이상 성능 최적화 작업을 주로 하다 보니 오히려 소프트웨어 복잡성이나 개발자의 생산성에 더 관심이 많이 가네요.

      좋아하기

댓글이 닫혀있습니다.