비대해진 코드, 추출해서 가볍게 만들기
코드로 계속 무언가를 만들어나가다보면 어느새 끝도 모르고 길어진 코드를 볼 수 있다. 프로그램은 점점 더 기능이 추가되고, 그 속에서 요구사항도 많아진다. 처음에는 단순하게 추가한 코드 파일이 몇 달이 지나고 거대한 파일이 된 경험은 아마 한 번쯤은 있을거다.
길어진 코드를 보면서 ‘적당히 다른 함수로 분리해야지’ 하는 생각이 들 수도 있다. 적당히 분리하는 것 말고 더 나은 해결책이 있을 수 있다. 코드를 잘 떼어낼수록 프로그램은 선명해지고 이해하기 쉬워진다. 유닛 테스트의 범위가 늘어나고 비슷한 종류의 작업을 하기가 더 쉬워진다. 길고 복잡한 코드베이스로부터 단순하고 명확함을 갖추는 것이 개발자의 스킬이다.
분리보다 추출하기
코드를 나눌 때는 모듈을 얼마나 독립적으로 나눌 수 있느냐가 중요하다고 생각한다. 거대한 코드를 독립적인 작은 모듈의 집합으로 바라보면 코드를 더 잘 나눌 수 있게 된다. 여러 가지의 일을 하는 함수일지라도 외부 맥락에 의존하지 않고 독립적으로 동작하는 코드가 되어야 한다.
function getSilverUsers(users) {
const result = [];
for (let i = 0; i < users.length; i++) {
const user = users[i];
if (user.age >= 65) {
result.push(user);
}
}
}
위의 코드는 단순하지만, 너무 길어서 당장이라도 분리하고만 싶은 코드라고 상상해보자. 코드를 분리할 때, getSilverUsers
라는 함수의 맥락에 얽혀있지 않은 독립성을 가지는 함수로 나눠보자.
function filter(arr, callback) {
const result = [];
for (let i = 0; i < arr.length; i++) {
if (callback(arr[i])) {
result.push(arr[i]);
}
}
return result;
}
function getSilverUsers(users) {
return filter(users, (user) => user.age >= 65);
}
filter
함수는 배열에서 조건에 따라 필터링하는 독립적인 함수다. 나는 이를 코드를 분리(separation)하는 것이 아닌 추출(extraction)하는 과정이라고 생각한다.
추출하는 과정은 큰 문제를 작은 문제로 분할하는 과정이다. 크고 복잡한 책임을 가진 함수에서 단순한 책임을 가진 함수로 만드는 일이다. 위의 예시에서 filter
는 배열의 필터링에 관한 책임을 맡았듯이 작은 문제는 이메일을 보내는 일이 될 수 있고, 두 좌표 사이의 거리를 계산하는 일이 될 수도 있다.
getSilverUsers
의 내부는 의사만 남은 깔끔한 함수가 되었다. 추출이 잘 된 코드는 코드의 세부구현이 아닌 의사적인 부분만 남게되어 의도를 이해하기가 쉬워진다. 각 모듈이 각각의 문제만을 해결할 수 있도록 독립적으로 잘 분리할 수 있게되면 테스트하기가 훨씬 편해지고 신뢰성을 담보하기 쉬워진다.
추상화의 벽
추출은 동작의 세부 구현을 감추고 의도를 드러내도록 만든다. 이걸 좀 더 극적인 형태로 발전시키면 어떤 데이터 구조나 도메인을 구성하는 데에도 적용할 수 있다. 자료 구조의 내부나 도메인에 관한 세부 구현은 감추고 이를 통해서 실행할 수 있는 연산(인터페이스)만 드러내면, 최적화 등과 같은 이유로 세부 구현을 통째로 바꿔도 외부에는 영향을 미치지 못한다. SICP와 같은 책에서는 이를 추상화의 벽(Abstraction Barrier)이라고 한다.
추상화의 벽
추상화의 벽을 이용하면 변화하기 쉬운 구조를 만들 수 있다. 자료구조를 수정해 최적화하기도 쉬워지고, 도메인과 관련된 내용이 바뀌어도 대응할 수 있고, 특정 라이브러리의 인터페이스가 바뀌어도 유연하게 대처할 수 있다.
추출을 통해 하나의 독립적인 구현을 만들어나가고, 발전시켜 추상화의 벽을 세운다면 변화로부터 안전하게 보호하면 더욱 단단한 프로그램을 만들어나갈 수 있다.