8장과 9장에서는 계층형 설계에 대해서 다룬다. 요컨대 소프트웨어를 개발할 때 왜 계층별로 나눠서 코드 의존도에 따라 기능을 설계하고 구현해야하는지, 그 이유를 설명하는 챕터다.
책에서는 '계층을 정확히 분리하기란 매우 어려우며, 좋은 설계를 위한 감각을 개발하고 따라가면 찾을 수 있지만 그걸 개발하는 방법에 대해서는 명확하게 말하기 어렵다'는 알 수 없는 소리를 한다.
대표적인 3계층
1. Presentation 계층
2. Domain(Business or Service) 계층
3. Data Access(Persistence) 계층
– 마틴 파울러의 책, PoEAA (Pattern of Enterprise Application Architecture: 엔터프라이즈 애플리케이션 아키텍처 패턴)
그래서 다른 레퍼런스를 찾아보니 "Clean Architecture: A Craftsman's Guide to Software Structure and Design(Robert C. Martin)"라는 책에서 보통 3계층, 혹은 4계층으로 구별한다고 저술한 걸 확인할 수 있었다.
상기 인용문에 적은 것과 마찬가지로 Presentation(웹) 계층, 도메인(비즈니스) 계층, Persistence(영속성) 계층으로 구성되어있으며 경우에 따라 Persistence 계층 하위에 데이터베이스 계층을 추가한 4계층 형태도 가능하다고 한다.
Robert C. Martin, 속칭 엉클 밥은 계층형 설계의 장점으로 각 계층의 관심사가 다른 계층과 철저히 분리되어 다른 계층이 할 역할에 대해 알 필요가 없는 "관심사의 분리"를 꼽았다.
계층형 설계의 장점인 관심사의 분리를 활용하려면 어떤 패턴으로 계층형 설계 구조를 만들어야할까?
책에서는 위 4가지 패턴이 설계 시에 가장 중요하다고 언급하고 있다. 하나씩 차근차근 알아보자.
코드로 예를 들어 설명하느라 이 패턴의 요점을 단번에 파악하기 쉽지 않지만, 요는 이렇다. 저수준의, 마치 for문 같은 반복문을 함수 내에 직접 쓰기보다는 이를 추출해 새로운 함수로 감싸 모든 함수의 수준을 비슷하게 만들어 필요한 곳에서 호출해 쓰는 패턴!
위의 이미지는 단계별로 함수를 호출하는 그래프지만, 예를 들어 가장 저수준의 함수를 main
함수에서 직접 호출해서 썼다고 생각해보자. 거기에 for문이나 조건문까지 이것저것 써야하는 상황이었다면?
당연히 읽기 어려워진다. 그렇기 때문에 직접 구현 패턴을 통해 함수의 추상화 단계를 비슷하게 맞추고 위의 이미지처럼 상위 단계 함수에서는 바로 하위 단계의 함수만 쓰도록 프로그래밍하면 main
에서는 banana
함수가 뭔지 신경쓰지 않아도 된다(오류가 나지 않는 이상).
직접 구현 패턴에서 중요한 점은 같은 단계에 있는 함수는 같은 목적을 가져야 한다는 것이다. 예컨대 가장 상위 단계의 함수는 '사람이 음식을 먹는 기능'이라면, 그 하위 기능은 '채식 요리 기능', '생선 요리 기능', '고기 요리 기능'처럼 요리 기능이 오고, 그 하위에는 '양배추','고등어', '닭' 등의 재료 수준의 함수가 오는 식이다. 우리가 식사를 할 때 샐러드에 어떤 재료들이 들어갔고, 생선은 얼마나 익혔으며, 토종닭인지 아닌지를 크게 개의치 않는 것처럼 가장 상위 단계의 함수는 그 하위 단계의 함수만 잘 알고 있으면 된다. 이것이 바로 직접 구현 패턴의 핵심이다.
직접 구현 패턴
- 직접 구현한 코드는 한 단계의 구체화 수준에 관한 문제만 해결한다.
- 호출 그래프는 구체화 단계에 대한 풍부한 단서를 보여준다.
- 함수를 추출하여 더 일반적인 함수로 만든다. → 재사용하기 쉬워진다.
- 함수는 바로 아래 계층에만 의존한다.
추상화 벽은 팀 간 책임을 명확하게 나누기 위해 사용한다고 나와있지만, 책에서 든 예시가 너무 터무니 없어서(?) 이해하기 어려웠던 패턴이었다(그도 그럴게, 마케팅 담당자와 개발자가 대화를 하는데 마케터가 직접 세일 관련된 코드를 만들기 위해 개발자가 추상화 벽을 만드는 대화였다😅).
이 희한한 이름의 벽은 세부 구현을 감춘 함수로 이뤄진 계층이다. 즉, 이건 직접 구현처럼 모든 계층을 나누는 기법의 패턴이 아니라 특정 계층을 기점으로 상위 단계와 하위 단계를 나누도록 하는 패턴이다. 이런 특징 때문에 추상화 벽은 API 개발 시에 활용하기 좋은 패턴이다. 최근에는 Next.js 등 프레임워크에 프론트엔드 자체 서버가 구현되어있기 때문에 프론트엔드 개발 시에도 API가 필요한 경우들이 있는데 이 때도 충분히 활용 가능할 것 같았다.
Eat()
Drink()
Fish()
Vegetable()
Meat()
fish[]
vegetables[]
meats[]
프론트엔드 개발자가 Eat()
, Drink()
두 기능을 개발하는데 어떤 요리를 먹고 마실지를 결정하는 함수들이 필요하다고 해보자. 하지만 정확히 어떤 재료들로 요리가 만들어지는지는 크게 중요하지 않을 때, 백엔드 개발자들은 API를 어떻게 구성해야할까?
Fish()
, Vegetable()
, Meat()
세 함수를 추상화 벽으로 만들기로 결정하고 이 함수들에서 사용하는 데이터 구조도 객체로 변경하기로 했다. 프론트엔드 개발자들이 당장 API가 필요하다고 하는 바람에 우선 추상화 벽 계층에 해당하는 함수들을 던져주고, 데이터 구조를 변경했지만 가장 상위 계층 함수인 Eat()
, Drink()
에는 전혀 문제가 발생하지 않았다.
즉, 추상화 벽 위에 있는 함수는 추상화 벽 하위에 어떤 데이터 구조가 있고, 그걸 어떻게 보내주는지 전혀 신경 쓸 필요가 없어지는 것이다. 이로 인해 책임 소재 또한 명확해진다.
언제 사용할까?
1. 구현의 잦은 변경이 예상될 때
2. 코드의 가독성을 높이고 싶을 때
3. 팀 간에 조율할 사항을 줄일 때
4. 주어진 문제에 집중할 때
이 패턴은 새로운 코드를 어느 계층에 추가하면 좋을지와 관련되어있다. 인터페이스 크기를 최소화하면 하위 계층에 불필요하게 기능이 커지는 걸 막을 수 있기 때문이다. 새로운 기능을 만들 때 하위 계층에 기능을 추가하거나 고치는 것보다 사우이 계층에 만드는 것이 핵심이다.
즉, 굳이 하위 계층에서 고치지 않고 상위 계층에서 최대한 문제를 해결하는데 사용하는 패턴인데 잘못하다가는 레거시 위에 모래성을 쌓는 격이 될 수 있으므로 조심해야한다.
코드 위치에 대한 설계를 결정하는 건 특히나 저연차의 개발자들에게는 어려운 일이다. 책에서는 예시로 든 함수가 액션이므로 함수 전체에 액션이 퍼져나간다는 점을 고려해 코드 위치를 결정했다. 이렇듯 코드가 가진 영향력과 추상화 단계를 고려해서 위치를 정하는 연습을 거듭하면 불필요한 인터페이스로 인해 생기는 변경이나 확장을 막을 수 있다.
편리한 계층은 Wrap-up에 가까운 내용이다. 이미 언급한 세 개의 패턴은 이론적으로 매우 중요하고 효과적으로 프로그래밍하도록 도와주는 건 사실이다. 하지만 실질적으로 지금 작업하는 코드가 편하고 가독성도 괜찮다면 코드가 지저분해졌을 때 설계를 적용해도 늦지 않는다는, 개발자들에게 위안을 주는 말들이 "패턴"이라는 용어로 감싸져서 다가온다.
이 챕터를 마지막으로 파트 1이 마무리되고 파트 2, 일급 추상으로 넘어간다. 파트 1은 추상적으로 함수형 프로그래밍에서 중요한 요소와 설계 패턴들을 설명해서 새로운 개념들을 조금 더 쉽게 이해했다.
파트 1에서는 유독 많이 나오는 말이 "챕터 10(그 이후 챕터들)에서 알아봅시다"였는데 이제 미뤄뒀던❔ 내용들을 학습할 준비가 드디어 된 것 같다. 앞으로 다룰 내용이 지금보다는 어렵겠다는 생각에 걱정도 되지만 발전하게 될 내가 기대되기도 한다. 착실하게 내용을 소화해보자!