[객체지향의 사실과 오해] 1장 협력하는 객체들의 공동체

Uno·2023년 9월 25일
0

Book

목록 보기
2/9
post-thumbnail

서문

책, 성공하는 사람들의 7가지 습관 중, 아래와 같은 내용이 있다.

시너지를 생각하라. -스티븐 코비-

cf) 책에서 다른 원칙

1. 자신의 삶을 주도하라.
2. 끝을 생각하며 시작하라
3. 소중한 것을 먼저 하라
4. 승-승 을 생각하라
5. 먼저 이해하고 다음에 이해시켜라
6. 시너지를 내라
7. 끊임없이 쇄신하라

이 인용 문구에 한 문장을 추가한 두 문장으로 책이 시작된다.
p20

시너지를 생각하라. 전체는 부분의 합보다 크다.


Q. 객체지향은 현실 세계에 대한 모방인가? 아니면 창조인가?

결론 먼저 말하면, "객체지향의 목표는 실세계를 모방하는 것이 아니다. 오히려 새로운 세계를 창조하는 것이다."(p21). 우리가 "학생" 이라는 Class 객체를 만든다고 가정해보자. 실제 학생은, 남자 / 여자, 나이, 학년, 그리고 MBTI 부터 해서 정말 다양한 요소를 가진 존재이다. 소프트웨어에서 혹시 '학생' 이라는 객체를 만들때 그렇게 작성하는가? 아니다. 여러 가지 속성중에서 현재 소프트웨어에서 다루는 몇 가지만 사용한다. 혹은 현실에는 존재하지 않거나 인식되지 않는 것들도 소프트웨어에서는 다룬다.


객체지향에서 협력

p29

이번 장을 모두 읽은 후에는 객체지향의 근본 개념이 실세계에서 사람들이 타인과 관계를 맺으며 협력하는 과정과 유사하다는 사실에 공감하게 될 것이다.

소프트웨어는 특정한 "문제" 를 풀기 위해서 존재한다. 그런데 그 문제가 너무나도 크다. 그래서 한번에 해결할 수 없다면 어떨까? 그렇다면 문제를 잘게 나눈다. 나눠진 문제를 각각 푼다. 그리고 각각 푼 "해" 를 합치고 합쳐서 원래 큰 문제를 해결한다.
객체지향도 마찬가지다. 한번에 해결하기 어려운 문제를 객체 단위로 문제를 나눠 가진다. 그리고 각자가 푼다. 그리고 각자 푼 정답을 합친다. 여기서 합치는 과정이 "협력" 이다. 객체지향에서는 "협력" 을 통해 더 큰 문제를 해결할 수 있게 된다.

아래 그림을 보면, 구내식당에서 관계다. 많은 관계가 있겠지만 그중에서 "손님 - 매표소 직원" 의 관계이다. 손님은 매표소 직원에게 표를 요청한다. 그러면 직원이 돈을 내라고한다. 손님은 돈을 낸다. 마지막으로 매표소 직원은 금액이 맞다면, 표를 준다.

손님이 돈을 훔쳐왔든, 열심히 일해서 벌었든, 비트코인으로 벌었든 신경쓰지 않는다. 그저 협력을 위한 돈을 지불하면 그만이다. 이 상태를 다른말료 "독립성" 이다. 자신이 어떤식으로 구현했는지는 완전히 독립성을 가진다. 그로 인해 손님은 역할과 책임이 생긴다.

매표소 직원도 마찬가지다. 표를 손으로 그려서 주든, 컴퓨터로 인쇄를 하든 아무런 상관이 없다. 오직 구내식당을 이용할 수 있는 표를 주기만하면된다. 추가로 잔돈을 주기도 해야하지만. 이 역시, 독립성이고, 그에 대한 매표소 직원의 역할과 책임이 주어진다.

이 글을 쓰다가, 그러면 객체지향 이전에는 어떤식으로 했을지 궁금했다. 그리고 그것은 "GOTO 문법" 이라는 것을 알 수 있었다. 그냥 원하는 동작이 있으면, 해당 코드로 "GoTo" 하는 것이다.그리고 유명한 사람이 논문으로 이것의 불편함을 기술했다.

Go To State Considered Harmful Edsger W. Dijktra
cf)이 논문을 작성한 사람은 "최단 경로 알고리즘" 을 고안한 사람이다.

GOTO 문법에서 객체지향으로 넘어온 이유를 느낄 수는 없다. 왜냐면 그 시대에 살질 않았으니 하지만 상상은 해볼 수 있다. 분명히, GoTo 위치가 바뀌거나, 코드가 한 에디터에 몇 천줄을 넘고 몇 만줄이 되었을 때를 상상해보자. 이 코드를 수정할 엄두가 나지 않는다. 특정 코드 줄로 이동하라는 명령어가 모조리 바뀌어 버릴지 모른다. 심지어 지금처럼 IDE 가 잘 되어있을리도 없고


객체지향 설계의 시작은 객체에 책임을 할당하는 것이다.

p30

객체지향 설계라는 예술은 적절한 객체에게 적절한 책임을 할당하는 것에서 시작된다. 책임은 객체지향 설계의 품질을 결정하는 가장 중요한 요소다. 책임이 불분명한 객체는 애플리케이션의 미래 역시 불분명하게 만든다. 얼마나 적절한 책임을 선택하느냐가 애플리케이션의 아름다움을 결정한다.

프로그래밍을 하면서 '기능 구현' 할 때를 상상해보자. 처음에 무엇부터 작성하는가. 사실 이것에 대해서도 고민이 많았다. 누구는 데이터 레이어 관련 코드를 먼저 작성한다기도한다. 또 누구는 UI 를 작성한다.(앱 개발을 기준으로 이야기 하는 중) 요즘 내 마음에 드는 것은 테스트 코드 먼저 작성하는 것이다. 그러면 테스트 코드를 작성한 다음에는 무엇을 작성하는가. 그것은 그떄 기분에 달려있지만, 왠만하면 데이터먼저 작성한다.

어째든, 기능 구현을 시작하면 우리는 그 기능을 책임질 "객체" 를 구현한다. 이 책임을 적절한 부여가 객체의 존재이유가 결정된다. 동시에 명확한 책임은 다른 유사한 역할을 하는 객체가 생성되는 것을 막는다. 그래서 이 책임 부여가 중요하다고 한다.

앱 개발자들 사이에서 많이 논의 되는 것이 MVC 패턴 vs MVVM 패턴 vs MVP 패턴 등이 있다. 이 셋은 클래스 기준의 아키텍쳐 패턴이다. 디자인 패턴이다 아키텍쳐 패턴이나 말이 많지만, 개인적으론 아키텍처패턴이라고 생각함 Link

위 세 패턴에는 Controller 의 책임 / ViewModel 의 책임 / presenter 의 책임이 다 다르다. 이 객체에 어떤 역할을 부여할지에 따라서 전체 코드의 구조가 변경된다.

이처럼, 객체의 책임할당은 코드를 작성하는 순간에 고민해야하는 중요한 활동이다.


객체 = 상태 + 행동

p32

흔히 객체를 상태(state)와 행동(behavior)을 함께 지닌 실체라고 정의한다.

먼저 상태랑 행동을 알아보자. 상태는 멤버변수, 변수 혹은 특정 값을 의미한다. 행동은 그런 값들을 이용해서 연산하는 것을 행동이라고 한다.

간단한 예시로, 소셜 로그인을 구현하는 코드를 들어보면, 아래와 같다.

class SocialLogin {
  
  /* Interface */
  bool googleLogin() {
    _requestGoogleLogin();
    return true;
  }
  
  
  bool kakaoTalkLogin() {
    _requestKakaoLogin();
    return true;
  }
  
  
  /* Implements */
  void _requestKakaoLogin() {
    // do something
  }
  
  void _requestGoogleLogin() {
    // do something
  }
}

여기 코드에는 어떠한 상태값도 없다. 상태 값이 없이, 특정 동작을 구현한 클래스가 의미가 있을까? 의미를 찾을라고하면 못찾을 건 없다. 하지만 그렇다면 그 로직을 굳이 클래스로 묶을 의미가 없을 수도 있다. 상태값이 있음으로 클래스로 경계를 나눈 의미가 더욱 명확해지기 때문이다.

다른 예시코드를 보자

class UIClass {
  /* Memeber Variables */
  bool isLoggedInKakao = false;
  bool isLoggedInGoogle = false;
  
  SocialLogin socialLogin;
  UIClass({required this.socialLogin}) {
    this.isLoggedInKakao = socialLogin.kakaoTalkLogin();
    this.isLoggedInGoogle = socialLogin.kakaoTalkLogin();
  }
  
  /* Interface */
  bool get isLoggedIn => (isLoggedInKakao || isLoggedInGoogle);  
}

여기서는 멤버변수라는 이름으로 2 개의 상태값이 있다. 그리고 아래에는 한 개의 인터페이스가 있다. 내가 말하는 용어를 정리하면 다음과 같다.

  • 상태 => 변수
  • 인터페이스 => public method
  • 행동 => public method + private method

프로그래밍을 처음 시작할 때는, 이런 것들에 대해서 별 의미를 두지 않았다. 하지만 코드를 깔끔하게 쓰고, 여러 가지를 배우면 배울수록 이런 정의들이 필요해지는 순간이 오는 것 같다. 객체를 생성하는 순간에는 잘 모르겠지만, 이 코드가 시간이 지나면 지날수록 잘 나눠진 책임은 오래 살아남게 된다. 그리고 오랜시간 유효한 코드가 되어준다.

이렇게 두 개의 객체가 협력하는 모습은 main 함수에서 볼 수 있다.

void main() {
  final socialLogin = SocialLogin();
  final mainUI = UIClass(
     socialLogin: socialLogin,
  );
  
  final isSuccess = mainUI.isLoggedIn;
  // do something...
}

/** UIClass */
class UIClass {
  /* Memeber Variables */
  bool isLoggedInKakao = false;
  bool isLoggedInGoogle = false;
  
  SocialLogin socialLogin;
  UIClass({required this.socialLogin}) {
    this.isLoggedInKakao = socialLogin.kakaoTalkLogin();
    this.isLoggedInGoogle = socialLogin.kakaoTalkLogin();
  }
  
  /* Interface */
  bool get isLoggedIn => (isLoggedInKakao || isLoggedInGoogle);  
}

/** Logic */
class SocialLogin {
  
  /* Interface */
  bool googleLogin() {
    _requestGoogleLogin();
    return true;
  }
  
  
  bool kakaoTalkLogin() {
    _requestKakaoLogin();
    return true;
  }
  
  
  /* Implements */
  void _requestKakaoLogin() {
    // do something
  }
  
  void _requestGoogleLogin() {
    // do something
  }
}

Interface = 무엇, Private Method = 어떻게

p32

객체의 자율성은 객체의 내부와 외부를 명확하게 구분하는 것으로부터 나온다. 객체의 사적인 부분은 객체 스스로 관리하고 외부에서 일체 간섭할 수 없도록 차단해야 하며, 객체의 외부에서는 접근이 허락된 수단을 통해서만 객체와 의사소통해야한다. 객체는 다른 객체가 '무엇(what)'을 수행하는지 알 수 있지만 '어떻게(how)' 수행하는지에 대해서는 알 수 없다.

이전에 작성한 코드를 위 인용된 구문 내용을 적용하면 아래와 같다.

<SocialLogin 클래스>

  • 접근이 허락된 What : googleLogin() / kakaoTalkLogin()
  • 은닉화된 How : _requestKakaoLogin() / _requestGoogleLogin()
    (참고로, Dart 에서 private 을 _ 로 나타낸다.)

객체의 자율성의 기준은 타 객체의 접근 가능성으로 나타냈다. 여기까지보면, 다음과 같은 질문을 할 수 있다.

Q. 귀찮게 뭐하러 Private / Public 을 나누나요?

코딩을 혼자할 때는 나도 이렇게 생각했었다. 여러 명과 함께 작업을 하면서 생각이 바뀌었다. 프로그래밍의 내적동기를 일으키기 위해서는 극단적인 예시의 도움이 간절하다.

만약, 하나의 프로젝트를 작업하는 개발자가 100명이라고 가정해보자. 그리고 그 100명의 개발자들은 기본적으로는 서로 다른 기능을 구현한다. 그 기능 구현이 동기적으로 개발하는 것이 아니라 비동기적으로 개발한다. 즉, 내가 로그인 기능을 구현함과 동시에 다른 개발자가 회원가입을 구현하거나 홈화면에 보여줄 API 연동을 구현하는 등 동시다발적으로 구현중인 환경이다.

A 라는 개발자가 소셜로그인 기능을 사용해서 댓글기능을 구현해야한다. 로그인이 되어 있다면, 정상적으로 댓글이 추가되지만, 로그인이 안된 상태라면, 로그인할 수 있도록 화면 전환 혹은 기존에 저장되어 있는 토큰을 통해서 토큰을 갱신해주어야 한다.

그래서 SocialLogin 클래스의 구성을 봤는데, 내가 작성한 코드가 아니라 모두가 Public 이라고 생각해보자. 그러면 댓글 기능구현하는 개발자가 어떤 메서드를 써야할지 알 수 있을까? 지금은 한개의 클래스이지만, 이런 클래스가 하루에도 10 개씩 나온다고 생각해보자. 아마 개발할 때마다 미간에 주름이 파동칠 것이다.

코드를 작성할 때는, 실수할 수 있는 가능성 조차 제거하는 것이 베스트이다. 그래서 외부에서 접근을 희망하지 않는다면 Private 으로 두는 걸 권장하는 것이다. 만약 Dart 언어처럼 public 이 아니면 테스트할 수 없어서 어쩔 수 없는 경우에는 Interface(=추상클래스) 를 이용하여 의존성을 주입하면 된다. 이렇게 되면 SOLID 의 DIP 도 지킬 수 있다.

위 코드를 바탕으로 Interface 를 두면 다음과 같다.

abstract class SocialLogin {
	bool googleLogin();
	bool kakaoTalkLogin();
}

class SocialLoginImpl implements SocialLogin { ... }

class UIClass {
  SocialLogin socialLogin;
  UIClass({required this.socialLogin}) { ... }
}

기존 Class 이름을 SocialLogin -> SocialLoginImpl 로 수정했다. 그리고 SocialLogin 이라는 추상클래스를 추가했다.

책 몇 페이지 더 넘기면, 내가 작성한 내용과 유사한 내용이 있다.

p35

외부의 요청이 무엇인지를 표현하는 메시지와 요청을 처리하기 위한 구체적인 방법인 메서드를 분리하는 것은 객체의 자율성을 높이는 핵심 메커니즘이다. 이것은 캡슐화(encapsulation) 라는 개념과도 깊이 관련돼 있다.


클래스 지향 프로그래밍(X), 객체 지향 프로그래밍(O)

p38

훌륭한 객체지향 설계자가 되기 위해 거쳐야 할 첫 번재 도전은 코드를 담는 클래스의 관점에서 메시지를 주고받는 객체의 관점으로 사고의 중심을 전환하는 것이다. 중요한 것은 어떤 클래스가 필요한가가 아니라 어떤 객체들이 어떤 메시지를 주고받으며 협력하는가다. 클래스는 객체들의 협력관계를 코드로 옮기는 도구에 불과하다.
...
클래스의 구조와 메서드가 아니라 객체의 역할, 책임, 협력에 집중하라. 객체지향은 객체를 지향하는 것이지 클래스를 지향하는 것이 아니다.

이 책에서 클래스 지향 프로그래밍이라는 "오해" 가 쌓인 이유에 대해서 에스키모 비유를 하는데, 정말 좋은 비유라고 생각된다.

이 책 이름에 나오듯 "오해" 는 클래스 지향 프로그래밍으로 받아들여지는 현재의 객체 지향을 의미한다. 그리고 진짜 객체인 역할, 책임 그리고 협력의 주체인 "객체"에 집중하자고 한다. 책 뒷부분에 구체화되어 나오겠지만, 미리 내용을 예측해보자.

클래스도 객체이다. 그러면 클래스가 아닌 객체는 무엇이 있을까? 구조체도 있을 수 있다. 추상클래스까지도 포함가능할 것 같다. 더 작은 단위까지 간다면, 람다함수들 혹은 JSON 까지도 될 수 있지 않을까? 그러니까 어디에 담느냐가 중요한게 아니다. 그것은 계속 바뀔지도 모른다. 하지만 무엇을 하느냐를 보면 된다. 결국은 자신의 역할과 책임이 있고, 그것을 바탕으로 다른 객체와 협력한다면, 그것이 객체인 것이다.

참고자료

profile
iOS & Flutter

0개의 댓글