"깊이"가 다른 게임개발자 허민영

유저에서 게임까지, 철학에서 코딩까지, 본질을 보는 게임개발

소프트웨어 공학/프로그래밍 패러다임

객체지향의 한계와 그 너머로

허민영 2025. 8. 21. 19:54

객체지향 프로그래밍의 완전한 객체화 실패와 해결 방안

1. 문제에 대한 분석

1.1 재사용성의 근본적 딜레마

객체지향 프로그래밍이 추구하는 완전한 객체화가 실패하는 가장 근본적인 이유는 클래스 재사용성구성요소 재사용성 사이의 해결할 수 없는 상충관계에 있다.

클래스 전체를 재사용하면 불필요한 기능까지 포함되어 메모리 비효율과 무거운 객체 생성 문제가 발생한다. 반대로 클래스 구성요소를 개별적으로 재사용하려면 복잡한 의존성 관리와 설계 패턴이 필요해지며, 이는 객체지향의 핵심 원칙인 캡슐화와 응집성을 약화시킨다.

1.2 동적 생성/소멸의 복잡성

객체지향에서 가장 큰 병목은 동적 객체 생성과 소멸이다. 런타임에 언제, 얼마나 많은 객체가 생성될지 예측할 수 없어 메모리 파편화와 가비지 컬렉션 부담이 발생한다. 또한 순환 참조나 댕글링 포인터 같은 참조 관리 문제도 해결하기 어려운 복잡성을 만든다.

1.3 State의 본질적 모순

가장 근본적인 문제는 객체 내부의 State 존재 자체다. State가 시간에 따라 변화하면 동일한 객체라도 호출 시점에 따라 다른 결과를 반환한다. 이는 완전한 예측 가능성과 재사용성을 불가능하게 만든다. 객체지향이 추구하는 "독립적이고 캡슐화된 존재"라는 이상과 "시간에 종속적인 가변 상태"라는 현실 사이에는 해결할 수 없는 모순이 존재한다.

1.4 "찰흙" 문제

현재 객체지향 언어들이 제공하는 기본 클래스는 너무 원시적이고 유연해서 개발자에게 과도한 자유도를 제공한다. 이로 인해 비효율적인 구조와 일관성 없는 설계가 양산되며, 재사용을 위해 만든 범용 클래스들이 오히려 복잡성만 증가시키는 결과를 낳는다.

2. 언어 레벨의 해결책

2.1 타입 시스템의 진화: Rust Trait System

Rust의 trait system은 언어 차원에서 객체 관계를 규격화한 대표적 사례다. Trait을 통해 "어떤 타입이 할 수 있는 일"을 명확히 정의하고, 컴파일 타임에 타입 안전성과 메모리 안전성을 보장한다.

특히 소유권(ownership) 시스템과 결합되어 객체의 생명주기를 컴파일 타임에 결정함으로써 런타임 복잡성을 제거한다. Monomorphization을 통해 제네릭 코드를 각 타입별로 특화시켜 zero-cost abstraction을 달성한다.

2.2 필요한 언어 차원의 개선사항

완전한 객체화를 위해서는 다음과 같은 언어 레벨의 지원이 필요하다:

  • 관계의 명시적 구분: 상속, 구성, 집약, 의존 등을 언어에서 문법적으로 구분
  • 컴파일 타임 최적화: 객체 조합 패턴을 미리 분석해서 런타임 오버헤드 제거
  • 제약의 체계화: 각 관계 유형별로 허용되는 패턴과 금지된 패턴을 언어에서 강제

3. 프레임워크 레벨의 해결책

3.1 Spring IoC Container: 거시적 생명주기 관리

Spring은 IoC(Inversion of Control) 컨테이너를 통해 객체의 생성, 조립, 생명주기를 완전히 프레임워크가 관리하도록 했다. 개발자는 의존성을 선언만 하면 되고, 실제 객체 생성과 주입은 ApplicationContext가 처리한다.

이를 통해 동적 생성/소멸의 복잡성을 개발자로부터 숨기고, 싱글톤이나 프로토타입 같은 스코프별 메모리 관리 전략을 자동화했다. 순환 참조 탐지와 의존성 주입 순서 최적화도 프레임워크가 담당한다.

3.2 React Hooks: 함수형 생명주기 추상화

React Hooks는 클래스 컴포넌트의 복잡한 생명주기 관리를 함수형으로 추상화했다. useState와 useEffect를 통해 상태 관리와 부수 효과를 선언적으로 처리할 수 있게 했다.

특히 useEffect의 cleanup 함수를 통해 설정과 해제 로직을 같은 곳에 위치시켜 메모리 누수를 방지하고, Custom Hooks를 통해 상태 로직을 재사용 가능한 블럭으로 모듈화할 수 있게 했다.

3.3 프레임워크 접근법의 한계

이런 프레임워크들은 특정 도메인에서는 효과적이지만, 여전히 학습 곡선이 가파르고 각자 다른 패러다임을 요구한다. 또한 언어 차원이 아닌 라이브러리 차원의 해결책이므로 근본적 한계가 있다.

4. 프로그래밍 패러다임 레벨의 해결책

4.1 DOD(Data-Oriented Design)의 근본적 접근

가장 근본적인 해결책은 Data-Oriented Design이 제시하는 데이터와 동작의 완전한 분리다. 객체에서 State를 완전히 제거하고 별도의 데이터 저장소에서 관리하며, 객체는 순수한 동작(behavior)만 담당하게 한다.

4.2 State 외부화의 효과

State가 객체 외부로 분리되면:

  • 순수 함수적 객체: 입력에 대해 항상 동일한 출력을 보장하는 완전 예측 가능한 객체
  • 메모리 최적화: 데이터를 타입별로 연속 배열에 저장하여 캐시 친화적 메모리 레이아웃 달성
  • 병렬 처리 최적화: 데이터 경합이 없어 안전한 병렬 처리 가능
  • 시간의 명시화: State 변화를 이벤트 스트림으로 처리하여 과거/현재/미래 상태를 모두 추적 가능

4.3 실제 적용 사례

이미 일부 영역에서는 이런 접근이 적용되고 있다:

  • ECS(Entity Component System): 게임 개발에서 Entity(ID), Component(데이터), System(로직)을 완전 분리
  • 함수형 프로그래밍: 불변 데이터 구조와 순수 함수를 통한 상태 관리
  • 이벤트 소싱: State를 이벤트의 누적으로 관리하여 시간에 따른 변화 추적
  • Functional Reactive Programming: 데이터 스트림과 변환 함수의 조합

4.4 패러다임 전환의 필요성

완전한 객체화를 달성하려면 "상태를 가진 객체"에서 "상태 없는 객체 + 외부 데이터"로의 근본적 패러다임 전환이 필요하다. 이는 절차적 프로그래밍에서 객체지향으로 넘어갔던 것과 비슷한 수준의 패러다임 변화를 요구한다.

5. 결론

5.1 완전한 객체화의 조건

진정한 "세팅된 블럭"은 다음 조건을 만족해야 한다:

  • 완전한 예측 가능성: 언제, 어디서 사용해도 동일한 결과 보장
  • 생명주기 투명성: 생성/소멸의 복잡성이 완전히 캡슐화됨
  • 최적화된 내부 구조: 메모리와 성능이 블럭 레벨에서 이미 최적화됨
  • 명확한 책임 분리: 각 블럭이 단일하고 명확한 책임을 가짐

5.2 해결책의 층위별 기여도

각 해결책은 서로 다른 층위에서 기여한다:

  • 언어 레벨: 타입 안전성과 메모리 안전성의 컴파일 타임 보장
  • 프레임워크 레벨: 특정 도메인에서의 생명주기 관리 자동화
  • 패러다임 레벨: State와 동작의 근본적 분리를 통한 완전한 예측 가능성

5.3 미래의 방향

객체지향의 완전한 객체화는 단일 접근법으로는 달성할 수 없다. 언어 차원의 관계 규격화, 프레임워크 차원의 생명주기 관리 자동화, 그리고 궁극적으로는 DOD가 제시하는 데이터와 동작의 분리라는 패러다임 전환이 모두 필요하다.

특히 State가 객체 내부에 존재하는 한 완전한 객체화는 불가능하다는 통찰은 중요하다. 미래의 프로그래밍 패러다임은 상태 관리와 동작 정의를 근본적으로 분리하는 방향으로 진화해야 할 것이다.