OOP Introduction

Updated:

OOP(Object Oriented Programming)

컴퓨터 프로그래밍의 패러다임 중 하나이다. 객체 지향 프로그래밍은 컴퓨터 프로그램을 명령어의 목록으로 보는 시각에서 벗어나 여러 개의 독립된 단위, 즉 객체들의 모임으로 파악하고자 하는 것이다. 각각의 객체는 메시지를 주고받고, 데이터를 처리할 수 있다.

즉, 현실 세계를 프로그래밍으로 옮겨와 프로그래밍하는 것을 말한다. 현실 세계의 사물들을 객체라고 보고 그 객체로부터 개발하고자 하는 애플리케이션에 필요한 특징들을 뽑아와 프로그래밍 하는 것이다. 이것을 추상화라한다.

OOP 로 코드를 작성하면 이미 작성한 코드에 대한 재사용성이 높다. 자주 사용되는 로직을 라이브러리로 만들어두면 계속해서 사용할 수 있으며 그 신뢰성을 확보 할 수 있다. 또한 라이브러리를 각종 예외상황에 맞게 잘 만들어두면 개발자가 사소한 실수를 하더라도 그 에러를 컴파일 단계에서 잡아낼 수 있으므로 버그 발생이 줄어든다. 또한 내부적으로 어떻게 동작하는지 몰라도 개발자는 라이브러리가 제공하는 기능들을 사용할 수 있기 때문에 생산성이 높아지게 된다. 객체 단위로 코드가 나눠져 작성되기 때문에 디버깅이 쉽고 유지보수에 용이하다. 또한 데이터 모델링을 할 때 객체와 매핑하는 것이 수월하기 때문에 요구사항을 보다 명확하게 파악하여 프로그래밍 할 수 있다.

Object vs Instance vs Class vs Method

  • 객체(Object)

소프트웨어 세계에 구현할 대상이며, 클래스에 선언된 모양 그대로 생성된 인스턴스이다. 객체는 모든 인스턴스를 대표하는 포괄적 의미도 가지고 있다. 객체는 자신 고유의 속성(attribute)을 가지며 클래스에서 정의한 행위(behavior)를 수행할 수 있다.

  • 인스턴스(Instance)

클래스를 바탕으로 소프트웨어 세계에 구현된 구체적인 실체다. 객체를 소프트웨어에 실체화하면 그것을 인스턴스라고 부른다. 인스턴스는 객체에 포함된다고 말할 수 있다.

  • 클래스(Class)

같은 종류(또는 문제 해결을 위한)의 집단에 속하는 속성(attribute)과 행위(behavior)를 정의한 것으로 객체지향 프로그램의 기본적인 사용자 정의 데이터형(user defined data type)이라고 할 수 있다. 객체를 만들어 내기 위한 설계도 혹은 틀이라고 생각하면 된다.

  • 메서드(Method)

클래스로부터 생성된 객체를 사용하는 방법으로서 객체에 명령을 내리는 메시지라 할 수 있다. 메서드는 한 객체의 서브루틴(subroutine) 형태로 객체의 속성을 조작하는 데 사용된다. 또 객체 간의 통신은 메시지를 통해 이루어진다.

Example

// Class
class Circle {
	int radius;

	// Method
	public void setRadius(int value){
		self.radius = value;
	}
}

public class Example {
	public static void main(String[] args) {

		// Instance 생성
		Circle c1 = new Circle();
		c1.setRadius(10);
	}
}

OOP Features

OOP에 특징을 4가지로 분류를 하면 다형성, 추상화, 캡슐화, 상속라고 하며 5가지로 나누면 은닉화가 추가됩니다. 은닉화캡슐화에 비해서 좀 더 구체적인 개념이라고 생각하면됩니다.

여기서는 4가지로 분류를하여 정리를 하겠습니다.

추상화 (Abstraction)

OOP에서 추상화라는 개념은 ‘객체에서 공통된 속성과 행위를 추출하는 행위’ 라고 정의가 많이 되는데 저는 이 개념은 상속에더 적합하다 생각하여 좀 더 추가를 하겠습니다.

추상화에 개념은 ‘현실 세계를 프로그래밍으로 옮겨와 프로그래밍하는 행위, 현실 세계의 사물들을 객체라고 보고 그 객체로부터 개발하고자 하는 애플리케이션에 필요한 특징들을 뽑아와 프로그래밍 하는 행위’ 라고 생각합니다.

예를들어 사람이라는 클래스를 정의할 때 사람들의 특징은 나이, 성별, 키, 몸무게 등등이 있습니다. 이 특징을 추출하여 프로그래밍 해보겠습니다.

Example

class Person{
public:
	int age;
	bool sex;
	float height;
	float weight;
	void eat();
	void sleep();
}

다형성 (Polymorphism)

다형성이란 같은 자료형에 여러 가지 객체를 대입하여 다양한 결과를 얻어내는 성질을 의미한다. 하나의 타입으로 다양한 실행 결과를 얻을 수 있으며 객체를 부품화하여 유지 보수를 용이하게 한다. 그리고 다형성을 통하여 오버라이딩과 오버로딩을 사용할 수 있다.

오버라이딩(Overriding) vs 오버로딩(Overloading)

오버라이딩재정의라고 표현한다. 상위클래스에서 정의한 메소드를 하위클래스에서 다시 정의한다. 이때 하위클래스에서는 상위클래스에 메소드 이름, 파라매터, 반환타입 전부다 같아야한다.

Example

class Person(){
public:
	void eat(string food){
		System.out.println("사람이 %s을 먹습니다.", food);
	}
}

class Student() extends Person{
public:
	void eat(string food){
		System.out.println("학생이 %s을 먹습니다.", food);
	}
}

오버로딩은 같은 이름의 함수를 여러 개 정의하고, 매개변수의 유형과 개수를 다르게 하여 다양한 유형의 호출에 응답하게 한다. 함수를 호출시 매개변수에 따라 매칭되어 함수를 실행시킨다.

Example

class Caluclator(){
public:
	void sum(int a, int b);
	void sum(float a, float, b);
	void sum(int a, int b, float c);
}

다음과 같은 경우도 다형성으로 다형성에 포함된다고 합니다.. 저는 이번에 찾아보면서 처음 알게되었습니다.

C++과 같은 객체지향언어에서 제공하는 StringValue()과 같이 범용 메소드 이름을 정의하여 형태에 따라 각각 적절한 변환 방식을 정의해둠으로써 객체의 종류와 상관없는 추상도가 높은 변환 형식을 구현할 수 있다.

Example

//숫자를 문자열로 바꾸는 경우
string = number.StringValue();

//날짜를 문자열로 바꾸는 경우
string = date.StringValue();	

캡슐화 (Encapsulation)

실제로 구현되는 부분을 외부에 드러나지 않도록 캡슐로 감싸 이용방법만 알려주는것이다. 이는 다른말로 데이터를 절대로 외부에서 직접 접근을 하면 안되고 오로지 함수를 통해서만 접근해야하는데 이를 가능하게 해주는 것이 바로 캡슐화이다.

은닉화는 캡슐화와 많이 혼용되지만 캡슐화에 비해 비교적 구체적인 개념이다. 내부 데이터, 내부 연산을 외부에서 접근하지 못하도록 은닉(hiding) 혹은 격리(isolation)시키는 것이며, 변수에 접근지정자를 private 로 지정하여 setter , getter 를 사용해 변수의 접근, 제어한다. UI 나 인터페이스 설계에서 핵심적인 부분이다.

예를들면 리모컨을 사용할 때 내부구조가 어떻게 된지는 모르지만 우리는 버튼을 이용하여 접근할 수 있다고 생각하면된다.

상속 (Inheritance)

상속은 객체들 간의 관계를 구축하는 방법이다. 그리고 상속을 통하여 코드가 줄어든다. 하위 클래스에서 속성이나 오퍼레이션을 다시 정의하지 않고 상속받아서 사용함으로써 코드가 줄어든다. 그리고 좀 더 범용성있게 사용할 수 있다. 사람이라는 클래스로 상속을 나타내 보겠습니다.

Example

class Person{
public:
	int age;
	bool sex;
	float height;
	float weight;
	void eat();
	void sleep();
}
clss Student extends Person{
public:
	float score;
	string school;
	void study();
}

학생이라는 하위 클래스는 상위 클래스가 가지고 있는 모든 자료와 메소드를 물려받아 자유롭게 사용할 수 있지만, 또한 자신만의 자료와 메소드를 추가적으로 덧붙임으로써 새로운 형태의 클래스로 발전할 수 있다.

아래는 많은 분들이 잘 모르시거나 헷갈리는 예제 몇 가지를 준비해 알아보겠습니다.

추상 클래스(Abstract Class) vs 인터페이스(Interface)

Java에서는 추상클래스인터페이스 두 가지 개념이 있는데 정확한 의미와 차이를 몰라서 정리를 했습니다.

추상클래스의 목적은 기존의 클래스에서 공통된 부분을 추상화하여 상속하는 클래스에게 구현을 강제화시켜 메서드의 동작을 구현하는 자식클래스로 책임을 위임 공유의 목적이라고 할 수 있다.

Example

abstract class AbstractClass

class Class extends AbstractClass

인터페이스의 목적은 구현하는 모든 클래스에 대해 특정한 메서드가 반드시 존재하도록 강제한다. 말그대로 인터페이스는 뼈대 라고 할 수 있다. 뼈대를 기반으로 해서 구현을 하는 것이다. 마치 1개의 나사 설계도를 가지고 여러 개의 나사를 제품으로 만드는 것처럼 말이다.

Example

interface Interface

class Class implements Interface

다중 상속도 가능하다.

class Class implements Interface1, Interface2

그리고 가장 큰 특징으로 추상클래스에 상속받은 클래스는 기존 메소드에서 확장이 가능하지만 인터페이스로 구현된 클래스는 인터페이스에서 제공된 메소드만 사용가능하며, 확장이 불가능하다.

가상함수 vs 순수 가상함수

C++ 에서는 virtual 키워드가 존재하여 가상함수를 구현할 수 있다.

가상함수virtual 키워드로 함수를 정의하며 순수가상함수는 함수를 정의하고 끝에 virtual void func()=0 과 같이 빈 함수를 만들어줍니다.

먼저 예제를 보겠습니다.

Example

class AHG {
    
public:
    int value = 0;
    void func0() {
        cout << "AHG func0" << endl;
        cout << value << endl;
    }
    // 가상함수
    virtual void func1() {
        cout << "AHG func1" << endl;
    }
    // 순수 가상함수
    virtual void func2() = 0;
};

class Child : public AHG {
    
public:
    void setValue(int temp) {
        value = temp;
    }
    void func0() {
        cout << "Child func0" << endl;
    }
    void func1() override {
        cout << "Child func1" << endl;
    }
    void func2() override {
        cout << "Child func2" << endl;
    }
};

int main(int argc, const char * argv[]) {
    // insert code here…
    Child child;
    AHG* ptr = &child;
    
    child.setValue(1);
    ptr->func0();   // AHG func0 \n 1
    ptr->func1();   // Child func1
    
    return 0;
}

C++ 에서는 순수가상함수는 인터페이스를 자식에게 전달하여 재정의 즉 오버라이딩을 하기 위해 사용합니다. 즉, 재정의를 하지 않으면 오류가 발생하여 반드시 자식 클래스에서 재정의를 해야합니다.

가상함수는 함수내부 구현이 되어있는 인터페이스를 자식에게 전달합니다. 하지만 함수의 내부구현이 되어있어서 자식클래스에서는 함수를 다시 정의해도 되고 안해도 됩니다.

쉽게 말하자면 순수가상함수는 ‘너는 이 기능이 꼭 필요해 그리고 그 기능은 너가 알아서 선언해 만약 선언하지 않으면 오류가 날꺼야’ 이며 가상함수는 ‘이 기능을 물려줄건데 너가 선언을 안해도 기본적으로 작동이 되게 만들어줄게’ 입니다.

마지막으로 위 코드에서 AHG 클래스에 func0Child 클래스에 func0은 서로 다른 함수입니다. 우연히 이름이 같게된 함수입니다.

is - a vs has - a

간단하게 설명하면 is-a 관계는 상속을 통하여 사용한다. 즉, ‘사람은 학생이다.’ 관계는 상속을 사용한다.

하지만, has-a 관계는 객체 합성을 통하여 사용한다. 즉, ‘자동차는 바퀴를 가지고 있다.’ 바퀴라는 클래스와 자동차 클래스를 따로 구현하여 객체합성을 통하여 사용한다.

has - a 관계만 예제로 알려드리겠습니다.

Example

class Car(){
public:
	void go(){
		Tire t = new Tire;
		t.run();
	}
}

class Tire(){
public:
	void run(){
	}
}

OOP SOLID (객체 지향 설계)

SRP (단일 책임 원칙, Single Responsibility Principle)

클래스는 단 하나의 책임을 가져야 하며 클래스를 변경하는 이유는 단 하나의 이유이어야 한다.

SRP를 적용하면 무엇보다도 책임 영역이 확실해지기 때문에 한 책임의 변경에서 다른 책임의 변경으로의 연쇄작용에서 자유로울 수 있습니다. 뿐만 아니라 책임을 적절히 분배함으로써 코드의 가독성 향상, 유지보수 용이라는 이점까지 누릴 수 있으며 객체지향 원리의 대전제 격인 OCP뿐 아니라 다른 원리들을 적용하는 기초가 됩니다.

OCP (개방-폐쇄 원칙, Open/Closed Principle)

확장에는 열려 있어야 하고 변경에는 닫혀 있어야 한다.

이것은 변경을 위한 비용은 가능한 줄이고 확장을 위한 비용은 가능한 극대화 해야 한다는 의미로, 요구사항의 변경이나 추가사항이 발생하더라도, 기존 구성요소는 수정이 일어나지 말아야 하며, 기존 구성요소를 쉽게 확장해서 재사용할 수 있어야 한다는 뜻입니다.

LSP (리스코프 치환 원칙, Liskov Substitution Principle)

상위 타입의 객체를 하위 타입의 객체로 치환해도 상위 타입을 사용하는 프로그램은 정상적으로 동작해야 한다.

LSP를 한마디로 한다면, “서브 타입은 언제나 기반 타입으로 교체할 수 있어야 한다.”라고 할 수 있습니다. 즉, 서브 타입은 언제나 기반 타입과 호환될 수 있어야 합니다. LSP도 역시 서브 클래스가 확장에 대한 인터페이스를 준수해야 함을 의미합니다. 다형성과 확장성을 극대화 하려면 하위 클래스를 사용하는 것보다는 상위의 클래스(인터페이스)를 사용하는 것이 더 좋습니다.

ISP (인터페이스 분리 원칙 Interface Segregation Principle)

인터페이스는 그 인터페이스를 사용하는 클라이언트를 기준으로 분리해야 한다.

ISP는 한 클래스는 자신이 사용하지 않는 인터페이스는 구현하지 말아야 한다는 원리입니다. 즉 어떤 클래스가 다른 클래스에 종속될 때에는 가능한 최소한의 인터페이스만을 사용해야 합니다. ISP를 ‘하나의 일반적인 인터페이스보다는, 여러 개의 구체적인 인터페이스가 낫다’라고 정의할 수 도 있습니다.

DIP (의존관계 역전 원칙 Dependency Inversion Principle)

고수준 모듈은 저수준 모듈의 구현에 의존해서는 안된다.

의존 관계의 역전 Dependency Inversion이란 구조적 디자인에서 발생하던 하위 레벨 모듈의 변경이 상위 레벨 모듈의 변경을 요구하는 위계관계를 끊는 의미의역전입니다. 실제 사용 관계는 바뀌지 않으며, 추상을 매개로 메시지를 주고 받음으로써 관계를 최대한 느슨하게 만드는 원칙입니다.

Leave a comment