객체지향과 탈 국지화
    2024-02-04 10:00
    OOP

    최근 두 권의 책을 병행해서 읽는 중인데 서로 조금 상반되는 내용을 읽게 되어 내 생각을 정리해 보는 시간을 가져보았다.

    한 권은 펠리너 헤르만스가 쓴 '프로그래머의 뇌'이고 다른 한 권은 조영호 님이 쓴 '오브젝트'이다.

    두 권 모두 읽게 된 계기가 있다. 프로그래머의 뇌는 기능을 추가하거나 수정할 때 코드를 읽고 영향도를 파악하는 역량이 부족하다고 느껴서 읽게 되었고 오브젝트는 작년에 한 번 읽었지만 내용을 다 까먹기도 했고 마침 사내 스터디를 모집해서 읽게 되었다.

    두 책의 상반된다고 느꼈던 부분을 먼저 정리해 보았다.

    코드 분석과 인지 과정

    '프로그래머의 뇌'에서는 개발자가 코드를 읽을 때 세 가지 인지 과정이 일어난다고 설명하고 있다.

    첫 번째는 LTM(Long Term Memory)으로 컴퓨터의 하드디스크같이 장기 기억 저장소라고 할 수 있다. 이는 반복 학습을 통해서 개발자가 습득한 지식으로 이미 알고 있는 지식을 기반으로 코드를 청크내어 읽고 분석할 수 있기 때문에 코드를 파악하는데 드는 인지적 부하를 줄여준다.

    다음과 같은 피보나치수열의 경우, 재귀 함수의 기본 구조에 대한 이해가 LTM에 없다면 코드를 파악하고 분석하는데 더 큰 비용과 노력이 필요하게 된다.

    fun fibonacci(n: Int): Int { return if (n <= 1) n else fibonacci(n - 1) + fibonacci(n - 2) } fun main() { println(fibonacci(10)) }

    두 번째는 STM(Short Term Memory)으로 컴퓨터의 메인 메모리와 유사하게, 한 번에 한정된 정보만을 처리할 수 있다. 새로운 코드를 읽을 때, 개발자는 변수 이름, 함수 호출, 알고리즘의 로직 등과 같은 여러 정보를 STM에 저장한다. 하지만 STM의 용량에는 제한이 있기 때문에 많은 정보를 저장하게 되면 일부를 잃어버리게 된다.

    세 번째는 작업 기억 공간으로 컴퓨터의 프로세서와 같아서 LTM과 STM의 정보를 조합하여 연산하는 등의 사고 작용을 담당한다.

    객체의 역할, 책임, 협력

    '오브젝트'에서는 객체의 역할, 책임, 협력의 중요성에 대해서 강조하고 있다.

    하나의 시스템은 여러 객체의 협력을 통해서 만들어지고, 이 과정에서 각 객체는 고유의 역할과 책임을 가지고 다른 객체로부터의 메시지를 처리한다.

    이렇게 설계된 객체의 품질을 측정하는 척도로 응집도와 결합도가 사용될 수 있다. 잘 설계된 객체일수록 내부 상태 값 간의 응집도가 높고 다른 객체와의 결합도는 낮다. 이런 설계는 변경에 대해 유연하게 대응할 수 있게 한다. 반면, 응집도가 낮고 결합도가 높은 설계는 유연성이 떨어지게 된다.

    이러한 응집도와 결합도를 결정하는 것은 캡슐화다. 즉, 잘 추상화된 객체가 좋은 설계를 의미하며, 객체의 품질은 캡슐화 수준에 따라 결정된다.

    확실히 추상화가 가져오는 이점은 많다. 추상화는 복잡한 시스템을 단순화하고, 코드의 재사용성을 높이며, 유지보수를 용이하게 하고, 소프트웨어 설계의 유연성을 향상한다.

    그러나 코드의 인지 과정에 대해 고려해 볼 때, 추상화 수준이 높아질수록 코드를 파악하는 데 필요한 노력이 증가할 수 있다는 생각이 들었다. 추상화가 복잡성을 감추는 동시에, 그 이해를 위해 더 많은 맥락을 파악해야 하기 때문이다.

    코드의 탈 국지화에 의한 인지 과부하

    객체 지향적으로 잘 작성된 코드는 SRP 원칙을 준수하며 변경 지점이 한곳이 되어 유지보수하기 좋은 코드가 된다. 하지만 코드가 유지보수하기 좋게 수정되었다고 해서 반드시 가독성까지 좋아지는 것은 아니다.

    여러 객체의 협력이 필요한 복잡한 비즈니스 로직을 가정해 보자. 해당 로직을 구현하는 데 필요한 메시지들은 여러 적절한 객체에게 할당될 것이다. 그리고 각 객체는 자신에게 할당된 메시지를 처리하기 위해 필요한 정보를 갖고 있는 전문가에게 또 다른 메시지를 전달할 것이다.

    이때 캡슐화를 높이기 위해서 메서드에 전달하는 인자의 수마저 최소화한다면 개발자는 내부 구현을 파악하기 위해서 해당 메서드가 정의되어 있는 객체들을 찾아 찾아 넘나들어야만 한다.

    이렇게 탈 국지화된 코드는 여러 군데에서 메서드의 내부 구현을 찾아봐야 하므로 작업 기억 공간에는 어려움을 줄 수 있다.

    '프로그래머의 뇌'에서는 유지보수하기 좋은 코드를 작성하기보다는 장기적으로 가독성이 높은 코드로 리팩터링 하는 방식을 인지적 리팩터링이라고 소개하고 있다.

    그렇다면 객체지향을 통한 유지보수와 인지적 리팩터링을 통한 가독성 중 무엇을 선택해야 할까?

    표식을 통한 인지 과부하 개선

    변경될 가능성이 거의 없는 복잡한 비즈니스 로직이라면 인지적 리팩터링을 통한 가독성 개선을 고려해 볼 수 있다고 생각한다.

    하지만 그 외에 변경될 가능성이 조금이라도 존재하는 영역은 유지보수를 고려한 설계를 해야 한다고 생각한다.

    대신 탈 국지화에 의한 인지 과부하를 줄이기 위한 수단이 필요한데, 책에서는 표식을 남기는 것이 도움이 된다고 설명하고 있다. 여러 파일을 거쳐서 내부 구현을 파악하고 다니다 보면, 내부 구현을 파악하고 돌아왔을 때 그 내용을 잊어버리는 경우가 있을 수 있는데, 이때 내부 구현에 대한 내용을 함축한 표식을 보게 되면 쉽게 상기할 수 있다.

    여기에서 표식은 주석이 될 수 있다. 주석 사용 여부가 종종 개발자들 사이에서 논쟁거리가 되곤 하지만 개발자들은 코드를 읽을 때 주석 문에 굉장히 많이 의존하며 고수준의 주석 문은 코드를 청크 단위로 쪼개는 데 도움을 준다고 논문을 기반으로 책에서는 설명하고 있다. (반면 저수준의 주석 문은 오히려 청킹 작업에 부담을 준다)

    그렇다고 모든 함수의 결과를 주석으로 다는 것에 대해선 부정적으로 생각한다. 주석도 코드의 일부이기 때문에 코드를 파악하는 데 드는 시간이 늘어나기 때문이다. 코드 자체가 표식의 역할을 할 수 있다면 더 효율적일 것이다.

    주석 대신 메서드명과 변수명이 자신의 역할을 잘 표현하고 있다면 코드를 표식으로 사용하는게 가능해진다. 이는 코드의 가독성을 자연스럽게 향상시키며, 개발자가 코드를 읽고 이해하는 데 소요되는 시간을 줄요준다.

    즉, 가독성과 유지보수성 사이의 트레이드 오프를 최소화 하기 위해서는, 코드의 구조를 잘 설계하는 것과 더불어, 각 구성 요소의 네이밍에도 신경을 써야한다.

    마무리

    두 책을 읽고 나서의 나의 견해를 정리하자면 변화의 가능성이 없는 로직의 경우 가독성을 우선시해도 괜찮다고 생각한다. 그러나 대부분의 경우 변화를 예측하기란 어려운 일이기 때문에 가능한 모든 영역에서 유지보수를 고려한 객체 지향적 설계를 해야만 한다고 생각한다. 대신 의미 있는 변수명과 메서드명을 부여하여 탈 국지화된 코드를 파악하는 과정에서 표식 역할을 할 수 있도록 하는 게 중요하다.

    변수명과 메서드명의 중요성은 OOP에 대해서 알기 이전인 프로그래밍을 처음 배웠을 때부터 들었던 내용이다.

    그만큼 중요하다는 것이었는데 다시 한번 기본의 중요성을 느낄 수 있었고, 어느 순간 네이밍의 중요성을 잊고 지냈던 자신을 반성하는 시간을 가지는 계기가 되었다.