소프트웨어 변경 - 1

2022/02/01
Software Engineering

프로그램을 제작하는 과정에서 프로그램의 변경은 필수불가결하게 일어날 수 밖에 없는 작업입니다.
일반적으로 프로그램을 변경하는 이유는 크게 4가지로 나뉩니다.

새로운 기능(feature)의 추가

새로운 기능의 추가는 프로그램의 변경 이유 중에서 가장 기본적인 이유 입니다.
기존의 프로그램이 제공하지 않는 기능을 고객이 요구할 때 소프트웨어를 변경하게 됩니다.

버그 수정

프로그램의 동작이 의도한대로 진행되지 않을 경우 우리는 버그가 있다고 이야기 합니다.
버그는 프로그램의 신뢰성을 떨어뜨려 고객이 프로그램에 대해 신뢰를 하지 못하게 되는 큰 원인 중 하나입니다.
따라서 버그의 수정 역시 프로그램을 변경하는 큰 이유중 하나를 차지합니다.

새로운 기능 추가와 버그수정

실제 업무를 하면서 이 둘을 구분 짓는 건 쉬운일이 아닙니다. 관점에 따라 하나의 수정사항이 새로운 기능의 추가 일 수도, 버그 수정(혹은 동작의 변경) 일 수도 있기 때문입니다.
기본적으로는 동작 변경의 여부가 이 둘을 나누는 가장 중요한 포인트가 됩니다. 따라서 프로그래머의 관점에서는 기존 코드를 변경하는 경우에는 동작 변경, 새로운 코드를 추가해 적용하는 경우 동작의 추가로 간주할 수 있습니다.

예를 들어 홈페이지의 로그를 좌상단에서 우상단으로 이동한다고 했을 때, 기존의 좌상단의 로고를 보여주던 코드가 사자리고, 새롭게 우상단에 로고를 보여주는 코드가 추가 되기 때문에(물론 css등을 수정하여 html코드를 건드리지 않고도 변경이 가능합니다.) 이 경우에는 새로운 기능의 추가로 볼 수도 있을 것입니다.

설계 개선

남은 두가지 이유는 앞서 언급한 두 이유와는 다르게, 프로그래머가 필요에 의해 자발적으로 진행하는 경우 입니다. 이 중 설계 개선은 소프트웨어 구조를 좀 더 수정하기 쉽게 변경하고 싶을 때를 나타냅니다. 이렇게 변경하는 경우에는 기존의 프로그램의 동작을 바꾸지 않는 것이 특징입니다. 만약 설계 개선을 통해 동작이 변경된다면 이는 버그이기 때문에 다시 변경이 필요하게 됩니다. 많은 경우에 이와 같은 경우로 인해 자주 시도하지는 않습니다.

위와 같이 설계 개선하는 것을 리팩토링이라고 표현하는데, 기본적으로 프로그램의 동작이 변경되지 않았음을 확인할 테스트 코드를 작성하고 이를 이용해 변경된 코드를 검증하는 식으로 작업을 진행하게 됩니다. 가장 중요한 점은 앞에서도 언급한 버그가 나타나지 않게 리팩토링 전후에 기능상의 변경이 없어야 합니다.

최적화

최적화는 리팩토링과 비슷하지만 어떤 기능을 수행하는 시간을 줄이거나 사용하는 리소스를 줄이기 위해 프로그램을 변경하는 것을 말합니다.

위와 같은 이유로 프로그램을 변경할 때 중요한 점은 의도하지 않은 동작이 변경되지 않았음을 확인할 수 있어야 한다는 것입니다. 다만 코드의 수정이 어떤 영향을 미칠지 어렵다는 것이 문제입니다. 따라서 안전한 변경을 위해선 내가 지금 수정하는 코드가 얼마나 많은 코드에 영향을 미칠지를 정확히 이해하는 것이 필요합니다.

위험한 변경

프로그램을 변경하면서 위험을 최소화 하기 위해서는 다음 3가지를 따져볼 필요가 있습니다.

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

대부분의 팀들은 변경이 필요할 때, 기존 코드에 최소한의 변경만을 하며, 문제가 없으면 변경하지 않는 정책을 가지는 경우가 많습니다. 짧은 기간으로 봤을 때는 효율적으로 보일 수 있으나 장기적으로 봤을 땐 코드가 비대해지고 가독성이 떨어지는 원인이 됩니다. 하지만 좋은 시스템을 가진 팀이라면 변경에 확신을 가지고 진행할 수 있겠지만 나쁜 시스템을 가진 팀이라면 변경에 확신을 가지지 못하고 위와 같이 단기적으로만 대응 할 수 밖에 없습니다. 이런 나쁜 시스템이 지속 되면 컴포넌트가 비대해질 뿐만 아니라 개발자 개인의 실력을 키울 수 있는 기회도 놓치게 됩니다.

피드백 활용

프로그램을 변경하는 과정에서 아무리 신중하게 작업을 한다고 해도 그 작업이 안전해진다는 보장은 없습니다. 변경의 안정성을 높이기 위해서는 적절한 도구와 기법을 통해 변경이 완전한지 피드백을 받을 수 있는 시스템을 구축하는 것이 필요합니다. 바로 테스트코드 입니다. 잘 짜여진 테스트코드는 새롭게 변경된 코드가 기존에 제공하던 기능을 그대로 제공하는지 새로운 기능을 정확히 제공하는지 확인 할 수 있고 문제가 있는 부분도 빠르게 찾아낼 수 있게 합니다. 이러한 피드백을 통해 더욱 신중하고 안전하게 코드를 변경 할 수 있습니다.

일반적으로 회귀 테스트를 통해 많은 팀에서 테스트를 진행하고 있습니다. 프로그램을 실제 사용자 입장에서 테스트 하기 때문에, 직관적으로 문제를 확인 할 수 있지만 프로그램의 크기가 커질 수록 테스트에 소요되는 시간도 오래걸리고 문제가 발생했을 때, 이를 바로 확인하기도 어려운 문제가 있습니다. 따라서 각 코드의 유닛 테스트를 통해 테스트의 범위를 줄여 테스트의 소요 시간을 줄이고 문제가 생길 경우 원인이 되는 부분을 빠르게 확인 할 수 있도록 해야 합니다.

단위 테스트 (유닛 테스트)

유닛 테스트는 프로그램 내부의 독립적인 개별 컴포넌트를 테스트하는 것을 의미힙니다. 이때 컴포넌트는 프로그램 내에서 가장 작은 동작 단위를 나타냅니다. 절차지향 프로그래밍 언어에서는 함수를 뜻하고 객체지향 프로그램에서는 클래스를 의미합니다. 이렇게 작은 단위의 테스트를 통해 빠르게 오류 위치를 파악 하고 각 테스트의 실행 시간을 줄이고 최대한 적은 코드로 세밀한 부분까지 테스트 하게 됩니다. 그로인해 테스트 코드 작성 및 실행에 대한 피로감을 줄여 더 유용하게 사용될 수 있게 합니다.

좋은 단위 테스트의 조건

위에서 말한 장점을 살리기 위해 아래와 같은 조건을 만족하는 것이 좋습니다.

  1. 실행 속도가 빠릅니다.
  2. 오류 위치 파악에 도움이 됩니다.

기본적인 단위테스트는 특별한 이유가 없는한 0.1초 이내로 끝나야 합니다.

단위 테스트가 아닌 테스트

위에서 언급한 특징들 때문에 아래의 테스트들은 단위 테스트라고 보기 어렵습니다.

  1. 데이터베이스와 연동해야하는 테스트
  2. 네트워크를 통해 통신해야하는 테스트
  3. 파일시스템을 수정해야하는 테스트
  4. 테스트 실행을 위해 코드 외적인 부분에 대해 별도의 작업이 필요한 테스트

완벽한 테스트를 위해 필요한 테스트이긴 하나 단위테스트 레벨에서는 이 부분을 분리하여 실제 원하는 부분만 테스트 할 수 있어야 합니다.

테스트를 통한 코드 보호

레거시 프로젝트를 수정할 땐 변경할 코드에 테스트 코드 부터 먼저 추가해야 합니다. 테스트 코드가 새롭게 변경한 코드의 정합성을 판단하는 데에 도움을 주고 기본적인 버그를 잡을 수 있게 합니다. 다만 항상 모든 레거시 클래스에 단위 테스트 코드를 추가하기는 어렵습니다. 의존 관계 때문입니다. 경우에 따라선 이 의존 관계의 분리를 위해 원래 진행하고자하는 코드 변경 보다 더 큰 변경이 발생 할 수도 있습니다. 경우에 따라서는 의존관계로 묶이는 클래스들을 묶어 큰 단위 테스트를 만들 수 있지만, 어느 부분에서는 의존관계를 끊어줘야하기 때문에, 결국에는 어느정도의 정리가 불가피하게 됩니다.
따라서 의존성이 생기는 부분을 확인하여 인터페이스를 추출하거나 필요한 정보들만 전달하는 형태로 리팩토링하고 테스트코드를 작성하는 방향으로 진행합니다. 물론 오류가 생길 수 있는 경우라면 무리해서 리팩토링을 진행하는 것 보다는 보수적으로 진행하는 것도 좋은 방법입니다.

레거시 코드를 변경하는 순서

앞서 나온 내용들을 정리하여 다음과 같은 순서로 작업할 수 있습니다.

  1. 변경 지점을 식별합니다.
  2. 테스트 루틴을 작성할 위치를 찾습니다.
  3. 의존 관계를 제거합니다.
  4. 테스트 코드를 작성합니다.
  5. 변경 및 리팩토링을 수행합니다.

감지와 분리

많은 레거시 코드들이 클래스들간의 의존관계에 의해 원하는 객체들만 테스트코드로 감싸기 어려운 경우가 많습니다.
여러가지 경우가 있겠지만 지금은 스트 대상 클래스가 다른 클래스에 주는 영향을 알아봐야 할 경우에 사용할 수 있는 방법에 대해 이야기 하려고 합니다.

테스트코드를 작성할 때 의존관계를 제거하는 이유

  1. 감지 - 컴포넌트 내부에 계산된 값을 확인하고 싶을 때, 이를 감지하기 위해 해당 값을 사용하여 통신하는 의존 관계의 클래스를 분리합니다.
  2. 분리 - 코드를 테스트코드로 실행할 때, 코드를 분리하기 위해 의존 관계의 클래스를 분리합니다.

가짜 객체

단위테스트를 위해 코드를 분리한 후에는 코드 내부에서 사용되는 값을 감지 할 수 있다고 했습니다. 이를 위해서 필요한 것이 바로 가짜 객체 입니다. 대부분의 경우 내부에서 계산된 값은 의존성을 가지는 협업객체로 전달이 되는데, 이 때 협업객체를 우리가 테스트하기 좋은 형태의 가짜 객체를 만들어 주입하여 내부의 값을 감지하는 것입니다.

1
2
3
4
5
6
7
8
9
10
11

public class SessionManager {

private RedisClient client;
private Decryptor decryptor;

public Session getSession(String key) {
return decryptor.decrypt(client.get(key));
}
}

위와 같은 예제에서 SessionManager.getSession을 실행하는 일반적인 상황이라면 실제 Redis 인스턴스에 키를 조회하여 데이터를 불러온 후 복호화 객체를 통해 데이터를 복호화 하여 데이터를 리턴하는 형태를 가지게 되는데, SessionManager의 getSession 메소드의 단위테스트에서는 조회된 데이터가 실제로 복호화 객체에 전달 되는지가 중요하기 때문에, 아래와 같은 가짜 객체들로 테스트를 할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

class MockRedisClient implements RedisClient {
Session get(String key) {
return DEFAULT_SESSION;
}
}

class MockDecryptor implements Decryptor {
Session lastDecrypt;

Session decrypt(Session session) {
this.lastDecrypt = session;
return session;
}
}

void someTestcode() {
var mockRedis = new MockRedisClient();
var mockDecryptor = new MockDecryptor();
var sessionManager = new SessionManager(mockRedis, mockDecryptor);
var result = sessionManager.getSession("key");

assert result.equals(DEFAULT_SESSION);
assert mockDecryptor.lastDecrypt != null && mockDecryptor.lastDecrypt == DEFAULT_SESSION;
}

고정 된 값만 리턴하는 가짜 RedisClient, 복호화는 하지 않고 마지막으로 복호화 요청한 객체만 저장하는 가짜 Decryptor를 이용해 위와 같은 테스트 코드를 작성했습니다.
위 테스트코드를 통해 SessionManager의 getSession메소드가 우리가 의도한 대로 실행됨을 확인 할 수 있었습니다. 실제 객체들을 사용하지 않는 만큼 프로그램 전체로 봤을 때는 완벽한 테스트라고 하기는 어렵겠지만 이 단위테스트가 테스트하는 SessionManager라는 클래스에 대해서는 충분히 테스트를 해볼 수 있기 때문에, 충분한 신뢰성을 가질 수 있다고 할 수 있으며, 또한 버그가 있을 경우 어떤 분이 문제일지 바로 확인 할 수 있기 때문에 디버깅 시간을 크게 줄일 수 있습니다.

mock

최근엔 위의 예제 처럼 직접 가짜객체를 만들지 않고 mock 객체를 만들어 사용하는 것이 더 일반적입니다.
자바의 경우 mockito, powermock, 코틀린의 경우 mockk라는 라이브러리를 통해 해당 클래스 또는 인터페이스의 mock객체를 별도의 클래스 구현없이 만들고 일관된 형태로 관리할 수 있어 단위테스트작성에 많은 도움이 됩니다.


출처
이 글은 레거시 활용 전략의 1~3 장까지의 내용을 정리한 글입니다.