리팩토링 깊게 들여다보기

리팩토링이 중요한 이유

가독성

의도를 전달하기 위한 코드의 성질

코드가 의도한 대로 작동한다는 가정이 있으면 코드가 무슨 일을 하는지 파악하기가 매우 쉽다는 뜻

코드에서 의도를 전달하는 방법

  1. 코딩 컨벤션을 지정하고 따르는 것

  2. 주석 달기

  3. 변수 네이밍

  4. 메서드 네이밍

  5. 클래스 네이밍

읽기 힘든 코드

function checkValue(str: boolean) { //<- 바람직하지 않은 메서드명,인자명 
  // 값 체크 <- 그저 이름만 반복하는 주석
  
  if (str !== false) { // <- 이중 부정은 읽기 어려움
    // 반환 <- 쓸데 없는 주석
    return true
  } else {
    return str // <-이 시점에는 항상 false이기 때문에 그냥 fales로 표현하는 것이 더 명확함
  }
}

읽기 쉽게 작성된 동일 코드

function isTrue(bool: boolean) {
  if (bool) return true
  else return false
}

단순화한 동일 코드

function isTrue(bool: boolean) {
  return bool
}

유지보수성

버그를 고치거나 기능을 추가하기 위해 일부 기능을 변경해야 할 때마다 새 코드를 어디에 놓을지 후보 위치(context)를 조사하는 것으로 시작, 현재 코드가 무슨 일을 하는지 파악하고, 새로운 목표를 수용하기 위해 코드를 안전하고 빠르게 수정할 수 있는 방법은 무엇인지 찾으려고 시도한다.

유지보수성은 얼마나 많은 후보를 조사해야 하는지를 나타내는 표현이다.

읽고 살펴봐야 할 코드가 많아질수록 시간이 더 오래 걸리고 무언가를 놓칠 가능성이 높다. 따라서 유지보수성은 변경 시 발생하는 위험과 밀접하게 관련있다.

조사 단계에서 시간이 오래 걸린다는 것은 코드 유지보수성이 나쁘다는 징후이며 개선을 위해 노력해야 한다.

어떤 시스템에서 한군데서 무언가를 수정하면 관련 없어 보이는 다른 곳에서 문제가 발생한다. 그런 시스템을 취약하다(fragile)라고 표현한다.

불변속성과 전역상태

취약성의 근원은 일반적으로 전역상태(global state)이다.

여기서 전역(global)은 우리가 고려한 범위를 벗어난 것을 의미한다. (상태의 개념은 좀 더 추상적)

범위가 제한되지 않은 불변속성

  • 메서드로 생각하면 내부 변수가 전역 변수를 참조하는 것

  • 중괄호(스코프) 외부의 모든 것이 중괄호 내부의 모든 것에 대해 전역상태로 간주된다.

  • 데이터가 전역적일 경우 데이터가 연결된 다른 변수를 통해 누군가 읽거나 변경할 수 있어 실수로 데이터가 손상될 수 있다.

  • 코드에 상태(조건)를 명시적으로 확인하지 않는(가정설정문[assertion]으로만 확인하는) 속성을 불변속성(invariant)이라 한다.

    • e.g) 이 숫자는 절대 음수일 수 없다, 이 파일은 확실히 존재한다. 등

    • 시스템이 변경되거나 새로운 개발자가 합류될 때 불변속성이 유효한 상태로 유지되기란 거의 불가능

  • 변수를 명시적으로 체크해서 불변속성을 제거함으로써 유지보수성을 향상시킬 수 있다.

    • 리팩터링은 코드가 수행하는 작업이 변경되선 안되는데, 대신 이런 경우 불변속성을 더욱 쉽게 볼 수 있도록 서로 가깝게 이동시켜 유지보수성을 향상시킨다.

"함께 변하는 것은 함께 있어야 한다"는 의미의 불변속성의 범위제한(localizing invariants)이라고 한다.

코드가 하는 일을 바꾸지 않고 유지보수하기

  • 리팩터링 중에는 코드가 느려져도 거의 신경 쓰지 않는다.

    • 대부분의 시스템에서 성능은 가독성과 유지보수성보다 가치가 떨어진다.

    • 성능이 중요한 경우 프로파일링 도구나 성능 전문가의 지도를 받아 리팩토링과 다른 단계에서 처리해야 한다.

  • 리팩터링을 할 때는 블랙박스의 경계를 고려해야 한다.

    • 기본적으로 리팩터링 중인 코드를 예약해서 다른 사람이 변경하지 않게 해야한다.

    • 예약한 코드가 적을수록 변경 사항이 충돌할 위험이 낮아진다.

    • 따라서 적절한 리팩터링 범위를 결정하는 것은 어렵고 중요한 조정 작업이다.

리팩터링의 세 가지 핵심

  • 의도를 전달함으로써 가독성 향상

  • 불변속성의 범위제한을 통해 유지보수성 향상

  • 범위 밖의 코드에 영향을 주지 않고 1항과 2항을 수행

속도, 유연성 및 안정성 확보

리팩터링 패턴에는 구체적이고 지역적인(변수명 변경)것 부터 추상적이고 전역적인 것에 이르기까지 여러 수준이 있다

코드의 품질에 가장 큰 영향을 미치는 것은 역시 아키텍처 변경이다.

상속보다는 컴포지션 사용

범위가 제한되지 않은 불변속성을 도입하는 일반적인 방법인 상속을 권장하지 않고 컴포지션을 권장한다.

상속을 사용한 코드

interface Bird {
  hasBeak(): boolean
  canFly(): boolean
}

class CommonBird extends Bird {
  hasBeak() {return true}
  canFly() {return true}
}

class Penguin extends CommonBird {
  canFly() {return false}
}

컴포지션을 사용한 코드

interface Bird {
  hasBeak(): boolean
  canFly(): boolean
}

class CommonBird implements Bird {
  hasBeak() {return true}
  canFly() {return true}
}

class Penguin implements Bird {
  private bird = new CommonBird() // Composition
  hasBeak() { return bird.hasBeak()}
  canFly() { return false }
}
  • Bird에 새로운 메서드를 추가한다면 두 경우 모두 CommonBird에 메서드가 추가된다.

class CommonBird implements Bird {
  //...
  canSwim() {return false}
}
  • Penguin이 새로운 메서드 canSwim()을 구현하지 않았기 때문에 컴파일 오류가 발생

  • 따라서 이를 인지하고 수동으로 추가하고 펭귄이 수영할 수 있는지를 반환해야 한다.

    • Penguin이 다른 새처럼 동작하기를 원할 경우 hasBeak처럼 간단하게 구현 가능하다.

    • 반대로 수영할 수 없는 경우 canSwim()을 재정의(override)해야 한다는 것을 작업자가 기억해야 한다.

유연성

컴포지션을 중심으로 만들어진 시스템을 사용하면 다른 방식보다 더 깔끔하게 코드를 결합하고 재사용할 수 있다.

컴포지션을 많이 사용하는 시스템으로 작업하는 것은 레고 블록을 가지고 노는 것과 같다.

모든 것이 서로 맞게 조립돼 있으면 부품을 교체하거나 기존 부품으로 새로운 것을 만드는 것이 놀라울 정도로 빨라진다

이런 유연성(flexibility)은 대부분의 시스템이 원래 프로그래머가 상상하지 못했던 방식으로 사용된다는 것을 알게되면 더욱 중요해진다.

수정이 아닌 추가로 코드를 변경

컴포지션의 가장 큰 장점은 추가(additional)로 변경이 가능하다는 것이다.

  • 기존 기능에 영향을 주지 않고 기능을 추가하거나 변경할 수 있음을 의미한다.

  • 이 속성을 개방-폐쇄(open-closed) 원칙이라고 한다.

    • 소프트웨어 구성요소들은 확장에 대해 열려 있어야 하고, 수정에 대해 닫혀 있어야 한다는 의미

프로그래밍 속도

  • 새로운 것을 구현하거나 버그를 수정해야 할 때 가장 먼저 하는 일 중 하나가 주변 코드를 고려하여 아무것도 손상시키지 않는 것이다.

  • 이때 다른 코드를 건들지 않고 변경할 수 있다면 그 모든 시간을 아낄 수 있게된다.

  • 물론 계속해서 코드를 추가하면 코드베이스가 빠르게 늘어나 문제가 될수는 있다.

  • 어떤 코드가 사용되고 사용되지 않는지를 주의를 기울이고 사용하지 않는 코드는 빠르게 제거해야 한다.

안전성

  • 추가에 의한 변경 방식을 따르면 기존 코드를 항상 보존할 수 있다.

  • 새 코드가 실패할 경우 이전 기능으로 대체하는 기능을 구현하는것은 쉽다.

  • 이를 통해 기존 기능에 새로운 오류가 발생하지 않게 할 수 있는데 불변속성의 범위를 제한해 오류를 줄이는 것에 더해서 시스템의 안정성(stability)이 더욱 향상된다.

리팩터링과 일상 업무

리팩터링은 프로그래머의 일상 업무가 되어야 한다.

코드를 리팩터링하지 않고 그냥 전달하기만 하면 다른 프로그래머의 시간을 뻇는셈이 된다.

  • 열악한 소프트웨어 아키텍처는 이자가 붙는데 이를 기술부채(technical debt)라고한다.

    • 레거시 시스템에서는 변경하기 전에 먼저 리팩토링을하자.

    • 코드를 변경한 후에도 리팩터링을 하자.

코드를 전달하기 전 리팩터링을 수행하는 것을 다음과 같이 표현한다.

"항상 여러분이 왔을 때보다 더 좋게 만들어 놓고 떠나세요" - 보이스카우트 규칙

학습 방법으로서의 리팩터링

리팩터링은 배우는데 시간이 오래 걸리지만 결국 익숙해진다.

  • 더 나은 코드의 장점을 보고 경험하는 것은 코드를 작성하고 생각하는 방식을 바꾼다.

  • 안정성이 더 확보되면 이 안전성을 어떻게 활용할 수 있을지 생각하기 시작한다.

  • 리팩터링은 코드를 연구하는 완전히 다른 방법이다. (독특한 관점을 제공)

    • 코드를 이해하지 않고도 코드를 개선할 수 있는 방법도 있다.

    • 최소한의 코드 작업으로 매우 이해하기 쉬운 결과를 얻을 수 있다.

새로운 팀원이 합류하게 되면 입문용 작업으로 종종 리팩터링이 사용된다.

고객을 바로 상대하지 않고도 안전한 환경에서 코드를 활용해 학습가능하기 때문

좋은 관행이지만, 일상적인 리팩터링을 소홀히 한 경우에나 가능하다.

소프트웨어 분야에서 '도메인' 정의하기

도메인이란 실생활의 특정 측면을 모델링한 것

도메인에는 흔히 사용자와 전문가, 고유한 용어, 그리고 고유의 문화가 있다.

예시

  • 도메인: 2D 게임

  • 사용자: 플레이어

  • 도메인 전문가: 게임 또는 레벨 디자이너

  • 고유한 용어: 플레이어가 먹을 수 있는 '플럭스'와 같은 단어를 도입하여 게임이 자체 용어를 사용

  • 고유한 문화: 게임과 상호작용하는 방법에 대해 흔히 예상하고 있는 것들 (돌과 상자는 중력에 영향을 받지만 열쇠와 사용자는 그렇지 않은것처럼)

소프트웨어 개발할 때 도메인 전문가와 긴밀하게 협력해야 할 때가 많은데 이는 그들의 용어와 문화를 배워야 한다는 것을 의미한다.

프로그래밍 언어는 모호성을 허용하지 않기 때문에 떄떄로 전문가조차도 낯선 새로운 코너케이스(conner case)를 찾아야 한다. *(변수와 환경적인 요소로 인해 코드에 문제가 발생할 수 있는 경우를 의미)

요약

  • 리팩터링은 기능 변경 없이 코드의 의도를 전달하고 불변속성의 범위를 제한하는 것

  • 상속보다 컴포지션을 사용함으로써, 추가를 통한 변경으로 개발 속도, 유연성, 안정성을 확보

  • 리팩터링을 일상 업무에 포함시켜 기술 부채가 쌓이지 않도록 해야함

  • 리팩터링을 연습하면 코드에 대한 독특한 관점을 얻을 수 있으며, 이로 인해 더 나은 해결책을 찾을 수 있다.

Last updated