관리 메뉴

나만을 위한 블로그

[이펙티브 코틀린] 아이템 26. 함수 내부의 추상화 레벨을 통일하라 본문

책/Effective Kotlin

[이펙티브 코틀린] 아이템 26. 함수 내부의 추상화 레벨을 통일하라

참깨빵위에참깨빵_ 2023. 1. 9. 02:09
728x90
반응형

개발자 관점에서 컴퓨터에서 가장 낮은 추상화 계층은 하드웨어다. 개발자는 일반적으로 프로세서를 위한 코드를 작성하므로, 하드웨어 위의 관심 있는 계층은 프로세서 제어 명령이다. 이런 프로세서 제어 명령은 원래 0, 1로 이뤄지지만 이를 쉽게 읽을 수 있게 일대일로 대응된 어셈블리라는 언어로 표현한다.

하지만 어셈블리어로 프로그래밍하는 건 굉장히 어렵고, 오늘날 우리가 쓰는 것과 같은 앱을 만드는 것은 상상도 할 수 없다. 프로그래밍을 간단하게 할 수 있게 엔지니어는 한 언어를 다른 언어(일반적으로 낮은 레벨 언어)로 변환하는 프로그램인 컴파일러를 만들었다.

최초의 컴파일 언어는 어셈블리어로 작성됐으며 텍스트로 작성된 코드를 어셈블리 명령어로 변환했다. 그리고 최초의 컴파일 언어는 더 나은 프로그래밍 언어를 만드는 데 사용됐고, 그 언어는 또 더 나은 프로그래밍 언어를 만드는 데 사용됐다. 그렇게 C, C++ 등의 높은 레벨 언어들이 탄생했다.

이런 프로그래밍 언어는 프로그램, 앱을 만들 때 사용된다. 이후에 추상 머신과 인터프리터 언어의 개념이 등장했다. 물론 현대적인 자바와 자바스크립트 등의 언어를 여기 포함시키기는 어렵지만 추상 계층이라는 일반적인 개념은 계속 남았다.

 

계층이 잘 분리되면 무엇이 좋은가? 어떤 계층에서 작업할 때 그 아래의 계층은 이미 완성돼 있으므로 해당 계층만 생각하면 된다는 것이다. 즉 전체를 이해할 필요가 없어진다. 예를 들어 어셈블리어, JVM 바이트코드가 뭔지 몰라도 프로그래밍할 수 있다. 개발자는 일반적으로 특정 계층에서 작업하며 가끔 그 위에 추가로 계층을 올려 사용한다.

 

추상화 레벨

 

일반적으로 컴퓨터 과학자들은 어떤 계층이 높은 레벨인지 낮은 레벨인지를 구분한다. 높은 레벨로 갈수록 물리 장치로부터 점점 멀어진다. 프로그래밍에선 일반적으로 높은 레벨일수록 프로세서로부터 멀어진다고 표현한다. 높은 레벨일수록 걱정해야 하는 세부적인 내용들이 적다. 하지만 무엇이든 좋을 수는 없다. 높은 레벨일수록 단순함을 얻지만 제어력을 잃는다. C언어는 메모리 관리를 직접할 수 있다. 반면 자바는 가비지 컬렉터가 자동으로 메모리를 관리해주기 때문에 메모리 사용을 최적화하는 것이 굉장히 힘들다.

 

추상화 레벨 통일

 

코드도 추상화를 계층처럼 만들어 사용할 수 있다. 이를 위한 기본 도구가 함수다. 컴퓨터 과학이 높은 레벨, 낮은 레벨을 확실하게 구분하고 있는 것처럼 함수도 높은 레벨, 낮은 레벨을 구분해서 써야 한다는 원칙이 있다. 이를 추상화 레벨 통일(Single Level of Abstraction, SLA) 원칙이라 부른다.

버튼 하나만 누르면 커피를 만들 수 있는 커피 머신을 나타내는 클래스를 만든다고 가정한다. 커피를 만드는 것은 커피 머신의 여러 부분들이 필요한 복잡한 작업이다. 아래처럼 makeCoffee라는 함수 하나를 갖는 CoffeeMachine 클래스를 만든다. 이 함수 내부에는 여러 로직들을 구현할 수 있을 것이다.

 

class CoffeeMachine {
    fun makeCoffee() {
        // 여러 변수 선언
        // 복잡한 로직 처리
        // 낮은 수준의 최적화
    }
}

 

하지만 이렇게 작성하면 makeCoffee()가 수백 줄이 될 수도 있다. 오래된 프로그램들은 이런 식으로 한 함수에 수많은 로직을 때려 넣어서 개발된 경우가 많다. 이런 함수는 함수를 읽으면서 세부적인 내용을 하나하나 신경써야 하므로 읽고 이해하는 게 거의 불가능에 가깝다. 만약 이런 코드에서 물의 온도를 수정해 달라는 요청을 받았다고 가정한다. 어딜 어떻게 수정해야 할지 감도 안 올 것이다. 그래서 최근에는 아래처럼 함수를 계층처럼 나눠서 쓰는 것이다.

 

class CoffeeMachine {
    fun makeCoffee() {
        boilWater()
        brewCoffee()
        pourCoffee()
        pourMilk()
    }
    
    private fun boilWater() {}
    private fun brewCoffee() {}
    private fun pourCoffee() {}
    private fun pourMilk() {}
}

 

이제 이 함수가 어떻게 동작하는지 확실하게 확인할 수 있다. makeCoffee()는 누군가가 낮은 레벨(boilWater(), brewCoffee() 등)을 이해해야 한다면 해당 부분의 코드만 보면 된다. 매우 간단한 추상화를 추출해서 가독성을 향상시킨 것이다.

이처럼 함수는 간단해야 한다. 이는 '함수는 작아야 하며 최소한의 책임만 가져야 한다'는 일반적인 규칙이다. 또한 어떤 함수가 다른 함수보다 좀 복잡하다면 일부분을 추출해서 추상화하는 게 좋다. 모든 추상화 레벨에서 추상 요소(메서드 또는 클래스)를 조작한다. 각각의 추상 요소가 어떤 내용을 담고 있는지 확인하고 싶다면 정의로 이동해서 확인하면 된다.

추가로 이런 형태로 함수를 추출하면 재사용과 테스트가 쉬워진다. makeCoffee()는 물 끓이고 커피 내리고, 커피를 붓고, 우유를 넣어서 라떼를 만드는 과정이다. 만약 에스프레소 커피를 만드는 기능을 추가한다면 우유만 안 넣으면 된다. 또한 boilWater(), brewWater() 같은 작은 함수도 테스트할 수 있기 때문이다.

 

프로그램 아키텍처의 추상 레벨

 

추상화 계층이라는 개념은 함수보다 높은 레벨에서도 적용할 수 있다. 추상화를 구분하는 이유는 서브시스템의 세부사항을 숨김으로써 상호 운영성과 플랫폼 독립성을 얻기 위함이다. 이는 문제 중심으로 프로그래밍한다는 의미다.

이런 개념은 모듈 시스템을 설계할 때도 중요하다. 모듈을 분리하면 계층 고유의 요소를 숨길 수 있다. 앱을 만들 때는 입출력을 나타내는 모듈(프론트엔드의 뷰, 백엔드의 HTTP 요청 처리 등)은 낮은 레벨의 모듈이다. 그리고 비즈니스 로직을 나타내는 부분이 높은 레벨의 모듈이다.

계층이 잘 분리된 프로젝트를 계층화가 잘 됐다고 부른다. 계층화가 잘 된 프로젝트를 좋은 프로젝트라고 부른다. 계층화가 잘 된 프로젝트는 어떤 계층 위치에서 코드를 봐도 일반적인 관점을 얻을 수 있다.

반응형
Comments