객체지향(OOP)과 절차적(PP)의 차이에 대하여

최동민·2022년 12월 7일
1

객체지향 프로그래밍(OOP : Object Oriented Programming)'과 절차적 프로그래밍 (PP : Procedure Programming)

먼저 객체지향과 절차지향에 대해 알아보기 전에 OO지향이란 단어에 대해서 짚고 갈 필요가 있습니다. 아마 여러분들 뿐만 아니라 대부분 통용되는 말로 쓰이는 것이 있습니다.

절차지향 언어 : C언어
객체지향 언어 : Java, Python, C# 등등..

대다수의 사람들이 알고 구분하고 있는 이것이 틀린 말은 아닙니다. 다만 엄밀히 말하자면 어디까지나 '지향'이라는 것이고 하나의 프로그래밍 패러다임이지 C언어는 절차적으로만 프로그래밍한다. Java는 객체지향적으로 프로그래밍을 해야한다.가 아니라는 것이죠.

아래의 코드는 Java 언어이지만 객체지향적 코드는 살펴볼 수 없습니다.

import java.util.Scanner;
class Main {	
	public static void main(String[] args) {
		Scanner in = new Scanner(System.in);
		
		int first = in.nextInt();
		int second = in.nextInt();
		
		char type = in.next().charAt(0);
		
		if(type == 'a') {
			System.out.print(first + second);
		}
		if(type == 'b') {
			System.out.print(first - second);
		}
		if(type == 'c') {
			System.out.print(first * second);
		}
		
	}
}

방금 위에서 코드를 보았듯이 필요한 경우 어떤 언어이건 상관없이 절차지향적으로 코딩할 수 있습니다. 반대로 C언어로도 객체지향적으로 코딩할 수는 있습니다. 유사 객체지향이랄까요.

그리고 여러분이 반드시 기억하셔야 할 것은 기본적으로 모든 코드는 '절차적인 틀'이 있습니다. 사실 절차지향이라는 말은 올바른 말이 아니고 절차적 프로그래밍이라고 하는데, 기본적으로 절차라는 틀을 깨지 않는 선에서 객체지향적 프로그래밍을 하느냐 마느냐의 차이라고 보는게 좀 더 올바른 관점이지 않을까 합니다.
그래서 절차지향 프로그래밍에 대해 검색하면 '절차지향'이 아닌 '절차적 프로그래밍'이라고 합니다. 사실 조금만 생각해보아도 프로그래밍의 기본 틀이 절차를 기반으로 두고 있는데 이 절차를 지향한다는 것도 웃기긴 하죠.
정리하자면 OO지향이란 코딩하는 방식 또는 방법론의 차이이지 특정 언어가 특정지향만 지원한다는 것은 아니라는 것이죠.

또한 객체지향 프로그래밍의 반대 개념이 절차지향이라고 오해하는 분들이 많은데 절대 반대개념이 아닙니다. 어디까지나 프로그래밍 접근 방법이 조금 상이할 뿐 정반대 개념이 아니죠. 대개 절차적 프로그래밍은 '데이터를 중심으로 함수'를 만들어 쓰는편이고, 객체지향은 '데이터와 기능(함수)들을 묶어 하나의 객체'로 만들어 씁니다.
그렇기 때문에 정확한 표현으로 말하자면 '객체지향 프로그래밍(OOP : Object Oriented Programming)'과 절차적 프로그래밍 (PP : Procedure Programming)이라는 말이 더 정확합니다.

그러면 어떻게 프로그래밍 언어의 지향을 나눌까?

그러면 왜 C를 절차적 프로그래밍 언어, Java를 객체지향 프로그래밍 언어라고 부르는 것일까요? 이는 언어에서 지원하는 기능과 특징에 있습니다. 객체지향 언어와 절차적 언어를 구분하는 방법이야 많지만 대개 아래와 같은 기준아래 절차적 언어와 객체지향 언어가 나뉩니다.

  1. 캡슐화, 다형성, 클래스 상속을 지원하는가?
  2. 데이터 접근 제한을 걸 수 있는가?

보통은 위 기준을 만족하면 객체지향, 만족하지 않으면 절차적 성격이 강해집니다.

과거에는 지금과 같은 큰 규모의 하드웨어와 소프트웨어가 필요하지 않았습니다. 그래서 오래된 언어들인 C, 포트란, 코볼같은 언어들이 절차적 언어의 대표였죠. 하지만 점점 소프트웨어의 발전 속도가 빨라지면서 자연스레 코드도 복잡해지기 시작했습니다. 그렇다보니 복잡한 알고리즘을 짤 때 절차적 프로그래밍을 하면 순서도(흐름도)가 꼬이기 시작하면서 코딩한 코드가 사람이 읽으면서 동작을 이해할 수 없게 되는 일명 '스파게티 코드'가 되어버린다는 단점이 대두되었습니다.
이러한 문제를 극복하기 위한 대안으로 객체지향적 프로그래밍이 나온 것이죠. 그리고 이 대안은 현재까지도 대세를 유지하고 있습니다.

이렇게 언어의 패러다임이 변화하면서 앞서 말했던 절차적 프로그래밍 언어들도 객체지향의 특징들을 가져오기 시작합니다. 예로들어 C++가 있죠. C언어 기반으로 여기에 더해 상속, 캡슐화 등을 지원하게 됩니다. 또한 그 외의 언어들도 자체 업데이트를 통해 객체지향 프로그래밍의 특징들을 하나 둘씩 갖고오게 되죠.

다만 이러한 것 때문에 대표적으로 C++같은 경우 객체지향 언어라고 하는 사람들이 꽤나 있습니다. 하지만 방금전에 말했지만 하나의 패러다임일 뿐 객체지향적 특징을 지원한다고 하더라도 객체지향 언어라고는 하지 않습니다. C++ 같은 경우 객체지향보다는 멀티 패러다임 언어라고 하는 것이 더욱 올바를 겁니다.
아무래도 C를 계승해서 온 언어라 C의 패러다임도 포함되어 있어 코딩 스타일의 유사점이 많이 있습니다.

이렇게 각 언어별 지향점이 조금씩 상이하다보니 코딩하는 사람 또한 스타일이 굳어지게 됩니다. 특히 프로그래밍에 처음 발을 들여놓을 때 C언어를 먼저 배우고 C++를 배운 사람하고 Java언어를 먼저배우고 C++를 배운 사람하고 코딩 스타일의 차이가 확연하게 납니다. 또한 알고리즘을 짜기위한 문제 접근 방식도 차이가 조금씩은 있습니다.

절차적 프로그래밍 (PP : Procedure Programming)

먼저 절차적 프로그래밍(Procedure Programming)입니다. 단어 그대로 '절차적'으로 코드를 구성한다는 것입니다. 쉽게 말하면 '흐름도(순서도)'라고 보시면 됩니다.

데이터에 대한 순서를 파악하고 필요한 기능을 함수로 만들어 절차적으로 진행시키는 방법입니다.

객체지향 프로그래밍(OOP : Object Oriented Programming)

즉, 고객이 할 수 있는 행위(기능)들과 데이터들을 하나로 묶어 고객이라는 객체 만들고, 자판기에서 할 수 있는 행위들과 데이터들을 묶어 자판기라는 하나의 객체로 묶어서 푸는 겁니다.

그리고 각 객체의 메소드나 필드를 호출하면서 서로간의 상호작용을 통해 알고리즘을 구성하는 방식인 것이죠.

"잠깐. 객체 설명할 수 있어?"

Java 프로그래밍 언어를 배우며 수없이 듣고 사용해왔던 키워드.
하지만 객체를 만들고 생성하는 방법, 즉 프로그래밍 언어로써의 객체 정의는 알지언정 '객체'라는 단어 그대로의 정의는 잘 모르고 갑니다.

일단 객체에 대해 한 단어로 말하자면 '실체'라고 보면 됩니다. 잘 이해가 안되어도 괜찮습니다. 잠깐 17세기로 돌아가 철학 이야기를 해보도록 하죠.

17세기의 철학은 우리가 흔히 말하는 근대 철학의 시초가 되는 시점입니다. 이는 다시말해 철학이 태동하던 시대였는데, 한 가지 특징이라 하면 바로 자연과학의 등장이죠. 과거의 자연학은 '사물의 본질은 무엇인가'에 대한 의문에 대해 이를 설명하기 위한 그 자체의 형상 또는 본질을 규명하는 데에 집중해왔습니다. 하지만 17세기에 접어들면서 이러한 관점과는 달리 사물의 본질(성질)을 규명하는 것이 아닌 자연현상이 발생하는 법칙을 설명하고자 하는 움직임으로 바뀌게 됩니다.

조금 어렵게 말하면 사물의 본성이나 형상보다도 현상 상호의 관계를 문제로 한다는 것이죠. 이러한 관점은 여러분이 잘 아는 아이작 뉴턴, 갈릴레오 갈릴레이 등을 통한 물리학(역학)의 새로운 패러다임과 비약적 발전을 이루게 되죠. 과거에는 자연은 영적, 동적인 신의 부수적 역할이었다면 17세기 자연철학은 철학과 과학을 신학에서 벗어나 독립적인 학문으로서 자리를 잡기 시작했죠.

이러한 철학적 변화는 단순히 자연현상 뿐만 아니라 가치관 사고방식을 변화시키게 됩니다. 데카르트가 이성을 가진 인간을 신으로부터 독립시키게 되었죠. 여기서부터 '실체'에 대한 연구가 시작됩니다. 방금 인간을 신으로부터 독립시키게 되었다고 했죠? 즉, 우리가 객관적으로 인지할 수 있는 주체인 '나'는 신이 없어도 사고를 할 수 있다는 것이죠.

"나는 생각한다. 고로 존재한다" - 데카르트

이 말은 누구나 한 번쯤은 들어봤을겁니다. 앞서 말한 철학적 패러다임을 매우 함축적으로 보여주는 문장이죠.
그리고 주체에는 항상 객체(대상)가 붙어다니기 마련입니다. 간단하게 생각해보죠. 주체(나)가 밥을 먹는다는 행위를 한다면 밥을 먹히는 대상인 밥이라는 대상이 있어야 합니다. 이러한 대상을 객체라고 말하죠.

한마디로 주체는 실체인 내가 스스로를 가리키는 말이 되고, 객체는 주체가 사고하는 무언가가 되는 것이죠.

즉, 제가 여러분을 바라볼 때 제 스스로는 주체가 되고, 여러분은 객체가 되는 것입니다. 반대로 여러분들 입장에서는 여러분들 스스로가 주체가 되고 제가 객체가 되겠죠. 이제 조금은 감이 오시나요?

객체에 대해 한 문장으로 설명하자면 이렇습니다.

'인간 중심에서 객관적으로 인지할 수 있는 대상들이 객체(Object)다.'

이를 프로그래밍과 접목시켜본다면 앞서 봤던 객체지향 분석에서 왜 자판기와 고객을 분리시켰는지 이해할 수 있을겁니다. 어떤 동작들을 중심으로 절차에 따라 함수를 만들고 진행하는 방식이 아닌 동작의 주체가 누군지 분류하여 동일성을 갖는 기능들을 하나의 묶음으로 만들어 하나의 자판기라는 객체와 고객이라는 객체를 만드는 것이죠.

예시

고객이 음료수를 사는 과정을 간단하게 절차적 프로그래밍과 객체지향적 프로그래밍이 어떻게 다른지 한 번 보도록 하죠.

  • 절차적 프로그래밍
public class Main {
	
	// 주스 잔여 개수 
	static int Orange_juice = 10;
	static int Apple_juice = 20;
			
	public static void main(String[] args) {
 
		int customer_changes = 1000;
		String customer_has = null;
		
		// 오렌지 주스가 먹고싶군요 
		String want_juice = "Orange juice";
		
		if(want_juice.equals("Orange juice")) {
			if(Orange_possible(customer_changes)) {
				int changes = getOrangeJuice();
				System.out.println("오렌지 주스가 정상적으로 구매되었습니다");
				customer_has = want_juice;
				customer_changes -= changes;
			}
			else {
				System.out.println("오렌지 주스를 구매하실 수 없습니다");
			}
		}
		
		else if(want_juice.equals("Apple juilce")) {
			if(Apple_possible(customer_changes)) {
				int changes = getAppleJuice();
				System.out.println("사과 주스가 정상적으로 구매되었습니다");
				customer_has = want_juice;
				customer_changes -= changes;
			}
			else {
				System.out.println("사과 주스를 구매하실 수 없습니다");
			}
			
		} else {
 
			System.out.println("없는 물품입니다");
		}
		
		System.out.println("잔액 : " + customer_changes + "\t갖고있는 음료 : " + customer_has);
	}
	
	
	// 오렌지주스 구매 가능? 
	static boolean Orange_possible(int pay) {
 
		if (Orange_juice > 0) {
 
			if (pay >= 500) {
				return true;
			}
		}
 
		return false;
	}
	
	// 오렌지 주스 꺼내기 
	static int getOrangeJuice() {
		Orange_juice--;
		return 500;
	}
	
	// 사과주스 구매 가능? 
	static boolean Apple_possible(int pay) {
 
		if (Apple_juice > 0) {
 
			if (pay >= 300) {
				return true;
			}
 
		}
		return false;
	}
	
	// 사과주스 꺼내기 
	static int getAppleJuice() {
		Apple_juice--;
		return 300;
	}
}

이렇게 '필요에 의해 함수들을 만들고 순차적으로 진행하는 방식'을 절차적 프로그래밍이라고 합니다. 반면에 객체로 묶으면 어떻게 변하는지 보도록 하죠.

  • 객체지향적 프로그래밍
public class Main {
	public static void main(String[] args) {
 
		Customer customer = new Customer(1000);
		Vending_Machine vm = new Vending_Machine(10, 3);
		
		// 오렌지 주스가 먹고싶군요 
		String want_juice = "Orange juice";
		
		
		int pay = vm.buy(want_juice, customer.getChanges());
		
		// 지불비용이 0원일 때 == 구매 실패시 
		if(pay == 0) {
			customer.resetting_juice(pay, null);
		}
		else {
			customer.resetting_juice(pay, want_juice);
		}
		
		System.out.println(customer);
	}
}
 
// 고객 
class Customer {
 
	private int changes;
	private String hasJuice = null;
 
	public Customer(int changes) {
		this.changes = changes;
	}
 
	public int getChanges() {
		return changes;
	}
 
	public void setJuice(String hasJuice) {
		this.hasJuice = hasJuice;
	}
 
	// 잔액 설정 
	public void resetting_juice(int changes , String hasJuice) {
		this.changes -= changes;
		this.hasJuice = hasJuice;
	}
	
	public String toString() {
		return "잔액 : " + changes + "\t갖고있는 음료 : " + hasJuice; 
	}
 
}
 
// 자판기
class Vending_Machine {
 
	// 자판기에 남아있는 주스 개수
	private int Orange_juice; // 오렌지 주스 가격 : 500
	private int Apple_juice; // 사과 주스 가격 : 300
 
	public Vending_Machine(int Orange_juice, int Apple_juice) {
		this.Orange_juice = Orange_juice;
		this.Apple_juice = Apple_juice;
	}
 
	// 오렌지주스 판매가 가능한지 검사 
	private boolean Orange_possible(int pay) {
 
		if (Orange_juice > 0) {
 
			if (pay >= 500) {
				return true;
			}
		}
 
		return false;
	}
 
	// 사과주스 판매가 가능한지 검사 
	private boolean Apple_possible(int pay) {
 
		if (Apple_juice > 0) {
 
			if (pay >= 300) {
				return true;
			}
 
		}
		return false;
	}
	
	public int buy(String kind, int pay) {
		
		if(kind.equals("Orange juice")) {
			if(Orange_possible(pay)) {
				Orange_juice--;
				System.out.println("오렌지 주스가 정상적으로 구매되었습니다");
				return 500;
			}
			System.out.println("오렌지 주스를 구매하실 수 없습니다");
			return 0;
		}
		
		else if(kind.equals("Apple juice")) {
			if(Apple_possible(pay)) {
				Apple_juice--;
				System.out.println("사과 주스가 정상적으로 구매되었습니다");
				return 300;
			}
			System.out.println("사과 주스를 구매하실 수 없습니다");
			return 0;
		}
		
		System.out.println("없는 물품입니다");
		return 0;
	}
}

차이점이 보이시나요? 각각의 객체에서 수행할 수 있는 함수들과 상태(필드)를 묶어서 하나의 클래스로 만들고 각각 객체를 만들어서 활용하죠.

"그러면 어떠한 방식이 좀 더 좋다는 것이지?"

여기에 정답은 절대 없습니다. 각각의 장단점에 대해 살펴보죠

절차적 프로그래밍

절차적 프로그래밍 장점
1. 객체나 클래스를 만들 필요 없이 바로 프로그램을 코딩할 수 있다.
2. 필요한 기능을 함수로 만들어 두기 때문에 같은 코드를 복사하지 않고 호출하여 사용할 수 있다.
3. 프로그램의 흐름을 쉽게 추적할 수 있다.

절차적 프로그래밍 단점
1. 각 코드가 매우 유기성이 높기 때문에 수정하기가 힘들다. (새로운 데이터나 기능을 추가하기가 어려움)
2. 프로그램 전체에서 코드를 재사용 할 수가 없어 프로젝트 개발 비용과 시간이 늘어날 수 있다.
3. 디버그(오류 검사)가 어렵다.

객체지향 프로그래밍

객체지향 프로그래밍 장점
1. 모듈화, 캡슐화로 인해 유지보수에 용이하다.
2. 객체지향적이기 때문에 현실 세계와 유사성에 의해 코드를 이해하기 쉽게 만든다.
3. 객체는 그 자체가 하나의 프로그램이기 때문에 다른 프로그램에서 재사용이 가능하다.

객체지향 프로그래밍 단점
1. 대부분의 객체 지향 프로그램은 속도가 상대적으로 느려지고 많은 양의 메모리를 사용하는 경향이 있다.
2. 현실 세계와 유사성에 의해 코드를 이해하기 쉽게 만드는 만큰 설계 과정에 시간이 많이 투자된다.

보통의 경우 절차적으로 코딩할 때에는 대개 프로젝트 규모가 엄청 크지 않고 재사용할 일이 크지 않은 경우에 많이 쓰는 편입니다. 앞서 말했듯이 프로그램 자체가 가벼워지고 오히려 객체를 만드는 것보다 개발 시간이 확연히 줄어들기 때문이죠.
반대로 매우 큰 규모의 프로젝트라던가 코드를 재사용을 고려해야 할 때는 객체 지향 프로그래밍이 유용합니다. 대신에 초기 개발비용은 소모되더라도 유지보수 관점에서 매우 안정적이기 때문이죠.

출처 링크

profile
코드를 두드리면 문이 열린다

1개의 댓글

comment-user-thumbnail
2023년 6월 26일

와... 좋은글 감사합니다.

답글 달기