OOP란 ?
Object Oriented Programming의 약자로, 말 그대로 객체를 지향하는 프로그래밍 기법입니다.
조금 풀어서 얘기를 해 보면, 프로그래밍이라 함은 근본적으로는 문제(요구사항) 를 가진 누군가(고객) 에게 해결방법 을 제공하는 일입니다.
다시 말해, 고객의 요구사항을 해결하는 것
이 프로그래밍의 본질이며 객체지향 프로그래밍이란 문제 해결을 하는데 있어 객체를 중심으로 설계하는 프로그래밍 패러다임을 말합니다.
객체란 ?
그렇다면 객체(Object) 란 무엇일까요?
사전적 정의로는 의사나 행위가 미치는 대상, 프로그래밍 관점에서 보면 상태를 가지고 기능을 수행하거나 기능이 적용되는 주체입니다.
대표적인 객체지향 언어인 Java를 예로 들면 아래와 같은 Class를 통해 만들어진 각각의 구현체들을 우리는 객체라고 부릅니다.
public class Person {
private int age;
private String name;
public void hello() {
System.out.println("안녕 내 이름은 " + name + ", " + age + "살이야.");
}
}
객체지향 언어란 ?
객체지향 언어라고 하면, 대표적으로 Java가 있습니다. 그 외에 C++, C#, Python, Kotlin 등등이 있죠.
이들의 공통점은, 객체지향 프로그래밍 이라는 패러다임을 언어 레벨에서 지원을 해 준다는 점입니다.
객체지향의 대표적인 특징인 캡슐화, 상속, 다형성, 추상화의 개념을 언어가 지원을 해 주기 때문에, 개발자는 쉽게 객체지향적인 프로그래밍을 할 수 있습니다.
그렇다면, 객체지향 언어를 쓴다면 객체지향적으로 프로그래밍을 하는 걸까요?
대답은 아니오 입니다. 말씀 드렸듯이, 객체지향 프로그래밍은 하나의 패러다임입니다. 언어는 잘 할수 있게 도와줄 뿐, 객체지향 언어를 사용한다고 객체지향적으로
프로그래밍을 했다고 볼 수는 없습니다. 만약 그랬다면 SOLID 원칙이나, 수많은 객체지향 설계에 관한 책들은 만들어지지도 않았을 거에요.
단순히 절차적인 프로그래밍을 할 지, 객체지향 프로그래밍을 할 지는 개발자의 역량에 달려있습니다.
객체지향을 지양한다면 ?
위에서 잠시 절차적 프로그래밍을 언급했습니다. 객체지향이 하나의 패러다임이라면, 객체지향이 존재하기 이전의 패러다임도 분명 존재 하겠죠.
객체지향적으로 프로그래밍을 하지 않았을 때를 상상해 봅시다.
Java를 사용해서 프로그래밍을 하는데, class가 없다고 생각해보세요! 굉장히 끔찍한 상황이라고 생각 할 수 있지만, 그게 객체지향이 등장하기 전 프로그래밍 패러다임이었습니다.
그렇다면, 객체지향적으로 프로그래밍 하지 않았을 때는 어떤 식으로 프로그래밍을 했고 객체지향과는 어떤점이 다를까요 ?
순차적 프로그래밍
순차적 프로그래밍은 사실 하나의 패러다임이라고 보기는 어려운 개념입니다. 왜냐면 애초에 프로그램은 어떤 프로그램이든 순차적으로 동작하기 때문이죠.
우리가 흔히 알고있는 플로우 차트를 있는 그대로 프로그래밍 하게 되면, 그게 순차적 프로그래밍입니다.
예를 하나 들어볼까요? 철수는 수학시험을 보고 있습니다. 시험 결과가 80점이 넘으면 집에 갈 수 있고, 아니면 남아서 재시험을 봐야 합니다
라는 내용을
플로우차트로 그리면 아래와 같습니다.
대단한 플로우는 아니다
이걸 순차적으로 프로그래밍 한다면 아래와 같은 모양이 될거에요, 예시를 위해 조금 극단적으로 느껴질 수 있습니다.
int score;
exam:
score = random();
if (score >= 80) {
goto home;
}
goto exam;
home:
println("끝났다!");
이 플로우를 철수가 수행하게 될 겁니다. 어떤 생각이 드시나요 ? 사실 너무 짧은 예제라 잘 와닿지 않으실 수 있습니다.
중요한 건 조건에 의해 원래 수행하던 작업과 다른 플로우로 이동이 필요 한 경우, 순차적 프로그래밍에서 취했던 방법은 goto
를 이용해 플로우를 강제로 바꿨다는 점이죠
위 예제에서는 굳이 그럴 필요가 없었고 읽기 힘든 상태도 아니지만, 코드량이 늘어나도 계속 그럴까요?
조건문과 반복문 만으로는 제어하기 힘든 경우가 분명히 생기고, 그걸 goto
를 사용해 해결하다 보니 개발자도 도대체 프로그램이 어떻게 흘러가는지
알아보기 힘든 지경이 된 거죠.
💡 goto는 절대악이다
라는 얘기를 하는 건 아닙니다. 편리하다고 갓난아이에게 불을 쥐여주지 않는 것과 같은 의미입니다.
절차적 프로그래밍
goto
에 호되게 당한 개발자들은 이대론 안 되겠다는 생각을 합니다. 여기저기서 흐름을 강제로 바꾸어 스파게티 덩어리가 된 코드는
어떤 사이드이펙트가 발생할 지 몰라 유지보수가 굉장히 어려운 상태가 되었기 때문이죠.
그래서 ‘아, 기존의 흐름을 바꾸지 말고 다시 원래대로 돌아오도록 해야겠다!’ 라는 발상에서 만들어진 게 프로시저(Procedure) 흔히 알고 있는 함수(Function) 의 개념입니다.
💡 프로시저, 함수, 메서드의 차이에 대해서 왈가왈부하는 경우가 있으나, 어디서 쓰이는지와 뉘앙스의 차이가 있을 뿐 사실 다 같다고 봐도 무방합니다.
함수를 만들고 보니 같은 기능을 수행하기 위해 같은 형태의 데이터 집합을 만들게 되고, 동일한 기능을 여러 상태에서 사용할 일이 생기면 이 작업이 여러 번 반복되니 변수명 짓기부터 여간 골치가 아픈게 아닙니다.
그러다가 다시 생각하게 되죠. ‘어? 이 기능을 사용하는 애들은 어차피 늘 같은 데이터 집합이 필요한데, 이걸 그냥 묶어서 갖고 다니면 안될까?’ 하는 아이디어로 탄생한 게 바로 구조체(Struct) 입니다.
struct Answer {
String name;
int score;
}
Student exam(name) {
Answer a;
a.name = name;
a.score = random();
}
void study(String name) {
Answer answer;
while(true) {
answer = exam(name);
if(answer.score >= 80) {
break;
}
}
println(answer.name + "끝났다!");
}
이렇게 함수와 구조체라는 개념이 생기고, 이를 절차적 프로그래밍이라고 부르게 되었습니다. 이렇게 생긴 대표적인 언어로 C언어가 있죠.
객체지향 프로그래밍
나올건 다 나온 것 같은데.. 그럼 객체지향 프로그래밍에선 또 뭐가 달라졌을까요?
함수와 구조체를 사용하던 개발자들은 다시 무언가를 깨달았습니다. 유사한 기능을 하는 함수에는 비슷비슷하게 생긴 구조체가 필요 하다는 것을요!
이를 더 효율적으로 관리할 수 있는 방법이 없을까 고민하던 개발자들은 ‘함수와 구조체를 묶어보면 어떨까?’ 하는 생각을 하게 되었고, 마침내 Class 가 탄생했습니다.
class Student {
public String name;
public int score;
public Student(String name) {
this.name = name;
}
public void exam() {
this.score = random();
}
}
class Study {
public static void main(String[] args) {
Student 철수 = new Student("철수");
while(true) {
if (철수.score >= 80) {
break;
}
}
System.out.println(철수.name + " 끝났다!");
}
}
위 두 예제와 차이가 보이시나요? 드디어 변수명을 철수
라고 지을 수 있게 되었습니다. 여기서 나오는 철수
라는 변수에 담긴 게 바로 객체입니다.
그럼, 왜 이제서야 우리는 철수를 철수라고 부를 수 있게 된 걸까요?
앞의 예제에서는 단독으로 온전히 기능을 수행할 수 있는 단위가 없었습니다. 함수가 존재했으나 상태를 가질 수 없어 외부의 데이터에 의존해야 했고, 구조체가 있었으나 스스로는 기능하지 못해 자신의 데이터가 어떻게 쓰일 지 결정 할 수 없었죠.
독립적으로 자신의 역할을 수행 할 수 있는 단위가 생겼기 때문에 현실세계에 존재하는 모든 것을 역할에 따라 나누어 개념을 정의할 수 있게 되었고, 이 개념을 실제로 구현한 것이 바로 객체입니다.
이번 요구사항에서 우리는 이름과 점수를 가지고 시험을 보는 무언가가 필요했었죠.
순차적, 절차적 프로그래밍에서는 시험을 보고 80점 이상이면 끝난다라는 절차가 존재 했고, 입력값으로 이름을 넘겨 결과를 받아보았죠. 그 안에서 마땅히 철수를 표현할 수 있는 방법이 없었습니다. 인간보다는 컴퓨터가 이해하기 쉬운 언어였죠.
Class가 생기고 나서야 우리는 학생 이라는 개념 을 정의하여 학생의 역할을 수행하는 변수 철수 를 철수라고 부를 수 있게 된거죠. 그래서 객체지향 언어를 인간 친화적인 언어 라고 표현 합니다. 추가로, Class는 하나의 역할을 정의한 것이기 때문에, 같은 역할을 하는 영희가 추가가 되는 경우 같은 Class를 사용하기만 하면 됩니다. 이를 재사용성이 높다 라고 표현합니다.
정리하자면, Class는 역할에 필요한 정보와 수행할 수 있는 기능을 정의 한 개념 이며 객체는 역할을 수행하거나 대상이 되는 개념의 구현체 이고 이렇게 역할을 나누고 각각의 역할에 맞는 개념을 정의해 이를 토대로 절차를 설계하는 것을 객체지향 프로그래밍이라 부릅니다.
💡 간혹 절차적 프로그래밍을 객체지향의 반대말로 이해하시는 분이 있는데, 이는 잘못 된 내용입니다. 필요에 따라 점차 패러다임이 발전한 것으로 이해하는게 맞습니다.
Class는 왜 필요했을까 ?
위의 예제를 보고 ‘그냥 구조체랑 함수를 합치면 객체지향인건가?’ 라고 생각하시는 분들도 있을 것 같습니다.
표면적으론 그렇게 보일 수 있습니다만, 이 일련의 과정들은 결국 하나의 목적을 지향하고 있습니다. 바로 유지보수의 용이성 이죠.
겉모습 외에도 Class는 유지보수를 용이하게 하기 위해 갖게 된 네 가지 특징을 갖고 있는데요, 이러한 특징들을 언어 레벨에서 지원을 해 주어야 비로소 객체지향 언어 라고 불립니다. 해당 내용은 다음 포스트에서 다루겠습니다.