객체 지향 프로그래밍의 기본 개념
객체의 세 가지 특성
객체 지향 프로그래밍은 현실 세계를 모델링하여 코드를 작성할 수 있게 합니다. 현실 세계의 모든 ‘것’을 객체라 부르며, 객체는 다음 세 가지 고유한 특성을 가집니다.
- 정체성(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)’ 원칙을 위반하며, 해당 로직은 도메인 모델 자체에 속해야 할 숨겨진 추상화입니다.
서비스 객체 사용에 대한 조언
연사는 서비스 객체 사용을 권장하지 않으며, 다음을 제안합니다.
- 사용하지 마십시오: 서비스 객체 대신 해당 행동이 속해야 할 도메인 모델에 로직을 배치하거나, 누락된 추상화(새로운 클래스)를 찾아 도메인 모델을 풍부하게 만드십시오.
- 도메인 지식 최소화: 불가피하게 서비스 객체를 사용해야 한다면, 도메인 모델에 대한 지식을 최소화하고 단순히 메시지를 위임하는 역할을 수행하게 하십시오. 의존성 주입을 활용하여 객체들이 프로세스의 세부 사항을 캡슐화하게 하고, 서비스 객체는 흐름 제어 역할만 하도록 합니다.
- 중첩 호출 금지: 다른 서비스 객체에서 서비스 객체를 호출하는 것은 피해야 합니다. 이는 아키텍처 계층을 혼란스럽게 하고 변경 비용을 증가시킵니다.
- 진입점 활용: 서비스 객체를 비즈니스 도메인의 진입점(예: 컨트롤러 액션, 워커)으로만 활용하는 것을 고려하십시오.