이번에는 객체지향 프로그래밍의 개념에 대해서 간단간단하게 설명해보는 포스팅을 올려볼까 한다. 


> 일단 객체지향 프로그래밍이라는 프로그래밍 패러다임이 새로 생겨나게 된 배경에 대해서 설명해보겠다. 

객체지향 프로그래밍 패러다임이 있기 전에는 절차지향 프로그래밍이라는 패러다임이 있었는데, 이는 문제를 해결하는 절차들, Step by Step에 포커스를 맞춘 방식의 프로그래밍 패러다임이었다. 그런데 하드웨어는 급속도로 발달하지만, 소프트웨어는 개발 속도도 늦고 버그도 많고 해서 하드웨어의 발전 속도를 못 따라가는 일이 생겼는데, 이를 간단히 이야기 해서 '소프트웨어 위기'라고 불렀다.


이를 해결하기 위해서 소프트웨어도 하드웨어 부품들을 조립하는 것 처럼, 소프트웨어도 객체라는 부품으로 나누어서 개발을 하고, 이런 객체들을 조립하는 식으로 해보자 라는 아이디어가 객체지향 프로그래밍이다.


이제 객체지향 프로그래밍이 어떤 것인지 예제를 통해서 알아보자. 이 포스팅의 독자는 C언어에 대해서는 기본적은 다 알고 객체지향 개념은 잘 모르는 사람이라고 가정한다.


C언어에서의 문자열(String)은 char형 배열로 표현된다. 하지만 이 방식은 여러모로 불편한 점이 많다. 예를 들어서 scanf("%s");와 같은 코드로 문자열을 입력받을 때, 문자열의 크기가 얼마나 될 지 모르기 때문에 배열의 크기를 너무 작게 지정하면 버퍼 터짐(Buffer overflow) 현상이 나타날 것이고, 너무 크게 지정하면 메모리의 낭비가 될 것이다.


다음은 C언어로 문자열을 입력받는 코드이다.

#include <stdio.h> int main() { char str[100]; // 입력받을 문자열의 길이가 99보다 크다면 Buffer overflow 발생 scanf("%s", str); }


하지만 C에서 객체지향의 개념이 추가된 C++언어로 같은 동작을 하는 코드를 작성하면 다음과 같이 나타난다.


 
#include <iostream>
#include <string>
using namespace std;
int main() {
	string str;
	cin >> str;
}


위에 있는 C로 작성된 코드의 경우는 입력 받을 문자열의 길이가 99이하 라고 가정하고 100만큼의 메모리만 할당된 char 배열을 선언해서 입력을 받았다. 따라서 99보다 길이가 작은 문자열을 받으면 안쓰는 메모리가 필연적으로 생기고, 99보다 길이가 크다면 버퍼 오버플로가 발생한다.

하지만 C++로 작성한 코드의 경우 string이라는 객체를 사용하면 입력 받을 문자열의 길이에 대하여 상관할 필요가 없다. string 객체 내부적으로 알아서 입력크기에 맞게 메모리 할당을 유동적으로 해주도록 하는 코드가 이미 작성이 되어 있고 그 코드는 흔히 나타날 수 있는 버그들은 알아서 잘 수정이 되어있는 상태이다. 따라서 우리는 이전에 어떤 누군가가 작성한 string이라는 객체 클래스를 사용함으로써 문자열을 이렇게 쉽게 다룰 수 있게 된 것이다.


그리고 이 string 객체를 사용하는 개발자는 string 객체 내에 멤버변수나 메소드가 어떻게 정의되어있는지는 알 필요가 없다. 단지 해당 객체에 어떤 함수가 있고, 그 함수는 어떤 인자를 받고, 어떤 리턴값을 주는지만 알면 된다. 예를 들면 해당 문자열의 길이를 알고 싶다면 개발자는 다음 length함수를 호출하기만 하면 된다.


#include <iostream> #include <string> using namespace std; int main() { string str("Lorem ipsum"); size_t len = str.length(); cout << len << '\n'; return 0; }

단지 개발자는 string 클래스에 length라는 함수가 있고 이 함수의 원형은 size_t length(void); 이며 리턴값이, 해당 문자열의 길이라는 것만 알면 되고, 내부적으로 해당 함수가 어떻게 구현되어있는지 알 필요가 없다. 따라서 string 클래스 내부 구조에 신경을 쓸 필요도 없다.


이러한 특징을 "추상화"라고 하며 이는 객체지향 프로그래밍의 4대 특징 중 하나이다. 해당 클래스 사용자는 클래스에 대해서 '추상적'으로만 알면 된다는 것이다. length함수 -> 문자열 길이 리턴 이라고 '추상적'으로만 알면 되고 length 함수 호출 시 어떠한 내부 기작이 일어나고 메모리에서 어떤 일이 일어나는지 '구체적'으로 알 필요가 없다는 뜻이다.


여기까지가 객체지향 프로그래밍에 있는 객체를 '사용하는 입장'에서 알아본 것이다.


이제 객체라는 개념이 C언어 문법에서 C++문법으로 확장될 때 프로그래밍 언어의 문법적인 관점에서는 어떻게 해석되는지 알아보자.


C언어에는 구조체라는 문법이 있다. Composite Data Type의 대표격이며, Primitive Data Type들과 Composite Data Type들을 묶어서 한 덩어리로 만들 수 있다. 한 예를 들어서 C언어에서 64bit 정수형으로 나타낼 수 있는 값 보다 훨씬 큰 정수 값을 다루기 위한 big_integer라는 구조체를 만든다고 생각해보자. 아마 다음과 같이 구성할 수 있을 것이다.


struct big_integer { char *digits; int length; char sign; // 0이면 양수, 1이면 음수를 나타낸다. };


숫자의 길이를 알 수 없으므로 char 형 포인터를 이용해서 동적 메모리 할당을 통해서 배열에 자릿수별 숫자를 저장한다고 설계하자. 그리고 그 숫자의 길이는 length라는 변수에 저장되고, 양수 음수의 여부는 sign이라는 값을 통해 저장한다. 1bit의 값만 저장하면 되므로 bool 자료형을 쓰면 좋겠지만, 안타깝게도 C언어에는 bool 자료형이 없다.


이제 저렇게 구조체를 구성했으니, 해당 big_integer에 기본적인 연산능력들을 넣어주기 위한 함수들을 만들어야 한다. 일단 대충만 봐도 값 할당을 위한 함수와, big_integer간의 사칙연산을 위한 함수들과 C 문자열을 big_integer로 바꿔주는 함수, 그리고 big_integer를 C 문자열로 바꿔주는 함수도 필요할 것 같다. 일단 사칙연산 함수들의 프로토타입만 한번 생각해보자.

struct big_integer* add(struct big_integer lhs, struct big_integer rhs); struct big_integer* subtract(struct big_integer lhs, struct big_integer rhs); struct big_integer* multiply(struct big_integer lhs, struct big_integer rhs); struct big_integer* divide(struct big_integer lhs, struct big_integer rhs);


함수에서 각 좌항 우항을 넣어서 호출 시, 함수 내부에서 big_integer 구조체를 malloc으로 동적할당한 뒤 필요한 값들을 넣고 리턴을 해준다고 생각해보면 위와 같은 프로토타입이 나타날 수 있다.


함수 명들은 직관적으로 add, subtract, multiply, divide등으로 명명하였다. 하지만 만약에 big_integer 구조체 외에 큰 수의 부동소수점을 나타내기 위한 big_float라는 구조체가 더 있다면 어떠할까?


비슷한 방식으로 big_float라는 구조체가 선언될 것이다. 그리고 해당 구조체에서 사용할 함수들도 생겨나야 하는데 그 함수들도 모두 big_float 구조체의 사칙연산을 처리해야할 것이다. 이렇게 해당 구조체에서만 사용될만한 함수 이름이 겹치기 쉬워진다. 따라서 해당 함수들의 이름 앞에 사용될 구조체의 이름을 붙이거나, 아니면 함수 오버로딩과 같은 방식을 통해서 이러한 문제를 해결해야 한다.


그리고 만약 big_integer라는 구조채를 다른 사람들도 사용할 수 있도록 배포한다고 하면, 구조체 뿐만 아니라 해당 구조체용 함수들도 같이 배표해야 한다.


물론 그렇게 해도 된다. 그래서 C언어에서도 객체지향 프로그래밍 패러다임을 따르는 것 처럼 프로그래밍이 가능하다. 하지만 큰 소프트웨어 프로젝트를 진행할 때는 이러한 부분들을 프로그래밍 언어차원에서 지원을 해 주면 개발자들이 좀 더 직관적이고 편하게 개발을 할 수 있다. 사람이 할 수 있는 실수들과 햇갈릴 수 있는 것들을 도구 차원에서 덜 햇갈리게 하고 실수를 방지하는 장치들을 추가해주는 것이다.


그렇게 객체지향 프로그래밍 언어인 C++에서는 객체지향 프로그래밍을 위하여 C에서 사용되는 구조체들을 다음과 같이 확장해 나갔다.

struct big_integer { char *digit; int length; char sign; struct big_integer* add(struct big_integer lhs, struct big_integer rhs); struct big_integer* subtract(struct big_integer lhs, struct big_integer rhs); struct big_integer* multiply(struct big_integer lhs, struct big_integer rhs); struct big_integer* divide(struct big_integer lhs, struct big_integer rhs); };

big_integer라는 구조체에 해당 구조체를 위해 필요한 함수들을 구조체에 포함시켜버렸다. 언어차원에서 C절차지향 -> C++객체지향으로 발전한 것은 간단하게 생각하면 구조체에 함수도 같이 포함시킬 수 있다는 점인 것이다.


이렇게 관련있는 데이터(구조체 맴버변수)에 함수들도 묶어버린 것을 "캡슐화"라고 한다. 이 특징 역시 추상화와 같이 객체지향 프로그래밍 4대 특징 중 하나이다. 데이터와 동작을 묶어서 하나의 캡슐로 묶어버린 것이라고 생각하면 이해하기 쉽다.


이렇게 해서 이 하나의 big_integer라는 구조체에 함수를 포함시키는 확장을 함으로써, 이 구조체가 소프트웨어의 부품으로서 동작할 수 있게 된 것이다.

-----------------------------------------

다음 포스팅에서 이어진다.

+ Recent posts