객체 지향 프로그래밍: 객체 간의 대화와 서비스 객체 안티패턴

Gavin Morrice — Objects Talking to Objects | Baltic Ruby 2025

작성자
Baltic Ruby
발행일
2025년 08월 30일

핵심 요약

  • 1 객체 지향 프로그래밍(OOP)은 정체성, 상태, 행동을 가진 객체들이 메시지를 통해 상호작용하며 복잡한 시스템을 구축하는 강력한 패러다임입니다.
  • 2 추상화, 다형성, 확장, 캡슐화는 OOP의 핵심 원칙으로, 코드의 관리 용이성, 유연성, 견고성 및 재사용성을 극대화하여 미래 변화에 대한 비용을 최소화합니다.
  • 3 서비스 객체는 프로세스를 중앙 집중화하려는 시도로, 객체 지향 패러다임을 위반하며 과도한 의존성, 전이적 의존성, 숨겨진 추상화 등 여러 문제를 야기하는 안티패턴으로 지양해야 합니다.

도입

본 강연은 Cleo의 Gavin(Bodacious)이 객체 지향 프로그래밍(OOP)의 근본적인 아이디어와 복잡한 시스템에서의 프로세스에 대해 논합니다. 연사는 우주와 같은 자연계의 시스템이 확장성, 견고성, 유연성을 갖추고 있음을 예시로 들며, 우리가 이러한 특성을 가진 시스템을 구축하는 방법을 이론적으로 탐구하고자 합니다. 이를 위해 주전자(kettle)를 비유로 삼아 객체 지향의 기본 개념을 설명하고, 현대 소프트웨어 개발에서 흔히 나타나는 안티패턴인 서비스 객체의 문제점을 지적합니다.

객체 지향 프로그래밍의 기본 개념

객체의 세 가지 특성

객체 지향 프로그래밍은 현실 세계를 모델링하여 코드를 작성할 수 있게 합니다. 현실 세계의 모든 ‘것’을 객체라 부르며, 객체는 다음 세 가지 고유한 특성을 가집니다.

  • 정체성(Identity): 다른 객체와 구별되는 고유한 존재성입니다. 예를 들어, 두 대의 동일한 주전자라도 각각은 고유한 정체성을 가집니다. Ruby에서는 object_id를 통해 이를 확인할 수 있습니다.
  • 상태(State): 객체의 특정 속성들을 나타냅니다. 이는 시간에 따라 변할 수 있으며, 주전자의 온도, 물의 양, 전원 켜짐/꺼짐 등이 이에 해당합니다.
  • 행동(Behavior): 객체가 수행할 수 있는 기능과 목적입니다. 주전자의 경우, 물을 채우고, 비우고, 가열하는 등의 행동이 있습니다.

Ruby에서는 해시(Hash)와 같은 모든 것이 객체로 간주되며, 이는 시스템을 더욱 단순하게 만듭니다. 복잡한 객체는 더 작은 객체들의 조합으로 이루어지며, 객체들은 메시지를 통해 상호작용하여 시스템에 생명을 불어넣습니다.

객체 지향 프로그래밍의 4가지 핵심 원칙

복잡성을 관리하고 유연하며 견고한 시스템을 구축하기 위해 다음 네 가지 핵심 원칙이 중요합니다.

  • 추상화(Abstraction): 불필요한 세부 사항을 숨기고 본질적인 것에 집중하는 과정입니다. 이는 클래스나 모듈의 인터페이스를 정의하는 데 사용되며, 코드를 단순화하고 유연성을 높입니다. 좋은 추상화는 세부 사항이 변경되어도 본질은 유지됩니다.
  • 다형성(Polymorphism): 객체의 ‘무엇(what it is)’이 아닌 ‘무엇을 하는지(what it does)’에 기반하여 상호작용을 정의하는 것입니다. 예를 들어, ‘주전자’ 대신 ‘물 가열기(water heater)’라는 추상적인 개념을 사용하여, 주전자든 냄비든 물을 가열할 수 있는 모든 객체를 유연하게 처리할 수 있습니다.
  • 확장(Extension): 기존 추상화를 변경하지 않고 그 위에 새로운 행동을 추가하는 것입니다. 상속(inheritance)이나 조합(composition)을 통해 이루어지며, 기존 시스템에 영향을 주지 않고 재사용 및 확장을 가능하게 합니다.
  • 캡슐화(Encapsulation): 객체의 내부 상태와 작동 방식을 숨기고 외부에는 엄격하게 제어된 인터페이스만 노출하는 것입니다. 이는 객체의 일관성을 보호하고, 내부 구현이 변경되어도 외부 사용에 영향을 주지 않아 유연성과 견고성을 높입니다.

서비스 객체 안티패턴

프로세스의 분산과 서비스 객체의 문제점

자연계의 복잡한 시스템(벌집, 은하계 등)이나 인공물(주전자, 시계 내부 기어)에서 프로세스는 중앙에서 통제되는 것이 아니라, 개별 구성 요소들의 상호작용을 통해 ‘발생’합니다. 그러나 소프트웨어 개발에서는 프로세스를 단일 위치에 정의하려는 경향이 있으며, 이는 ‘서비스 객체’라는 형태로 나타납니다.

서비스 객체는 일반적으로 프로세스를 설명하는 이름을 가지며, call, execute와 같은 단일 클래스 메서드를 가집니다. 연사는 서비스 객체가 객체 지향 패러다임을 위반한다고 주장합니다.

  • 정체성 및 상태 부재: 서비스 객체는 인스턴스를 가질 수 없고, 고유한 정체성이나 지속적인 상태를 가지지 않습니다. 이는 일시적인 내부 상태만을 가지는 Proc 객체와 유사합니다.
  • 제한된 행동: 단일 명령 실행에 그치며, 객체와 상호작용하거나 다양한 메시지를 보낼 수 없습니다. 이는 ‘반응 원칙(reactive principle)’을 위반합니다.
  • 절차적 사고방식: 서비스 객체는 분산되어야 할 프로세스를 중앙 집중화하려는 시도로, 객체 지향적 사고에서 벗어나 절차적 명령 목록을 작성하는 것과 같습니다.

서비스 객체 사용 시 발생하는 문제

  • 과도한 의존성: 서비스 객체는 여러 도메인 모델에 대한 많은 의존성을 가지게 되어, 코드 변경 시 파급 효과가 커지고 디자인이 취약해집니다.
  • 전이적 의존성(Transitive Dependencies): 서비스 객체가 사용자(User)를 통해 구독(Subscription)에 접근하고, 다시 구독을 통해 결제(Payment)에 접근하는 등, 여러 단계를 거쳐 의존하는 경우가 발생합니다. 이는 ‘데메테르의 법칙(Law of Demeter)’을 위반하여 캡슐화를 깨뜨리고 시스템을 취약하게 만듭니다.
  • 숨겨진 추상화: 서비스 객체 내부에 도메인 모델의 비즈니스 로직(예: 구독 활성화 방법)이 직접적으로 구현되는 경우가 많습니다. 이는 ‘묻지 말고 시켜라(Tell, Don’t Ask)’ 원칙을 위반하며, 해당 로직은 도메인 모델 자체에 속해야 할 숨겨진 추상화입니다.

서비스 객체 사용에 대한 조언

연사는 서비스 객체 사용을 권장하지 않으며, 다음을 제안합니다.

  • 사용하지 마십시오: 서비스 객체 대신 해당 행동이 속해야 할 도메인 모델에 로직을 배치하거나, 누락된 추상화(새로운 클래스)를 찾아 도메인 모델을 풍부하게 만드십시오.
  • 도메인 지식 최소화: 불가피하게 서비스 객체를 사용해야 한다면, 도메인 모델에 대한 지식을 최소화하고 단순히 메시지를 위임하는 역할을 수행하게 하십시오. 의존성 주입을 활용하여 객체들이 프로세스의 세부 사항을 캡슐화하게 하고, 서비스 객체는 흐름 제어 역할만 하도록 합니다.
  • 중첩 호출 금지: 다른 서비스 객체에서 서비스 객체를 호출하는 것은 피해야 합니다. 이는 아키텍처 계층을 혼란스럽게 하고 변경 비용을 증가시킵니다.
  • 진입점 활용: 서비스 객체를 비즈니스 도메인의 진입점(예: 컨트롤러 액션, 워커)으로만 활용하는 것을 고려하십시오.

결론

객체 지향 프로그래밍은 확장 가능하고 복잡하며 견고하고 변화에 강한 시스템을 구축할 수 있는 강력한 패러다임입니다. 이러한 패러다임을 벗어나는 패턴은 회의적으로 접근해야 하며, 프로세스는 중앙에서 정의되는 것이 아니라 객체들이 서로 대화하는 과정에서 자연스럽게 '발생'해야 합니다. 객체 지향 원칙을 충실히 따르면 미래의 변경 비용을 최소화하면서 유연하고 견고한 시스템을 구축할 수 있습니다.

댓글 0

댓글 작성

0/1000
정중하고 건설적인 댓글을 작성해 주세요.

아직 댓글이 없습니다

첫 번째 댓글을 작성해보세요!