절차 지향 프로그래밍은 코드가 위에서 아래로 순차적으로 프로그래밍 방식을 연상시킨다. 혹은 순차 지향 프로그래밍과 같은 것이 아닌가 하는 생각도 든다.
나는 완전히 잘못 알고 있었다.
순차 지향 프로그래밍은 말 그대로 코드를 위에서 아래로 프로그래밍하는 방식이다.
어셈블리 언어를 생각하면 쉽게 이해할 수 있다. 어셈블리 언어는 코드를 위에서 아래로 순서에 맞게 프로그래밍하고 실행될 뿐이며, 목적지 주소인 레이블 개념과 분기 명령어를 포함하여 작성할 수 있다.
그럼 절차 지향 프로그래밍은 뭘까?
절차 지향 프로그래밍은 영어로 Procedure Oriented Programming이다. 여기서 Procedure는 함수를 의미한다.
절차 지향 프로그래밍은 함수 지향 프로그래밍이다.
C언어를 생각하면 쉽게 이해할 수 있는데, 어셈블리어와 같은 동작을 하는 프로그램일지라도 main함수에서 실행되며 다른 함수를 정의하고 사용하는 것이 가능하다.
프로그래밍 언어를 사용한다고 그 프로그래밍 언어의 패러다임을 제대로 이해하고 사용하는 것은 아니다.
나는 자바를 주 언어로 사용하지만 사실 절차 지향적 프로그래밍을 하고 있었을지 모른다.
이제부터 절차 지향 프로그래밍과 객체 지향 프로그래밍을 비교하며 객체 지향 프로그래밍의 패러다임에 대해 알아갈 것이다.
절차 지향 프로그래밍은 책임을 구분할 수 없을까? 그렇지 않다.
절차 지향 프로그래밍은 함수 지향 프로그래밍이라고 하였다. 따라서 함수 단위로 책임을 부여하면 된다.
int absolute(int a){
return a < 0 ? -a : a
}
absolute 함수는 입력 값을 항상 양수 값으로 반환한다는 책임을 가지고 있다.
다만 객체 지향과 다른 점은 책임을 부여하는 대상이다.
객체 지향에서는 함수가 아닌 객체에 책임을 할당한다. 하지만 이것은 C언어로도 가능하다. 그저 구조체 하나를 만들고 책임을 부여하면 되는 일 아닌가?
따라서 책임을 부여하는 대상 하나로 객체 지향 패러다임의 모든 것을 설명하기는 이르다.
그렇다면, 절차 지향과 객체 지향을 구분하는 또 다른 요인은 무엇이 있을까?
역할을 주목해야 한다. 코드 변화를 통해 살펴보자.
public class Order{
private List<Food> foods;
private double transactionFeePercent = 0.02;
public long calculateRevenue() {
long revenue = 0;
for(Food food : foods){
revenue += food.calculateRevenue();
}
return revenue;
}
public long calculateProfit() {
long income = 0;
for(Food food : foods){
income += food.calculateProfit();
}
return (long) (income-calculateRevenue()) * transactionFeePercent;
}
}
Order 클래스는 Food를 팔아서 얻은 총매출과 총이익을 계산하는 책임을 가지고 있다.
Order 가 가지고 있는 책임을 역할로 분리하여 추상화 해보자.
public interface Calculable {
long calculateRevenue();
long calculateProfit();
}
매출과 이익을 계산하는 역할을 자바의 추상화를 통해 역할로 분리하였다.
이제 Order 클래스에 생기는 변화를 살펴보자
public class Order implements Calculable{
private List<Calculable> foods;
private double transactionFeePercent = 0.02;
@Override
public long calculateRevenue() {
long revenue = 0;
for(Calculable food : foods){
revenue += food.calculateRevenue();
}
return revenue;
}
@Override
public long calculateProfit() {
long income = 0;
for(Calculable food : foods){
income += food.calculateProfit();
}
return (long) (income-calculateRevenue()) * transactionFeePercent;
}
}
Order 에 할당되어 있던 책임을 Calculable 인터페이스로 분리하였고 Order 에 다형성을 지원할 수 있도록 변화하였다.
객체 지향에서는 책임을 객체에 할당하지 않는다. 객체를 추상화한 역할에 책임을 할당한다.
C언어의 구조체는 데이터를 한 곳에 모으는 역할을 하며 Java의 클래스처럼 추상화를 지원하지 않는다.
이제 절차 지향 언어와 객체 지향 언어의 차이가 명확해졌다. 다형성이다.
솔직히 코드만 놓고 보면 어떤 장점을 가져다주는지 모르겠다. 다형성은 어떤 이점을 가져다 줄까?
역할을 추상화 하여 다형성이라는 특성을 잘 이용하면 큰 장점을 얻을 수 있다.
역할을 이용해서 통신을 하면 실제 객체가 어떤 객체인지 알 필요가 없다.
내가 부여한 책임과 역할을 할 수 있는 객체라면 협력 객체가 어떤 객체인지 알 필요 없다는 말과 같다.
이는 확장성에 도움을 준다. 만약 개발 도중에 새로운 요구 사항이 생기면 그 역할을 하는 새로운 구현체만 만들어주면 된다. 다시 코드로 살펴보자.
public class Order{
private List<Calculable> foods;
private List<BrandProduct> brandProducts;
private double transactionFeePercent = 0.02;
public long calculateRevenue() {
long revenue = 0;
for(Calculable food : foods){
revenue += food.calculateRevenue();
}
for(BrandProduct brandProduct : brandProducts){
revenue += brandProduct.calculateRevenue();
}
return revenue;
}
public long calculateProfit() {
long income = 0;
for(Calculable food : foods){
income += food.calculateProfit();
}
for(BrandProduct brandProduct : brandProducts){
income += brandProduct.calculateRevenue();
}
return (long) (income-calculateRevenue()) * transactionFeePercent;
}
}
만약 음식점의 규모가 커져서 브랜드화 되었다고 가정하자. 그래서 음식점의 굿즈를 팔게 되었고, 매출과 이익에 굿즈 판매에 대한 수치를 포함해야한다.
이 코드는 매출과 이익 계산을 담당하는 역할을 분리하지 않았다.
BrandProduct에 대한 목록이 멤버 변수로 추가되었고, 이를 계산하는 로직 또한 추가 된 것을 볼 수 있다.
그렇다면 역할을 분리한 코드에는 어떤 변화가 있을까?
public class Order implements Calculable{
private List<Calculable> items;
private double transactionFeePercent = 0.02;
@Override
public long calculateRevenue() {
long revenue = 0;
for(Calculable item : items){
revenue += item.calculateRevenue();
}
return revenue;
}
@Override
public long calculateProfit() {
long income = 0;
for(Calculable item : items){
income += item.calculateProfit();
}
return (long) (income-calculateRevenue()) * transactionFeePercent;
}
}
foodlist가 items로 변경되었다.
이 코드에서 Order는 Calculable이라는 역할에 의존하고 있기 때문에 Order의 코드가 크게 바뀔 필요가 없이 기능을 확장할 수 있다.
이로써 구체적인 것(클래스)가 아니라 추상적인 것(역할, 인터페이스)에 집중할 때 유연한 설계가 가능하다는 것을 알 수 있다.
나는 Java를 학습하면서 언어의 문법과 추상화, 상속과 같은 언어의 특징에 집중하여 공부했다.
하지만 객체 지향 언어로서의 본질은 역할, 책임, 협력에 있으며 추상화, 상속, 다형성, 캡슐화는 본질을 효과적으로 다루기 위한 수단임을 알게되었다.
또한 내가 배워왔던 것처럼 객체 지향은 실세계를 반영하지 않는다.
Order 클래스를 보면 스스로 이익과 매출을 계산한다. 실세계에서는 주문이라는 행위가 이익과 매출 계산을 의미하지 않는다.
객체 지향 패러다임은 오히려 자아를 가진 객체들이 서로 소통하며 프로그램을 이루는 것이라는 것을 알게되었다.