구조적 언어가 비 구조적 언어에 비해 발전한 부분을 나열해 보면 다음과 같다.

1. 소프트웨어의 구조적 분할 가능 : 전체가 한 덩어리였던 소프트웨어를 함수 단위로 체계적으로 분할 할 수 있게 되었다.

2. 함수간의 호출 가능 : 함수가 다른 함수를 호출할 수 있게 되면서 전체 소프트웨어의 동작 순서를 일목 요연하게 알 수 있게 되었다.

3. 함수 명명 가능 : 함수에 이름을 지을 수 있게 되면서 현재 하려는 작업의 목적을 분명하게 알 수 있게 되었다.


그러나 아직도 구조적 언어에는 문제가 많이 남아 있었고, 이것이 객체지향 언어의 탄생 배경이 되었다.


구조적 언어의 문제점

1. 변수에 대한 구조적 분할이 어려움 -> 전역 변수 문제 : 전역 변수의 출현을 효율적으로 막지 못했다.

2. 변수와 함수의 연관 관계가 불분명 : 어떤 함수가 어떤 변수를 다루는지 직관적으로 알기 힘들다.

3. 함수 내부에 대한 이해 필요 : 여전히 함수 내부를 들여다 봐야 전체 소프트웨어의 흐름을 이해할 수 있다.


구조적 언어는 많은 발전에도 불구하고 여전히 위와 같은 문제점이 있었다. 위의 문제들 때문에 아래와 같은 의도하지 않은 결과물이 나오곤 했다.

1. 분명히 똑같은 절차로 동작 시킨 것 같은데 결과는 다르게 나오는 소프트웨어 : 전역 변수의 변화를 예상하지 못해서 발생하는 문제.

2. 오류를 수정할 때 다른 함수의 동작에 영향을 미침 : 역시 전역 변수가 가장 큰 문제의 원인이다.

3. 하나의 오류를 수정하기 위해 여러 함수를 고쳐야 함


이러한 문제점의 근본적인 원인은 적절한 정보 은닉이 이루어지지 않았기 때문이다. 객체지향 언어는 이와 같은 문제점을 해결하고자 다양한 문법적인 지원과 설계의 원칙을 제시하고 있다.


우선 객체지향 언어의 아버지라 불리는 앨런 케이의 관점을 먼저 이야기 해보자.


"왜 사람들은 (큰) 컴퓨터를 작은 컴퓨터로 나누려고 하지 않는가?"

비 구조적 언어에서 구조적 언어로의 이행을 촉발한 것은 소프트웨어 규모의 확대였다. 앨런 케이는 이것을 명확히 이해하고 매우 직접적인 방식으로 해결하려고 했다. 즉, 큰 컴퓨터를 작은 컴퓨터로 나누려고 했던 것이다. 앨런 케이에게 있어서 이 작은 컴퓨터가 바로 객체였다.


"객체지향 언어에 있어서 가장 중요한 것은 메시지(메시징)이다."

여기서 메시징이라는 것은 쉽게 바꿔 말하면 상대 객체가 가진 공개(public) 메소드를 호출하는 것을 말한다. 앨런 케이의 말에 따르면 객체에게 있어서 가장 중요한 것이 공개 메소드라는 말이다.


앨런 케이의 관점을 종합해 보면 객체지향 언어는 사람이 큰 컴퓨터가 하는 일을 이해하는 것보다는 작은 컴퓨터(객체)가 하는 일을 이해하는 것이 쉽고, 그 작은 컴퓨터는 다른 컴퓨터의 메시지만 이해하고 있으면 된다는 것이다. 이것은 객체가 자신의 역할을 수행하는데 필요한 최소한의 지식만을 알고 있으면 된다는 말이다.


객체가 알아야 할 것

1. 자기 내부

2. 자기와 협력하는 객체의 외부(메시지 = 공개 함수 = 인터페이스 = API)


나머지 정보들은 모두 은폐 되는 것이 바람직하다. 그럼 이제 은폐되어야 할 정보들을 살펴보자.


캡슐화

우리가 객체지향 언어의 최소 공개 원칙 중에 첫번째로 꼽을 수 있는 부분이다. 캡슐화란 일반적으로 변수와 함수를 묶어 클래스로 선언하고, 클래스에서 외부에 노출할 부분만 선택적으로 노출시키는 것을 말한다. 이를 통해서 구조적 언어가 가지고 있던 전역 변수 문제가 해결되었다. 캡슐화가 감추는 정보를 나열해 보면 다음과 같다.

1. (대부분의) 변수 : 이로써 전역 변수 문제가 거의 해결 되었다.

2. private 함수 : 외부에서 호출할 필요가 없는 함수는 은폐되었다. 이로써 외부 객체는 협력하고자 하는 객체의 공개 함수의 외형만 알고 있으면 된다.


타입화(= 구현 은폐)

이 부분은 놓치기 쉬운데, 모든 객체는 타입(= 클래스)을 가지고 있다. 외부 객체는 해당 객체의 구현 전체에 신경 쓸 것이 아니라 그 객체의 타입만 알면 된다. 즉 타입 이외의 모든 정보는 은폐되어야 한다는 말이다.

1. 인터페이스 중심의 설계(외부적 관점과 내부적 관점의 완벽한 분리 및 은폐) : 인터페이스를 중심으로 설계하라는 원칙은 구현 내부에 대해서는 신경쓰지 말라는 말이다. 객체지향 언어에서는 상대의 인터페이스만 알면 협력이 가능하다.

2. 타입 중심의 설계 : 객체지향 언어는 모든 단위가 클래스(=타입)로 이루어져 있다. 클래스는 단순한 속성과 행위의 집합이 아니다. 클래스가 가진 것 중 가장 중요한 것이 바로 타입이다. 타입은 내부 구현을 감춰준다.

구현의 은폐는 수정에 용이한 코드를 만들어 준다. 예를 들어 기존의 객체가 문제가 있거나 다른 기능을 수행하는 객체를 사용해야 한다고 했을 때, 기존의 코드가 타입에만 의존하고 있었다면 그 코드는 수정하기 용이하다. 실제 동작시에 새로 만들어진 객체를 할당해 주기만 하면 기존 코드는 그대로 사용할 수 있기 때문이다.


타입 은폐

심지어는 타입 조차도 감추는 것이 좋다. 정확하게 말해서 상위 타입이 외부에 노출되어 있다면 하위 타입을 감추는 것이 좋다는 말이다.

List<String> list = new ArrayList<String>();

많은 코드에서 이와 같은 형태로 ArrayList를 사용한다. 그 이유는 ArrayList를 코드에서 사용하는 순간 그 코드는 오직 ArrayList를 위한 코드가 되기 때문이다. 객체지향에서는 구체적인 타입을 밝히는 것을 꺼려 한다. 구체적인 타입을 밝히는 순간 그 코드는 그 구체적인 타입 "만"을 위한 코드가 되기 때문이다. 그러면 다른 구체적인 타입을 위한 코드를 또 만들어야 한다. 바로 중복이 발생한다. 또 구체적인 타입에 대해 작성한 코드를 다른 타입에 대해 작성하기 위해서는 상위 타입으로 작성된 코드에 비해 더 많은 수정이 필요하다. 그래서 가능한 한 하위 타입을 은폐하라는 것이다.


SOLID 원칙 중 LSP (Liskov Substitution Principle)은 타입 하이딩에 대한 대표적인 설계 원칙이다. 하위 타입을 구현할 때 상위 타입으로 치환 가능하도록 만들어야 한다. 그래야 하위 타입을 감출 수 있다. 감춰야 하는 이유는 이미 설명한 바와 같다.


다형성 역시 하위 타입을 감추는 목적 중 하나이다. 다형성은 다양한 하위 타입별로 작성되어야 할 코드들을 하나로 묶어준다. 이는 중복을 제거하고 간결한 코드를 만들어 준다.


Factory Method 패턴 : Factory Method 패턴은 기본적으로 생성할 객체의 구체적인 타입을 감추기 위해 만들어진 패턴이다. Factory Method 패턴에서 리턴되는 객체는 항상 상위 타입이다. 그리고 특정 하위 타입의 구체적인 생성자 호출을 하지 않는다. 따라서 객체의 생성 이후에는 그 객체가 정확히 어떤 타입인지 알 수 없다.


최근에는 반대로 (안전이 보장되는 한) 최대 노출의 원칙을 주장하는 소프트웨어 기술도 있다. 바로 단위 테스트이다. 테스트 가능 설계 중 한가지 방식으로써 충분히 의미 있는 주장이다.


단위 테스트를 위한 최대 노출 원칙(테스트 가능 설계)

- 동작에 영향을 주지 않는다면 모든 정보를 제공하라(getter) : 정보 은닉의 목적은 안전한 구현이다. 테스트 가능 설계를 위해 동작에 아무런 영향을 주지 않는 정보 제공 함수(getter)는 충분히 만들어 놓을 가치가 있다.

- 모든 의존 객체에 대한 의존성 주입 함수를 제공하라 : 다른 객체에 의존성이 있을 경우, 그 객체는 주입(setter) 함수를 꼭 만들어 주자. 그러면 단위 테스트 시에 해당 객체를 다른 객체로 대체함으로써 테스트를 용이하게 할 수 있다.




Posted by 이세영2
,