본문 바로가기
Study/디자인 패턴의 아름다움

[디자인 패턴의 아름다움] 2. 객체지향 프로그래밍 패러다임 - 2.1~2.2 정리

by Nahwasa 2024. 3. 3.

스터디 메인 페이지

목차

    - ☆ 표시가 붙은 부분은 스터디 중 나온 얘기 혹은 제 개인적인 생각이나 제가 이해한대로 적어놓은 것으로, 책에 나오지 않는 내용입니다. 따라서 책에서 말하고자 하는 바와 다를 수 있습니다.

    - 모든 이미지의 출처는 디자인패턴의 아름다움(왕정 저) 책 입니다.

     

     


     

     

    CHAPTER 02. 객체지향 프로그래밍 패러다임

    • 패러다임쪽은 공식적인 정의가 명확하지 않은 부분들이 많다. 즉, 사람마다 생각이 다른 부분이 많고, 따라서 내 생각을 적기위한 사족이 좀 많이 들어갔다. 그 부분에 남들과 생각이 다른 부분도 있을꺼고, 내가 현재 잘못 생각하고 있는 것도 있을꺼다. 결국 답은 없고, 토론이 필요한 부분이라 생각된다. 아무튼 그러니 책 내용 정리만을 보고 싶다면 '☆'이 안붙어 있는 항목만 보면 된다.(☆ 붙고 시작하는 얘기들이 사족이다.)
    • ☆ 그럼 공식적인 정의가 명확하지 않는 부분에 대해, 면접볼 때 "객체지향이 뭔가요?" 이런 질문은 뭘까? 내 생각엔, "너가 객체지향을 바라보는 관점을 알려줘"거나, "나랑 토론하자." 중 하나라고 생각된다. 전자일 확률이 높지 않나 싶다. "아무튼 뭐라도 생각을 가지고 있냐?" 느낌으로.

     

    2.1 객체지향이란 무엇인가?

    객체지향 프로그래밍과 객체지향 프로그래밍 언어

    • 객체지향 프로그래밍은 프로그래밍 패러다임 또는 프로그래밍 스타일을 의미한다. 코드를 구성하는 기본 단위로 클래스 또는 객체를 사용하고, 코드 설계와 구현의 초석으로 캡슐화, 추상화, 상속, 다형성의 4가지 특성을 사용한다.
    • 객체지향 프로그래밍 언어는 클래스 또는 객체 문법을 지원하며, 이 문법은 객체지향 프로그래밍의 4가지 특성인 캡슐화, 추상화, 상속, 다형성을 쉽게 구현할 수 있다.
    • 꼭 객체지향 프로그래밍 언어를 사용하지 않더라도 객체지향 프로그래밍을 할 수 있다. 반면 객체지향 프로그래밍 언어를 사용하더라도 해당 언어로 작성된 코드가 반드시 객체지향 프로그래밍 스타일이라는 법은 없다.
    • ☆ 특히 3번째 부분이 내 기존 생각과 비슷하여 맘에 들었다. 첫 번째 설명의 '프로그래밍 스타일'이라는 부분도 맘에 들었다. C언어는 대표적인 절차지향 언어이지만, 내 경우에도 객체지향 개념이 어느정도 들어가 만들어진 리눅스 커널의 VFS 부분을 분석한 후 비슷한 스타일로 짜본 리스트 코드가 있다. 그리고 알고리즘 문제 풀 때 내 경우 대표적인 객체지향 언어인 자바를 주로 사용하지만, 난이도 높은 구현문제가 아닌 이상 절차지향 스타일로 짠다. 전자의 경우 누군가 "이건 객체지향으로 짠 코드가 아니라"고 한 적이 있다. 난 내가 객체지향 개념을 가지고 짠거니 객체지향이 맞다고 생각했었다.
      • 객체지향 스타일로 짜려고 노력했던 C언어 코드 : C언어 - 이중 연결 리스트(doubly linked list) 구현 - 객체지향
      • ☆ 절차지향으로 짜여진 자바 코드 : 예시
        • ☆ 내가 자바로 알고리즘 문제를 풀 때 절차지향 스타일로 짜는 이유는, 복잡하지 않은 문제에 절차지향을 안 쓸 이유가 없다고 생각했기 때문이다. 또한 데이터 크기에 따라 다르겠지만 아무래도 객체지향 스타일로 짜게 되면 속도가 몇백 ms 수준으로 차이가 나기 마련이다. 알고리즘에서 몇백 ms면 TLE(시간초과) 뜨냐 안뜨냐 부분에서 꽤 큰 차이다.
    • ☆ 개인적으로 '객체지향'에 대해 가장 마음에 드는 설명은 '오브젝트'책에 나왔던 설명으로, "자율적으로 협력하는 객체들의 공동체를 창조하는 것" 이라는 설명이었다.

     

    엄격하게 정의되지 않은 객체지향 프로그래밍 언어

    • 현재 객체지향 프로그래밍과 객체지향 프로그래밍 언어에 대한 공식적이고 통일된 정의는 없다.
    • 1960년에 객체지향 프로그래밍이 등장한 이후 이 두 개념은 끊임없이 진화해왔기 때문에 명확한 정의를 내릴 수 없을 뿐만 아니라 명확한 정의를 내릴 필요조차 없다.
    • 단순하게 이해해보면, 객체지향 프로그래밍은 객체 또는 클래스를 코드 구성의 기본 단위로 사용하는 프로그래밍 패러다임 또는 프로그래밍 스타일로서 반드시 캡슐화, 추상화, 상속, 다형성의 네 특성을 갖출 필요는 없다.
      • 다만 객체지향 프로그래밍 과정에서 소프트웨어 엔지니어들은 이러한 특성들을 이용해 다양한 객체지향 코드 설계 사상을 구현하는 것이 더 쉽다는 것을 발견하고 그렇게 결론을 내려왔다.
      • 예를들어 객체지향 프로그래밍 과정에서 'is-a'의 클래스 관계(e.g. '개는 동물이다')를 자주 마주치게 되며, 이러한 특성을 상속하는 것이 'is-a' 코드의 설계 사상을 잘 지원할 수 있을 뿐만 아니라, 코드 재사용의 문제까지도 해결할 수 있게 된다. 이러한 흐름에 따라 상속은 객체지향 프로그래밍의 네 가지 특성 중 하나가 되었다.
    • 프로그래밍 언어가 클래스 또는 객체의 문법적 개념을 지원하고, 이를 코드 구성의 기본 단위로 사용하는 한 단순히 객체지향 프로그래밍 언어로 간주될 수 있다고 생각한다.
    • 객체지향 프로그래밍의 네 가지 특성을 완벽하게 지원하느냐 또는 일부를 지원하지 않느냐를 판단 기준으로 사용할 수 없다.
    • 객체지향 프로그래밍과 객체지향 프로그래밍 언어를 정의할 필요가 전혀 없다.
    • ☆ 마음에 드는 내용인 것 같다. 기존 내 생각과 비슷하며, 이로써 토론 시 내 말보다 신빙성 있을 reference를 하나 더 얻은 것 같아 좋았다(?).
    • ☆ "ㅇㅇ가 없으니 이건 객체지향이 아니야!" 이런건 의미 없다고 생각한다. 정의 자체를 내리는건 철학쪽에 가깝다고 생각한다. 결국 경력이 얼마 안되는 개발자인 내 입장에서 중요한건 현재 진행중인 프로젝트에서 뭔가를 사용하려 할 때 그 명분과 팀의 합의라 생각한다. 명분을 위해 내 관점이 필요하고, 팀의 합의 시 토론을 해야하니 명분이 필요하다.

     

    객체지향 분석과 객체지향 설계

    • 객체지향 소프트웨어 개발의 3단계 : 객체지향 분석, 객체지향 설계, 객체지향 프로그래밍
    • 분석과 설계 앞에 객체지향이라는 단어가 붙는 이유는 객체나 클래스에 대한 요구 사항을 분석하고 설계하기 때문이다.`
    • 객체지향 분석 : 무엇을 해야 하는지 알아내는 것
    • 객체지향 설계 : 그 일을 어떻게 해야 하는지를 정의
    • 객체지향 프로그래밍 : 앞에서 진행했던 분석과 설계의 결과를 코드로 구체화하는 것

     

    UML에 대한 참고 사항

    • 객체지향이나 디자인 패턴을 설명할 때, 설계 사상을 표현하기 위해 UML을 활용한다.
    • UML 클래스 다이어그램을 그리는 데는 많은 학습 시간이 필요하다.
    • 완벽하게 UML로 표준화된 문서를 작성하고 의사소통할 수 있더라도 실제 그로 인해 얻을 수 있는 이익이 크지 않은 경우가 많다.

     


     

    2.2 캡슐화, 추상화, 상속, 다형성이 등장한 이유

    캡슐화 (encapsulation)

    • 캡슐화는 정보 은닉 또는 데이터 액세스 보호라고도 하는데, 접근 가능한 인터페이스를 제한하여 클래스가 제공하는 메서드를 통해서만 내부 정보나 데이터에 대한 외부 접근을 허가하는 것을 뜻한다.
      • 여기서 '인터페이스' == '클래스가 제공하는 메서드' 이다. 일반적으로 public으로 제공하는 메서드를 public interface 라고 하거나, '메시지'라고 하기도 한다.
    • 캡슐화 특성은 프로그래밍 언어 자체에서 이를 지원하는 문법을 제공해야 하는데, 이 문법을 접근제어라고 한다. (public, private, ..)
    • 클래스의 속성에 대한 접근을 제한하지 않으면 모든 코드가 클래스의 속성에 접근하고 값을 변경할 수 있게 된다. 얼핏 이것이 더 유연하게 느껴지지만, 과도한 유연성은 제어할 수 없음을 의미한다.
      • 한마디로 캡슐화가 적용된 클래스를 짠 사람의 입장에서, 결국 public interface만 변경되지 않는다면 그 내부는 무슨짓을 하든 상관없다. 즉 오히려 캡슐화를 해야 결합도가 줄어들어 자율성이 높아진다.
    • 또한 클래스는 캡슐화를 통해 필요한 작업만 노출하는 제한된 메서드를 제공하여 클래스의 사용성을 향상시킬 수 있다. 클래스의 속성이 외부에 노출되어 있으면 비즈니스 세부 사항에 대한 충분한 이해 없이 속성을 올바르게 조작하는 것이 불가능하다.
      • 즉, public interface 사용법만 알면 되니깐 알아야 할 정보의 양이 줄어드는 셈이다. 그 내부에서 무슨짓이 일어나는진 상관없다.
    • '데이터와 기능을 객체 내부로 함께 묶은것' 이것 까지만을 캡슐화라고 하기도 한다. 그리고 이 책 처럼 거기에 '접근제어'까지 넣은 경우를 캡슐화라고 하기도 한다.

     

    추상화 (abstraction)

    • 캡슐화는 주로 정보를 숨기고 데이터를 보호하는 반면, 추상화는 메서드의 내부 구현을 숨기는 것을 의미한다. 따라서 클래스를 사용할 때 기능의 구현 방식에 대해 고민하지 않고, 메서드가 제공하는 기능에만 집중할 수 있다.
      • 개인적으로 '메서드의 내부 구현을 숨기는 것' 보다는 '불필요한 정보를 제거하고, 현재의 문제 해결에 필요한 핵심만 남기는 것'으로 추상화를 설명하는게 더 좋은 것 같다. 예를들어 '강아지.eat()' 만 알면 되지, '말라뮤트.eat()', '사모예드.eat()', ... 이걸 전부 알 필요가 없다면 그냥 강아지.eat()로만 추상화 하면 될꺼다. 근데 이번엔 모든 동물에 eat()을 짜고 싶다. '강아지.eat()', '새.eat()', ... 이번엔 이것들을 알 필요가 없다. 마찬가지로 이번엔 강아지도 추상화되어, '동물.eat()'으로 빼면 될꺼다. 즉 더 상위 개념으로 빼서, 문제 해결에 필요한 핵심만 남기는 것이 추상화라 생각한다.
    • 객체지향 프로그래밍에서 많은 경우에 Java의 Interface 키워드와 abstract 키워드처럼 프로그래밍 언어에서 제공하는 인터페이스와 추상화 클래스의 두 가지 문법을 통해 추상화 특성을 구현한다.
    • 추상화 특성은 인터페이스나 추상화 클래스와 같은 특별한 문법에 의존하지 않아도 구현할 수 있다. 예를 들어 C언어에서 제공하는 malloc() 함수를 사용할 때 내부적으로 어떻게 메모리를 할당하는지 알 필요가 없다는 뜻이다. 추상화는 는 객체지향 프로그래밍에서뿐만 아니라 아키텍처 설계를 이끌어낼 때 사용되는 일반적인 설계 사상이다. 추상화 특성을 구현하기 위해 프로그래밍 언어가 특별한 문법을 제공할 필요가 없으며, 함수라는 기본 문법만 제공해도 충분하다. 이렇게 말하는 이유는 클래스의 메서드가 프로그래밍 언어에서 함수라는 문법을 통해 구현되기 때문이다. 실제로 코드의 구현 내용은 그 자체로 추상화되는 함수의 내부에 포함된다. 우리가 함수를 사용할 때 해당 함수가 내부적으로 어떻게 동작하는지를 굳이 알지 않아도 함수의 이름과 인자, 반환값 등을 주석 또는 문서를 통해 확인할 수 있다면 바로 사용할 수 있다.
      • 'abstraction without interface or abstract class' 이런 키워드로도 이전에 찾아봤던 적이 있었는데 명확한 답을 얻지 못했었다. 이 책에서 명확히 정리해준 것 같아 많이 좋았다.
    • 복잡한 시스템에 직면했을 때 인간의 두뇌가 견딜 수 있는 한계가 제한적이기 때문에 중요하지 않은 세부 정보는 무시해야 한다. 구현이 아닌 기능에만 초점을 맞춘 설계 사상인 추상화는 뇌가 불필요한 많은 정보를 걸러내는 데 도움이 된다.
    • getNaverCloudPictureUlr()은 추상적인 사고의 결과로 지어진 이름이 아니다. getPictureUrl() 같이 보다 추상적인 이름을 사용하는 것이 좋다. 이렇게 메서드 이름을 정의하면 내부 구현이 완전히 다른 서비스를 참조하도록 수정하더라도 메서드 이름을 수정할 필요가 없게 된다.
    • 자 그럼 여기서 문제가 생기는데, 그럼 '캡슐화'는 '추상화'가 아니냐? 라고 하면 내 생각엔 캡슐화도 추상화다. 즉, 문법적인 부분에서 "아 이건 추상화 한건데, 이건 캡슐화한거네." 이렇게 구분은 할 수 없다고 생각한다. 애초에 두 개념이 동시에 들어가는 경우가 대부분일 것이기도 하고, 그냥 둘은 목적의 차이이고, 명확히 이분화 할 필요는 없다고 본다. 중요한건 이런게 있단걸 알고 내가 짤 때 이런걸 생각하고 짰냐, 무지성으로 짰냐라고 생각한다.

     

    상속 (inheritance)

    • 상속은 '고양이는 포유류의 일종이다'처럼 클래스 사이의 'is-a' 관계를 나타내는 데 사용된다.
      • ☆ 참고로 'is-a'는 상속(inheritance), 'has-a'는 합성(composition)으로 나타낼 수 있다. 또한 'is-a'는 현실세계에서의 'is-a'가 아니라 사용자 입장에서의 문제이다. 예를들어 현실세계에서 '펭귄은 새' 이지만, 사용자가 새는 날기를 원한다면 해당 프로젝트에서 펭귄과 새는 'is-a' 관계라 할 수 없다.
    • 단일 상속은 하위 클래스가 단 하나의 상위 클래스만을 상속하는 것을 말하며, 다중 상속은 하위 클래스가 여러 상위 클래스를 동시에 상속할 수 있음을 의미한다.
      • 자바의 경우 다중상속은 안된다 (extends A, B 이런거 안됨). interface는 다중으로 가능하다. 다만 interface도 default 메서드가 있고, 그게 중복되면 다중으로 안된다. 다중상속이 안되는 이유는 간단한데, extends A, B 를 했는데 A에도 aa()가 있고, B에서 aa() 가 있다면 뭘 써야할지 모르기 때문이다. c++은 다중상속이 되는데 그럼 c++은 이걸 어떻게 해결했냐면, 해결 안했다. 그냥 개발자가 알잘딱하게 쓰라고 냅둔 경우이다.
    • 추상화와 달리, 프로그래밍 언어가 상속을 표현하는 문법을 별도로 지원해야 한다.
    • 상속의 가장 큰 역할은 코드의 재사용이라고 할 수 있다. 상위 클래스의 코드를 재사용하고 동일한 코드를 불필요하게 반복적으로 작성하는 것을 방지할 수 있다.
    • 상속 계층 구조가 너무 깊고 복잡하면 코드의 가독성과 유지 관리성이 떨어진다. 특정 클래스의 기능을 이해하기 위해서는 해당 클래스의 코드를 살펴보는 것으로 끝나는 것이 아니라 상위 클래스의 코드, 심지어 상위 클래스의 상위 클래스도 계층에 따라 모두 살펴볼 필요가 있다.
    • 상속은 논란의 여지가 있는 특성이기도 하다. 그러므로 상속은 적게 사용해야 하며, 심지어는 사용하면 안 되는 안티 패턴으로 간주되기도 한다.
      • 이것의 가장 큰 이유가 상속을 코드의 재사용을 위해 주로 사용해서 그런 것 같다. 코드 재사용이 아니라 타입 확장을 위해 사용하는 경우 상속이 크게 문제되진 않는 것 같다. 많은 경우에 상속으로 문제가 생긴 경우 합성으로 처리하면 좀 더 깔끔해지는 것 같다.
      • 코드 재사용이 아니라 타입 확장을 위해 상속을 사용하려면, 다음을 만족할 때만 상속을 사용하는게 좋다. 'is-a' 관계를 모델링 하면서, 행동 호환성(LSP, 리스코프 치환 원칙)을 만족하는 경우. 예를들어 정사각형은 직사각형이다. 'is-a' 관계는 맞지만, 직사각형 대신 정사각형을 사용하는건 LSP를 만족하지 않는다. 예를들어 50x100 짜리 직사각형을 만들고, 거기에 정사각형을 사용한 경우 50x100 짜리 정사각형은 불가하다. 즉, 사용자가 차이점을 인식하지 못한 채 기반 클래스의 인터페이스를 통해 서브클래스를 사용할 수 있어야 한다.
      • 자바의 Stack도 상속으로 인해 문제가 발생하게 된 코드이다. 해당 내용 및 합성을 통해 상속의 문제를 해결하는 예시를 보려면 '[세미나] queue, stack, deque (기초 자료구조 및 알고리즘 4회차)' 글의 ppt 25,26 page를 한번 봐보자.

     

    다형성 (polymorphism)

    • 다형성이란 코드를 실행하는 과정에서 하위 클래스를 상위 클래스 대신 사용하고, 하위 클래스의 메서드를 호출할 수 있는 특성을 의미한다.
    • 말이 좀 애매할 수 있는데, 결국 다형성은 컴파일 시점엔 실행될 메서드를 모르고, 실행 시점에(runtime) 실행될 메서드를 결정하는걸 의미한다(동적 바인딩). 즉, 동일한 메시지를 수신했을 때 개체의 타입에 따라 다르게 응답할 수 있는 능력이 다형성이다. 예를들어 '이 글'의 맨아래에 있는 코드를 한번 봐보자. 알고리즘 문제 푼 코드인데, op.proc()이 실행될 때 컴파일 시점엔 어떤 Operation 구현체의 proc()이 실행될지 모른다. 실행 시(runtime)에 입력값에 따라 알아서 개체의 타입에 따라 실행될 구현체가 결정된다.
    • 또한 다형성은 상속을 통해서만 구현 가능한것도 아니다. 자바의 제너릭도 결국 다형성이고(매개변수 다형성), 오버로딩도 다형성이다(오버로딩 다형성).

     

     

     

     

    댓글