Self-Dev/Design Patterns R&D

[2] 객체 지향 프로그래밍 소개 - OOP의 기둥들

Khadra 2024. 5. 26. 15:48

출처 : 디자인 패턴에 뛰어들기 - 알렉산더 슈베츠 도서


객체 지향 프로그래밍(OOP)은 프로그래밍 패러다임 중 하나로, 다음의 4가지 핵심 개념에 기반을 둔다.

추상화, 캡슐화, 상속, 다형성 각각의 개념을 간단한 예제로 이해할 수 있다.


1. 추상화

추상화는 실생활의 객체를 특정 맥락에서만 필요한 속성과 행동으로 모델링하는 것이다.

  • 불필요한 세부 사항은 생략한다.
  • 중요한 부분만을 남긴다.

그림 예제-1)


위 그림과 같이, Airplane 클래스는 비행 시뮬레이터와 항공 좌석 예약 시스템에서 다르게 추상화될 수 있다.

  • 비행 시뮬레이터: 비행의 물리적 특성과 조작에 관한 정보 포함

  • 항공 좌석 예약 시스템: 좌석 배치와 예약 가능 여부 정보 포함

  • 예제 코드

      class Airplane 
      { 
      public: 
          void fly() 
          { 
              // 비행 시뮬레이터용 메소드 
          } 
    
          void bookSeat() 
          { 
              // 좌석 예약 시스템용 메소드 
          } 
      };

따라서, 추상화는 맥락에 따라 핵심적인 개념 또는 기등들로 제한되는 실제 객체 및 현상의 모델이다.
해당과 관련된 모든 세부 정보는 높은 정확도를 나타내며, 나머지는 생략한다.


2. 캡슐화

캡슐화는 객체와 상호작용할 수 있는 제한된 인터페이스만 제공하는 것을 의미한다.

  • 예시: 자동차 엔진을 시동하려면 버튼을 누르거나 키를 돌리기만 하면 되므로, 운전자는 시동 스위치, 핸들, 몇 개의 페달이라는 단순한 인터페이스만 사용 할 수 있고, 실제 엔진 작동 방식은 숨겨져 있다. 즉, 운전자에게 있는 건 시동 스위치, 핸들, 그리고 몇 개의 페달이라는 단순한 인터페이스가 전부이며, 이를 각 객체가 인터페이스를 갖는 방식임을 알 수 있다.
  • 예제 코드
      class Car 
      { 
      private: 
          void startEngine() 
          { 
              // 엔진 시동 
          } 
      public: 
          void start() 
          { 
              startEngine(); // 운전자는 시동만 걸 수 있음 
          } 
      };

캡슐화한다는 것은?

  • 객체의 상태와 행위를 외부로부터 비공개(private)로 만든다라는 의미이다.
  • 자신의 클래스의 메서드 내에서만 접근이 가능할 수 있다.
  • 조금 덜 제한적인 보호(protected) 접근제한자가 존재한다.
    • 이는 클래스의 멤버를 자식 클래스에서도 사용할 수 있게 해준다.

이를 통해 내부 구현의 변경이 외부에 미치는 영향을 최소화할 수 있다는 특징을 가지고 있다.

대부분 프로그래밍 언어의 인터페이스들, 추상 클래스들, 그리고 추상 메서드들은 추상화 및 캡슐화 개념들에 기반을 둔다.
현대 OOP 언어들에서의 인터페이스 메커니즘은 일반적으로 interface 또는 protocol 키워드로 선언되며, 객체 간의 상호작용에 대한 계약을 정의할 수 있도록 한다.

이는 인터페이스들이 객체들의 행동에만 관심을 두는 이유이자, 인터페이스에서 필드를 선언할 수 없는 이유이기도 하다.
그림 예제-1)



위 예제 그림은 fly(origin, destination, passengers)((출발지, 목적지, 승객)을 인수로 받는 비행) 메서드가 있는 FlyingTransport(비행 운송 수단) 인터페이스의 그림이다.

Airport 클래스는 FlyingTransport(비행 운송 수단) 인터페이스에 의존하고, accept(vehicle:FlyingTransport) 메서드를 보다시피 인자값인 FlyingTransport가 없을 시 동작을 하지 못하기에 때문이다.

또한 FlyingTransport(비행 운송 수단) 인터페이스는 인스턴스로 만들 수 없으며, 자식 클래스인 airplane(비행기), Helicopter(헬리콥터), DomesticatedGryphon(애완독수리) 클래스에서 구현함을 나타낸다.

따라서, 이러한 클래스들의 fly 메서드에 대한 구현을 원하는 방식으로 변경할 수 있을 뿐더러, 메서드 시그니처들이 인터페이스에 선언된 것들과 동일하게 유지되는 한 Airport 클래스의 모든 인스턴스는 나의 비행 객체들과 잘 작동할 수 있다.


3. 상속

  • 확장성을 위해 사용한다.

  • 편리한 유지/보수가 가능하다.

  • 오류 발생 시 수정 범위가 감소된다.

  • 코드 재사용성을 높일 수 있다.

  • 상속을 사용 시 자식 클래스들이 부모 클래스와 같은 인터페이스를 갖는다.

  • 어떤 메서드가 부모 클래스에서 선언되었을 경우 자식 클래스에서 그 메서드를 숨길 수 없으며, 자식 클래스들에 어울리지 않는 추상 메서드들을 포함하여 모든 추상 메서드를 구상해야한다.

  • 예제 코드

      // 예시: 동물 클래스에서 개 클래스가 상속받아 추가 기능을 더합니다. 
      class Animal 
      { 
      public: 
          void eat() 
          { 
              // 모든 동물의 공통 행동 
          } 
      };
    
      class Dog : public Animal  
      {  
      public:  
          void bark()  
          {  
              // 개만의 행동  
          }  
      };
    

그림 예제-1)


위 그림은 단일 클래스의 확장과 여러 인터페이스의 동시 구현을 표현한 UML 다이어그램이다.

보통 자식 클래스는 하나의 부모 클래스만 확장할 수 있지만, 모든 클래스가 동시에 여러 인터페이스를 구현할 수 있다라는 것이다. 즉, 부모 클래스가 인터페이스를 구현할 경우 모든 자식 클래스들 또한 그 인터페이스를 구현해야한다는 점을 알게 되었다.


4. 다형성

  • 하나의 인터페이스가 여러 실제 타입을 가질 수 있게 하는 기능이다.
  • 이를 통해 동일한 인터페이스를 사용하여 다른 객체들의 메소드를 호출할 수 있습니다.

예제 그림-1)


위 그림과 예시로 대부분의 Animals(동물들)는 소리를 낼 수 있다라 가정하여 이해를 해본다.

이로써, 모든 자식 클래스들이 기초 makeSound(소리내기) 메서드를 오버라이드해야 각 자식 클래스가 해당 동물의 소리를 올바르게 낼 수 있다라고 예상할 수 있다.

그러므로 우리는 이 메서드를 바로 추상으로 선언할 수 있다.
이는 부모 클래스에서 해당 메서드의 디폴트 구현을 생략할 수 있게 된다. 하지만, 모든 자식 클래스들은 강제적으로 이 메서드를 각자 구현해야 한다는 점이 있다.

  • 예제 코드

      class Animal  
      {  
      public:  
          virtual void makeSound() = 0; // 순수 가상 함수  
      };
    
      class Cat : public Animal  
      {  
      public: 
          void makeSound() override  
          {  
              std::cout << "야옹!" << std::endl;  
          }  
      };
    
      class Dog : public Animal  
      {  
      public:  
          void makeSound() override  
          {  
              std::cout << "멍멍!" << std::endl;  
          }  
      };
    
      int main()  
      {  
          Animal\* bag\[\] = {new Cat(), new Dog()};  
          for (Animal\* a : bag)  
          {  
              a->makeSound(); // 야옹!, 멍멍!  
          }  
          return 0;  
      }

예시2)
큰 가방에 여러 고양이와 개들을 넣었다 가정하고, 눈을 감은 상태에서 가방에서 동물을 하나씩 꺼낸다.
꺼낸 동물은 어떤 동물인지 모른다. 하지만, 그 동물을 힘껏 껴안아보면 이 동물은 구상 클래스에 따라 특정한 소리를 낼 것이다.

  • 예제 코드

      bag = [new Cat(), new Dog()]; 
      foreach (Animal a : bag) 
          a.makeSound() // 야옹!  멍멍!
    

위 예제 코드 프로그램은 a 변수 안에 포함된 객체의 구상 유형을 알지 못하지만, 다형성이라는 특별한 메커니즘 덕분에, 프로그램은 그 메서드가 실행되어 적절한 행동들을 실행하는 객체의 자식 클래스를 추적할 수 있게 된다.

따라서, 다형성은 객체의 실제 클래스를 감지 및 해당 객체의 구현을 현재 맥락에서 이것의 실제 유형을 알 수 없는 경우에도 호출할 수 있는 프로그램의 기능이다. 또한 객체가 다른 무엇가인 척할 수 있는 기능이라고도 생각할 수 있다.

일반적으로는 객체가 확장하는 클래스 또는 인터페이스인척 가장할 수 있다는 것이다.
위 예시에서는 가방이 든 개와 고양이가 일반적인 동물인 척 가장하고 있었음을 알 수 있었다.


결론

객체 지향 프로그래밍의 4가지 기둥인 추상화, 캡슐화, 상속, 다형성은 복잡한 소프트웨어 시스템을 더욱 구조적이고, 유연하며, 유지보수가 용이하게 만들어 준다. 따라서, 이러한 개념을 잘 이해하고 적용함으로써 더욱 효율적이고 확장 가능한 프로그램을 개발할 수 있을 것을 알게 되었다.