-
객체지향 프로그래밍분석과탐구 2023. 10. 22. 00:42
객체지향 프로그래밍
객체지향 프로그래밍을 이야기하다 보면 개체지향 프로그래밍이란 용어도 보게 된다. 개체는 시스템 안에서 협력하는 개념에 가깝고, 객체는 독립성을 가진 대상을 가리키는 쪽에 가깝다. 객체지향의 object는 개념상으론 개체 쪽에 가까운 것으로 보이는데, 개체보단 객체라는 단어가 주로 쓰인다. MSDN에선 개체지향 프로그래밍으로 명시한다. 일단, 객체가 좀 더 널리 쓰이니, 객체로 하겠다.
객체지향 프로그래밍에 대하여 글을 쓴 계기는 객체지향의 사실과 오해라는 책을 읽고 생각을 정리하고 싶어서다. 책 1권만 읽은 사람이 가장 무섭다던데, 책을 그대로 요약할 필요는 없어 보이고 여태까지 배운 것들, 경험한 것들을 짬뽕하여 내가 생각해본 객체지향을 정리할 생각이다.
객체지향과 절차지향
객체지향 프로그래밍과 절차지향 프로그래밍의 차이점은 무엇인가. 그것은 코드 진행의 최소 단위를 객체로 보면 객체지향, 프로시저(함수, 루틴, 서브루틴)로 접근하면 절차지향이다. 코드 진행의 최소 단위란 말은 코딩하면서 먼저 나오는 것을 의미한다. 객체가 먼저 나오느냐, 프로시저가 나오느냐.
//객체지향 오브젝트1.행위(오브젝트2); //절차지향 프로시저1(인자1, 인자2);
다른 말로 하면, 프로그래머가 다른 사람의 코드를 바라보는 시점을 의미하기도 한다. 절차지향에서 코딩하는 상상을 해보자. 어떤 계산을 하기 위해 함수를 호출한다. 함수의 이름, 인자의 이름과 타입, 리턴 타입 그리고 약간의 설명으로 함수를 호출한다. 함수 내부는 관심에서 벗어나있다. 객체지향에서 코딩하는 상상을 해보자. 어떤 객체의 인터페이스를 기반으로 그 객체를 가져다 쓴다. 역시 내부 구현은 관심에서 벗어나있다.
프로그래머가 코딩하는 과정에선 못 느낄 수 있다. 클래스를 구현한다고 치면, 인터페이스뿐만 아니라 내부 구현까지 같이 코딩한다. 프로시저를 구현한다고 쳐도, 프로시저의 시그내쳐뿐만 아니라 내부 구현까지 하는 것이 일반적이다.
그러나, 다른 사람이 작성한 코드를 가져와서 쓰는 상상을 해보자. 다른 사람이 작성한 클래스, 함수 등을 가져다 쓰는 순간을 떠올리면 또 다를 것이다. 다른 사람이 작성한 클래스는 인터페이스만을 고려한다. 함수도 마찬가지. 내부 구현은 관심 밖이다.
그래서, 객체지향과 절차지향의 차이점을 코드 진행의 최소 단위가 객체인지 프로시저인지로 구별한다.
메시지와 메소드
객체지향 프로그래밍에서 중요하게 여기는 것은 객체 그 자체보단 객체 간의 협력으로 보인다. 객체는 다른 객체에게 요청하고, 요청받은 객체는 행동을 하고, 응답을 돌려준다. 요청 보내는 것을 메시지라고 한다. 메시지의 관점에선 이런 코드가 객체를 가장 잘 표현한 것 같다.
class Object { // object는 동적으로 만들 수 있는 오브이다. public object doSomething(string msg, object arguments) { switch() { case 'type1': // 어떤 행위1 break; case 'type2': // 어떤 행위2 break; default: // 정의되지 않은 행위 break; } return object } }
다른 객체로부터 메시지와 정보를 받아서 어떤 무언가를 하고(do something)을 하고 그 응답을 반환한다. 이 관점에선, 인터페이스가 doSomething하나로도 충분하다. 그런데, 위 코드엔 문제점이 있다. 바로 정보를 전달하기 위한 인자와 응답이 너무 동적이라는 것이다. 코드만 봐서는 도대체 메시지에 어떤 정보를 전달해줘야 하는지 알기 어렵다. 어떤 응답이 오는지도 알 수 없다. 이를 알려주는 문서를 만들어서 참고해야 한다.
예를 들어, 달리기라는 메시지를 준다면 인자에 속력과 방향이 필요할 것이다. 응답으로 달리는지 달릴 수 없는지, 달린다면 어느 정도 속력으로 달리는지 등을 반환할 수 있을 것이다. 먹는다라는 메시지라면 인자에 음식이 있을 것이다. 응답으로 먹는지 안 먹는지, 먹는다면 얼마만큼 먹는지를 반환할 수 있을 거고. 이와 같은 추가 정보가 코드상에 전혀 반영되어 있지 않다.
이처럼 메시지로 처리를 하려면 동적인 정보가 넘쳐나서 코딩하기 어렵다. 그래서 메소드란 개념이 나온 게 아닌가 싶다. 메소드가 반영된 코드는 이럴 것이다.
class Object { public int Run(int direction, int speed) { ... } public bool Eat(Food food) { ... } }
여기서 메시지를 발견할 수 없지만, 이전 코드처럼 코드 외에 특별한 문서가 필요로 하진 않아 보인다. 그래서 메시지에 대한 구현체로 메시지 자체를 처리하는 방식보단 위와 같은 메소드 방식을 사용하는 게 아닐까 싶다.
Tell. Don't ask
Tell. Don't ask는 객체에게 요청을 할 때, 어떤 일을 할 수 있는지 확인하지 말고 요청하라는 것이다. 할 수 있는지 없는지는 객체가 자율적으로 판단한다.
// Tell. Don't ask가 적용안된 상태 if(human.IsRunable()) { human.Run(right, 10m/s); } // Tell. Don't ask가 적용된 상태 human.Run(right, 10m/s); // Run 내부에서 뛸 수 있는지 확인한다. class Human { public int Run(int direction, int speed) { // 뛸 수 있는지 확인해야 하는 코드는 구현으로 숨겨진다. if(this.isRunable()) { ... } } }
위와 같은 원칙은 정말 좋은 접근 방식이다. 왜 좋을까?
첫 번째로, 어떤 일을 시키기 전에 어떤 상태를 확인해야 하는 과정이 생략된다. 다시 말해서, 프로그래머는 어떤 클래스의 메소드A를 호출하기 전에 메소드 B를 호출해야 한다는 것을 기억할 필요가 사라진 것이다. 인지 과정에서 주의를 기울여야 할 대상이 반으로 줄어든 것이다. 그만큼 집중력을 다른 일에 쏟을 수 있다. 코딩하는 입장에서 봐도 너무 좋다.
두 번째로, 인터페이스의 숫자를 줄일 수 있다. 뛰는 걸 시키기 전에 뛸 수 있는지 확인하고, 먹는 것을 시키기 전에 먹을 수 있는지 확인하고.... 확인을 위한 메소드가 인터페이스에서 사라진다. 한 번이라도 읽어봐야 하는 메소드의 숫자가 줄어든 것이다.
위 원칙이 지켜진 클래스를 가져다 써야 된다고 생각하면 매우 편할 것이다. 불필요하게 확인해야 하는 메소드도 없고, 어떤 메소드를 호출하기 전에 반드시 호출해야 하는 메소드를 확인할 필요도 없고. 정말 좋은 접근방식이다.
인터페이스부터 작성하는 프로세스
객체지향 프로그래밍을 지켜보다면, 인터페이스부터 작성하는 것을 볼 수 있다. 인터페이스 1개에 구체클래스 1개가 대응하는 코드도 많이 본다. 예전의 나는 이것을 매우 불편하게 느꼈다. 인터페이스 1개에 구체클래스가 1개라면, 인터페이스가 왜 필요하지? 인터페이스 때문에 어떤 코드가 호출되는지 확인하기도 어렵고.... 뭐 이런 생각들... 지금은 저런 방식의 장점을 이해하고 받아들이고 있다.
그러니까, 객체지향 프로그래밍에서 중요한 건 객체 간의 협력이고, 객체와 객체는 서로 메소드를 호출하여 요청과 응답을 주고받는다. 중요한 것은 행위이고 상태는 구현으로 감춰져 있다. 그런데, 클래스부터 작성을 시작하면, 나도 모르게 상태를 많이 고려하고 있다는 것을 느낀다. 즉, 객체 간의 협력과 행위가 중요하므로 인터페이스를 우선해야 하고 상태를 후순위로 둬야 하는데, 이 둘을 동등하게 혹은 상태를 우선하게 되는 방식의 코딩을 하게 되는 것이다.
그런데, 인터페이스를 먼저 코딩하게 되면 강제성을 가지고 교정이 되는 것이다. 마치 교정 젓가락을 사용하는 느낌이랄까. 인터페이스는 내부 상태를 허용하지 않기 때문이다. 이것이 생각보다 많은 도움이 된다는 것을 느낀다. 인터페이스 1개에 구체클래스 1개가 구현되는 상황이 여전히 불편하게 느껴지지만, 인터페이스를 먼저 작성하는 방식이 시스템에 조화로운 객체를 작성하는데 도움이 된다는 것을 받아들였다.
객체지향이 잘 안 되는 이유
가장 큰 이유는 절차지향부터 배워서 그런 것 같다. 나를 포함한 대부분의 사람은 특별하지 않기에, 어떤 것을 먼저 배우면 그게 기준이 된다. 롤에선 튜토리얼에서 블루팀(왼쪽아래)부터 시작하기에, 블루팀의 승률이 레드팀(오른쪽 위) 보다 높다고 하더라. 비슷하게 절차지향부터 배운 것이 프로그램을 기계에서 도는 명령어의 집합으로 이해하는 것에 도움을 주지만, 객체지향 시스템으로 가는데 방해되는 게 아닐까.
그다음 이유는 디버깅할 때 매서운 맛을 본 경험이 객체의 상태에 집착하게 만들어서 그런 게 아닐까 싶다. 프로그램에 문제가 생기면 디버거를 열고 디버깅을 하게 되는데, 객체지향 프로그래밍을 하면 디버깅이 난해해지는 경험이 있었다. 콜스택을 보고 어떻게 호출되는지 봐야 하는데, 인터페이스를 기반으로 하다 보면 지금 호출하는 객체는 뭐고 호출되는 객체는 뭔지 파악하기가 어려웠다. 그래서 객체의 상태가 어떤지 집착하게 되고 상대적으로 시스템 안에서 협력하는 객체를 만드는 것에 소홀한 게 아닌가 싶다(내가). 인터페이스보다 상태를 보게 되고... 미숙해서 그랬겠지만, 아무래도 사람은 남 탓을 하게 되기 마련이다.
TDD(OOP에 웬 TDD?)
TDD에 대하여 잘 모른다. 강의도 보고 개인 프로젝트에도 써보고 해 봤지만, 여전히 잘 모르겠다. 그런데, TDD를 인터페이스를 완성해 나가는 과정으로 이해했더니 나아지긴 했다. 물론 그래도 잘 모르겠다.
먼저, 객체지향 프로그래밍에선 구현하려는 대상을 도메인으로 잡고, 도메인에서 불필요한 것을 처내고 집중하고 싶은 것을 걸러서 모델을 뽑아낸다. 모델에서 협력하는 존재로써의 객체를 만들어내고, 그 객체를 바탕으로 코드를 구성한다. 순서를 매기면 다음과 같다.
- 도메인 분석
- 도메인 ㅡ> 모델
- 모델 ㅡ> 객체
- 객체 ㅡ> 클래스(코드)
3번 과정에서 구체적으로 객체가 어떤 협력을 할지 결정한다. 협력이란 객체가 다른 객체와 어떤 메시지를 주고받을 수 있는지 정하는 것이다. 4번 과정에선 메시지를 기반으로 인터페이스를 정하고, 구현한다.
위 4번 과정에서 3번 과정에서 잘못 생각했다는 것을 발견한다. 그래서 3번과 4번 과정이 계속 반복된다. 보통 이과정이 3번은 반복되어야 스스로 만족할 수 있는 구조가 나오더라.
TDD는 이 3번과 4번 과정을 기계적으로, 국소적으로 하기 위한 방식이 아닌가 싶다. 생각, 도식화로 이루어진 객체를 코드로 바꾸는 과정에선 항상 수정이 일어난다. 그래서, 아예 따로 기존 시스템과 격리된 공간을 만들고, 이곳에서 짧은 주기로 많이 반복한 다음에 기존 시스템으로 편입하는 과정을 거치는게 아닐까 싶다. 편입 과정에서도 비슷하게 3~4번이 반복되지만 횟수도 줄고 시간도 좀 줄여줄 수 있는
TDD가 이름에 테스트가 들어가니까 디버깅이랑 관련이 있다고 여기게 되고, 또 디버깅에 도움이 되긴 하는데, 그 관점으로 접근하면 "아니 디버깅을 위해 이렇게 하는 게 맞나?" 같은 여러 생각이 들고 회의적으로 느끼고 영 아니다 싶게 느끼는 게 아닌가 싶다. 개발하는 일련의 과정으로써 받아들이고 익숙해지면 효율이 올라갈거같아서 생각날때마다 보는게 아니런지.
요즘 생각하는 것은, 구현하기 전에 어떻게 테스트를 해야 그것을 제대로 만들었는지 확인할 수 있을지 생각하는 점이다. 구현하고 테스트를 생각하는 것과 테스트를 먼저 생각하고 구현하는 것은 순서를 바꿨을 뿐이지만, 생각보다 효율적이다.
참고
- 내가 코딩에서 쌓은 경험과 출처가 기억 안나는 많은 아티클
- 객체지향의 사실과 오해
'분석과탐구' 카테고리의 다른 글
스트리밍 서비스가 궁금하다 (0) 2023.12.16 클라우드 트래픽 비용 비교 (0) 2023.11.29 JWT와 세션 (0) 2023.07.22 락이 필요한 경우 어떻게 처리할까 고민 (0) 2023.06.09 도커로 MySQL 다루기 (0) 2023.06.09