OOP (Object Oriented Programming)

객체 지향 프로그래밍

객체 지향 프로그래밍 전의 프로그래밍 패러다임은 컴퓨터가 사고하는 데로 프로그래밍을 했다면, 객체 지향 프로그래밍은 현실 세계를 프로그래밍으로 옮겨와 프로그래밍을 한다. 즉 인간 중심적 프로그래밍 패러다임 이라고 볼 수 있다.

장점

  • OOP는 재사용성이 높은 코드를 작성할 수 있다.

—> 중복 소스 관리 : 객체의 특징을 뽑아와서 코드로 작성하므로, 중첩되는 소스코드를 묶어서 관리하는 과정을 통해서 중복 소스를 줄일 수 있다

—> 재사용성 : 자주 사용 되는 로직을 라이브러리로 만들어 두면 계속해서 재사용 할 수 있다. 라이브러리를 재사용 하면 다음 프로젝트의 개발 시간이 줄어들고 사용할 때 마다 버그를 수정하면 신뢰성과 버그 감소 효과를 얻을 수 있다.

—> 공유 : 다른 사용자의 공개된 라이브러리를 사용하는 방법에 대해서만 인지하면 쉽게 사용할 수 있다. 이는 생산성 향상에 도움을 준다.

—> 디버그 : 객체 단위로 디버깅을 쉽게 할 수 있다.
—> 프로그래밍 : 데이터 모델링을 할 때 객체와 matcing 하는 게 수월하기 때문에 요구사항을 보다 명확하게 파악해서 프로그래밍 할 수 있다.

단점

  • 객체 간의 정보 교환이 모두 메시지 교환을 통해 이뤄지므로 시스템에 많은 Overhead가 발생한다.

—> 하드웨어의 발전을 통해서 많은 부분 보완됨

  • OOP의 단점은 함수형 프로그래밍 패러다임의 등장 배경을 통해서 알 수 있다. 바로 객체가 상태를 갖는다는 점이다. 변수가 존재하고 이 변수를 통해 객체가 예측할 수 없는 상태를 갖게 되어 애플리케이션 내부에서 버그를 발생 시킨다.

객체 지향적 설계 원칙 (SOLID)

  • SRP (Single Responsibility Principle) : 단일 책임 원칙
    클래스는 단 하나의 책임을 가져야 하며 클래스를 변경하는 이유는 단 하나의 이유이어야 한다.
  • OCP (Open-Closed Principle) : 개방-폐쇄 원칙
    확장에는 열려 있어야 하고 변경에는 닫혀 있어야 한다.
  • LSP (Liskov Substitution Principle) : 리스코프 치환 원칙
    상위 타입의 객체를 하위 타입의 객체로 치환해도 상위 타입을 사용하는 프로그램은 정상적으로 작동해야 한다.
  • ISP (Interface Segregation Principle) : 인터페이스 분리 원칙
    인터페이스는 그 인터페이스를 사용하는 클라이언트 기준으로 분리해야 한다.
  • DIP (Dependency Inversion Principle) : 의존 역전 법칙
    고수준의 모듈은 저수준의 모듈의 구현에 의존되서는 안된다.

객체(Object)와 클래스(Class)의 차이

객체는 상태를 갖고있어 상태가 변경되는 변수나 클래스를 말한다.
클래스는 객체를 만드는 추상적인 틀이라고 생각하면 된다.

슈크림 붕어빵과 팥 붕어빵이 있다. 이 두 빵의 필수적인 요소를 간추려 내면 공통적인 붕어빵의 특징이 나온다. 이러한 과정을 후술할 추상화라고 한다.

추상화로 만들어진 붕어빵은 클래스이다. 붕어빵 클래스로 슈크림 붕어빵과 팥 붕어빵을 만들어 낼 수 있다. 슈크림 붕어빵을 생성할 때 붕어빵 클래스의 변수 Type을 슈크림으로 하고 팥 붕어빵은 팥으로 지정하자. 그렇게 만들어진 슈크림 붕어빵과 팥 붕어빵은 객체이다. 이렇게 만드는 과정을 인스턴스화 라고 한다.

추상화

추상화는 어떤 사물에 대해서 필수적인 요소 및 핵심 기능들을 간추려 내는 걸 뜻한다.

예를 들어, 사과와 딸기의 필수적인 요소와 핵심 기능을 간추려 보자.

사과 : 과일, 달다, 단단하다. 중간 정도의 크기, 빨간색 (풋사과도 있지만.)
딸기 : 과일, 달다, 물렁하다. 작은 크기, 빨간색

그렇다면 정리된 걸 추상화 계층으로 표현할 수 있다

추상화 계층

각 레이어로 분리해서 구별하는 일을 추상화 계층이라고 한다.
OSI 7계층 등이 추상화 계층에 해당된다.

   과일
    |
(사과, 딸기)

과일은 사과와 딸기를 포함하는 개념이다. 그러므로 과일은 사과와 딸기의 상위에 있는 상위 클래스가 된다.

상속

객체 지향 프로그래밍에서 상속은 중요한 요소로 객체의 구조를 만들거나, 객체의 공통점을 뽑아내서 중복된 코드를 줄일 수 있다.

예를 들어 수박과 사과를 파는 시뮬레이터를 제작한다고 가정하자. 그렇다면 수박과 사과의 특징을 추출해서 소스코드로 표현하면 아래와 같다.

class Apple
{
public:
  std::string name;
  int price;
  ...
}

class WaterMelon {
public:
  std::string name;
  int price;
  ...
}

위의 소스코드를 봤을 때 중복되는 코드 이름과 가격이 보인다. 이걸 상속을 통해서 중복되는 코드를 줄일 수 있다.

class Fruit {
public:
  std::string name;
  int price;
}

class Apple : public Fruit {
  ...
}

class WaterMelon : public Fruit {
  ...
}

과일 클래스를 만들어서 사과와 수박이 Fruit를 상속하도록 구현했다. 상속을 하니, 사과와 수박이 부모 클래스로 과일을 둔다는 걸 소스에서 명시적으로 알 수 있게 되었다. 객체의 구조에 대해서 명확하게 알 수 있으므로 효과적인 소스코드 커뮤니케이션이 가능해진다.

캡슐화 (Encapsulation)

객체나 클래스를 작성할 때 외부에서 멤버 변수나 함수를 호출하여 값을 변경하면 안되는 상황이 존재한다.

class Fruit {
public:
  std::string name;
  int price;
}

class Apple : public Fruit {
  ...
}

class WaterMelon : public Fruit {
  ...
}

Apple.name = '자두'
WaterMelon.name = '사과'

Apple.name, WaterMelon.name 객체의 멤버 변수에 직접 접근해서 이름을 바꿨다. 이런 상황이 존재하면 되는 걸까? 당연히 이런 상황이 발생하면 되지 않는다. 이런 접근을 막기 위해 숨기는 행동을 은닉화라 한다.

은닉화에는 public, protected, private 3가지의 접근 지정자가 존재한다.

  • public: 전체 공개
  • protected: 상속받은 자식 클래스 내에서만 사용 가능
  • private: 선언한 클래스만 사용 가능

위의 이름을 바꾸지 못하게 하는 상황에서는 protected를 사용할 수 있다.

부모 클래스에서 name, price 선언해 놓았고, Apple, WaterMelon의 내부에서는 이름을 초기화 해야 한다.
그러므로, 자식 클래스에서 사용할 수 있게 Protected를 사용해야 한다.

class Fruit {
protected:
  std::string name;
  int price;
}

class Apple : public Fruit {
  Apple()
  {
    this.name = '사과';
  }
  ...
}

class WaterMelon : public Fruit {Apple()
  {
    this.name = '수박';
  }
  ...
}

Apple.name = '자두' // 에러!
WaterMelon.name = '사과' // 에러!

접근 지정자를 통해 데이터나 메소드를 숨겨두어 외부에서 접근이 불가능하게 만들면 예외 상황에 대해서 대응이 쉽게 가능하다. 또한 유지 보수가 쉽게 개발을 할 수 있게 설계할 수 있다. 이는 라이브러리를 제작할 때 매우 중요하다.

다형성 (Polymorphism)

다형성은 프로그램의 각 요소들(상수, 변수, 식, 함수, 메소드)이 다양한 자료형에 속하는게 허가되는 성질을 뜻한다. 반댓말로 단형성(Monomorphism)이 있다. 단형성은 각 요소들이 한가지 형태만 가지는 성질을 말한다.

OOP에서 상속은 중요한 위치를 가지고 있다. 상속은 앞서 말한대로 코드 길이도 줄여주고 구조를 직관적으로 알아볼 수 있게 해준다. 상속은 다형성의 중요한 예를 사용할 수 있는데, 아래와 같다.

class Parent {...}
class Child : Parent {...}

Parent parent = new Parent() // 허용
Child child = new Child() // 허용
Parent parent = new Child() // 허용, 다형성
Child child = new Parent() // 불가, 에러!

Parent parent = new Child() 를 보면 Parent 타입 임에도 불구하고 Child 타입이 들어간다. 다형성이 뜻하는 여러 타입에 속할 수 있다는걸 허가하는 성질이다. 하지만 그 반대로 Child child = new Parent()는 에러가 나온다. 그 이유는 Child 타입은 Parent의 Property를 가지고 있지만, ParentChild의 Property를 가지고 있지 않기 때문이다.

정리하자면 형(Type)의 관점에서 상속받은 자식 클래스의 객체는 부모 클래스에 대해 다형성을 가질 수 있다.

함수(Function)의 관점에서 다형성을 살펴보면 함수(메소드) 오버라이딩(Overriding)함수(메소드) 오버로딩(Overloading)두 가지로 나뉠 수 있다.

함수(Function)와 메서드(Method)의 차이

함수(Function)는 전역, 지역에서 단독으로 작성될 수 있는 객체를 말한다.
과거, 절차 지향적 프로세스의 기준으로 작성 되었을 때 반복적으로 사용되는 일을 함수로 만들어 전역에서 사용했다. 그렇기 때문에 함수는 독립적으로 존재해야 했다.

시간이 지나면서 OOP의 개념이 대두되고 클래스나 객체 내부에서 사용되는 함수는 객체의 상태를 변경하거나 조작하는 일을 했다. 객체에 의존적인 관계가 되어 독립적인 일을 한다는 함수의 뜻과 다른 일을 하므로 명칭이 필요해졌고 메서드(Method) 라고 지칭하게 되었다.

함수(메소드) 오버로딩(Overloading)

오버로딩(Overloading) 단어에서 느껴지는 의미는 “불러올 때 한계치 위에 있는 걸 불러오는” 느낌이다. 오버로딩이 프로그래밍에서 쓰이는 뜻과 비슷하지만 약간 다르다. 프로그래밍에서 오버로딩은 동일한 이름의 함수 혹은 메서드를 중복 선언하여 같은 이름, 다른 매개변수를 불러오는 방법 을 뜻한다.

두 함수는 동일한 역할과 같은 Parameter를 받기 때문에 에러가 나와 쓰지 못한다.

int Add(int a, int b)
int Add(int a, int b) // 동일한 함수, 에러!

두 함수는 Parameter를 통해서 다른 역할을 하는 걸 알 수 있다. 첫 번째 함수는 a, b를 더하는 결과를 반환하고 두 번째 함수는 a, b, c를 덧셈한 결과를 반환한다.

int Add(int a, int b)
int Add(int a, int b, int c) // 다른 역할, 함수 오버로딩

오버로딩을 지원하지 않는다면 아래와 같이 써야 한다.

int AddDouble(int a, int b);
int AddTriple(int a, int b, int c);

오버로딩으로 동일한 Action을 하는 함수명을 Action 단위로 간략하게 나타낼 수 있다.

함수(메소드) 오버라이딩(Overriding)

오버라이딩(Overriding) 단어에서 느껴지는 의미는 “위에 올라타는” 느낌이다. 오버라이딩이 프로그래밍에서 쓰이는 뜻과 비슷하다. 프로그래밍에서 오버라이딩은 부모 클래스의 메소드의 동작 방법을 덮어써서 변경하는 것 을 말한다.

질럿과 드라군이라는 객체가 있다. 이 두 객체의 공격을 상속을 통해 구현하면 아래와 같다.

class Unit
{
  virtual void Attack(int damage, Unit* Target) {...}
}

class zealot : Unit {...}
class dragoon : Unit {...}

// 사용
zealot.Attack(...)
draoon.Attack(...)

앗! 그런데 zealot은 2회 공격을 하기 때문에 일반 공격 메소드를 두 번 불러야 한다. 부모 클래스의 메소드 명과 같지만, 다른 행동이 필요할 때 사용하는게 오버라이딩이다. 오버라이딩을 통해 수정하면 아래와 같다.

class zealot : Unit
{
  void Attack(int damage, Unit* Target)
  {
    Unit::Attack(damage / 2, Target);
    Unit::Attack(damage / 2, Target);
  }
}

이름을 바꾸지 않고 사용하는 이유를 스타크래프트의 드래그해서 유닛을 컨트롤하는 예시로 들 수 있다.

Unit* controlUnits = new Unit[12];

void Initialize() {
  for (int i = 0; i < 12; i++) {
    if(i % 2 == 0) controlUnits[i] = new Zealot();
    else controlUnits[i] = new Dragoon();
  }
}

void onRightClick(type, cursorPoint) {
  for (int i = 0; i < 12; i++)
  {
    if (type === CURSOR_TYPE.ATTACK) controlUnits[i].Attack(15, ...);
  }
}

오버라이딩을 통해서 동일한 함수를 호출하여 각기 다른 액션을 취하도록 구현할 수 있다.

추상 클래스(Abstract Class)

코딩을 할 때 클래스를 이용해서 객체를 생성한다. 하지만 이 클래스가 추상화 레벨이 높다면, 객체로 생성해도 제대로 된 동작을 할 수 없다. 그래서 “이 클래스는 추상화 레벨이 높아 너무 추상적이야. 네가 원하는 게 제대로 안나올 수 있어” 라고 말해주는 것이 Abstract Class의 역할이다.

여러 언어에서 추상 클래스는 interface혹은 abstract 로 구현된다. 두 가지 모두 제대로 된 객체를 만들어라 라고 경고하는 의미이다.

Reference

추상클래스(abstract class)의 존재 이유?

추상 자료형 - 위키백과, 우리 모두의 백과사전

추상화 (컴퓨터 과학) - 위키백과, 우리 모두의 백과사전

다형성 (컴퓨터 과학) - 위키백과, 우리 모두의 백과사전

상속 (객체 지향 프로그래밍) - 위키백과, 우리 모두의 백과사전

ToRy 삼촌 (http://samchonlab.kr) : 네이버 블로그

함수와 메서드는 구분해서 말했으면 좋겠다

코딩교육 티씨피스쿨

JaeYeopHan/Interview_Question_for_Beginner

오버로딩(Overloading) - 프로그래밍 입문

상속시 부모클래스 맴버함수 사용

[JAVA객체지향디자인패턴] 캡슐화(Encapsulation) 란 무엇인가?

캡슐화 - 위키백과, 우리 모두의 백과사전