지난 포스팅 "객체지향이란 무엇인가? (https://p-tech.tistory.com/2)"에서 객체지향은 "캡슐화, 상속, 다형성"을 이용하여 "High Cohesion, Less Coupling"을 달성하여 좋은 프로그램을 개발하기 위한 방법론이라고 정의했다. 따라서 이번 포스팅에서는 "캡슐화, 상속, 다형성"이라는 개념 대해서 먼저 정의하고, 어떻게 사용하고, 왜 사용하는지를 주절거려보려고 한다.
근데 먼저 뇌피셜을 하나 주장하고 넘어가겠다. OOP는 세 가지 개념을 통해서 High Cohesion, Less Coupling을 달성하려는 방법론이라고 했는데, High Cohesion, Less Coupling이라는 것도 결국은 관념적인 것이다. 실존하고 완벽하게 객관화되는 내용이 아니기 때문에, OOP가 생각하는 가장 이상적으로 High Cohesion, Less Coupling인 "상태"가 존재한다고 볼 수 있다. OOP가 생각하는 이상적인 프로그램의 상태는 다른 방법론에서는 이상적이지 않다고 할 수도 있다. 다른 패러다임에서는 OOP의 방식대로 구현한 이상적인 상태에서는 코드 재사용과 유지보수가 힘들 수도 있으니까. 그럼 OOP가 생각하는 이상적인 프로그램의 상태는 뭘까? 나는 그것은
"너는 인터페이스만 알면 돼"
라고 생각한다.
"OOP는 실세계를 기반으로 모델링하는 패러다임이다." 나는 이 말을 그렇게 좋아하지는 않지만 틀린 말은 아니라고 생각한다. 기본적으로 OOP 자체가 시뮬레이션 툴을 개발하기 위한 Simula에 뿌리를 두고 있기도 하고. 근데 내가 이 말을 싫어하는 이유는 오해의 소지가 높기 때문이다. 정확하게는 "실세계의 모델링 방식을 기반으로 하는 패러다임" 정도로 얘기해야 된다고 본다. 갑자기 이런 말을 하는 이유는, 실세계에서 생각해볼 때 "내가 전화기 내부의 동작 원리를 알아야만 하던가?"라는 발상이 OOP에서 굉장히 중요하기 때문이다. 어떻게 보면 실세계에 존재하는 생물과 사물들이 그 자체로 "High Cohesion, Less Coupling" 상태라고 볼 수 있기 때문에, 이러한 실세계의 모델링 방식에 영감을 얻었다고 생각한다. 근데 이 실세계에서는 어떠한 "물건"이 어떻게 이루어졌고, 어떤 원리로 작동하는지 궁금해하는 건 그 물건을 만드는 사람이나 고치는 사람뿐, 사용하는 사람은 사용법만 알면 된다. 그래서 OOP에서도 모든 것이 객체로서 규정되고, 객체 간에는 서로의 "사용법 = 인터페이스"만 알면 되는 상태가 이상적인 프로그램의 상태라고 보고 있으며, 이렇게 "인터페이스만 알면 돼"의 상태를 만들기 위해서 캡슐화, 상속, 다형성이 필요하다고 정의한 것이다.
내가 포스팅하는 캡슐화, 상속, 다형성의 세 가지 개념은 이러한 관점을 토대로 한다.
1. 캡슐화(Encapsulation) : 위키피디아에 따르면 다음 두 가지 중 하나로 정의하거나 조합하여 정의한다고 한다.
- 객체의 일부 Component로의 직접적인 접근을 제한하는 언어 메커니즘.
- 데이터와 그 데이터를 이용해 Operating 하는 메서드를 쉽게 Bundling 할 수 있는 언어 구성
논문의 내용을 출처로 한 정의고, 영어 원문이기 때문에 딱딱한 번역체지만 결국 1) 객체 외부로부터의 접근을 제한한다. 즉, 은닉한다. 2) 데이터와, 그 데이터와 상호작용하는 메서드를 하나로 묶는다. 두 가지가 핵심이다. '두 가지 중 하나로 정의하는' 경우도 있다고 하지만, 내 생각에는 두 정의가 모두 필요하다고 생각한다.
그럼 캡슐화는 OOP에서 왜 핵심적인 개념으로 사용되는 걸까? OOP에서 캡슐화는 왜 필요할까? 일단 내가 1번과 2번 정의 두 가지 모두 필요하다고 주장하는 데는 다 이유가 있다. 잘 살펴보면 1번은 Coupling을 해소하는 내용이고 2번은 Cohesion을 강화하는 내용이다. 애초에 내가 다른 객체와의 상호작용을 위해, 데이터에 직접적으로 접근할 수 있다면 그 자체로 강하게 Coupling 되는 것이고, 데이터와 메서드가 따로따로 정의되고 다른 파일에서 정의된다고 한다면 매우 약하게 응집되어 있는 것이다. 즉, OOP에서의 가장 작은 단위의 프로그램이라고 볼 수 있는 객체들부터 "좋은 객체"를 만들고자 하는 것이다.
또한 캡슐화는, 그 자체로 인터페이스화 하는 것이다. 전화기가 동작하게 하는 부품 같은 것은 눈에 보이지도 않고, 어떻게 동작하는지도 모르지만 내가 전화기를 사용하기 위해서는 "전화 걸기" 같은 동작 법만 알면 된다. 이 처럼 어떤 "기능"을 위해 필요한 데이터와, 내부 동작을 하나의 클래스 내에서 private 하게 정의하고, 이 클래스의 목적인 "기능"만을 public 하게 정의한다. 이 public 한 "기능 셋" 자체가 곧 인터페이스라고 볼 수 있다.
2. 상속(Inheritance) : 클래스를 생성할 때, 다른 클래스의 타입과 구현을 가진 상태로 생성할 수 있는 것. 흔히 물려받는다라고 표현하는데, 그런 관념적인 어휘보다는 "내가 코드를 구현하지 않아도 구현된 상태로 생성할 수 있다"는 것이 더 정확하다고 생각한다.
흔히 상속을 하는 이유, 상속의 목적으로 초점이 맞춰지는 것은 "재사용성"이다. 내가 원하는 클래스는 이 클래스랑 거의 비슷한데 조금 달라! 이런 상황에 당연히 상속으로 거의 비슷한 부분은 구현하지 않고, 조금 다른 부분만 추가하거나 Override 하면 된다. 하지만 나는 이게 "본질"이라고 생각하지 않는다. 상속에서 중요한 것은 "구현"을 가진 상태로 클래스를 생성할 수 있다는 것이 아니라, "타입"을 가지고 생성할 수 있다는 점이다. OOP에서는 클래스 자체가 타입, 자료형이 되는데, 상속받지 않은 클래스의 경우 단 하나의 타입만을 가지게 된다. (JAVA에서는 기본적으로 Object 타입을 상속받고 있긴 하지만). 그러나 상속을 받을 경우에는, 그 클래스는 자체가 하나의 타입이면서 동시에 부모 타입으로도 참조될 수 있다. 이 것이 왜 중요하냐면, 바로 이 지점에서 "다형성"이라는 특징이 생성되기 때문이다.
3. 다형성(Polymorphism) : 다형성의 원어인 Polymorphism은 Poly + morph + -ism이다. Poly는 많다, morph는 형태라는 뜻을 가져, "여러 형태를 동시에 가진다"는 뜻이 된다. 여러 형태를 가진다, 근데 주어가 없다. 무엇이 여러 형태를 가진다는 말일까? 그건 이름(name)이다. 하나의 이름이 여러 가지 형태를 가진다. 프로그래밍 언어 요소는 무엇이든 결국 이름을 가짐으로써 참조될 수 있다. 이는 데이터도 마찬가지고, 함수도 마찬가지이다. 흔히 다형성을 이루는 루트는 Overriding과 Overloading 두 가지로 소개된다. 하지만 나는 이러한 설명보다는 객체의 다형성, 함수의 다형성으로 설명해야 된다고 생각한다. 나는 객체의 다형성이 훨씬 중요하다고 생각하고, Overriding과 Overloading은 모두 함수의 다형성과 연관되어 있기 때문이다. 왜 이 둘이 함수의 다형성과 연관되어 있는지를 살펴보자.
Overloading이 더 쉽기 때문에 먼저 소개를 하면, 예를 들면 같은 Class 안에 똑같이 plus라는 이름의 두 개의 method가 있다고 하자. 하지만 하나는 integer를 두 개 받아 더하는 method고 다른 하나는 double 타입 숫자 두 개를 받아 더하는 method이다. 이 둘은 같은 이름을 가지지만 서로 다른 동작을 하는, 두 개의 형태를 가지는 다형성을 가진다. 사실 Overloading은 OOP에서 그렇게 중요한 개념은 아니다. 단순한 편의를 좀 더 줄 뿐, OOP의 핵심적인 부분과 닿아있지는 않기 때문이다.
// Overloading의 예시
public class Example {
public int plus(int a, int b) { return a+b; }
public double plus(double a, double b) { return a+b; }
}
Overriding의 경우를 보자. 먼저 하나 말해두면 나는 Overriding은 다형성이 아니라고 생각한다. Overriding의 예를 들면, Super라는 class에 foo라는 method가 있다. 또, Super를 상속받은 Sub라는 클래스에서는 foo라는 함수를 재정의(Override)했다. 그리고 Caller라는 method가 Super 타입의 인자 A를 받아서 foo를 호출한다고 하자. 이 인자로는 Super 객체도 받을 수 있지만 Sub 객체 역시 받을 수 있다. 그러면 A.foo()라는 이름의 함수를 호출하는 건데, 상황에 따라 Super.foo()가 호출될 수도 있고 Sub.foo()가 호출될 수도 있다. 하나의 이름에 두 개의 형태를 가지는 다형성이 생긴다.
// Overriding 예시
public class Super {
public void foo() {
// Do Super
}
}
public class Sub extends Super {
@Override
public void foo() {
// Do Sub
}
}
public class Main {
public static void caller(Super A) {
A.foo(); // A에 할당된 실제 객체에 따라 호출되는 foo()가 달라진다.
}
}
하지만 나는 여기에서 의문이 생긴다. Overriding이 다형성을 얻을 수 있게 해 준 것인가? 정확하게는 A라는 이름의 변수에 Super 타입과 동시에 Sub 타입의 객체가 참조될 수 있다는 사실 때문에, Overriding 된 다른 구현의 함수가 호출될 수 있었던 것 아닌가? Overriding이 아닌, 상속이라는 개념이 다형성을 준 게 더 맞는 말이 아닌가 싶은 거다. 그리고 나는 이 상속을 통해 획득한 다형성을 지금은 객체의 다형성이라고 부르고 있다. 함수형 프로그래밍의 경우 함수도 객체로 다루고, JAVA도 8 버전부터는 Method 객체도 생겼지만, 기본적으로 OOP에서는 객체와 함수가 분리된다고 보기 때문이다. 여기에 더불어 내가 Overloading도 Overriding도 아닌 객체의 다형성이 더 중요하다고 한 것은, 이 것이 OOP의 핵심과 맞닿아 있다고 생각하기 때문이다.
나는 다형성이 이 세 가지 개념 중에서 가장 중요한 것이라고 생각한다. 지난 포스팅에서 언급했듯이, 내 기준에서 가장 매력적인 OOP의 결과물인 IoC 도 다형성에 의해 만들어질 수 있었다. 특히 상속에 의한 객체의 다형성이 핵심이라는 것은 부품화가 가능해지기 때문이다. 어떤 물건이 있는데 특정 파트에 호환되는 규격이 Standard라고 하자. 그리고 Standard라는 규격은 그 파트에 조립될 수 있는 물건들이 지원해야 되는 기능 셋을 정의한다. 그러면 상황에 따라서 Standard라는 규격을 지켜 만든 A라는 물건이, 또는 B, C를 조립해서 사용할 수 있을 것이다. 이는 마치 자동차에서 바퀴라는 부품은 반드시 동그란 원형이여야 하고, 그 자동차에 끼울 수 있는 휠 사이즈를 가져야 하는 등의 규격이 존재하는 것과 비슷하다. 그 규격만 맞으면, 눈길에서는 스노우 타이어를, 고속도로에서는 그에 맞는 안정적인 타이어를 갈아 끼울 수 있는 것과 같다. 여기서의 Standard와 같은 규격을 OOP에서는 인터페이스라고 부르고, A, B, C를 인터페이스를 구현한 클래스라고 부른다. 상황에 따라 이렇게 규격에만 맞으면 어떤 클래스의 객체든 갈아 끼우기만 하면 되는 것은 Coupling이 굉장히 낮은 상태라고 볼 수 있다. 이 처럼 바로 다형성이 객체와 모듈을 "부품화" 하여 다른 프로그래밍 패러다임과는 가장 차별적으로 프로그래밍할 수 있게 해주는 요소라고 생각한다.
원래 이번 포스팅에서 SOLID까지 하려고 했는데, 이미 너무 길어져서 다음 포스팅으로 넘겨야겠다.
'OOP' 카테고리의 다른 글
[OOP #3] OOP와 SOLID (0) | 2019.12.03 |
---|---|
[OOP #1] 객체지향이란 무엇인가? (0) | 2019.11.14 |