기술은 항상 발전하는데
- 왜 우리의 프로젝트는 여전히 힘든가?
- 어떻게 하면 프로젝트를 성공적으로 할 수 있을까?
- 과연 중요한 것은 무엇인가?
라는 의문이 있었습니다.
복잡해서?
어플리케이션에는 많은 복잡성이 있기 때문이었습니다.
어플리케이션 개발은 결국 복잡도와의 싸움이며 이 복잡도는 크게 2가지로 나눌 수 있습니다.
- 필수적 복잡성(Essential complexity) : 어플리케이션이 해결해야하는 본질적 문제에 따른 복잡성 - 도메인
- 부수적 복잡성(Accidental complexity) : 어플리케이션 개발을 위해 사용하는 도구에 따른 복잡성 - 언어, 기술, 도구
은탄환은 없다(No silver bullet - 프레데릭 브룩스)
그 중에서 부수적 복잡성에 집중하고 있었으며, 이 부수적 복잡성을 해결하기 위해서 에너지를 주로 사용하니 결국 필수적 복잡성을 해결할 여력이 없어지게 되는 것이었습니다.
기술 관련된 내용에 집중하고 어느 순간 이 복잡성에 집중하고 해결하기 위한 인지과부하가 생겨서 진짜 문제인 도메인 해결을 등한 시 하게 됩니다.
- 어플리케이션은 계속 변화하고
- 이 변화는 결국 필수적 복잡성을 증대시키고,
- 여력이 없기 때문에 일회성 코드(절차적인 코드)가 계속 반복되고,
- 이 복잡성이 감당할 수 없을 만큼 커져서 결국 개편을 시도합니다.
- 하지만 이것도 결국 복잡함이 해결되지 않은 레거시에 의존하게 됩니다.
- 결국 악순환의 고리가 반복되는 것입니다.
이로 인해 소프트웨어 위기 가 발생하게 됩니다. - 유지보수를 하면 할 수록 비용이 더 증가 - 이는 기술적 부채와 연결할 수 있습니다.
시간이 지날수록 생산성이 저하
출처 : https://www.butterfly.com.au/blog/website-development/clean-high-quality-code-a-guide-on-how-to-become-a-better-programmer
소프트웨어 위기 해결사 OOP
이 소프트웨어 위기를 구원하는 여러 방안들 중 OOP 가 등장하게 됩니다.
제가 OOP에 관심을 가지게 된 것도 위와 같은 과정을 직접 겪었기 때문입니다. OOP의 강력한 무기(캡슐화, 추상화, 다형성, 상속, SOLID)를 바탕으로 이 복잡성을 다룰 수 있다고 믿었기 때문입니다. 물론 OOP를 익히기 위해서는 그에 상응하는 댓가(학습곡선)가 필요하지만 이를 넘어서면 OOP를 바탕으로 어떤 복잡한 도메인 일지라도 다룰 수 있다고 생각합니다.
간단한 산수 예를 들면
가정
- 프로젝트를 기존방식대로 하면 비용:5
- 프로젝트를 OOP방식대로 하면 비용:3
- OOP를 익히는 비용:5
라고 한다면
- 기존방식대로 프로젝트 2회 :
5 + 5 = 10
- OOP방식대로 프로젝트 2회(OOP 익히는 비용 포함) :
5 + 3 + 3 = 11
2회일 경우 기존방식이 유리합니다. 하지만 3회 이상부터는 OOP를 익힐 시 더 낮은 비용이 들어갑니다. 또한 유지보수 상황에서도 필수적 복잡성을 더 효율적으로 해결할 수 있는 OOP의 비용이 당연히 더 낮을 것입니다.
학습곡선이 있는 기술이라도 그에 상응하는 보상(복잡성 감소)이 있다면 배우는 것이 좋지 않을까요?
넷플릭스와 같은 회사는 항상 최고의 인재만을 추구한다고 합니다. 결국은 그것이 서비스의 품질 향상으로 이어지고 있구요. 당장 인력 비용은 높아지지만 복잡성을 감소시키고 유지보수 비용을 낮출 수 있다면 장기적으로 회사 입장에서도 그것이 더 이득이 되지 않을까요?
추상화
잠깐 추상화의 이야기를 해보자면 원래 추상화라는 개념은 인간이 근본적으로 가지고 있는 개념이며 아리스토텔레스의 철학에서도 확인할 수 있습니다. - 분류추상화
예를 들면 인간은 개라는 추상화 존재를 인지합니다. 바둑이, 점박이, 스누피 등 여러 개(Dog) 인스턴스가 있는데, 본능적으로 내제된 추상화를 바탕으로 “털로 덮여 있으며 인간을 잘 따르는 4족 보행의 동물”을 개로 분류(Classification) 수 있습니다. 실질적으로 개는 존재하지 않습니다. 바둑이가 존재할 뿐이죠. 만약 모든 개(Dog) 인스턴스를 각각 기억할려면 아마도 인지과부하가 걸릴 것입니다.
이 추상화를 통해서 문제를 더 단순하게 해결할 수 있습니다.
그런데 OOP가 더 복잡한 거 같아요
하지만 많은 개발자들은 절차적 코드를 더 편하게 생각합니다.
Divide and Conquer
가장 큰 이유는 OOP의 강점인 캡슐화를 제대로 다루지 못해서라고 생각합니다.
한 트랜잭션이 존재할 시 만약 OOP방식이라면 서비스에서 각종 도메인 로직을 위임기반으로 호출했을 것입니다. 그 중 결제알림을 이용할 시 해당 서비스에서는 결제를 알림도메인을 호출했을 것입니다.
결제
-> 결제알림
-> ?
결제알림 도메인 로직은 적절한 인터페이스로 추상화되어 있고 결제 입장에서는 상세 결제 알림을 알 필요가 없습니다. 그저 결제알림만 호출하면 그뿐이죠 결제의 책임은 여기까지 이고 알림의 책임은 결제알림에 위임하면 되는 것입니다.
결제알림의 세부구현을 알 필요가 없는 것(정보 은닉)입니다. OOP는 이것을 캡슐화해서 결제알림 인터페이스를 클라이언트(결제) 측에 제공할 뿐입니다. 그 캡슐화의 경계가 대부분 인터페이스(추상화)가 됩니다. 클라이언트의 인터페이스 호출과 구상체의 인터페이스 구현 사이
즉, 캡슐화 경계(Scope)를 기점으로 나누고 정복하는 것(Divide and Conquer)입니다. 일반적으로 관심사의 분리에서 시작합니다.
관심사를 분리해서 분리된 조각 별로 단순하게 해결할 수 있습니다.(복잡성 낮아짐) 하지만 많은 개발자들이 머릿속에서 Stack 처럼 모든 흐름을 기억하고 가려고 하기 때문입니다.
기존 흐름은 머릿속에서 휘발시키고 현재 관심사 영역만 집중합시다. - 작업 끝나면 그 때 다시 불러오면 됩니다.
결제를 할 때는 결제 도메인에 집중하고 다시 결제알림 모듈을 개발할 시에는 결제알림 구현에 오롯이 집중해야합니다. - 결제는 잊어버리고 이런 식으로 OOP에 접근하는 사고방식이 있어야지 그 복잡성의 늪에서 빠져나올 수 있습니다.
적절한 추상화
또한 경계 사이마다 적절한 추상화 가 필요합니다.
이 적절한 추상화를 할 수 있는 설계 능력이 중요한 핵심 중 하나인데, 이 부분이 가장 어려운 것 같습니다. 너무 일반적이거나 너무 구체적이면 아래와 같은 문제점이 보이게 됩니다.
추상화 레벨 | 결과 |
---|---|
높음 | 오버엔지니어링, 도메인 표현이 약해짐 |
적절 | Good! |
낮음 | 결합도 높아짐, 재사용성 낮아짐 |
TDD
경계 사이 구현 전에 테스트를 먼저 생성하면 금상첨화입니다.
But
컴포넌트 사이 이동에 비용은?
- 각 컴포넌트 이동 시 이전 구현에 대한 휘발을 확실히 해야합니다. - 하지만 그게 어려움
- 의식적으로 방금 개발한 내용은 잊어버리도록 해야합니다. - 지속적 훈련 필요?!
- 마치 Context switching 와 비슷? - 그럼 휘발시키는 것도 비용?
요약
- 기술의 복잡성 함정에 빠지지 말고 도메인에 먼저 집중 (No silver bullet)
- 캡슐화 경계를 바탕으로 Divide and Conquer
- 적절한 추상화