현대 C++에서 객체지향과 함수형 사고를 연결하기
Source: Dev.to
위에 제공된 소스 링크만으로는 번역할 본문이 없습니다. 번역을 원하는 전체 텍스트(마크다운 형식 포함)를 제공해 주시면, 요청하신 대로 한국어로 번역해 드리겠습니다.
의존성이 C++ 시스템을 테스트하고 진화시키기 어렵게 만드는 이유 — 그리고 함수형 사고가 이를 어떻게 바꾸는가
복잡성을 다루는 것은 소프트웨어 개발에서 성공하기 위해 필수적입니다. 그러나 실제 C++ 프로젝트에서는 시스템이 오히려 반대 방향으로 진화하는 경우가 많습니다.
- 긴밀한 결합 – 구성 요소들이 서로 강하게 결합되어 변경이 시스템의 큰 부분에 파급됩니다.
- 흩어진 상태 – 상태가 객체 그래프 전역에 퍼져 있어 시간이 지남에 따라 어떻게 변하는지 투명성이 떨어집니다.
- Emergent behavior(출현 행동) – 많은 상호작용하는 부분들로부터 행동이 나타나 개별 조각을 독립적으로 이해하기 어렵습니다.
이러한 특성은 테스트에서도 나타납니다. 테스트를 가능하게 하려면 상당한 양의 보일러플레이트와 간접화가 필요합니다. 특정 시스템 상태를 설정하려면 많은 사전 작업이 필요하고, 테스트를 유지하는 비용도 커집니다. 작은 프로덕션 변경조차도 테스트 코드의 큰 부분을 업데이트하도록 강요하기 때문입니다. 결과적으로 시스템을 올바르게 만들기 어렵고, 일단 동작하게 되면 경직되고 진화시키기 힘든 구조가 됩니다.
테스트 중심 관점
문제를 테스트 관점에서 바라보면 문제가 더 명확해집니다.
- 단위 테스트 가능한 코드 – 코드를 더 작은 단위로 구조화하여 독립적으로 실행할 수 있도록 합니다.
- 관심사의 분리 – 테스트할 단위를 제공합니다.
- 의존성 주입(DI) – 모듈을 독립적으로 실행하기 위해 모듈의 의존성을 전달합니다(보통 생성자 인자를 통해).
단일 모듈을 격리된 상태에서 실행해야 하는 단위 테스트에서는 모든 의존 모듈을 모킹해야 합니다. 실제 의존성 대신 테스트가 모크를 생성자 인자로 전달합니다.
첫 번째 문제: 모킹 없이는 테스트가 불가능
- 모킹은 기술적으로 작동합니다 – 단위 테스트가 가능해집니다.
- 비용이 높습니다: 모크를 작성하고 유지해야 하며, 테스트 전용 코드와 전체 테스트 복잡성이 증가합니다.
- 모든 모크가 올바르게 구현되어야만 테스트가 유용합니다.
두 번째 문제: 인터페이스가 긴밀한 결합을 만든다
- 모크는 교체하려는 실제 컴포넌트와 동일한 인터페이스를 구현해야 합니다.
- 인터페이스에는 종종 여러 상호 의존적인 함수 시그니처와 비 trivial한 전후 조건이 포함됩니다.
- 인터페이스에 대한 어느 작은 변경도 모든 구현과 모든 모크에 영향을 미쳐 유지 보수 비용이 늘어납니다.
- DI와 다수의 모크가 결합되면 이 인터페이스 수준의 결합이 특히 두드러지고 비용이 많이 듭니다.
- 우리는 테스트 가능성을 유연성에 대가로 교환하게 됩니다 – 좋지 않은 거래입니다.
세 번째 문제: OOP에서의 상태 처리
- 클래스 내부에 캡슐화된 상태는 테스트에서 수정하기 어렵습니다.
- private mutable 상태는 완전히 숨겨져 있어 테스트가 직접 설정할 수 없습니다.
- 특정 내부 구성을 만들기 위해 테스트는 공개 인터페이스를 통해 간접적으로 상태를 조작해야 하며, 이는 종종 여러 메서드 호출, 복잡한 시퀀스, 모크 사용을 요구합니다.
- 이러한 간접 설정은 복잡성을 추가하고 테스트를 깨지기 쉬운 상태로 만듭니다.
네 번째 문제: 분산된 상태
- 상태가 객체 그래프 전역에 흩어져 있어 숨겨진 의존성이 생기고 이를 파악하기 어렵습니다.
- 상태ful 객체가 많을수록 상태가 어떻게 변하는지 추론하기 어려워지며, 앞서 언급한 테스트 복잡성이 더욱 가중됩니다.
다섯 번째 문제: 상속에 의한 결합
- 인터페이스를 공유하는 구현들의 긴밀한 결합은 보다 일반적으로 상속이 계층 내에서 긴밀한 결합을 만든다고 표현할 수 있습니다.
- 한 번 클래스 계층이 구축되면, 기본 클래스 인터페이스를 변경하는 것이 거의 불가능에 가깝게 되며 모든 파생 클래스에 영향을 미칩니다.
- 계층이 깊어질수록 문제는 악화됩니다: 작은 조정조차도 전파되어 계층을 경직되고 수정 비용이 크게 늘어나게 합니다.
근본 원인 요약
이 모든 문제는 복잡성을 크게 증가시키며, 이는 서두에서 설명한 고통으로 이어집니다. 근본적인 원인은 의존성입니다:
- 서로에게 의존하는 컴포넌트들은 DI를 통해 연결될 때… (이하 내용은 다음 파트에 이어집니다)
Source: …
only be executed in isolation by introducing mocks.
- The broader an interface, the more complex the dependency between components sharing it.
- Inheritance creates dependencies between classes within hierarchies.
- Hidden, distributed, and evolving state creates implicit dependencies across the system.
우리가 살펴본 모든 문제는 다음과 같이 요약됩니다: 의존성이 시스템의 일부를 함께 변경하고, 함께 설정하고, 함께 이해하도록 강요한다는 점입니다. 이러한 의존성이 서로를 강화하면서 복잡도는 급격히 증가합니다.
코드를 구조화하는 다른 방법
수년간 따라온 설계 아이디어에 의문을 품고, 이러한 의존성을 관리할 대안적인 방법을 찾기 시작했습니다. 블로그 포스트 Mocking is Code Smell은 문제를 보다 명확히 보게 해 주었고, 특히 유용한 함수형 아이디어를 탐구하도록 이끌었습니다.
간단히 말해, 함수형 프로그래밍은 전통적인 C++ 도구 상자를 보완할 수 있는 기술들을 제공합니다. 이러한 기술이 효과적인 이유는 시스템을 설계하는 방식을 바꾸고, 그에 따라 어떤 의존성이 나타나는지를 바꾸기 때문입니다.
순수 함수가 바꾸는 것
| 측면 | 전통적인 OOP | 함수형 접근 |
|---|---|---|
| 의존성 선언 | 객체 주입을 통한 암묵적 선언 | 명시적, 모든 의존성을 함수 인자로 전달 |
| 목킹 | 주입된 객체를 대체하기 위해 필요 | 순수 함수는 숨겨진 협력자가 없으므로 보통 불필요 |
| 상태 | 객체에 숨겨지고, 가변적이며, 흩어져 있음 | 숨겨진 상태 없음; 모든 상태는 명시적으로 전달되고 불변 |
| 인터페이스 크기 | 많은 책임을 포괄하는 경우가 많음 | 단일 변환에 집중, 더 좁음 |
| 상속 | 행동 공유를 위해 사용, 계층 결합을 초래 | 사용되지 않음; 함수 조합이 상속을 대체 |
순수 함수를 빌딩 블록으로 사용할 때:
- 의존성이 명시적으로 변함 – 객체 내부에 숨겨지는 대신 인자로 전달됩니다.
- 목킹이 감소 – 외부 가변 협력자에 의존하지 않으므로 목킹할 것이 거의 없습니다.
- 상태가 가시화 – 함수가 필요로 하는 모든 상태가 직접 제공되어 숨겨진, 분산된 상태가 사라집니다.
- 인터페이스가 작아짐 – 함수 시그니처는 일반적으로 클래스 인터페이스보다 좁아 의존성을 단순하게 유지합니다.
- 상속 계층이 사라짐 – 전체 클래스 계층 결합 문제를 제거합니다.
요약
- 의존성이 주입, 광범위한 인터페이스, 상속, 숨겨진 가변 상태 등을 통해 누적될 때 C++ 시스템은 테스트와 진화가 어려워집니다.
- 함수형 프로그래밍 기법—특히 순수 함수—은 이러한 의존성을 다시 드러내어 명시적이고, 좁으며, 불변하게 만듭니다.
- 순수 함수(또는
std::function, 람다, 값 의미 타입과 같은 함수형 추상화)를 중심으로 C++ 코드베이스의 일부를 재설계하면 목킹 필요성을 크게 줄이고, 상태 처리를 단순화하며, 복잡성을 촉진하는 긴밀한 결합을 끊을 수 있습니다.
이러한 아이디어를 채택한다고 해서 OOP를 완전히 포기한다는 뜻은 아닙니다; 두 패러다임의 강점을 결합하여 의존성을 제어하고 C++ 시스템을 더 쉽게 테스트하고, 진화시키며, 유지보수할 수 있게 만드는 것입니다.
# Class of Dependencies
In conclusion, when you compose your business logic out of pure functions, dependencies become **explicit, fewer, and simpler**. This directly mitigates complexity.
> **Note:** I am not claiming that functional programming will solve every problem, nor that it can be applied in C++ without limits. There are, however, aspects that are highly valuable when designing C++ systems. I advocate for **complementing OOP with FP where appropriate*
*.
코드 구조를 다르게 잡는 데는 노력이 필요하며, 그 정도는 여러분의 학습 경로에 따라 달라집니다. 저처럼 OOP‑중심의 C++ 스타일 배경을 가지고 있다면, 여러분의 입장을 충분히 이해하고 있으니 이러한 기법과 설계 선택을 더 명확히 설명할 수 있습니다.
저의 원래 동기는 시스템 설계나 함수형 프로그래밍에 대해 글을 쓰는 것이 아니었습니다. 단순히 이러한 아이디어를 실제 C++ 코드에 효과적으로 적용하는 방법을 찾고 싶었을 뿐입니다. 어느 정도 탐구하고 실용적인 기법들을 발견한 뒤, 자연스럽게 이를 공유하고 논의하고 싶어졌습니다.
funkyposts 블로그는 전통적인 C++와 함수형 프로그래밍 사이에 다리를 놓고, 현대 C++에서 실용적인 FP 패턴을 보여주는 것을 목표로 합니다. 목표는 현실적인 제약 조건을 고려하면서 시스템 구조를 개선할 구체적인 방법을 제공하는 것입니다. 또한 피드백과 다양한 관점을 환영하여 이러한 아이디어를 더욱 다듬고자 합니다.
다음 단계는 이러한 통찰을 실제에 적용해 기존 설계를 전체를 다시 쓰지 않고도 개선하는 것입니다. 이 시리즈의 다음 글에서는 그 방법을 보여줄 예정입니다.
Upcoming Posts in the Series
-
Handling Side Effects in Modern C++: Interfacing Pure Functions with Our Imperative World
Using the functional core–imperative shell pattern to mitigate dependencies. -
When One Shell Isn’t Enough: Scaling the Functional Core–Imperative Shell Pattern with Actors in C++
How to structure growing C++ systems into actor‑driven core–shell pairs that isolate dependencies.
More posts in this series coming soon.
About the Blog
Part of the funkyposts blog — bridging object‑oriented and functional thinking in C++.