객체지향/개념과 원리

[객체지향] Object-Oriented Programming 핵심 개념의 이해

Peter Ahn 2020. 8. 14. 13:39
반응형

 

배경

 

데이터 흐름(Flow)에 기반한 절차지향적 프로그래밍 방법은 복잡한 로직을 갖는 큰 규모의 소프트웨어 개발에는 적합하지 않습니다.

 

하드웨어 성능이 폭발적으로 성장하면서 요구되어지는 소프트웨어는 점점 복잡해지고 거대해졌는데

기존의 전통적인 절차지향 개발 방법 으로는 소프트웨어를 설계 및 구현하는데 많은 어려움이 생긴 것이죠.

(사실 소프트웨어 공학 - software engineering 이라는 개념도 생소할 시기였습니다)

 

이러한 문제를 해결하기 위해 프로그램을 함수(procedure) 단위로 나누어 구조화하는 구조적 프로그래밍 방법이 대두되었는데, 

상위로부터 하위로 쪼개나가는 방식이기 때문에 Top-Down 방식이라고도 합니다. 

하지만 함수는 데이터를 처리하는 부분은 구조화할 수 있었지만 데이터는 구조화하지 못해  전역 네임스페이스 포화 문제를 유발하게 되었습니다. 

 

출처: IT World

 

 

 

또한 데이터의 상태에 따라 다르게 동작하는 함수들이 많아지면서 각 함수들로 전달되는 변수들이 적절한 값을 갖는지 확인하는 코드가 늘어났으며,

문제가 발생했을 때 함수에 영향을 주는 변수를 추적하는 것이 프로그램의 규모가 커질 수록 어려워졌습니다.

프로그램에 버그가 있는 경우 원인을 분석하는데 그만큼 많은 시간과 노력이 더 필요해지는 것이죠.

 

즉, 소프트웨어의 유지 관리 비용이 규모에 따라 대폭 증가하는 문제가 발생합니다. 

이것이 하드웨어가 무어의 법칙(Moore's law)대로 폭발적으로 성장 하던 시기에 소프트웨어의 위기(software crisis)를 초래한 가장 큰 이유 중에 하나입니다. 

 

 

이러한 문제들을 해결하기 위해 등장한 것이 객체지향 프로그래밍(Object-Oriented Programming) 입니다. 

(줄여서 OOP 라고도 많이 불립니다.)

 

객체지향 프로그래밍은 큰 문제를 작게 쪼개는 방식이 아니라 작은 문제들을 해결할 수 있는 객체들을 만든 뒤, 이 객체들을 조합해서 큰 문제를 해결하는 Bottom-Up 방식을 지향합니다.

 

각각의 객체들은 다른 객체들과 독립적으로 운용이 가능하며 객체 외부에서 접근이 가능한 인터페이스를 제한하여 잘못 사용되어지는 경우를 최소화 할 수 있습니다. 

 

독립적이라는 것은 다른 객체와의 의존성(dependency)이 낮다는 의미인데,

하나의 객체의 코드를 수정했을 때 해당 객체를 사용하는 객체는 코드를 수정하지 않아도 되면 의존성이 낮은 약한 결합 관계(weak coupling)이고, 반대로 다른 객체의 코드도 수정해야 되는 경우는 의존성이 높은 강한 결합 관계(strong coupling)라고 할 수 있습니다. 

 

객체간 독립성이 높계 설계되어 있다면 요구사항 변경 등의 이유로 코드 수정이 필요할 때 상대적으로 적은 범위의 코드만 수정해도 되기 때문에 유지 관리 비용을 낮출 수 있습니다. 

 

또한 외부에서 사용하는 인터페이스 외에는 객체 내부로 숨겨(information hiding) 객체의 기능(method)이나 속성(property)이 설계와 다르게 동작할 위험을 최소화 할 수 있기 때문에 신뢰성이 높습니다. 

 

객체들의 독립성/신뢰성을 높게 설계 해 놓으면 별다른 수정 없이 재사용 할 수 있고,

요구사항이 변경되어 코드를 수정하게 되도 제한된 영역만 수정하면 되기 때문에 개발 기간과 유지관리 비용을 비약적으로 줄일 수 있게 되었습니다. 

 

 

객체지향 핵심 개념

 

객체지향(Object-Oriented)이라고 하면 객체를 지향한다는 말이 선뜻 이해가 되지 않을 수 있습니다. 

원문의 oriented를 지향(志向)이라고 바꾸지 않고 그대로의 의미를 한번 보겠습니다.

 

 

-Oriented 를 [~에 주된 관심을 두다], [~를 주된 관심으로 삼다] 정도로 해석하면 좀 더 이해가 쉽습니다. 

즉, 객체지향 프로그래밍은 객체(object)를 주된 관심으로 삼는 프로그래밍 방법론을 뜻하는 것입니다.

 

그렇다면 객체는 무엇을 말하는 걸까요?

여기에서 말하는 객체를 이해하려면 먼저 객체 지향의 핵심 개념 중에 하나인 추상화라는 개념을 이해해야 합니다.

 

 

 

추상화 (abstraction)

 

추상화(抽象化 : abstraction)란 현실 세계에서 특정한 대상을 관찰하여 핵심적이고 특징적인 공통점들을 뽑아내는 과정을 말합니다.

 

피카소는 추상화(抽象畫 : abstract painting)의 대가입니다. 피카소의 그림들은 얼핏 단조로워 보일 수 있지만 핵심적인 특징들이 매우 잘 나타나 있습니다. 

이것은 그가 그림을 그리기 전에 오랜 기간 대상을 관찰하였기 때문에 가능했던 것이죠.

 

아래의 그림은 오랜 기간 황소를 관찰하고 그림을 그리면서 핵심적인 특징만 남겨두고 제거해나간 과정입니다.

마지막 황소 그림을 보면 단순한 선 몇 개로 황소를 표현하였지만 특징이 잘 나타나 있어 한눈에 봐도 황소라는 것을 알 수 있습니다. 

 

황소 (피카소), 1945

 

추상화(abstraction)에서 핵심적인 특징이라는 것은 대상에 대한 현재 내가 구현하고자 하는 프로그램에서의 관심사라고 할 수 있습니다.  

그 외의 특징들은 어떤 관점에서는 중요한 특징들일지 몰라도 현재 프로그램을 구현하는데 불필요하기 때문입니다.

 

이렇게 줄이고 줄여서 뽑아낸 공통적인 특징들은 크게 속성(attribute)행위(behavior)로 나뉘게 되는데 이것들을 관련있는 것들끼리 묶어놓은 것을 캡슐화(encapsulation)라고 합니다. 

그리고 관련있는 것들을 하나로 묶는 개념적(추상적)인 주체가 바로 클래스(class)입니다.

 

attribute = property = member variable = field = state
behavior = method = member function = operation

 

객체(object)는 클래스로부터 실체화된 것을 말합니다.

클래스는 추상적이기 때문에 눈에 보이지 않고 개념적으로만 존재하지만 객체는 눈에 보이는 실체가 있습니다. 

 

예를 들어 "바나나"라는 클래스가 있다고 하면 우리는 개념적으로 바나나가 갖고 있는 특징들을 떠올릴 수 있습니다. 껍질은 노란색이고 속은 하얗고 맛은 달고 길쭉한 모양이라는 특징들을요. 

하지만 실제로 바나나를 먹기 위해서는 마트에 가서 진열대에 놓여있는 실제의 "바나나(object)" 사와야 합니다.  

마트 직원에게 "바나나 어디 있나요?" 라고 물어볼 때 마트 직원이 안내를 해 줄 수 있는 것은 나와 마트 직원이 모두 바나나라는 추상적인 개념을 알고 있기 때문에 가능한 것입니다. 

진열대에는 바나나가 여러 개 있을 수 있죠. 그 중에 내가 어떤 바나나를 사서 먹었다면 그 바나나는 다른 바나나들 구별되는 유일한 실체입니다.

 

위 예문에서 글씨색으로 클래스와 객체를 구분해놓았는데 차이를 아시겠지요?

개(dog)라는 클래스가 있다면 우리집 강아지 로이, 옆집 강아지 초코는 객체인 것이죠.

 

 

 

캡슐화(encapsulation)

 

위에서 설명드린대로 캡슐화란 속성과 행위들을 관련있는 것끼리 묶는 것입니다. 

이 때 중요한 것은 외부에서 접근이 필요한 부분을 제외하고는 내부로 숨기는 것입니다. 

 

우리가 알약을 먹을 때 중요한 것은 약의 성분이 인체에 미치는 영향이지 약의 맛이나 식감이 아니기 때문에 캡슐 속으로 숨기는 것과 마찬가지이죠.

 

이렇게 외부로부터 세부적인 내용을 숨기는 것을 정보 은닉(information hiding)이라고 합니다. 

세부 구현을 숨기는 목적은 클래스 내부 구현의 응집도(cohesion)를 높이고 외부 다른 클래스와의 결합도(coupling)을 낮추는 데 있습니다. 

 

일반적인 객체지향 언어에서는 이 때문에 접근제한자(access modifier)라는 문법을 지원합니다. 

  • public : 클래스 외부에서 제한 없이 접근 가능 (nobody)

  • private : 클래스 외부에서 접근 불가 (클래스 내부에서만 접근 가능)

  • protected : 상속한 하위 클래스에서만 접근 가능

 

 

 

상속(inheritance)

 

상속은 객체지향의 핵심 기능 중에 하나로 대상이 되는 클래스의 모든 특징들을 물려 받는 것을 말합니다. 

 

어떤 임의의 클래스 B가 다른 클래스 A를 상속 받게 되면 A 클래스의 속성과 행위들을 모두 물려받게 됩니다. 

이 때 클래스 A와 B는 상속 관계(inheritance relation)에 있다고 하며 클래스 A부모 클래스(parent class), 클래스 B를 자식 클래스(child class)라고 부릅니다. 

 

Vehicle hirerachi

 

어떤 클래스를 이미 상속 받은 클래스를 다시 다른 클래스가 상속을 받을 수 있습니다. 

WheeledVehicle 클래스는 Vehicle 클래스의 Child 클래스인 동시에 Bicycle이나 Car 클래스의 Parent 클래스가 되는 것이죠.

 

그렇게 되면 클래스 간의 관계가 위와 같은 계층형 구조(hierachical structure)를 형성하게 되는데요,

이런 형태의 관계에서는 아래로 내려갈수록 구체화(specialize)된다고 하고 위로 갈수록 일반화(generalize)된다고 표현합니다. 

 

클래스가 구체화 될수록 고유의 특징들이 더 많이 생겨나게 되고, 일반화 될수록 더 많은 객체에 영향을 주게 됩니다. 

 

부모 클래스로부터 특징들을 물려받게 되면 이미 구현된 세부 내용을 다시 구현할 필요가 없기 때문에 코드의 재사용성(resusability)이 향상됩니다. 

많은 분들이 클래스 간의 상속 관계를 형성 할 때 이처럼 코드의 재사용성에 주안점을 두게 되는데 자칫하면 더 큰 혼란을 야기할 수 있어 주의가 필요합니다. 

 

상속 관계가 성립하려면 두 클래스 간의 관계가 is-a 관계여야 합니다.  is-a 관계란 [~은 ~이다] 라고 부를 수 있는 관계입니다. (예를들면 바나나과일이다)

즉, 자식 클래스가 들어간 문장이 있을 때 자식 클래스를 부모 클래스로 대체하여도 의미가 성립되어야 합니다. 

 

위 그림에 있는 WheeledVehicle 은 "바퀴가 있는 탈 것"이라는 뜻입니다. 

  • 자동차(Car)는 WheeledVehicle 이다.

  • 자전거(Bicycle)은 WheeledVehicle 이다. 

  • 보트(Boat)는 Vehicle 이다. 

세 문장 모두 is-a 관계에 있기 때문에 제대로 된 상속 관계라고 볼 수 있습니다. 

 

일반적으로 가장 많이 하는 실수는 has-a 관계인 클래스를 상속하는 경우입니다.

has-a 관계는 사람-팔, 자동차-바퀴, 새-날개 등을 예로 들 수 있습니다. 

즉, 하나의 클래스가 다른 클래스의 일부로 속할 때 has-a 관계가 성립됩니다.

 

이러한 has-a 관계의 클래스를 코드 재사용성 측면만 고려하여 상속을 하게 되면 자연스러운 모델링이 되지 못하고 큰 혼란을 불러 올 수 있습니다. 

 

그렇다면 상속은 어떠한 경우에 사용하는 것이 좋을까요?

바로 다음에 설명드릴 다형성을 구현하기 위해서 사용하는 것을 권장드립니다.

물론 기본적으로 is-a 관계가 성립되어야 겠죠?

 

 

 

다형성(polymorphism)

 

다형성은 그리스어에서 기원된 단어의 합성어로 '여러 형태가 존재한다' 라는 뜻입니다.

객체지향에서 다형성이란 하나의 속성이나 행위가 상황에 따라 다른 의미로 해석될 수 있는 특징을 말합니다.

 

다형성이야말로 객체지향의 꽃이라고 할 수 있습니다. 

그만큼 중요하고 핵심이 되는 개념 중에 하나입니다.

 

다향성을 구현하는 방법 중에 대표적인 방법이 바로 위에서 설명드린 상속(inheritance)을 이용한 방법입니다.

상위 클래스의 메소드를 하위 클래스에서 재정의(override)하여 상위 클래스의 참조변수가 어떠한 하위 클래스의 인스턴스참조하느냐에 따라 동작이 달라지는 개념으로 이러한 방식으로 구현되는 다형성을 서브타입 다형성(subtype polymorphism) 이라고 합니다. 

 

예를들어 이동(move)라는 행위는 여러 가지 형태를 가질 수 있습니다. 

"현재 위치에서 특정 위치로 옮겨간다" 라는 목적은 같지만 실제로 이동하는 대상에 따라서 이동하는 방식이 달라질 수 있는 것이죠.

직립보행을 하는 사람은 두 발로 걷거나 뛸 수 있으며 자동차나 자전거 등의 이동 수단을 사용 할 수도 있습니다. 

개나 고양이 같은 동물들은 네 발로 이동을 하고 새나 날개가 있는 곤충들은 날아서 이동 할 수도 있습니다.

 

이처럼 이동(move)이라는 행위에 대해 여러 가지 형태가 존재하기 때문에 전통적인 프로그래밍 방법으로는 if 구문이 반복적으로 사용되는 복잡한 코드가 생겨날 가능성이 높습니다.

 

 

객체지향에서는 상속(inheritance)재정의(override)를 통해 효과적으로 구현해낼 수 있습니다. 

 

클래스간의 관계가 위와 같고 하위 클래스에서 Move() 메소드를 재정의 하고 있다고 전제하면 아래와 같이 Animal 클래스 타입의 참조로 다형성을 구현해 낼 수 있습니다. 

List<Animal> animals = new List<Animal>();

animals.add(new Human());
animals.add(new Dog());
animals.add(new Bird());

foreach(Animal animal in animals)
{
	animal.Move();
}

 

위 코드는 이해를 돕기 위해 C#으로 간단하게 작성한 코드입니다. 실제 구현은 세부적인 내용이 더 늘어나겠지만 전체적인 맥락은 크게 변하지 않습니다. 

 

animals List에 들어 있는 객체(instance)들은 각각 Human, Dog, Bird 타입의 인스턴스지만 Animal 타입으로 참조하고 있습니다. 

Animal 타입으로 일반화 하여 Move() 메소드를 호출하면 Animal 클래스에 정의되어 있는 Move()가 실행되는 것이 아니라 각각의 하위 클래스에 구현된 Move() 메소드가 호출됩니다. 

 

이렇게 비즈니스 로직(bussiness logic)을 일반화된 코드로 구현해놓으면 여러 가지 장점이 생깁니다. 

요구사항(requirements)이 변경되어 코드를 수정해야 하는 경우에 위처럼 일반화된 코드는 변경하지 않고 기존 클래스의 Move() 메소드만 수정하거나 클래스를 추가 구현하는 것으로 해결할 수 있습니다. 

 

또한 코드의 가독성(readability)이 높아지고 일관성이 생기기 때문에 개발자의 실수를 줄일 수 있게 됩니다. 

 

 

정리

 

지금까지 객체지향 개념이 만들어지게 된 배경과 핵심 개념에 대해서 설명드렸습니다. 

객체지향 프로그래밍을 한다는 것은 단순히 객체지향 언어를 사용하여 개발을 하는 것을 뜻하지는 않습니다. 

 

많은 분들이 객체지향을 공부 할 때 객체지향을 지원하는 언어(C#이나 Java 등)의 도서를 구매하여 문법이나 언어적인 특성을 익히는 것으로 시작하는데 문법 보다는 개념원리를 이해는 것이 우선입니다. 

 

그래야만 객체지향적으로 사고 할 수 있으며 보다 유연하고 안정적인 프로그램을 설계 할 수 있는 능력을 배양 할 수 있습니다. 

 

이 글을 읽으신 분들께 객체지향을 이해하는 데 도움이 되었길 바라며 앞으로 객체지향 원칙, UML, 디자인 패턴(design patterns) 등의 글들을 써나갈 계획입니다. 

 

 

-Peter의 우아한 프로그래밍

여러분의 공감과 댓글은 저에게 크나큰 힘이 됩니다. 오류 및 의견 주시면 감사하겠습니다.

 

 

 

반응형