(Working Effectively with Legacy Code) 001. Changing Software

Changing Software

소프트웨어를 개발하면서 코드의 변경은 비일비재하게 일어난다.

대부분의 코드 변경 목적이 좀 더 편한 유지보수를 하는 등의 좋은 목적을 가지고 있지만,

목적의 선의와는 관계없이 코드 변경이 꼭 좋은 결과를 가져오지 않는다.

그나마 리팩토링에 대한 문헌들이 이 분야에 대해 다루는 것이라고 볼 수 있다.

Working Effectively with Legacy Code 포스트 시리즈에서는 코드 변경에 대한 논의의 범위를 확대하고

코드 변경의 원리를 좀 더 깊게 다뤄본다.

기준이 되는 서적은 제목 그대로 Working Effectively with Legacy Code 서적을 기준으로 하며,

개인 경험과 여러가지 레퍼런스를 엮어서 포스팅할 계획이다.

한글판으로는 레거시 코드 활용 전략 서적으로 번역되어 있다.

참고 레거시 코드 활용 전략 : 손대기 두려운 낡은 코드, 안전한 변경과 테스트 기법

Four Reasons to Change Software

소프트웨어의 코드를 변경하는 이유는 크게 네 가지로 들 수 있다.

  1. 새로운 기능의 추가
  2. 버그 수정
  3. 설계 개선
  4. 자원 이용의 최적화

각각 하나씩 살펴보자.

1. 새로운 기능의 추가 (Adding Features)

새로운 기능의 추가는 소프트웨어의 변경을 위한 가장 기본적인 이유일 것이다.

사용자가 현재 제공되는 소프트웨어의 기능 외에 다른 기능을 요구하는 경우가 매우 빈번하기 때문이다.

예를 들어 주로 우상단에 배치된 이미지를 모종의 이유로 좌상단으로 옮긴다고 가정해보자.

이때 단순히 배치만 바꾸는 것이 아니라 애니메이션을 추가하는 경우, 새로운 기능의 추가 로 볼 수 있을 것이다.

단, 이는 개발자의 관점이다.

기존에 존재하는 이미지에 배치를 수정하고, 애니메이션을 추가하는 것이므로 신규 기능이라고 이해할 수 있기때문이다.

2. 버그 수정 (Fixing Bug)

위의 예시를 관점을 개발자에서 고객으로 바꾸어보자.

고객은 이미지의 배치에 문제 가 있다고 보고, 이 문제를 해결 하라는 의사로 배치를 변경하도록 요청한 것이라고 볼 수 있다.

즉, 관점에 따라 버그 수정 이라고 볼 수 있다.

이러한 관점에 차이에 따른 구분은 애석하게도 주관적인 요소가 매우 많이 개입된다.

허나, 새로운 기능의 추가버그 수정 도 결국 코드의 변경을 유발하는 것은 자명하다.

따라서 우리가 파악해야할 부분은 새로운 기능의 추가냐 버그 수정이냐가 아닌 이로인해 발생하는 코드의 변경이

소프트웨어의 동작을 변경시키는가 이다.

Behavioral change

상술한대로, 동작 변경에 집중해보자.

동작 이란, 사용자가 원하는 것이라고 볼 수 있다.

이는 소프트웨어에서 가장 핵심적인 부분으로서, 개발자들은 사용자가 정말로 원하는 동작을 구현해내는 직업이라고 볼 수 있다.

반대로 기존에 원하는 부분을 충족시키던 동작을 제거하면 사용자들은 서비스에 대한 신뢰를 잃어버릴 것이다.

다시 예시를 생각해보자.

좌상단에 없던 이미지가 생기는 것은 동작의 추가인가?

우상단에 있던 이미지가 없어지는 것은 동작의 제거인가?

아니면 둘 다인가?

우리는 개발자이므로, 개발자 관점에서 명확하게 구별하려면

기존 코드의 변경은 동작의 변경으로

새로운 코드를 추가하고 이를 호출한다면 동작의 추가로 간주하는 것이 좋다.

이처럼 새로운 동작을 추가하는 것과 기존의 동작을 변경하는 것에는 큰 차이점이 있다.

이번엔 코드로 예제를 살펴보자.

1
2
3
4
5
class CDPlayer {
fun addTrackListing(track: Track): Unit {
// ...
}
}

위의 클래스는 노래 목록을 추가하는 addTrackListing() 메서드를 가지고 있다.

이 때 노래 목록을 교체하는 메서드를 추가해보자.

1
2
3
4
5
6
7
8
9
class CDPlayer {
fun addTrackListing(track: Track): Unit {
// ...
}

fun replaceTrackListing(name: String, track: Track): Unit {
// ...
}
}

replaceTrackListing() 메서드를 추가하였을 때

애플리케이션의 새로운 동작을 추가했거나, 기존 동작을 변경헀는가?

어딘가에서 호출하지 않는 이상 단순히 메서드를 추가했을 뿐, 애플리케이션의 어떠한 동작도 변경하지않은 것이다.

단, 실무에서는 UI등의 고려사항으로 인해 아주 작은(호출시간이 1ms만큼 늘어나는 경우처럼) 변경이라도 발생할 수 있으므로,

어느 정도의 동작 변경 없이 동작을 추가하는 것은 거의 불가능하다.

3. 설계 개선 (Improving Design)

설계를 바꾸는 것도 소프트웨어의 변경 사유다.

주로 유지 보수를 좀 더 용이하게 하기위해서 수행하며, 일반적으로 소프트웨어의 동작은 건드리지않는다.

이 과정에서 기존에 제공되던 동작이 제거되는 경우 버그가 발생하게 된다.

이는 개발자가 구조의 변경을 꺼리는 이유이기도 하다.

참고 기존에 보장되던 동작이 정상적으로 동작하는 지 확인하는 테스트를 회귀 테스트라고 한다. 주로 영문명 그대로 리그레션 테스트라고 부른다.

위와 같이 동작의 변경없이 설계를 개선하는 행위를 리팩토링(refactoring) 이라고 부른다.

리팩토링의 기초 개념은 아래 두 가지이다.

  1. 소프트웨어의 기존 동작이 변경되지 않았음을 보장하기 위한 테스트를 작성한다.
  2. 테스트를 검증하면서 단계별로 작업을 진행하며 유지 보수성을 높인다.

일반적으로 시스템의 코드를 정리하는 작업은 굉장히 오랫동안 수행되어왔지만, 리팩토링의 개념과는 조금 다르다.

리팩토링은 위험 부담이 적은 소스 코드를 재구성하거나 위험 부담이 큰 소스 코드를 재작성을 의미하지 않으며,

소규모 변경을 계속해서 반복하면서 테스트로 뒷받침하는 것이 진정한 리팩토링의 개념이라고 볼 수 있다.

4. 자원 이용의 최적화 (Optimization)

최적화는 프로그램이 동작하기위해 점유하는 메모리나, 연산에 사용되는 시간을 줄이는 것을 말한다.

리팩토링과 마찬가지로 기존 동작을 보장하며 변경을 진행하는 것은 유사한 점이지만, 유지 보수성을 확보하기 위한 프로그램의 변경을 동반하진 않는다.

정리

일반적으로 시스템에서 어떤 작업을 할 때, 변경될 수 있는 요인은 구조, 기능, 자원 사용량을 들 수 있다.

이 요인들을 지금까지 설명한 네 가지의 변경 이유를 종합하여 살펴보자.

구분 새로운 기능의 추가 버그 수정 설계 개선 자원 이용의 최적화
구조 변경 변경 변경 -
기존 기능 변경 변경 - -
자원 사용량 - - - 변경

기능의 변경이 없는 리팩토링(=설계 개선)과 최적화는 유사해보일 수 있다.

위의 표에 새로운 기능 행을 추가하여 좀 더 상세하게 비교해보자.

구분 새로운 기능의 추가 버그 수정 설계 개선 자원 이용의 최적화
구조 변경 변경 변경 -
새로운 기능 변경 - - -
기존 기능 - 변경 - -
자원 사용량 - - - 변경

새로운 기능의 추가, 리팩토링, 최적화는 모두 기존 기능을 유지한다.

버그 수정의 경우 기존 기능을 변경하긴 하지만, 변경되지않는 코드에 비하면 매우 작은 경우가 일반적이다.

보다 실무적인 관점에서 살펴보자.

우리가 집중해야하는 부분은 얼마나 정확하게 변경할 것인가? 이다.

변경의 정확성을 확인해야 하는 대상은 소프트웨어의 일부분이지만, 변경이 되는 코드만 확인해서는 안된다.

중요한 것은 기존의 동작을 보장하는 것인데, 어떤 동작을 변경할 때 다른 동작이 변경되지않음을 확인하는 것은 매우 힘든 일이다.

다른 말로, A 코드를 변경할 때 B 코드에 어떤 영향을 미치게 될지 파악하는 것이 매우 어렵다는 것이다.

따라서 변경이 미치는 영향 범위를 정확히 파악하고 진행하는 것이 중요하다.

Risky Change

거듭 강조했듯이 코드의 변경을 진행하면서 기존 동작을 유지하는 것은 상당한 위험이 수반되는 쉽지않은 일이다.

이러한 위험을 최소화하기 위해서는 아래 세 가지를 체크해봐야 한다.

  1. 어떤 변경을 해야 하는가?
  2. 변경이 정확하게 이뤄졌는지 어떻게 확인할 수 있는가?
  3. 변경으로 인해 무언가를 손상시키지 않았는지 어떻게 확인할 수 있는가?

위험을 최소화한다는 것은 말 그대로 최대한 줄인다는 뜻이지 없앤다는 뜻이 아니다.

즉 어떠한 정책을 정하고 이에 따라 위험을 감수할지 안할지를 결정해야 한다.

보통 개발자들은 이러한 문제에 대해 매우 보수적으로 접근하며, 문제가 없는 코드는 수정하지않는 경우가 많다.

흔히들 말하는 “잘 돌아가는 레거시는 레거시가 아니다” 라는 우스갯소리도 있다.

그렇다고 잘 돌아가는 기존 코드에 추가적인 코드를 산입하는 것은 임시 방편은 될지언정, 완벽한 솔루션이 될 수는 없다.

클래스와 메서드의 추가를 두려워하면, 우리가 만날 것은 매우 비대해져 이해하기 어려운 코드밖에 없다.