객체지향이 직관적인 이유

심리학자 엘리자베스 스펠크와 필립 켈만의 실험에 따르면 인간은 선천적으로 인지 능력을 타고난다고 한다. 물리적이던 개념적이던 뚜렷한 경계를 지니고 똑같이 행동하는 물체를 하나의 유기적인 단위로 인식한다는 것이다. 복잡한 세상을 이해하기 위해 대상을 더 작은 집합으로 분해하려는 인간의 본능이다.

이렇게 인간이 분명하게 인지하고 경계를 지어 구별할 수 있는 것이 바로 객체다.1 현실 세계와 소프트웨어 세계는 모두 다양한 객체로 이루어진다는 것에서 시작되었지만 두 세계에 존재하는 객체는 전혀 다른 모습을 보인다. 그 차이점을 비교해보며 SW에서 말하는 객체가 무엇인지 정의해보자.

소프트웨어 객체의 특징

몇가지 예를 들어보자.

객체현실 세계sw 세계
전등사람의 손길 없이는 스스로 불을 밝힐 수 없다.외부의 도움 없이도 스스로 전원을 켜거나 끌 수 있다.
주문사람이 직접 주문 금액을 계산한다.주문 객체가 자신의 금액을 계산한다.
컴퓨터사람이 동작을 시켜줘야 일을 할 수 있다.스스로 전원을 켜거나 끌 수 있다.
자동차사람이 직접 시동을 켜고 운전해야한다.스스로 시동을 걸고 움직인다.
통장사람이 직접 돈을 인출해야한다.스스로 금액 인출이 가능하다.

표 작성하다가 문득 든 생각

예전에는 사람의 도움 없이 객체를 제어할 수 없는 세상이었지만 이제 자동차가 스스로 운전하고, 컴퓨터가 스스로 일을 한다. 이렇게 사람을 대신해서 객체를 제어해주는 것이 바로 ai 라고 정의할 수 있지 않을까?

이처럼 현실 세계에서의 객체는 대부분 사람의 개입이 있어야 하지만 sw 세계에서의 객체는 그렇지 않다. 현실의 객체는 수동적이지만, 소프트웨어 객체는 의인화되어 능동적이다. 이러한 이유는 바로 sw 객체의 주요한 특징 때문이다.

소프트웨어에서 말하는 ”객체“는 3가지 중요한 특징을 지닌다.

객체는 구별 가능한 식별자(identity), 특징적인 행동(behavior), 변경 가능한 상태(state) 를 가진다. 이는 실행 가능한 코드와 저장된 상태를 통해 구현된다. (p.47)

1. 식별자

객체는 어떤 상태에 있더라도 유일하게 식별 가능하다.

값과 객체를 구분하는 법

시스템 설계 시 우리는 어떤 것을 객체로 설정해야하고, 어떤 것을 값으로 사용해야할까?

값과 객체의 가장 큰 차이점은 값은 식별자를 가지지 않지만 객체는 식별자를 가진다는 점이다. (p.57)

객체 안에는 서로 구별할 수 있는 특정한 프로퍼티가 존재한다. 이 프로퍼티를 식별자라고 하며, 모든 객체는 식별자를 가진다.
식별자는 어떻게 객체를 구별할 수 있을까? 예를 들어 이름이 앨리스2이고, 키가 동일한2 두 사람3을 어느 누구도 같은 사람이라고 생각하지 않는다. 두 사람은 완전히 별개의 인격체이다. 반면, 어린 시절의 나와 현재의 나는 시간이 흐르는 동안 키도, 나이도, 모습도 전부 변했지만 상태와 무관하게 동일한 인격체이다. 따라서 사람은 일종의 식별자를 가지고 있는 객체라고 볼 수 있는 것이다.

객체
식별자 여부없음있음
시간에 따른 상태변하지 않음(불변)변함(가변)
두 인스턴스가 같은지 다른지 판단하는 기준상태4식별자
표현동등하다(equality)동일하다(identical)
지칭 용어valueentity

2. 상태

객체는 상태를 가지며 객체의 상태는 변경 가능하다.

상태가 필요한 이유

왜 상태가 필요할까? 행동의 결과는 과거의 모든 행동 이력에 의존한다. 객체가 다음 행동을 예측하려면 처음부터 현재까지 일어났던 과거에 어떤 행동이 일어났는지를 전부 알고 있어야 한다는 것이다.

이상한 나라의 앨리스가 문을 통과하기 위해서는 케이크를 먹어 키를 키우거나 부채질을 해서 문을 통과하기에 충분할 정도로 자신의 키를 작게 줄여야만 한다. (p.48)

앨리스가 현재 문을 통과할 수 있을지 없을지는 과거에 케이크를 몇번 먹었는지, 키가 얼마나 커졌는지, 부채질을 해서 얼마나 작아졌는지를 전부 계산하고서야 예측할 수 있다. 이 방법은 굉장히 복잡하고 까다롭지만, 사실 현재 앨리스의 키와 문의 높이, 두 상태만 알면 문을 통과하는 행동의 결과를 쉽게 예측할 수 있다.
이처럼 과거에 얽매이지 않고 현재를 기반으로 객체의 행동 방식을 이해할 수 있기 때문에 상태가 필요하다.

상태의 구성 요소

그런데 여기서 우리는 앨리스의 키, 위치, 문의 높이, 케이크의 양, 문 열림 여부는 객체라고 보지 않는다. 하지만 이는 모두 상태를 구성하는데 필요한 필수적인 요소들이며 숫자, 문자열, 양, 속도, 시간, 날짜, 참/거짓과 같은 단순한 값으로 표현된다. 이처럼 그 자체로 독립적인 의미를 가지기보다는 다른 객체의 특성을 표현하는 데 사용되는, 객체를 구성하는 모든 특징을 통틀어 객체의 프로퍼티(property) 라고 한다. 따라서 키, 위치, 케이크는 앨리스의 프로퍼티이며 문의 높이, 문 열림 여부는 문의 프로퍼티이고 케이크의 양은 케이크의 프로퍼티이다.

여기서 키, 위치, 양 등 단순한 값으로 표현할 수 있는 요소를 속성(attribute) 이라고 하고 케이크를 앨리스가 알고있는 것과 같이 참조 되는 객체와의 관계를 링크(link) 라고 한다.

링크(link) 이해 가이드

  1. 케이크는 앨리스의 프로퍼티가 아닌 객체다.
  2. 앨리스가 케이크를 먹는다는 것은 객체와 객체 사이의 요청과 응답.
  3. 요청과 응답 즉, 메시지를 주고 받는 작업은 객체와 객체 사이에 링크가 존재해야만 가능하다.
  4. 링크는 객체가 다른 객체를 참조 할수있다는 것을 의미하며, 한 객체가 다른 객체의 식별자를 알고 있다는 뜻이다.
  5. 앨리스가 만약 케이크를 버리거나, 다 먹어버린다면 둘 사이의 링크는 존재하지 않게된다.

객체의 상태는 객체에 존재하는 정적인 프로퍼티와 동적인 프로퍼티 값으로 구성된다. 객체의 프로퍼티는 단순한 값과 다른 객체를 참조하는 링크로 구분할 수 있다. (p.51)

즉, 객체가 가지는 프로퍼티의 타입은 객체5 혹은 단순한 값6 중 하나다.

여기서 중요한건 용어들이라기 보다는 객체는 자율적인 존재라는 것이다. 객체지향 세계에서 객체는 다른 객체의 상태에 직접 접근할수도, 다른 객체의 상태를 변경할 수도 없다. 스스로 자신의 상태를 책임져야만 한다. 객체는 스스로의 행동에 의해서만 상태가 변경되는 것을 보장함으로써 자율성을 유지한다.

3. 행동

객체의 상태를 변경시키는 것은 객체의 행동이다.

협력하는 객체들

  • 객체의 행동은 상태에 영향을 받는다.
  • 객체의 행동은 상태를 변경시킨다.

객체가 행동을 취하면 상태가 변경되는 부수 효과를 초래한다. 단, 객체는 자신의 상태만을 변경시킬 수 있을 뿐 외부의 객체가 다른 객체의 상태에 직접적으로 영향을 줄 수 없으니 간접적으로 다른 객체의 상태가 변하도록 알리거나 조회할 수 있는 방법이 필요하다.

객체는 자신에게 주어진 책임을 완수하기 위해 다른 객체를 이용하고 서비스를 제공하며 적극적으로 상호작용 한다. 이렇게 협력하기 위한 유일한 방법은 바로 요청 메시지를 보내는 것이다. 요청을 수신한 객체는 메시지에 응답하기 위해 요청을 처리하기 위한 행동을 동작하고 그 결과로 자신의 상태를 변경하여 반응함으로써 협력에 참여한다.[^8]

행동으로 인해 발생하는 결과(p.54)

  • 객체 자신의 상태 변경
  • 행동 내에서 협력하는 다른 객체에 대한 메시지 전송

상태를 캡슐화해야하는 이유

하지만 메시지 송신 객체는 메시지 수신 객체의 상태 변경에 대해서는 전혀 알지 못해야한다. 예를 들어 앨리스 객체에게 eatCake() 라는 메시지가 전달되었을때, 수신자는 메시지 이름만 보고서는 앨리스의 키가 줄어드는 상태 변경에 대해서는 알 수도, 예상할 수도 없다. 또 이에 대한 연쇄 반응으로 앨리스 객체가 케이크 객체에게 eaten(quantity)라는 메시지를 보낼 때도 케이크의 양이 줄어드는 등 상태가 변경된다는 사실조차 알 수 없다.

이것이 캡슐화가 의미하는 것이다. 객체는 상태를 캡슐 안에 감춰둔 채 외부로 행동만 노출한다. (중략) 메시지 송신 객체는 단지 자신의 요구를 메시지로 포장해서 전달할 뿐, 메시지를 해석하고 그에 반응하여 상태를 변경할지 여부는 전적으로 메시지 수신자의 자율적인 판단에 따른다.(p.56)

상태를 캡슐화하는 것은 단순히 데이터를 숨기는 것이 아니라 자기 자신의 데이터에 대한 책임을 지도록 만든다. 객체를 수동적인 데이터 박스로 보는 것이 아니라 스스로 생각하고 결정할 수 있는 능동적인 존재로 보는 것이다.

오직 객체 내부의 메서드에서 상태 변경을 유발하기 때문에 개발자들이 데이터가 어디서 바뀌었는지 예측할 수 있게 하고, 상관 없는 기능에서 오류가 나타나지 않는 방화벽 역할을 한다.
또한 동료가 만든 객체 내부 로직을 전부 알 필요가 없고 객체가 제공하는 인터페이스만 가지고도 충분히 상호작용이 가능하며, 객체 내부에서 데이터 저장 방식을 변경하는 등 구현을 변경해도 외부 코드에 영향을 주지 않아 결합도를 낮출 수 있다. 이는 프로젝트 규모가 커지는 등의 유지보수에서 유연하게 작동할 수 있다.

entity에 setter를 쓰지 않는 이유

캡슐화를 실현하는 구체적인 방법 중 하나다. 코드에서 setter를 남발한다면 해당 데이터의 값이 바뀌는지 알 수 없다.

  • alice.setHeight(“100”): 단순히 상태를 바꾼건지 뭘 먹은건지 부채질을 한건지 알 수 없음.
  • alice.eatCake(): 비즈니스 로직이 바로 읽힘

그리고 만약 케이크를 먹었을 때 다른 값도 바뀌게 하고 싶다거나, 케이크의 값도 바뀌게 하고 싶다면 따로 구현해서 각각 호출해야 한다. 만약 실수로 하나 빼먹는다면 데이터 오염이 발생한다.

가장 중요한건 entity에서 setter를 아예 쓰지 않음으로써 객체 입구를 아예 폐쇄하는 것이다. 생성자 혹은 빌더 패턴을 사용해서 유효한 값을 가진 객체를 만들거나, 실제 동작을 설명하는 메서드를 만들어서 대체할 수 있다.

마치며

결국 객체는 상태를 위해 존재하는 것이 아니라, 행동을 위해 상태를 가질 뿐이다. 객체를 단순히 데이터를 담는 구조체로 취급해서 외부에서 데이터를 꺼내 가공하는 절차지형적 코드를 짜지 않도록 주의해야겠다.

  1. 상태를 검증하고 변경하는 등의 구체적인 비즈니스 로직은 Service 단이 아닌 객체 내부에 구현하자. Service는 오로지 객체들에게 메시지를 전달하는 ‘오케스트레이션’ 역할을 수행한다.
  2. 객체는 서로 식별자를 통해서만 연결되게 하자. 객체 간의 직접적인 참조를 배제해서 결합도를 낮추어 향후 요구사항 변경이나 도메인 확장에 유연하게 대응한다.
  3. 의도가 드러나도록 인터페이스를 설계하자. 객체가 내부에서 데이터를 어떻게 처리하라는 방법(how)을 전달하는게 아니라, 무엇을 해야 하는지 목적(what)에 초점을 맞춰 객체 스스로 동작하도록 의인화해야겠다.

Footnotes

  1. 객체의 첫번째 특징; 식별자

  2. 상태 동일 2

  3. 타입 동일

  4. 2라는 값은 영원히 2라는 상태를 가진다. 따라서 세상의 모든 2는 전부 동등하다.

  5. 케이크

  6. 키, 위치