객체지향 프로그래밍이 뭐지?
객체 지향 프로그래밍은 컴퓨터 프로그래밍의 패러다임 중 하나이다. 객체 지향 프로그래밍은 컴퓨터 프로그램을 명령어의 목록으로 보는 시각에서 벗어나 여러 개의 독립된 단위, 즉 “객체”들의 모임으로 파악하고자 하는 것이다. 각각의 객체는 메시지를 주고받고, 데이터를 처리할 수 있다. (출처: 위키피디아)
“객체 지향 프로그래밍.. 그거 뭐 객체..로 코딩하는거 아니야?” 프로그래밍을 하면서 가장 가까운 개념이자, 설명하기에는 애매한 개념이다. 쉽게 말해 단어 그 자체로 ‘객체를 지향하는 프로그래밍’이다.
객체지향 프로그램밍(Object-Oriented Programming)은 프로그래밍 패러다임 중 하나로, 상태(state)와 행위(behave)로 이루어진 객체들간의 상호작용을 통해 프로그램을 설계, 개발 하는 것이다. 즉, 쉽게 말해 객체지향 프로그래밍은 객체를 만드는 것이다. ‘객체’ 라는 말은 Object를 직역한 말인데, 쉽게 말해 변수와 메소드를 그룹핑 한 개념이다.
’객체 / 지향 / 프로그래밍’ 세 단어를 나누어 생각해보자. 객체란 현실에 존재하는 대상이다. 우리 주변에 있는 사람, 사물, 동물 모두 객체(Object)라고 할 수 있다. 지향이란, ‘어떤 것을 목표하다, 원하다’라는 뜻이다. 따라서 ’객체 지향‘이란 현실의 존재하는 대상을 목표한다고 할 수 있다. 마지막으로, 프로그래밍은 컴퓨터 프로그램을 작성하는 일이다. 결국, 이 세 단어를 조합하면 **‘현실의 대상을 목표로 컴퓨터 프로그램으로 만드는 일’**이라고 할 수 있다.
객체지향 프로그래밍이 왜 생겼는지, 이전 시대의 프로그래밍 패러다임을 알아보자.
객체지향 프로그래밍 이전 패러다임
순차적 프로그래밍 (Sequential Programming)
초기에 등장한 순차적 프로그래밍은 말 그대로 ‘순차적으로 흘러가는 프로그래밍’이다. 순차적 프로그래밍은 ‘순차’를 중점으로 보며 코드의 흐름과 순서에 기반해서 프로그래밍한다. 흐름이 바로바로 눈에 보이기 때문에 ‘직관적’이라는 장점이 있다.
해당 프로그래밍에는 ‘구조’라는 개념이 없기 때문에 goto
문을 사용해야 한다. 문제는 프로젝트의 규모가 커지고 더 복잡해질 수록, goto
문이 범람하게 되고 엄청난 스파게티 코드가 된다. a -> b-> c 를 순차적으로 구현하다, c에서 b로 돌아갈 일이 생기면 계속 goto
를 활용해야한다.
goto
b goto
a goto
…
goto
문이 범람하게 되면 코드가 어지러워지고, ‘직관적’이라는 장점조차 사라지게 된다.
따라서 사람들은 코드의 중복을 피하기 위해 코드를 ‘단위화’할 방법을 찾게 되었는데, 이때 ‘절차적 프로그래밍’이 생겨난다.
절차적 프로그래밍 (Procedural Programming)
절차적 프로그래밍에서 ‘절차’란 프로시저(Procedure) 즉, 함수를 의미한다. 다만, 반환값이 없고 실행이 주가되는 함수를 의미한다.
따라서, 절차적 프로그래밍은 반복될 가능성이 있는 모듈을 재사용 가능한 프로시저 단위(함수 단위)로 나눈 프로그래밍이다. goto
문이 무분별하게 남발되던 순차적 프로그래밍과 달리, 반복되는 부분을 프로시저로 쪼개고, 각각의 프로시저 안에서 중복되는 부분을 for문
과 같은 반복문으로 구성하게 된다.
하지만 절차적 프로그래밍에서도 역시 문제가 존재한다. ‘프로시저’라는 추상적인 단위로는 물리적인 요소 (변수나 상수등의 값)을 관리하기 어렵기 때문이다.
예를 들어, 도서관리 프로그램을 작성한다고 하면 1) 책이라는 자료형이 필요하고, 2) 그 책에 대한 함수를 구현해야 한다. 하지만 절차적 프로그래밍에서는 이 둘을 따로 생각할 수 밖에 없다. 절차적(구조적) 프로그래밍에서는 논리적으로 묶여있을 수 없는 구조이기 때문이다. 따라서 이를 묶기 위한 새로운 패러다임이 필요했다.
객체지향 프로그래밍 등장
따라서, 특정 개념의 함수와 자료형을 묶어서 관리할 수 있는 객체지향 프로그래밍이 등장했다. 객체지향 프로그래밍에서 모든 객체는 그 내부에 **자료형(Field)과 함수(Method)**가 존재한다. 위 예시를 참고하자면, ‘책’이라는 객체 모델을 만든다면 그 책의 쪽수나,이름과 같은 자료형 뿐만 아니라 ‘책을 읽기’, ‘책을 대여하기’ 등의 함수들 역시 “책”이라는 객체로 묶어 놓는 것이다.
이렇게 객체지향 프로그램은 가능한 모든 물리적, 논리적 요소들을 객체로 만든다. 변수와 메소드를 그룹핑한 ‘객체’는 다른 객체로 부터 높은 수준의 독립성을 갖는다. 개발자들은 초반에 객체를 설계할 때만 시간을 쓰고, 시간이 지날수록 중복 코드를 최대로 줄일 수 있으며 객체와 객체간에 독립성이 확립되므로 유지보수에 도움이된다.
* 사진출처: object-oriented programming (OOP)
그렇다면 왜 객체지향 프로그래밍이 필요할까? 객체가 없다면 어떤일이 벌어질지 알아보자.
객체가 없다면…
하나의 프로그램은 변수와 메소드로 이루어져 있다. 변수에 값을 저장하고, 메소드로 연산을 묶어 결과를 낸다. 예를 들어, 아래와 같은 코드는 우항과 좌항을 더한 값을 출력하는 간단한 프로그램이다.
@left = 1
@right = 2
def sum
return @left + @right
end
puts sum() #결과: 3
위와 같이 작은 프로그램에서는 객체화 하지 않아도 문제가 발생하지 않는다. 하지만 아래와 같이 프로그램이 조금 더 커지면 어떻게 될까?
# 서로 다른 개발자가 right의 의미를 다르게 사용했을 때 발생할 수 있는 문제를 위한 인위적인 예제임
@left = 1
@right = 2
def sum
return @left + @right
end
@right = 10000
def my
return @right
end
puts sum() #결과: 10001
puts my() #결과: 10000
메소드 my
의 실행결과(10000)는 정상적으로 출력 되지만 메소드 sum
은 엉뚱한 값(10001)을 출력하고 있다. 나중에 합류한 개발자가 right의 의미를 변경했기 때문이다. 프로그램의 규모가 커지고 개발자가 많아질수록 이런 일은 빈번해질 것이다. 즉 서로 관련이 없는 로직인 sum과 my가 하나의 프로그램 안에서 동작하고 있기 때문에 서로에게 영향을 끼치고 있는 것이다.
그렇다면 이 문제를 어떻게 해결해야 할까? 바로 객체화를 시키면 된다. 위 예제를 객체화 시킨 예제를 알아보자.
class Calculator
def initialize(left, right)
@left = left
@right = right
end
def sum
return @left + @right
end
end
class User
def initialize(right)
@right = right
end
def my
return @right
end
end
s = Calculator.new(1,2)
puts s.sum #결과: 3
u = User.new(10000)
puts u.my #결과: 10000
객체화를 시키니 결과가 모두 올바르게 나왔다.
위 예제에서 Calculator, User라는 **클래스(Class)**를 생성하였다. 이 클래스는 객체 Calculator과 User에 대한 일종의 설계도라고 할 수 있다.
s = Calculator.new(1,2)
는 변수 s에 객체 Calculator을 담아서 저장한다는 의미다. 해당 코드가 실행되면 변수 s를 이용해서 Calculator 객체를 사용할 수 있게 된다. 이렇게 생성된 객체를 **인스턴스(instance)**라고 부른다. 즉 객체의 설계도를 클래스라고 부르고, 이 설계도를 바탕으로 실제로 사용할 수 있게 만들어진 객체를 인스턴스라고 한다. 이것들을 포괄해서 객체라고 부르기도 한다.
앞서 말했듯이 객체는 서로에게 독립적인 존재임으로 객체 Calculator과 User은 서로 영향을 주지 않는다. 따라서, @right
라는 변수에 서로 다른 값을 넣어도, 각각 클래스 내에서만 독립적으로 계산이 되기 때문에 결과가 모두 올바르게 나오게 된 것이다.
객체지향 프로그래밍 특징
객체지향 프로그래밍에는 네 가지 큰 특징이 있다. 추상화, 캡슐화, 상속, 다형성이다. 각각 어떤 개념인지 간단히 알아보고 예시를 들어보자.
* 사진출처: OOP Concepts in Java
1. 추상화 (Abstraction)
- 객체들의 공통적인 특징(기능, 속성)을 추출해서 정의하는 것을 말한다.
- 다시 말해, 실제로 존재하는 객체들을 프로그램으로 만들기 위해 공통적인 특성을 파악한 후, 필요 없는 특성을 제거해 하나의 묶음으로 만들어내는 과정을 가르킨다.
- 객체지향적 관점에서는 클래스를 정의하는 것을 추상화라고 할 수 있다.
예를 들어, 페르시안, 러시안 블루, 벵갈, 먼치킨, 스핑크스 모두 ‘고양이’ 라는 공통점이 있다. ‘고양이’ 라는 추상화 집합을 만든 후, 객체들의 공통 특징(울음 소리, 하악질, 털이 많이 빠짐, 귀여움, 집사를 부려먹음) 등을 추출해서 활용할 수 있다.
이때, 새로운 고양이인 ‘터키시앙고라’를 ‘고양이’ 집합에 추가할 수 있는데, ‘고양이’를 추상화로 구현했기 때문에 다른 코드를 건들이지 않고도 추가할 부분만 만들면 된다.
2. 캡슐화 (Encapsulation)
- 관련이 있는 변수와 함수를 하나의 클래스로 묶고, 외부에서 쉽게 접근할 수 없도록 은닉하는 것이다.
- 외부에서 객체를 손상시키는 일을 방지할 수 있도록, 객체 내부의 세부적인 동작에 대한 구현을 감추는 것이다. 예를 들어, 외부에서 접근할 필요 없는 것들은 접근 지정자를 private 으로 두어 접근에 제한을 둘 수 있다.
- 정보 은닉: 다른 객체에게 자신의 정보를 숨기고, 자신만의 연산을 통해 접근을 허용하는 것.
- 캡슐화는 높은 응집도와 낮은 결합도를 유지한다. 즉, 한 곳에서 변화가 일어나도 다른곳에 미치는 영향을 최소화 한다.
예를 들어, 캡슐 알약의 원리를 들 수 있다. 캡슐 알약 안에는 여러 종류의 약 성분이 들어있다. 어떤 성분이 들어있는지 하나하나 알지 못하지만, 약을 먹으면 해당 성분에 맞는 효과가 나타나게 된다. 즉, 캡슐 알약은 캡슐화를 통해 내부 성분을 은닉하고, 세부적인 성분에 대한 정보를 감춘다.
3. 상속성 (Inheritance)
-
이미 정의된 상위 클래스 (부모 클래스)의 모든 속성과 연산을 하위 클래스가 물려 받는 것.
-
기존 코드를 재활용해서 사용함으로써 코드의 생산성을 높여준다 (적은 코드로 원하는 기능 구현). 이미 작성된 클래스를 받아서 조금만 수정해 새로운 클래스를 생성하는 것을 예로 들 수 있다.
-
하지만 상속 자체를 코드 재사용의 개념으로 이해하면 안된다. (클래스간 결합도가 과도하게 높아져 유지보수가 어려움) 반드시 기능의 확장 관점으로써 ‘포함 관계’(IS-A) 일 때에만 사용해야 한다.
- IS-A: 부모 - 자식 클래스 관계처럼 포함 관계를 의미한다. (ex.고양이의 부모 클래스는 동물)
- HAS-A: 한 객체가 다른 객체에 속하는 **구성 관계(Composition)**이다. (ex. 자동차를 배터리를 가지고 있음)
-
즉, 상속은 자식 클래스를 외부로부터 은닉하는 캡슐화의 개념을 가지고 있다.
1번에서 들었던 ‘고양이’ 예를 다시 들어보자. ‘고양이’라는 부모 클래스가 있는데, 만약 ‘한국에서만 서식하는 토착 고양이’라는 특수한 집합을 만들고 싶다. 그렇다면 기존 ‘고양이’ 클래스 하위에 ‘토종 고양이’ 라는 자식 클래스를 생성한다.
이때 자식 클래스는 부모 클래스의 특징을 상속받는다. ‘토종 고양이’는 ‘고양이’의 특징을 상속받기 때문에 기존 고양이의 특성( 울음 소리, 하악질, 털이 많이 빠짐, 귀여움, 집사를 부려먹음)들은 모두 유지한 채, ‘한국에서만 서식하는 토착 고양이’라는 새로운 특성을 갖춘 클래스가 생성된다.
4. 다형성 (polymorphism)
- 하나의 변수 또는 함수(클래스의 객체)가 명령을 받았을 때, 상황에 따라 서로 다른 방식으로 동작하는 것.
- 동일한 명령을 각자 연결된 객체에 의존해서 해석하는 것을 뜻한다.
- 오버라이딩(Overriding)과 오버로딩(Overloading)이 있다.
- 오버라이딩: 부모클래스의 메소드와 같은 이름을 사용하며 매개변수도 같되 내부 소스를 재정의하는 것
- 오버로딩: 같은 이름의 함수를 여러 개 정의한 후 매개변수를 다르게 하여 같은 이름을 경우에 따라 호출하여 사용하는 것.
예를 들어, ‘고양이’ 클래스를 상속하여 페르시안, 러시안 블루 등의 객체를 만들었다고 하자. ‘고양이’의 ‘울음 소리()’ 메서드를 실행했을 때, 같은 고양이여도 각자 다른 울음 소리를 내는 것이 바로 ‘다형성’의 개념이다.
다형성은 상속과 함께 사용하여 큰 효율을 낸다. 상속을 통해 기능을 확장하거나 변경할 수 있게 하며, 같은 클래스 내에 코드를 간결하게 해준다.
객체지향 설계 원칙 (SOLID)
객체지향 설계 5대 원칙이 있다. SRP(단일 책임 원칙), OCP(개방-폐쇄 원칙), LSP(리스코프 치환 원칙), ISP(인터페이스 분리 원칙), DIP(의존 역전 원칙) 뜻하며 줄여서 SOILD 원칙이라고 부른다.
이번 포스트에서는 간단하게 정의만 보고 넘어가보자.
약어 | 개념 |
---|---|
SRP | 단일 책임 원칙 (Single responsibility principle)
한 클래스는 하나의 책임만 가져야 한다. |
OCP | 개방-폐쇄 원칙 (Open/closed principle)
“소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다.” |
LSP | 리스코프 치환 원칙 (Liskov substitution principle)
“프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다.” |
ISP | 인터페이스 분리 원칙 (Interface segregation principle)
“특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다.” |
DIP | 의존관계 역전 원칙 (Dependency inversion principle)
프로그래머는 “추상화에 의존해야지, 구체화에 의존하면 안된다.” |
- 출처: 위키피디아
객체지향 프로그래밍 장점 / 단점
마지막으로, 객체지향 프로그래밍의 장점과 단점을 살펴보자.
장점:
1) 코드 재사용 용이 모듈화된 객체, 그리고 상속을 통해 코드의 재사용을 높일 수 있다.
2) 생산성 향상 독립적인 객체를 사용함으로써 개발의 생산성을 향상시킬 수 있다. 이미 생성된 클래스를 상속 받거나, 객체를 재사요, 부분 수정 등 적은 노력으로 높은 효율을 만들어낸다.
3) 자연적인 모델링 가능 현실세계에서 사용하는 개념을 대입하여, 생각한 것을 그대로 구현할 수 있다.
4) 유지보수 용이 프로그램 수정, 추가시에도 캡슐화 덕분에 주변 코드에 영향이 덜 가기 때문에 유지보수가 용이하다.
단점:
1) 실행속도 느림 전반적으로, 객체지향 언어(C++, Java, ruby)는 컴퓨터의 처리 구조와 비슷한 절차지향 언어(C언어)보다 상대적으로 실행속도가 느리다.
2) 프로그램 용량이 커질 수 있음 객체단위로 프로그램을 많이 만들다보면, 불필요한 정보들이 들어갈 수 있는데 프로그램의 용량이 증가될 수 있다.
3) 설계에 많은 시간 소요 초기에 클래스별, 객체별, 상속 등의 구조 등을 모두 설계해야 하기 때문에 절차지향 언어에 비해 설계 시간이 많이든다.