지금까지 공부해 온 소프트웨어 영역의 기술들을 트리 형식으로 정리해 본 것이다.


소프트웨어 기술은 그 발전 속도가 너무 빠르다. 그리고 새로운 기술은 매일 매일 쏟아져 나온다. 이런 상황에서 이제 소프트웨어를 접한지 얼마 되지 않은 사람들은 어떤 것을 먼저 공부해야 할 지 갈피를 잡기 힘들 것이다. 개인적으로 이제껏 소프트웨어를 공부해 오면서 안타까웠던 점을 꼽자면 이런 급변하는 상황에서도 기술들 간에 어느 정도 줄기가 있다는 것, 그리고 줄기가 되는 기술들 간에 선후 관계가 있다는 것을 처음부터 알지 못했다는 것이다. 그런 관계를 알게 된 것은 이제 소프트웨어의 근간이 되는 기술들을 대부분 알게 된 이후였다. 개인적인 능력의 문제도 있겠지만 이 점을 미리 알았다면 그것들을 모두 익히는데 이렇게 오랜 시간이 걸리지는 않았을 것이다. 

그런 안타까움이 뭍어 있는 것이 바로 이 기술 트리다. 만약 이제 소프트웨어를 막 공부하기 시작한 사람이라면 이 트리에 맞춰 공부하기를 추천한다. 그리고 다른 수많은 기술들이 있지만 적어도 이 영역 내의 기술들은 소프트웨어를 하는 사람들이라면 거의 필수적인 기술들이라고 봐야 한다.

일부 개발 영역에 따라서는 더 중요한 것이 빠져 있을 수도 있다. 개발자라고 해서 모두 같은 영역에서 일하는 것이 아니기 때문이다. 웹 프론트, 백엔드, 임베디드, 데이터베이스 영역에서는 세부적으로 보다 더 중요한 기술도 있을 수 있다. 그래도 역시 위의 기술들이 뼈대를 이루는 것들이다. 그리고 그 중에서도 가장 중요하다고 생각되는 기술들은 볼륨 처리를 해 두었다. 저 중에서 볼륨 처리된 기술에 대해서 간략히 이야기해 볼까 한다.


객체지향(OOP, Object Oriented Programming)

현대 소프트웨어 개발에 있어서 가장 중요한 되는 개념이라고 생각하면 된다. 스크립트 언어나 함수형 언어를 접하게 되더라도, 그리고 구조적 언어를 통해 개발을 하게 되더라도 객체지향은 꼭 알고 지나가야 하는 개념이다. 트리에서도 보듯이 프로그래밍 언어의 기초 문법을 익히고 나서 소프트웨어를 구조적으로 작성하기 위해 배우는 첫번째 단계이며 이후 필요한 소프트웨어 기술들의 모태가 되는 기술이다. 즉, 객체지향을 모르고는 어떤 소프트웨어적인 개념도 제대로 이해하기 힘들고, 객체지향을 모르는 사람을 소프트웨어 개발자라고 말하기 어렵다.

불완전성의 관리 관점에서 보면 객체지향은 갈수록 대형화 되어 가는 소프트웨어를 작은 단위로 축소시켜 주는 역할을 한다. 하위 타입에 대한 은폐를 통해서 작성해야 할 코드의 양을 줄이면서도 수정 및 확장이 용이한 소프트웨어 구조를 만들어 준다. 상속을 통해서는 중복된 코드가 발생하는 것을 막아주고, 인터페이스와 타입의 개념을 통해서는 내부 구현에 대한 은폐를 가능하게 해준다. 변수 대신 객체를 바꿈으로써 조건문/제어문을 사용하는 대신 직접 행위를 변경할 수 있게 한다.

객체지향이 소프트웨어 영역에 가져온 영향력은 막대하다. 사실상 소프트웨어에 설계의 개념이 도입된 것이나 설계의 원칙이 도입되게 된 것, 올바르고 좋은 설계의 패턴, 소프트웨어의 가시화(UML) 등 거의 모든 소프트웨어 기술은 객체지향을 이용하거나 객체지향에서 파생된 것, 또는 객체지향을 개선한 것들이다. 현대의 대부분의 언어들은 객체지향을 온전히, 혹은 적어도 부분적으로 지원한다.


UML(Unified Modeling Language)

UML이 있기 전까지 소프트웨어는 비 가시적인 기술 영역이었다. 인간이 눈으로 얼마나 많은 양의 정보를 얻는지를 안다면 이것은 치명적인 문제였다. UML이 없었던 시절, 소프트웨어를 여럿이서 함께 개발한다는 것이 무척 어려웠을 것이다. 인간의 언어는 코드보다 부정확하다. 코드는 완벽하게 진실만을 이야기 하지만 구조를 이해하지 못한 상태에서의 코드는 줄거리를 모르는 대서사시처럼 장황하다. 인간의 언어로 대화하다가 서로 막히는 곳이 있으면 그 대서사시를 살펴봐야 한다. 이 와중에 일부 개발자들은 자신의 코드를 신성시 한다. 아마도 UML이 없던 시절에 소프트웨어를 바라보는 다른 엔지니어들의 시선은 그리 좋지 못했을 것이다. 소프트웨어 개발자 간에도 의사 소통이 신통치 않았을텐데 다른 분야의 사람들과 원활히 대화하기는 더욱 어려웠을 것이다.

사람들이 소프트웨어를 (자기 나름대로의 방법으로) 가시화 하기 시작했을 때에도 그 가시적인 도안들을 통한 커뮤니케이션이 원활하지 않았다. 작은 그룹에서는 통용될지 몰라도 의사소통의 단위가 커지면 가시화의 방식이 달라 서로 이해하기 어려웠다. 

UML은 이런 가시적인 툴로서는 최초로 보편적인 표시 언어로 사용된 것이다. 개발자들은 UML을 통해 비로소 서로의 코드를 보지 않아도 소프트웨어의 구조를 이해하게 되었고, 코드를 먼저 만들지 않고도 구현을 이야기 할 수 있게 되었다. 

아직까지는 코드와 유사한 수준의 소프트웨어 이해를 가능하게 하는 언어는 UML이 유일하다. 


디자인 패턴

디자인 패턴이 탄생한 후부터 개발자들은 좋은 설계를 인간의 언어로 말할 수 있게 되었다고 할 수 있다. 아기로 비유하자면 이제 막 첫 마디 단어를 말하는 그 시점만큼 극적인 일이다. 디자인 패턴이 있기 전에는 어떤 설계가 다른 설계보다 어떻게 나은지를 설명하기 위해 코드를 작성하거나 UML을 그리거나 자신이 하려고 하는 일에 대해서 상대방에게 인간의 언어로 수 십 분에 걸쳐 이야기 해야 했다. 디자인 패턴이라는 것이 개발자들이 설계 문제를 해결하던 여러 방법들에 이름을 붙여 놓은 것이기 때문에, 설계에 대해 한참 이야기를 하다 보면 서로 같은 이야기를 하고 있었다는 것을 알게 되었을 것이다. 디자인 패턴은 이런 "같은 이야기"들에 이름을 붙였다. 그 이후부터는 같은 이야기를 지루하게 반복하는 일이 없어졌다.

사람들이 잘 된 설계에 대해 이름을 붙이기 시작하면서 대화는 짧아지고 정밀한 설계에 대해 집중할 수 있게 되었다. 그러면서 다른 디자인 패턴들도 많이 생겨나게 되었고, 대화는 더욱 풍성해졌다. 같은 설계 문제에 대해 어떤 패턴을 적용하는 것이 더 나은 설계인지를 이야기할 수 있게 되었다. 

디자인 패턴을 모르고는 설계를 이야기 할 수 없다.


Unit Test(단위 테스트)

단위 테스트는 소프트웨어의 안전망이다.

단위 테스트 이전의 소프트웨어는 주로 정밀한 설계를 통한 구현 상에서의 오류 감소, 그리고 통합 테스트를 통한 디버깅이 불안전성 제거를 위한 거의 유일한 방법이었다. 이 방법을 제외하고는 인간의 두뇌가 유일한 불안정성 관리 도구였다. 불안전성의 원리 때문에 직접적으로 소프트웨어의 완전성을 증명할 수 없지만 유닛 테스트는 간접적인 방법으로 안전망을 구축해준다.

유닛 테스트의 유용성을 이야기 해보면 다음과 같다. 

우선 직접 작성하지 않은 소스에 유닛 테스트가 있을 경우, 소스의 의도를 파악하는데 도움이 된다. 필요한 경우에는 리팩토링을 통해서 소스를 더욱 잘 이해할 수도 있고, 설계를 바꿈으로써 소스의 흐름을 더 원활하게 가져갈 수도 있다. 

유닛 테스트는 구현에서 발생한 버그를 테스트 단계에서 발견하게 됨으로써 생기는 디버깅의 어려움을 감소시켜 준다. 버그는 발생한 시점에 발견하여 즉각 수정하는 것이 손쉬운데 이는 버그가 발생한 시점이 코딩 시점과 가까울수록 해당 버그의 문제점을 짚어 내기가 용이하기 때문이다.(사실 이 부분은 불완전성 관리의 도구가 오직 두뇌임을 명시적으로 보여주는 대목이다) 그런데 프로젝트가 커지면 커질수록 전통적인 개발 프로세스에서는 구현과 테스트 간의 간격이 더 벌어졌다. 대형 프로젝트일수록 더 정밀한 관리가 필요하고 더 나은 방식으로 문제점을 해결해야 함에도 전통적인 프로세스는 이 문제를 더 키우기만 할 뿐이었다. 유닛테스트가 생겨남으로써 일시적인 버그는 즉시 판단하고 제거할 수 있게 되었다.

유닛 테스트의 또 다른 이점은 설계에 준하는 수준의 소프트웨어 동작 지침을 제공한다는 것이다. 이는 TDD(Test Driven Development)가 추구하는 방향인데, 테스트 코드를 구현 코드보다 먼저 작성함으로써 구현 코드가 작성되어야 할 방향을 정해주는 것이다. 이로써 설계 단계에서 미비했거나 요구사항의 불확실성 때문에 완벽하지 못했던 설계를 유닛 테스트를 통해 보충해 줄 수 있다.


리팩토링

현대의 소프트웨어는 늘 수정된다는 특성이 있다. 그래서 요즘에는 완벽한 설계보다는 실행 가능하고 수정 가능한 설계를 추구하는 경향이 있다. 이에 따라 별다른 수정 사항이 없어도 구현 중에 일부 설계가 부적절한 것을 발견하게 되는 경우도 있고, 초기에는 잘 된 설계임에도 불구하고 기능적인 수정이 늘어나면서 설계의 효율이 떨어지는 경우도 있다. 이렇게 효율이 떨어진 설계를 널리 잘 알려진 좋은 설계, 즉 디자인 패턴을 중심으로 좋은 설계로 바꾸어 나가는 작업을 리팩토링이라고 한다.

이 과정은 근본적으로는 설계의 변경이지만, 이미 만들어진 기능에 대해 수행하는 작업이므로 실질적으로는 잘 동작하고 있는 코드를 수정하여 설계 맞추는 작업이라고 할 수 있다. 이 과정에서는 잘 동작하는 코드가 수정 중에 버그가 발생하지 않도록 안전장치를 해 둘 필요가 있다. 이 역할을 하는 것이 유닛 테스트이다. 리팩토링 과정은 어떤 경우에는 별다른 어려움 없이 끝날 수도 있지만 어떤 경우에는 상당한 시간 동안 진행 될 때도 있다. 이 때 리팩토링의 각 단계에서 기존 기능과 동일하게 동작함을 확인시켜주는 유닛 테스트는 필수적이다.

리팩토링은 디자인 패턴이 나온 이후에 생겨난 것이고, 유닛 테스트를 통해서 그 안정성을 보장 받게 되었다고 볼 수 있다. 또한 구현 이후에 설계를 변경한다는 점에서 정통의 소프트웨어 개발 프로세스와는 상반된 개념이기도 하다. 소프트웨어 분야는 아직도 한창 발전하고 있는 분야이기 때문에 혁신적인 사고가 언제든 기존의 사고를 제치고 자리 잡을 수 있다. 설계를 반영하여 코드를 작성하고, 이미 작성된 코드를 수정하고, 수정된 코드에 맞춰 설계를 변경하는 일련의 과정은 소프트웨어가 가진 유연성이라는 장점을 가장 잘 드러내는 과정이라고 볼 수 있다. 리팩토링은 개발자가 설계와 코드 안에서 자유로워 질 수 있음을 보여주는 기술이라 할 수 있다.


Agile

Agile은 전통적인 소프트웨어 개발 방법론의 단점을 보완하기 위해 생겨난 개발 방법론이다. 전통적인 개발 방법론은 철저한 요구사항 수집 및 분석, 이를 바탕으로 한 세밀한 설계, 설계에 딱 맞는 구현, 설계-구현에서의 부족한 점을 테스트를 통해 보완하는 구조로 되어 있다. 이는 개발 방법론이 정립되지 않았던 시기 보다는 나은 결과물을 내줄 수는 있었지만 현대의 소프트웨어 분야의 트렌드와는 잘 맞지 않는다. 현대에는 개발 시작 시점에 요구사항이 완벽한 경우가 별로 없고(거의 없다), 시장의 요구 변화에 맞춰 개발 진행 중에 상당 부분 변경이 이루어진다. 개발 중간에 수많은 요구사항들이 새로 생겨나고 없어지거나 수정된다. 또한 개발이 완료되었다고 해도 지속적인 수정 요청이 발생하기도 한다. 이러한 요구사항 변화를 기존 프로세스 상에 반영하는 것은 거의 불가능에 가깝다.

Agile은 현대 소프트웨어 개발 과정의 특성을 반영하고자 하는 프로세스이다. 시장은 항상 변하고, 이에 따라 요구사항은 항상 변한다. 시간이 지날수록 사용자의 요구사항은 더 많아지게 된다. Agile에서는 이러한 요구사항을 수용하기 위해서 요구사항들을 중요도, 개발 기간, 구체화 정도 등의 요소를 통해 순위를 매기고 이들 중 일부를 가지고 개발에 착수한다. 따라서 전체 요구사항을 모두 수집하는 방식에 비해 요구사항 분석이 짧다. 또한 요구사항의 개수가 적으므로 각 단계별 수행 시간도 짧아지게 된다. 이를 통해 프로세스의 기간을 단축시킬 수 있다.

이런 방식으로 1차 개발을 완료한 후 남아 있거나 새로 추가된 요구사항, 수정된 요구사항들을 모아 다시 같은 과정을 반복한다. 그리고 이 과정에서 소프트웨어 결과물은 항상 동작 가능한 상태를 유지한다.

Agile은 구현에서 테스트로 넘어가는 기간을 단축시켜 디버깅이 용이하게 해준다. 짧고 반복적인 개발을 통해서 전체 프로세스의 종료 시간을 예측하는데 도움을 준다. 새로운 요구사항이 나올 경우 다음번 주기에 바로 반영시킬 수 있으므로 고객 피드백이 빨라진다.

Posted by 이세영2
,

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

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
,

State 패턴

5.디자인패턴 2016. 8. 15. 18:24

참고

- 상태와 행위의 결별


상태 변수는 어떤 경우에도 좋지 않다. 상태 변수는 변수와 행위와의 결합을 만들어 내고, 이 과정에서 조건문들을 부수적으로 생산해 낸다. 따라서 언어적으로 허용되는 한 상태 변수는 최대한 없애주는 것이 좋다.


해결하고자 하는 문제

- 상태 변수에 의해 행위가 변경된다.
- 이 상태 변수가 행위를 변경하기 위해서 조건문을 사용한다.
- 상태 변수를 체크하기 위한 조건문이 너무 많다.

문제 코드

class Employee {

    public static final int ENGINEER = 1;

    public static final int MANAGER = 2;

    public static final int SALESMAN = 3;

    private int type;

    public void setType(int type){

        this.type = type

    }

    public Employee(int type){

        setType(type);

    }

    public int getAmount(){

        switch(type){

            case ENGINEER :

                return 100;

            case MANAGER :

                return 200;

            case SALESMAN :

                return 300;

        }

        return 0;

    }

} 

이 예제는 마틴 파울러의 "리팩토링"에서 사용된 예제이다. Employee 객체는 내부적으로 직원의 타입을 나타내기 위해 type 이라는 상태 변수를 사용하고 있다. 그리고 getAmount() 함수에서는 Employee의 type에 따라서 다른 결과값을 출력하도록 만들어져 있다.

이 소스에서 확인할 수 있는 것은 다음과 같다.

1. type 변수는 getAmount() 함수 내부에 switch - case의 각 구문과 1:1 매칭된다.

2. type 변수의 값에 따라서 다른 구문이 호출된다. ENGINEER 값일 경우 return 100, MANAGER일 경우 return 200이 실행된다는 말이다.


사실 이런 상황에서 type 변수는 오직 행위를 변경하고자 하는 목적에서 선언된 것이다. 만약 type을 통해 변경하고자 하는 행위를 직접 변경한다면 전체 소스는 매우 간결해지게 될 것이다.


이 목적을 달성하기 위해서는 일단 각 type 에 해당하는 객체를 구현하고, 각 객체별로 수행될 함수를 정의할 필요가 있다.


interface EmployeeType{

    public int getAmount();

}

class Engineer implements EmployeeType{

    public int getAmount() { return 100; }

}

class Manager implements EmployeeType{

    public int getAmount() { return 200; }

}

class Salesman implements EmployeeType{

    public int getAmount() { return 300; }

}

이와 같이 ENGINEER, MANAGER, SALESMAN에 대응하는 객체를 구현하였다. 이 객체들은 EmployeeType이라는 인터페이스를 상속 받고 있으므로 EmployeeType으로 선언된 멤버 변수에 자유롭게 변경하여 집어 넣을 수 있다.


이제 기존의 type  변수 대신에 새로 정의한 EmployeeType을 사용한다.

private EmployeeType type;

public void setType(EmployeeType type){

    this.type = type

}

public Employee(EmployeeType type){

    setType(type);

}

이제 getAmount() 함수 부분을 수정해야 한다. 이미 EmployeeType의 하위 객체들은 getAmount() 함수를 가지고 있다. 그리고 새로 정의된 type 변수에는 적절한 EmployeeType 객체가 할당되어 있다. 따라서 getAmount() 함수에서는 EmployeeType의 하위 객체가 가지고 있는 getAmount() 함수를 호출해 주기만 하면 된다.

public int getAmount(){

   return type.getAmount();

}

보면 알겠지만 기존의 int 타입 변수를 사용했을 때에는 getAmount() 함수 내부가 switch 문으로 구성되어 있어서 가독성이 많이 떨어졌다. 그럴 수 밖에 없었던 것은 type이 상태 변수 역할을 했기 때문이다. 상태 변수는 행위를 직접 변경시킬 수 없다. 행위와 상태 변수가 직접 대응될 수 없기 때문이다. 그래서 상태 변수의 값을 검사하는 조건문이 추가 될 수 밖에 없었다. 하지만 상태 변수 대신 행위를 직접 수행할 수 있는 객체에 대한 레퍼런스를 사용하면 조건문을 별도로 사용할 필요가 없다.


최종 결과

interface EmployeeType{

    public int getAmount();

}

class Engineer implements EmployeeType{

    public int getAmount() { return 100; }

}

class Manager implements EmployeeType{

    public int getAmount() { return 200; }

}

class Salesman implements EmployeeType{

    public int getAmount() { return 300; }

}

class Employee {

    private EmployeeType type;

    public void setType(EmployeeType type){ this.type = type; }

    public Employee(EmployeeType type){

        setType(type);

    }

    public int getAmount(){

        return type.getAmount();

    }

}


State 패턴 클래스 다이어그램


Posted by 이세영2
,

Strategy 패턴

5.디자인패턴 2016. 8. 15. 17:51

참고

- 상태와 행위의 결별


Strategy 패턴은 작업을 수행하는 대상 객체에 (변수 대신) 다른 객체를 인자로 넣어 줌으로써 대상 객체의 행위를 변경하는 패턴이다.


해결하고자 하는 문제

- 객체의 동작을 동적으로 변경하고자 한다.
- 객체의 동작이 다양하고 확장될 가능성이 있다.
- 상태 변수에 의해 조건문 중첩이 너무 많이 발생한다.

문제 코드

public static final int ADD_STATE = 0;

public static final int SUB_STATE = 1;

int static calculate(int state, int a, int b){

    if(state == ADD_STATE){

        return 5 + 10;

    }

    else if(state == SUB_STATE){

        return 5 - 10;

    }

    return 0;

}

public static void main(String[] args) {

    int result = 0;

    result = calculate(ADD_STATE, 5, 10);

    result = calculate(SUB_STATE, 5, 10);

}

위의 코드는 state 변수의 값에 따라서 간접적으로 calculate() 함수의 동작을 제어하도록 되어 있다. 상태 변수를 통해 행위를 변경시키는 코드는 좋은 코드가 아니다. 불필요한 상태 변수가 선언되고, if 문이나 switch 문과 같은 불필요한 제어문이 생성되기 때문이다. 가장 좋은 방법은 변경시키고자 하는 행위를 직접 넘겨주는 것이다.

우선 아래와 같이 변경하고자 하는 행위를 객체로 선언한다. 인자로 넘길 때 변경이 가능한 형태여야 하므로 동일한 인터페이스를 상속 받은 Add 클래스와 Sub 클래스를 선언해 준다.

interface IFunction{

    public int calculate(int a, int b);

}

class Add implements IFunction{

    public int calculate(int a, int b){

        return a + b;

    }

}

class Sub implements IFunction{

    public int calculate(int a, int b){

        return a - b;

    }

}


이렇게 선언된 클래스를 calculate() 함수가 인자로 받을 수 있도록 한다. 이 때 state 변수는 이제 필요 없으므로 제거한다.

static int calculate(IFunction function, int a, int b){

    return function.calculate(a, b);

}


public static void main(String[] args) {

    int result = 0;

    result = calculate(new Add(), 5, 10);

    result = calculate(new Sub(), 5, 10);

}


최종 결과

interface IFunction{

    public int calculate(int a, int b);

}

class Add implements IFunction{

    public int calculate(int a, int b){

        return a + b;

    }

}

class Sub implements IFunction{

    public int calculate(int a, int b){

        return a - b;

    }

}

public class Strategy {

    static int calculate(IFunction function, int a, int b){

        return function.calculate(a, b);

    }

    public static void main(String[] args) {

        int result = 0;

        result = calculate(new Add(), 5, 10);

        result = calculate(new Sub(), 5, 10);

    }

}


상태를 나타내는 state 변수 대신에 행위를 구현한 객체를 직접 넣어 줌으로써 코드가 더 간결해지게 된다.

Strategy 클래스의 calculate() 함수는 IFunction 타입의 객체를 인자로 받도록 되어 있다. main() 함수에서는 calculate() 함수를 통해서 실행하고자 하는 행위에 따라서 Add 클래스 혹은 Sub 클래스를 바꿔 넣어주면 행위가 변경된다.  



Posted by 이세영2
,

*이 글을 보기 전에

- "모든 악의 근원 : 불완전성의 원리"




어떤 사람들은 완벽하려고 하고, 어떤 사람들은 위험을 먼저 본다. 그리고 어떤 사람들은 위험보다 기회가 먼저 보인다.


불완전성의 원리를 증명한 쿠르트 괴델은 완벽해지려고 노력했던 사람인 듯 하다. 그리고 당대의 많은 수학자들은 수학의 완전성이 무너지는 것을 보고 그 분야를 연구하기를 외면했다.


하지만 지금 우리가 컴퓨터를 최초로 만든 사람들로 알고 있는 일련의 사람들은 그곳에서 기회를 찾았다. 이들이 불완전성을 무시했던 것은 아니다. 이미 불완전성은 다양한 형태로 소프트웨어 분야에서 명제화 되어 있다. 하지만 괴델이 증명에 사용했던 알고리즘의 시초에서 어떤 이들은 컴퓨터와 소프트웨어라는 자동 계산 기계에 대한 기회를 발견했다.


당시는 한창 2차 세계 대전이 진행 중이었고, 전쟁에서 이기기 위해 암호 해독, 무기 개발 등 다양한 연구를 수행하고 있었다. 연구를 위해서는 수학, 물리학 등 다양한 분야에서 당대 최고의 학자들을 모아 놓을 필요가 있었는데, 이들 중에 현대 컴퓨터 구조의 아버지라 불리는 폰 노이만이 있다.


그는 당대의 모든 학자들 중에서도 단연 최고의 천재라 불릴 만큼 대단한 사람이었다. 그에 대한 수많은 일화가 있고, 그것이 사실인지는 확인이 필요하지만 소프트웨어라는 것을 어떻게 생각했는지를 알 수 있는 일화가 하나 있다. 


컴퓨터는 만들어졌고, 컴퓨터를 구동시킬 알고리즘을 구현해야 했던 상황에서 당시에 활용할 수 있는 것은 0과 1로만 이루어진 기계어 뿐이었다. 폰 노이만의 제자들 중에서 기계어를 가지고 알고리즘을 구현하는 것이 무척 어려움을 느끼고 그보다 더 고급 언어(아마 어셈블리어 쯤 되지 않을까 한다)를 만들려고 하고 있었다. 고급 언어를 만들기 위해서는 언어를 정의하고, 정의된 형식대로 프로그램을 작성하고, 작성된 프로그램을 컴퓨터에 입력하고, 컴퓨터에 입력된 프로그램을 다시 기계어로 바꾸는 작업이 필요하다. 이러한 일련의 과정에는 컴파일러라는 프로그램이 필요하고 컴파일러는 컴퓨터의 컴퓨팅 능력이 없으면 동작하지 않는다.


요컨데 인간이 이해하기 쉬운 컴퓨터 언어를 만들어 사용하려면 컴퓨터의 연산 능력을 활용해야 한다는 의미이다. 폰 노이만이 이에 대해 한 말은 '완벽한 신의 언어가 있는데, 그걸 놔두고 저런 조잡한 걸 만들려고 하느냐?(위키)'였다.


개인적으로 저 일화가 진짜인지 확인하기는 어렵지만 일화가 알려주는 몇가지 중요한 문제들이 있어 짚어보고자 한다.


코드의 명료성

그렇다. 기계어는 인간이 이해하기 너무 어렵다. 얼마나 어렵냐면, 기계어로는 너무나 어려워서 기계어로 컴파일러를 만들고 조금 쉬운 언어를 정의해서(언어가 '조금' 더 쉬운 것이지 언어를 정의하는 일이나 새로 정의된 언어가 쉬운건 아니다) 그 언어를 컴퓨터에 입력할 수 있는 기계어 프로그램을 만들 생각을 하고 실천해야 했을 만큼 어렵다. 당시의 소프트웨어는 현대의 소프트웨어만큼 비 결정적이고 변덕스럽지 않다.(이 말의 의미는 잘 알고 있을 것이다. 현대의 소프트웨어게 사람들은 미래의 기대도 만족 시킬 수 있을 정도의 유연성을 요구한다. 미래의 기대는 아직 나타나지도 않았는데 말이다.) 폰 노이만과 같은 천재가 아니라면 기계가 바로 동작 시킬 수 있는 기계어로 현대의 소프트웨어를 만든다는 것은 불가능한 일이다.


이 일화에서 끄집어 내고 싶었던 부분은 범용 컴퓨터가 만들어진 시절부터 이미 코드의 이해 측면, 즉 인간이 이해할 수 있는 언어를 만들겠다는 생각이 존재했다는 점이다. 이것은 중요한 관점인데, 현대에도 잘 짜여진 코드의 중요한 덕목이 명료성이기 때문이다. 명료성이란 한마디로 말하자면 인간이 이해하기 편해야 한다는 특성이다. 어떻게 만들든지 기계어는 명료하지 않다. 현대의 언어로 만들어진 코드라고 해도 명료성을 추구하지 않으면 코드의 품질은 이루 말할 수 없이 저하된다. 하물며 기계어를 계속 다루었던 사람들이라면 (폰 노이만을 제외하고) 보다 고급 언어를 정의하여 사용하고 싶다는 욕구가 지금보다 더 강했을 것을 것이라 생각해 볼 수 있다.


현대의 언어를 접하면서 우리가 염두해 두어야 할 것은 언어의 문법을 이해하는 것보다 언어의 관점을 이해하는 것이 더 중요하다는 점이다. 문법은 우리에게 가능성을 열어준다. 문법이 정의되어 있지 않으면 우리는 어떤 생각을 코드로 표현할 수 없다. C언어에는 객체지향 문법이 없다. 따라서 C언어로 객체지향을 표현하기가 어렵다. 따라서 문법을 잘 알지 못하면 표현하고 싶은 것이 있어도 표현하지 못한다. 따라서 문법을 공부하는 것은 당연히 중요하다. 문제는 많은 이들이 문법을 공부하고 깊이 이해하면 이해할 수록 문법이 주는 자유에 빠져들어 간다는 것이다. 어떤 이들은 문법이 "허용된 자유의 범위"라고 생각한다. 마치 문법을 문자 그대로 "법"이라고 생각하는 것 같다. 그래서 법으로 정해진 범위 내에서 모든 것이 가능하다고 생각한다. 이러한 생각을 표현한 말이 "돌아가기만 하면 된다"는 말이다. 문법이 "법"으로써 강제하는 정도는 그리 심하지 않다. 개수를 나타내는 변수 이름을 count라고 쓰지 않는다고 해서 컴파일러가 문법 오류를 발생시키지 않는다. 그래서 어떤 사람들은 이것을 "자유"라고 칭하고 변수 이름을 a라고 짓는다. 이것은 인간의 인지 능력에 대한 테러에 가깝다.


문법을 "자유"라고 생각하는 것은 관점을 무시한 생각이다. 관점이란 언어를 디자인한 목적을 말한다. 현대의 많은 언어들이 객체지향 언어이다. 객체지향 언어는 문법도 상당히 어렵다. [상속에 대한 올바른 이해]에서도 이야기 했듯이 객체지향 언어를 처음 접한 이들은 문제 해결과 직접 연관이 없는 문법들을 공부하느라 머리가 아파진다. 하지만 그러한 문법이 왜 생겼는지에 대한 생각, 즉 언어가 드러내고자 하는 관점을 이해하지 않으면 문법도 이해하기 힘들다. 문법이 정해져 있다는 것은 일반 "법"과 마찬가지로 제약이 정해진다는 것이다. 제약이 정해졌다는 것은 "법"과 마찬가지로 해악을 바로잡겠다는 의지가 담긴 것이다. 인간의 의지가 담겨 있을 정도로 언어적 제약이 중요했기 때문이다.


그러면 문법에 어떤 의지가 담겨 있는 것일까? 딱 한가지 의지, 즉 관점만 이야기 해보자면 바로 "아무렇게나 한다고 모두 코드가 아니다"라는 관점이다. 어셈블리어를 정의한 이유는 기계어가 가졌던 완벽히 기계어적인 자유를 박탈해야 했기 때문이다. 0과 1로만 이루어진 코드가 가진 자유는 인간에게 허용되기에는 너무나 큰 자유다. 정확히 어떤 자유냐면 "인간이 이해하기 힘들 만큼 어려운 자유, 인간이 다루기에는 너무 이해하기 힘들 만큼의 자유"다. 불완전성의 원리에서 이야기 했듯이 불완전성을 다룰 수 있는 유일한 도구는 바로 인간의 두뇌다. 따라서 인간의 두뇌가 가진 인지 능력을 자꾸 벗어나려고 하는 코드는 제약을 가해 바로 잡을 필요가 있다. 어셈블리어의 관점이란 그것을 표현한 것이다.


그러면 C언어의 관점은 무엇일까? C언어는 구조적 언어의 관점을 포함하고 있다. 소프트웨어가 점점 커지고 복잡해질수록 거대한 소프트웨어를 부분으로 나누어 개발해야 한다는 생각이 자리 잡기 시작했다. 구조적 언어를 달리 표현하는 말이 "절차지향 언어"라는 말인데, 이 말은 구조적으로 분리된 부분들을 순서에 맞춰 수행시키겠다는 의미이다. 이것은 이전 언어인 어셈블리어가 가지지 못한 관점이다. 어셈블리어는 이해 측면에서는 기계어보다 나았을지 몰라도 역시 비 구조적 언어의 특성을 함께 가지고 있었다. 따라서 전체 소프트웨어가 부분 부분으로 분리되어 있지 않았고, 전체 소프트웨어의 절차, 즉 어떤 순서로 실행되는 것인지 이해하기가 어려웠다. 이러한 문제점을 보고 해결하려고 했던 노력이 담긴 것이 C언어이다. C언어는 문제를 함수라는 부분으로 나누고, 부분으로 나뉜 함수들을 어떤 순서로 실행할지를 정할 수 있다. 왜 이렇게 만들었을까? 언어가 발전해 온 이유, 즉 무엇인가를 왜 만들었을까에 대한 답은 항상 한가지이다. 인간이 이해하는데는 그렇게 만드는 것이 더 낫기 때문이다.


자 이제 말하고 싶었던 내용을 정리해 보면 이렇다. 소프트웨어를 만드는 프로그래밍 언어는 점점 더 인간이 이해하기 좋은 형태로 발전되어 왔다. 그 이유는 인간이 이해하지 않으면 소프트웨어를 관리하는 것이 불가능해지기 때문이다. 그래서 다시 언어의 관점을 정의해 보면 다음과 같다. 언어의 관점은 인간의 인지 능력을 보다 효율적으로 사용할 수 있도록 해야 한다는 것이다. 이것이 어셈블리어 같은 자연어 언어를 만들어 내고, C언어와 같은 구조적 언어를 만들어 내고, 객체지향 언어를 만들어 냈다. 함수형 언어나 스크립트 언어는 뭔가 특별한가? 아니다. 그것도 그들 나름대로 인간의 인지 능력에 최적화하려고 노력한 언어들이다. 어떤 부분에서는 인간이 신경쓰지 않아도 될 일들을 적절한 수준에서 감추었고, 어떤 부분에서는 기존의 언어들이 주지 못했던 유연성을 보충해 주었다. 이렇게 함으로써 인간이 해야 할 일과 인간이 하지 않아도 될 일들을 구분해 주었다. 인간이 해야 하는 일은 소프트웨어가 불완전성에 의해 오동작 하는 것을 방지하는 일이다. 인간이 하지 않아도 되는 일은 무의미한 반복, 비즈니스 로직과 상관 없는 코드의 갑작스런 출현 등이다.


비 구조적 언어

그렇다면 왜 처음부터 인간이 이해하기에 좋은 언어를 만들어 내지 못했을까?


폰 노이만은 괴델이 불완전성의 원리를 발표했던 장소에 있었다고 한다. 그리고는 "모든게 다 끝장 났다"고 말했다는 일화도 있다. 이 일화는 폰 노이만이 불완전성의 원리를 이해하고 있었다는 의미이다. 하지만 컴퓨터를 만들 생각을 한 걸 보면 그는 역시 기회를 더 많이 본 것이 아닐까 생각한다.


하지만 안타깝게도 그가 설계한 컴퓨터가 이해한 언어, 즉 기계어는 그 이후에 출현한 어셈블리어와 마찬가지로 비 구조적인 언어이다. 비 구조적 언어라는 것은 "구조가 없는 소프트웨어를 만들어 내는 언어", 즉 소프트웨어가 일정한 부분들로 분리될 수 없고 소프트웨어 전체가 하나의 단위로 만들어지는 언어라는 말이다. 현대에 소프트웨어를 공부하는 사람들이 이해하기 쉽게 설명하자면, 최근의 소프트웨어는 적어도 전체 소프트웨어를 다수의 변수와 다수의 함수로 구분시킬 수 있다. 객체지향 언어라면 객체 단위로도 구분지을 수 있다. 비 구조적 언어를 현대 언어로 개발하는 방식에 비유해 보자면 몇만 라인짜리 소프트웨어를 단 하나의 함수에 구현한 것이라고 볼 수 있다.(지금도 이렇게 하는 사람이 많지만...... 이렇게 만든 것이 쓸모 있다면 그를 폰 노이만으로 부르겠다) 지금은 몇 만 라인 코드가 흔하지만 당시에 언어는 기계어나 어셈블리어이다. 만만치 않은 일이었을 것이다. 그것도 함수 하나에다 구현하는 것은 더더욱 그렇다.


이렇게 된 데에는 두가지 원인이 있다. 


하나는 최초 괴델의 증명에 사용되었던 알고리즘의 시초는 구조적인 형태가 아니었다. 그것에 영감을 받아 만들어진 것이 폰 노이만의 컴퓨터이다. 그래서 소프트웨어를 구조적으로 만들어야겠다는 생각을 먼저 하지는 못했을 것이다. 최 우선으로 생각해야 할 것은 괴델이 만든 알고리즘이 동작 가능하도록 하는 것이었다. 그런 상황에서 소프트웨어의 머나먼 미래를 내다 보고 구조적 언어로 기계어를 설계한다는 것은 불가능했다.


두번째 원인은 폰 노이만의 천재성 때문이다. 앞서의 일화가 사실이라면 그에게는 기계어를 다루는 것이 현대의 고급 언어들을 다루는 일보다 쉬운 일이었을 것이다. 그런 그가 알고리즘을 구조적으로 구현할 필요성을 느끼지 못했을 것은 자명하다. 기계어로도 그런 일을 충분히 할 수 있는데 굳이 컴퓨터의 연산 능력을 사용해 가면서 컴파일이라는 비 생산적인 일을 해야 할 이유를 못 느꼈을 것이다. 어쩌면 컴퓨터의 구조는 단순한 것이 좋고, 인간이 이해하고 작성하기 편한 소프트웨어를 만드는 것은 컴퓨터의 연산 능력을 이용하는 편이 좋다고 생각했을지도 모른다. 하지만 아무래도 폰 노이만에게는 그런 생각은 없었을 것 같다.


사실 위의 두가지 원인은 결과를 놓고 유추해 본 것에 불과하다. 폰 노이만은 천재이면서도 인간에 대한 이해가 높은 사람이었다. 컴퓨터가 유용해지고 더 많은 사람들이 컴퓨터를 접하는 시기가 오면 소프트웨어도 보통의 인간들이 다룰 수 있는 수준이 되어야 할 것이라는 생각을 못하지는 않았을 것 같다. 그래서 다른 하나의 원인을 생각해 보자면 인간이 이해하기 쉬운 언어를 만들어 내는 일보다는 다른 일이 더 중요했고 시간이 부족했지 않을까 생각한다. 천재라고 해서 모든 일을 다 할 수는 없는 것이니 말이다.


소프트웨어의 예견된 위기

컴퓨터는 폰 노이만과 같은 천재들에 의해 만들어졌다. 그리고 암호 해독이나 무기 개발 등 다양한 분야에 필요한 계산 기능을 수행함으로써 그 유용성이 증명되었다. 그리고 컴퓨터가 계산한 결과로 만들어진 무기들이 정확하게 작동하는 것을 확인하면서 이제 사람들은 컴퓨터의 연산 능력을 계속 향상시켜서 더 많은 계산을 수행하게 만들어야겠다는 생각을 하게 된다. 폰 노이만 이외에도 수많은 천재 수학자, 물리학자들이 같은 일을 하고 있었지만, 그들 같은 천재는 세상에 그렇게 많지 않다. 


폰 노이만은 당시의 컴퓨터나 계산기들보다 더 빠른 계산을 할 수 있었다고 한다. 하지만 계산을 항상 천재들에게 의존하는 것은 효율적이지 못하다. 우선 인간은, 특히 그런 천재들은 돈만 준다고 해서 일하는 사람들이 아니다. 그들 나름대로의 지적 탐구 방향과 주어진 일이 맞아 떨어져야만 가능한 일이다. 그들도 의지를 가진 인간이기 때문이다. 하지만 컴퓨터에게는 그런류의 불확실성이 없다. 컴퓨터를 이용해서 더 많은 계산을 시키겠다는 것은 합리적인 생각이다. 비록 당대의 천재보다는 못했다 하더라도 기술을 계속 발전시키면 그렇게 되지 못할 것도 없었고, 실제로도 그렇게 되었다.


이처럼 컴퓨터의 유용성에 눈 뜬 과정은 상당히 짧았으나, 그와 함께 탄생했던 소프트웨어 기술의 중요성에 눈을 뜨기 시작한 것은 그보다 한참 후였다. 그 원인을 좀 생각해 보면 다음과 같다.


우선 초기 컴퓨터는 성능이 떨어졌기 때문에 지금처럼 거대한 소프트웨어를 만들 필요가 없었다. 컴퓨터를 이용하는 목적은 일반적인 사람들보다 더 빠른 공학적, 수학적 계산이다. 목적이 명확한 상태에서 성능이 부족한 컴퓨터에 작은 소프트웨어를 개발하는 것이다. 아마도 많은 소프트웨어가 단 한 사람의 개발자의 손에서 개발될 수 있었을 것이다. 이러한 환경에서는 굳이 컴파일러는 만들고 프로그래밍 언어에 대한 이론을 만들 필요가 없었다. 


두번째 이유는 초기 컴퓨터를 개발하고 소프트웨어를 개발하던 사람들이 당대 최고의 지성인들이었다는 점이다. 초창기 소프트웨어 개발자들의 전공은 다른 분야였겠지만 많은 사람들이 수학자나 물리학자, 공학자들이었고, 수학과 같은 논리적인 사고에 익숙했던 사람들이었다. 게다가 당시는 전시이다. 그들 중에서도 뛰어난 사람들만 끌어 모을 수 있었을 만큼 자본은 충분했다. 이런 이들이 모여 만든 소프트웨어다. 장담하건데 당시에는 소프트웨어에 작은 버그 하나만 나와도 수많은 천재들 앞에서 조롱을 당했을 것 같다. 이런 분위기라면 대부분의 소프트웨어가 완벽한 상태였을 것이고, 그런 상황에서 소프트웨어 기술을 개발한다는 것은 생각하기 힘들다.


하지만 시간이 지날수록, 즉 컴퓨터의 성능은 점차 좋아지고, 상업적인 이용 가치가 부각되면서 컴퓨터와 소프트웨어를 연구하는 사람들은 점점 더 많아지고, 이와 더불어 경쟁이 치열해 지면서 시간적 여유는 부족해지게 되었다. 특히 상업적으로 발전하게 된 상황을 보면 소프트웨어가 위기에 빠지게 된 이유를 이해할 수 있을 것이다.


최초의 컴퓨터가 군사적 목적이었다는 점은 자원이 풍부했다는 것을 의미한다. 소프트웨어에서 자원이 의미하는 것은 곧바로 인적 자원을 말한다. 즉 논리적 사고에 특화된 고급 인력들이 소프트웨어를 만들어 왔다. 하지만 컴퓨터와 소프트웨어 개발이 상업화 되면서 금전적 이득을 추구하게 되면서 초창기 우수한 인력들에 비해 낮은 비용으로 채용된 사람들이 그들의 자리에 들어 앉게 되었다. 이것은 그들과 현대의 수많은 소프트웨어 개발자들을 비하하고자 해서 하는 말이 아니다. 전체적으로 보면 소프트웨어 개발자 집단은 예전이나 지금이나 상당한 수준의 창의성과 지성을 갖춘 인재들이다. 하지만 상업적인 목적을 위해서 어느 정도 규모 이상의 인력을 확충하다 보면 아무래도 군사적 목적으로 끌어들인 최고의 인력들과 같은 수준의 비용을 지불하기 어렵고, 그러면 그렇게 뛰어난 사람을 모두 잡기는 어려웠을 것이다. 이 상황에서 나름대로 선발 기준을 잘 정립한다 해도 개중에는 질이 떨어지는 사람들이 끼어 들어오는 것을 막기가 힘들다. 그리고 그때나 지금이나 마찬가지로 소프트웨어는 개개인의 생산성을 측정하기가 상당히 어려운 분야이다. 대다수의 사람들이 창의성과 지적인 능력을 가지고 성실하게 일을 했다고 해도 문제의 대다수는 소수 인원의 무지에 의해서 발생한다. 그리고 이것이 상업적 목적, 즉 이윤 추구와 만나면서 개발 기간이 자주 축소되고, 요구사항이 자주 변경되며, 개개인의 사정에 의해 개발 인력이 자주 바뀌는 상황과 맞물리게 되면 아무리 뛰어난 인력이라고 해도 고전하는 것은 당연하다.


이렇게 고전할만한 요소들이 갖춰지자 이전에는 간과되었던 부분들이 중요한 부분으로 떠오르게 되었다. 소프트웨어도 하드웨어 개발에 준하는 개발 프로세스가 필요하다는 것, 소위 말하는 '통짜' 프로그램을 개발하는 것이 아니라 구조적인 개발, 즉 설계가 필요하다는 것, 그리고 설계의 의도를 반영할 수 있고, 이전의 비 구조적 언어의 관행을 돌아가지 않게 해 줄 단단한 문법을 갖춘 언어가 필요하다는 것이다.

Posted by 이세영2
,

enum의 활용법

4.JAVA 2016. 8. 13. 18:34

C언어에서 enum은 단순히 상수형 변수 역할에 지나지 않았다. 하지만 Java에서는 매우 다른 특성들을 지니고 있다. 이 특성들 중에는 특별한 것들도 있어서 기존과는 다른 여러 방식으로 enum을 활용할 수 있다.


먼저 enum의 실제 타입부터 알아보자.


enum의 실제 타입

enum Type{ // abstract class

    ADD,    // public static final class ADD extends Type{}

    SUB;    // public static final class SUB extends Type{}

}


이처럼 기본적으로 enum은 추상 클래스이다. 그리고 그 하위에 선언된 각 열거형 변수는 실제로는 변수가 아니고 enum의 타입을 상속 받은 하위 클래스이다. 이 하위 클래스는 외부에 노출되어 있고 생성할 필요가 없으며 런타임에 교체가 불가능하므로 public static final 타입을 갖는다.




enum은 기본적으로 추상 클래스이기는 하나 다른 클래스로부터 상속을 받지는 못한다. 하지만 interface는 상속을 받을 수 있다. 따라서 다음과 같은 형태로 구현이 가능하다.


enum에 인터페이스 상속 받기

interface Interface{

    public void api();

}

enum Type implements Interface{

    ADD{

        public void api(){ System.out.println("ADD api"); }

    },

    SUB{

        public void api(){ System.out.println("SUB api"); }

    };

}


이 인터페이스 상속 방법은 생각보다 강력하다. 이 특성을 이용해서 디자인 패턴에 나오는 수많은 패턴들을 enum을 통해 구현할 수 있다. enum이 public static final 이라는 점은 Singleton과 유사한 특성을 지니고 있다. 따라서 중복 생성이 안되면서도 일반 클래스 기능을 할 수도 있고, 필요한 때에는 enum의 특성을 활용할 수도 있다. 이 강력한 특성 때문에 Singleton 패턴, Abstract Factory 패턴, Factory Method 패턴, State 패턴, Strategy 패턴, Visitor 등 다양한 패턴의 enum 버전이 있다. 이들에 대해서는 디자인 패턴 항목에서 다룰 예정이다.




추상 클래스라면 함수를 선언할 수도 있어야 한다. 아래와 같이 함수 선언이 가능하다.


enum에서 함수 선언하기

enum Type{

    ADD,

    SUB;

    public void api(){ System.out.println("api()"); }

}




추상 클래스이기 때문에 추상 메소드의 선언도 가능하다. interface를 상속 받을 수 있다는 점에서도 이 점은 유추해 낼 수 있다.


enum에서 추상 메소드 선언하기

enum Type{

    ADD{

        public void api(){ System.out.println("ADD api"); }

    },

    SUB{

        public void api(){ System.out.println("SUB api"); }

    };

    abstract public void api();

}




같은 맥락에서 static 메소드 역시 가능하다. 실용적인 예제를 위해서 하위 타입의 개수를 알아내는 함수로 해보자.


enum에서 static 메소드 선언하기(하위 타입의 개수 알아내기)

public static int size(){ return values().length; }


보통은 Type.values().length를 통해 꺼내오기도 하지만 가독성 면에서 Type.size()를 호출하는 것이 더 나아 보인다.




클래스라면 생성자도 선언할 수 있어야 할 것이다. 생성자 선언은 아래와 같다.


enum에서 생성자 선언하기(enum 클래스를 인덱스로 활용하기)

enum Type{

    ADD(0),// 생성자에게 필요한 인자는 () 안에 넣는다.

    SUB(1); //

    int value;

    private Type(int value){ this.value = value; }   // 이것이 생성자.

    public int value(){ return value; }

}


역시 활용성을 위해서 enum 클래스를 인덱스로 활용할 수 있도록 value 라는 int 형 값을 생성시에 인자로 받도록 했다. Java의 enum은 클래스이기 때문에 C언어에서 처럼 값으로 활용할 수가 없다. 대신에 이처럼 생성자를 통해 인자로 받은 값을 가지고 있다가 값이 필요한 경우 value() 함수를 호출함으로써 값으로도 사용이 가능하다. 이 활용법은 "Effective Java" 라는 책에 수록된 내용이다. 생성자가 private인 것에 주목해야 한다. enum은 외부에서 생성이 불가능하기 때문에 생성자는 항상 private으로 선언해 주어야 한다.




다음은 문자열로 enum을 알아내는 함수이다. enum이 클래스라는 것은 이미 이야기 하였다. 따라서 하위 타입들도 모두 toString() 함수를 가지고 있는데, 그 결과 값은 기본적으로 자신의 선언된 이름과 같다. ADD.toString()은 "ADD" 값이 결과값이다. 이러한 특성은 매우 유용한데, 특히 데이터베이스에 저장할 때 그렇다. DB의 가독성 측면에서 문자열을 활용한 경우에 문자열을 입력받아 타입을 리턴하도록 하면 코드 상에서 문자열 대신 enum을 활용할 수 있으므로 코딩이 편리해진다.


enum 에서 문자열로 enum의 타입을 알아내기

public static Type getTypeByString(String str){

    for(Type each : values()){

        if(each.toString().equals(str)) return each;

    }

    return null;

}



enum 타입이 제공하는 기본 함수로 enum의 순서를 알 수 있는 함수가 있다.


public int ordinal();


위 ordinal() 이라는 함수인데 이 함수는 선언된 enum의 하위 타입이 몇 번째 순서로 선언되었는지를 알 수 있다.(순서의 시작 값은 0이다.) 가령 위에서 선언한 ADD 타입의 경우 0이 리턴되고, SUB의 경우 1이 리턴된다. 이 특성을 이용하면 enum을 인덱스로도 활용이 가능하다.



Java에서의 enum은 열거형의 특성과 클래스의 특성을 함께 가지고 있다는 장점이 있다. toString() 함수를 가지고 있다는 것만으로도 디버깅을 얼마나 쉽게 만들어 주는지 모른다. 그 밖에도 데이터 베이스와의 연동, switch-case 문에 대한 활용, 인터페이스 상속을 활용한 디자인 패턴 등 다양한 곳에 활용할 수 있다.

Posted by 이세영2
,

Telescoping Parameter 패턴은 "켄트 벡의 구현 패턴"에도 언급되었던 패턴이다.

기본적으로 이 패턴은 다수의 매개 변수를 가진 함수의 문제점을 해결하기 위한 패턴이다.


보통 생성자가 다수의 매개 변수 개수를 달리 하면서 생성이 가능한 경우에 주로 사용된다.


실제로도 많이 쓰이는 패턴이라서 Java 라이브러리의 ServerSocket 함수를 가지고 설명을 해볼까 한다.


우선 API를 기준으로 보면 Telescoping Parameter 패턴의 외형은 다음과 같다.


public ServerSocket(int port);

public ServerSocket(int port, int backlog);

public ServerSocket(int port, int backlog, InetAddress bindAddr); 


이처럼 매개 변수가 여럿이고 매개변수의 기본 값이 있는 경우에 인자 개수가 다른 API를 제공해 준다. 이렇게 하면 사용하는 입장에서는 필요에 따라 짧거나 긴 매개 변수를 가진 API를 호출할 수 있다. 이 모양이 마치 망원경을 접었다 폈다 하는 모양과 비슷하다고 해서 붙여진 이름이다.



내부 구현 시 고려 사항

이 패턴에 대해서는 다음과 같은 사항을 잘 생각해 봐야 한다. 이는 내부 구현에 있어서 지켜야 할 중요한 부분이다.


저 함수들은 모두 backlog 변수나 bindAddr 변수에 대한 기본 값이 있다는 전제 하에서 작성되었다. 따라서 결과적으로는 세 함수 모두 세 개의 파라메터 모두에 대한 설정을 하게 될 것이다.


이런 상황에서 매개 변수로 하려는 일은 동일할텐데 이를 각 함수에 구현하면 중복 구현의 문제가 발생한다. 따라서 아래와 같은 형태로 내부를 구현해 주어야 한다.


public ServerSocket(int port){

    this(port, 50, null);

}


public ServerSocket(int port, int backlog){

    this(port, backlog, null);

}


/* 결국 어떤 함수를 사용해도 아래 함수가 호출되게 된다 */

public ServerSocket(int port, int backlog, InetAddress bindAddr){

    setImpl();

    bind(new InetSocketAddress(bindAddr, port), backlog);

} 


실제 코드보다는 좀 더 간단하게 변경하였다. 매개 변수가 한 개인 경우 남은 두개의 기본 값을 채워 3개짜리 함수를 호출한다. 두 개짜리고 마찬가지로 3개짜리 함수를 호출하는 것으로 할 일을 마친다. 3개짜리 함수만 실제 필요한 동작을 수행하게 된다.



코드 중복의 방지

객체의 외부에서 객체로 변수 값을 직접 전달하는 기본 방식은 setter를 이용하는 것이다. 하지만 종종 생성자를 이용하여 변수를 초기에 셋팅하게 하는 것이 좋을 때가 있다. 이렇게 되면 생성자와 setter에 의해 변수가 셋팅되게 된다. 변수 셋팅은 아주 중요한 작업이다. 이 경우 생성자 내부에서는 setter를 호출해 주는 것이 좋다.


안티 패턴

class Example {

    int data;   

   

    public void Example(int data){

        this.data = data;

    }

   

    public void setData(int data){ this.data = data; }

}


이렇게 구현했을 경우 setData 내부에서 data가 새로 설정된 이후 동작을 변경해버리면 생성자를 통해 data를 변경했을 때에는 이것이 반영되지 않는다. 따라서 아래와 같이 구현하는 것이 좋다.


좋은 구현 방식

class Example {

    int data;   

   

    public void Example(int data){

        setData(data); // 이렇게 해야 setData() 함수 내부의 변경에 안전하다.

    }

   

    public void setData(int data){ this.data = data; }

}



이 문제에 대한 고려가 Telescoping Parameter 패턴의 구현에도 고스란히 반영되어 있다.


단순히 자꾸 함수를 호출하려는 것으로만 보일지 모르겠지만 실제로는 모든 함수들은 모든 매개 변수가 설정되는 것을 기대하고 있다. 따라서 매개 변수로 내부 변수를 설정하는 코드(예제에서는 bind() 함수를 호출하는 부분)는 한 곳으로 몰아 주기 위해 this 함수를 호출하는 것이다. 


Telecoping Parameter 패턴을 구현할 때에는 이 점을 꼭 염두해 두어야 한다.

'5.디자인패턴' 카테고리의 다른 글

Pluggable Selector 패턴  (1) 2016.08.21
State 패턴  (0) 2016.08.15
Strategy 패턴  (0) 2016.08.15
interface -abstract class - concrete class 패턴(인터페이스 구현 중복 해결 패턴)  (2) 2016.08.10
Enum Factory Method 패턴  (0) 2016.08.07
Posted by 이세영2
,

이 글을 통해 알리려고 하는 내용은 사실 객체지향에 국한된 얘기는 아니다. 그래서 우선 시작은 구조적 언어(C언어)를 중심으로 이야기 하려고 한다. 하지만 객체지향에서 이 부분이 더욱 강화되고 원칙화 된 것이므로 당연히 객체지향으로의 확장도 다루도록 하겠다.


속성과 행위

결과물은 객체지향 쪽으로 흘러갈테니 용어도 객체지향으로 시작하겠다. 속성이란 기존 구조적 언어에서는 변수라고 부르는 것이고, 행위란 함수라고 부르는 것이다. 변수는 데이터를 저장할 수 있는 것, 변경되는 것이며 함수는 제어, 연산을 수행하는 것, 변경할 수 없는 것으로 이해한다. 일반적으로는 맞는 이야기이지만 이 이해에 매몰되면 좋은 소프트웨어를 만들기 어려운 것이 사실이다.


속성을 통한 행위의 변경

행위(함수)는 변경될 수 없다. 이것은 사실이다. 이미 코드 상에서 행위를 정의한 이후에 이것을 런타임에 변경하는 것은 불가능하다. 하지만 상황에 따라서 다른 행위를 해야 하는 경우는 비일비재 하다. 그래서 이러한 경우에 보통 값에 대한 변경이 가능한 변수를 활용한다. 지금 상황(state)에서는 덧셈을 해야 한다면 상황을 표현하는 변수를 만들고, 이 변수 값(ADD_STATE)을 통해서 덧셈을 하도록 행위를 유도한다. 반대로 뺄셈을 해야하는 상황이라면 변수 값(SUB_STATE)을 변경해서 뺄셈을 하도록 유도한다. 이것은 속성과 행위의 특성을 보면 당연하다. 변경이 가능한 것(변수)을 이용해서 변경이 불가능한 것(행위)을 변경하는 것이다. 그래서 프로그래머들은 상황을 알려 주기 위해서 변수를 선언하고 이를 상태(state)라고 보통 부르고(flag를 사용하기도 한다), 행위의 변경은 이 상태 변수가 어떤 값을 가지는지에 따라 조건문을 작성하고 그에 종속된 행위들을 나열한다.


말로 설명하는 것보다 코드를 보는 편이 더 빠르겠다. 자 다음과 같은 코드를 한번 보자. 일단 이 문제는 구조적 언어로부터 출발해 보도록 하겠다.


상태에 따른 동작 변경

#define ADD_STATE (0)

#define SUB_STATE (1)

int calculate(int state, int a, int b){

    if(state == ADD_STATE){

        return a + b;

    }

    else if(state == SUB_STATE){

        return a - b;

    }

    return 0;

}

int main(){

    int result = 0;

    result = calculate(ADD_STATE, 5, 10);

    result = calculate(SUB_STATE, 5, 10);

}


위의 예제는 state 변수의 값에 따라서 덧셈과 뺄셈을 조건적으로 수행하는 코드이다. 위 코드를 요구사항 관점에서 기술해 보면 다음과 같다. "상태가 덧셈 상태이면 결과에 덧셈 결과를 저장하고, 상태가 뺄셈 상태이면 결과에 뺄셈을 저장하라." "상황에 따라서 다른 동작을 수행해야 한다"는 관점에서 살펴 봤을 때 위의 코드에 어떤 문제점이 있겠는가? state 변수의 값을 if문으로 살펴보는 것은 상황에 따라 다르게 동작해야 한다는 조건을 만족시키는 일이므로 전혀 이상하지 않을 만한 코드이다. 그리고 셀 수 없이 많은 프로그래머들이 이런 형태로 프로그래밍을 하고 있다. 어찌보면 상황에 따라서 다른 동작을 하라는 요구사항을 만족시키는 방법은 위의 코드 밖에 없어 보인다.


자 문제에 대한 관점을 조금씩 바꿔 나가기 위해서 다음과 같이 코드를 수정해 보도록 하겠다.


int add(int a, int b) { return a + b; }

int sub(int a, int b) { return a - b; }


먼저 위와 같이 덧셈과 뺄셈을 수행하는 코드를 별도의 함수로 독립 시킨 다음, 조건문이 있는 곳을 다음과 같이 수정한다.


     if(state == ADD_STATE) result = add(5, 10);

else if(state == SUB_STATE) result = sub(5, 10);


논리적인 결과는 첫번째 소스와 전혀 차이가 없다. 단지 덧셈 구문과 뺄셈 구문을 함수로 독립시켰을 뿐이니까.


그런 다음 한가지 생각을 떠올려 보자. state가 변수로서 하는 일은 오직 add()와 sub()함수를 바꿔서 실행 시킬 수 있도록 하는 것이다. 그렇다면 state 변수 대신 add() 함수와 sub 함수를 바꿔 넣어도 되지 않을까?


행위에 대한 직접 변경

int add(int a, int b) { return a + b; }

int sub(int a, int b) { return a - b; }

int calculate(int (*func)(int, int), int a, int b){

    return func(a, b);

}

int main(){

    int result = 0;

    result = calculate(add, 5, 10);

    result = calculate(sub, 5, 10);

}


생각하는 방식이 바뀐 것을 간단히 정리 해 보면 다음과 같다.


기존 : 상태 변수를 선언하고, 상태 변수에 상태 값을 할당하고, 상태 값을 체크하고 상태 값에 따라 덧셈과 뺄셈을 바꿔 계산한다.

변경 : 하고 싶은 행위에 따라 덧셈과 뺄셈을 하는 함수를 집어 넣는다.


간접적으로 행위를 변경하던 것을 직접 행위를 변경하는 것으로 바꾼 것이다. 이것이 일면 별 것 아닌 것처럼 보이지만 효과는 상당하다. 효과를 정리해 보면 다음과 같다.


1. 조건문이 사라졌다. 부수적으로 코드가 단순해졌다.

2. 이해하기가 쉬워졌다. 계산이 덧셈 뺄셈만이 아니라 곱셈 나눗셈 등 온갖 계산들이 나열되었다면 저 한가지 변화의 크기를 직감할 수 있을 것이다.

3. calculate 함수가 조건 체크에 실패 했을 때 return 0을 수행하던 것이 사라졌다. 이는 의도하지 않은 결과로 소프트웨어가 계속 동작하는 것을 방지해 준다.


생각보다 강력한 결과

이 방법은 상태 변수를 사용하는 모든 경우에 적용이 가능하다. 그 말은 상태 대신 행위를 직접 바꾸면 모든 상태 변수를 없앨 수 있고, 모든 조건문을 없앨 수 있다는 말이다. 이 결과로 유추해 보면 100 라인의 코드든 1000라인의 코드든 이론적으로는 조건문 없이 구현할 수 있다는 얘기다. 실제로 몇 달 전에 작성한 차트 그리기 프로그램(자바)은 5000라인(공백 제외)이었는데 조건문을 조사해 보니 60개 정도였다. 약 100라인에 if 문 혹은 switch 문이 한 개 있는 비율이다. 이것이 상태 변수를 없애고 행위를 직접 변경한 결과이다.


<바 차트, 라인 차트, 레전드 위치 변경, 레이블, 폰트 변경, 바 컬러 변경, 값에 따른 바 컬러 변경, 차트 흐름 고정, 테마 변경 등에 이르는 수많은 옵션들을 지원하도록 만들어졌지만 조건문은 단 60개 뿐이다>


상태에 종속적인 행위 변경

나는 상태를 선언하고, 상태에 대해 조건문을 작성하고 조건에 따른 행위를 수행하는 과정을 "상태 종속적 행위 변경"이라고 부른다. 지금도 많은 프로그래머들이 이러한 안티 패턴을 통해서 소프트웨어를 만들고 있다. 상태 변수를 선언하고, 상태에 따른 조건문을 작성하고 조건에 따라 다른 코드들을 집어 넣는다면 모두 "상태에 종속적인 행위 변경"이라는 안티 패턴을 사용하고 있는 것이다.


객체 지향에서의 행위 변경

객체지향 언어에는 상태 종속적 행위 변경 문제를 해결하는 디자인 패턴이 이미 정의 되어 있다. 전략 패턴과 상태 패턴이 바로 그것이다. 그리고 null 객체 패턴 + Special Case 패턴처럼 조금 덜 유명한 디자인 패턴도 상태 문제를 행위 문제로 바꿔주는 패턴들이다. 

전략 패턴에서는 A라는 객체가 B라는 객체에게 일을 시킬 때 일부 변경하고자 하는 행위가 있다면 이 행위를 객체화 하여 B에게 인자로 넘겨 준다. 이러한 방식으로 행위를 상태 변수 없이 변경한다. 맨 처음 예제가 사실 전략 패턴의 C언어 버전이라고 할 수 있다.


전략 패턴 예제

interface IFunction{

    public int calculate(int a, int b);

}

class Add implements IFunction{

    public int calculate(int a, int b){

        return a + b;

    }

}

class Sub implements IFunction{

    public int calculate(int a, int b){

        return a - b;

    }

}

class Calculator{

    public int calculate(int a, int b, IFunction function){

        return function.calculate(a, b);

    }

} 

< 맨 처음 예제를 Java로 전략 패턴 형태로 바꾸어 본 것이다. 함수 포인터 대신 객체를 넣는다는 점 만 다를 뿐이다>


상태 패턴은 상태가 필요한 객체가 각 상태를 객체화시킨다. 그리고 필요한 상태에 따라 상태 객체를 변경시킨다. 그러면 상태 객체의 메소드를 호출할 때마다 상태에 따라 행위가 바뀌게 된다.

null 객체 패턴은 인자로 넘어온 객체가 null인지 체크(null 상태인지 아닌지를 체크하는 것이다) 하는 대신 아무 동작도 하지 않는 Null 객체를 만들어 넘긴다. 그려면 객체를 받는 쪽에서는 넘어온 객체에 대해 null 인지를 체크하지 않아도 된다. 실행해도 아무 동작도 하지 않을 뿐이다.


객체지향 언어는 무 조건문/무 상태 언어이다

객체지향 언어를 사용하면 switch 문의 사용이 줄어든다는 얘기가 있다. switch - case 문에서 체크  되는 변수 대신 상위 클래스에 대한 레퍼런스를 선언하고, 각각 실행되는 문장들을 개별 객체화하여 레퍼런스에 할당해주면 switch-case문이 없어지기 때문이다.(이것이 디자인 패턴에서 말하는 상태 패턴이다) 여기서 조금 더 나가보면 이런 상태 변수들은 모두 객체 레퍼런스로 대체될 수 있고, 변수에 의해 바뀌는 행위, 즉 조건문 안의 행위들은 모두 개별 객체로 대체될 수 있다. 그러면 조건문들이 모두 사라진다.


객체지향을 이야기 할 때 다형성은 빼 놓고 이야기 할 수 있는 특징이다. 다형성은 여러 객체가 있지만 외부적 관점(즉 "타입" 관점)에서는 동일한 객체로 취급하겠다는 것을 의미한다. 동일하다는 것은 구분하지 않는다는 것이고, 구분하지 않는다는 것은 조건을 체크할 필요가 없다는 것이다. 그것은 상태에 따른 조건문 체크가 없는 것과 같다.


좀 더 나가보면 근본적으로 객체지향이 추구하는 방향은 상태를 보지 않겠다는 것이다. 오직 상대를 행위 대상으로만 바라본다. 인터페이스에는 변수가 없다. 즉 상태를 보지 않아도 된다는 의미다. 어느 객체가 동작 불가능한 상태에 있다고 가정하자. 이것을 외부에 알려줄 필요가 있을까? 객체가 동작 불가능한 상태라고 판단하면 그 객체는 실제 동작들을 객체화 해 두었다가 동작 객체를 동작 불가능한 객체로 바꾸기만 하면 된다. 그러면 외부에 자기 상태를 알려줄 필요가 없다.


객체와 객체 간에는 공유하는 변수가 없다. 상태는 변수의 부분집합이므로 상태도 공유되지 않는다는 의미이다. 인터페이스가 보여주는 것처럼 다른 객체의 상태를 몰라도 사용할 수 있다. 외부적 관점에서 상대방 객체는 상태가 없다. 그러면 조건문 없이 사용할 수 있다. 내부적으로는 상태를 객체로 대체한다. 그러면 상태 없이 구현될 수 있다. 그러면 조건문 없이 사용될 수 있다.


따라서 객체지향 언어는 무 조건문 / 무 상태 언어이다.


(구조적 언어도 함수 포인터라는 무 상태 언어적 특성을 가지고 있다. 하지만 이러한 관점이 널리 보급되지는 못했다. 아무래도 객체지향 언어에서 처럼 상태 제거를 위한 함수 포인터를 다중 중첩해서 사용하기가 어려운 문제가 있었을 것이다. 함수 포인터를 많이 사용한 코드는 사실 이해하기가 어렵다. 함수 포인터는 근본적으로 변수이고, 변수적인 타입만 체크 할 뿐 객체지향 언어의 "타입"처럼 특별한 제약이 없기 때문에 문제가 발생할 가능성이 많다.)


가장 단순한 적용 방법

우선 변수를 선언할 때 생각을 해보자. 변수가 상태를 나타낼 것이고, 조건문에 사용될 것인가? 그렇다면 그 변수는 필요 없다. 객체로 대체하라.


조건문을 만들기 시작했다. 조건문에 사용되는 변수가 하나 인가? 그러면 그 변수는 필요 없다. 객체로 대체하라.

조건문에 들어가는 변수가 여러개인가? 여러 변수를 하나의 변수로 바꿔라.


이 과정을 따르면 반복문을 제외하고는 단 한 줄의 코드도 들여쓰여지지 않은 코드가 만들어질 것이다.

Posted by 이세영2
,

객체지향은 참 어렵다.

이제 객체지향을 그래도 좀 이해하고 프로그래밍을 한다는 생각을 하고 있지만 객체지향 언어로 개발을 해 오면서도 그 개념을 제대로 이해하지 못하고 지낸 시간이 훨씬 더 길다는 점을 생각해보면 지금의 이해한 것을 다른 사람들에게 잘 설명하는 것이 얼마나 힘든 일일지 상상이 간다.


개인적인 경험으로 보면, 객체지향 언어를 배우고 잘 사용하게 될 때까지의 난관을 보면 대략 다음과 같다. 


처음 : 개념 및 문법에 대한 이해

처음에는 객체지향 언어 문법을 열심히 공부하게 된다. 객체지향 언어의 난감한 점은 우리가 풀어 내려고 하는 문제에 대한 직접적인 구현(변수 선언, 함수 선언, 조건문, 반복문, 연산자 등)을 이해하는데 들어가는 노력에 비해 객체지향 개념(클래스, 객체, 캡슐화, 상속, 다형성?, 재정의, 인터페이스, 추상 클래스 등) 자체를 이해하는 것이 훨씬 더 어렵다는 점이다. 어려운 이유는 간단하다. 객체지향 개념이 필요한 이유를 문법을 공부하는 당시까지는 전혀 알 수 없기 때문이다. 하지만 이 단계를 일단 거치고 나면 이제 마치 객체지향을 제대로 다룰 수 있겠다는 생각이 든다.


난관 : 잘 짜여진 객체지향 코드를 접했을 때

이제 어떻게든 클래스와 객체도 알고, 돌아가는 프로그램을 작성할 줄도 알게 된다. 그래서 개발에도 속도가 좀 붙고, 기능을 이렇게 저렇게 만들어 가게 된다. 그러다가 어느 순간 오픈 소스 혹은 잘 짜여진 라이브러리들을 접하고 충격을 받게 된다. 처음에는 너무 난잡해 보여서 이해가 가질 않는다. 어떤 기능이 어떻게 동작하는지를 보려면 이 클래스, 저 클래스 파일을 넘나 들어야 하고, 굳이 상속을 왜 받아서 하위 클래스를 이해하는데 상위 클래스로 이동해야 하고, 상속을 넘어서 인터페이스로 넘어 가면 구현부가 없는 것을 보고 당황해 해야 한다. 온갖 기교를 써가면서 어떻게든 여러 클래스를 같은 클래스 혹은 인터페이스에 대한 하위 클래스로 만들기 위해 노력하는 이유를 대체 알 수가 없다.


깨닫기 전 : 이제 잘하고 있다고 생각했는데

이제 상속을 통한 기능의 확장에 대해서도 이해하고 나름 중복 코드를 보고 줄일 줄 알게 되고, SRP 쯤은 알아서 기능도 잘 구분해서 구현할 줄도 알게 되었다. 하지만 그래도 누군가 와서 내가 만든 코드를 보고 이렇게 이야기 한다. 객체지향의 핵심은 다형성이다, 인터페이스를 중심으로 설계하라, 상속을 통해 기능을 확장하지 마라 등등.



개인의 능력에 따라서는 이 과정이 생각보다 짧았을 수도 있을 것이다. 하지만 개인적인 경험에 비춰 보자면 앞으로 이야기 할 내용을 미리 알고 있었을 정도가 되었다면 객체지향을 접한지 최소 5년은 되었다고 봐야 한다. 그보다 더 오래되었어도 이해하지 못하는 경우도 있다.(좋은 개발 선배가 있었다면 모르지만.) 혹시 아래 내용을 보고 "겨우 그거 이야기 하려고 이런거야?" 라고 하신다면 내 능력이 부족한 것이니 할 말은 없다. 아까운 시간을 빼앗아 죄송스럽다는 말을 할 수 밖에.



이제 본론으로 넘어가서 그러면 과연 상속이란 무엇인가? 우선 위키를 찾아보면 다음과 같다.


1. 객체 지향 프로그래밍(OOP)에서, 상속은 객체들 간의 관계를 구축하는 방법이다. 

2. ......클래스는 기존의 클래스로부터 속성과 동작을 상속받을 수 있다.

3. 그 결과로 생기는 클래스를 파생 클래스, 서브클래스, 또는 자식 클래스라고 한다. 상속을 통한 클래스들의 관계는 계층을 형성한다.


사실 이 내용 중에는 이후에 이야기 할 내용에 딱 맞는 내용이 없다. 하지만 살펴볼 가치는 있다.


우선 2번이 적절한 정의로서 확 와 닿을 것이다. 일반적으로 상속이라는 의미는 단어가 가진 의미와 마찬가지로 무엇인가를 "물려 받는다"는 의미이기 때문이다. 즉, 상위 클래스가 가진 속성(변수)과 행위(멤버 함수)를 내려 받는 것을 상속이라고 보통 말한다. 이것이 이해하기 쉬운 이유는 상속이라는 단어의 실생활 속에서의 의미와 가장 유사한 개념이고, 프로그래밍을 할 때 변수와 함수를 물려 받아 재 구현해야 하는 번거로움을 없애준다는 점에서 실질적인 이득을 주기 때문이다.


아주 마음에 들지는 않지만 1번과 3번이 그나마 좀 더 객체지향을 이해하는데 도움이 되는 정의이긴 하다.


자 그러면 저 정의들이 명시적으로 보여주지 못한 가장 중요한 부분은 무엇일까?


잘 이해가 안 갈 수도 있기 때문에 질문을 살짝 바꿔서, 상속을 받았을 때, 상속 받은 것 중에서 가장 중요한 것은 무엇인가?











정답은 "타입"이다.



사실 2번의 정의는 핵심을 빠뜨렸다. 클래스는 기존 클래스로부터 가장 중요한 것인 "타입"을 물려 받고, 속성이나 동작 "따위"의 소소한 것을 물려 받는다가 정확한 정의다.


객체지향에서 객체를 바라 볼 때 가장 중요한 것은 "외부적 관점"과 "내부적 관점"의 분리이다. 익히 알고 있는 속성과 행위가 아니다. 내부적 관점이라는 것은 객체의 동작을 어떻게 구현할 것인가 하는 점이다. 이 때 보이는 것이 속성과 행위이다. 이것을 잘 구현해야 하는 것은 당연한 것이다. 하지만 이것보다 중요한 것이 외부적 관점이다. 외부적 관점은 "그 객체를 어떤 객체로 인식할 것이며, 어떻게 다룰 것인가?" 이다.


만약 어떤 객체가 실제로는 Rectangle이라고 해도 (Shape을 상속 받았다 치고) 외부에서 인식하기를 Shape이라면 그것은 외부적 관점에서는 Shape이다. 그 실체가 Rectangle이라는 사실은 그 객체 내부에서나 중요한 것이지 밖에서 그 객체를 사용하는 입장에서는 아무런 의미가 없는 것이다. 그 객체는 그냥 Shape일 뿐이다. 오케스트라의 지휘자는 자신의 연주자들이 자신의 지휘에 맞춰 연주할 수 있느냐에 관심이 있지 그 사람이 바이올린 연주자인지 첼로 연주자인지는 관심을 가질 필요가 없다. 그저 지휘만 하면 알아서 연주를 할 것이고, 그것이면 충분하다.


그렇다면 외부적 관점, 즉 "타입"이 왜 그렇게 중요한지 간단한 예제를 통해 살펴보자.


List<Rectangle> rectangles = new ArrayList<Rectangle>();

rectangles.add(new Rectangle(1,2,3,4));

rectangles.forEach(each -> System.out.println(each));

List<Color> colors = new ArrayList<Color>();

colors.add(new Color(255,0,100));

colors.forEach(each -> System.out.println(each));


크게 이해하는데 어려움은 없을 것으로 생각된다. 두 개의 List를 만들어서 각각 Rectangle과 Color를 저장할 수 있도록 하고, 객체 하나씩을 만들어 넣은 후 이를 for 문을 이용해서 출력하는 구문이다. 반복되는 느낌은 있지만 어찌하겠는가? Rectangle과 Color는 엄연히 "타입"이 다른데!


그러면 다음을 보자.


List<Object> objects = new ArrayList<Object>();

objects.add(new Rectangle(1,2,3,4));

objects.add(new Color(255,0,100));

objects.forEach(each -> System.out.println(each));


예상했겠지만 둘의 출력 결과는 같다. 이것이 왜 가능한지도 이미 알고 있을 것이다. Java의 모든 객체는 Object 클래스를 상속 받았기 때문이다. 즉 Object "타입"이다. 코드 상에서, 즉 객체의 외부에서 (실제 객체는 서로 다르더라도) Object 타입으로 불려지게 되는 순간, 둘의 차이는 사라진다. 그리고 그와 함께 중복된 코드들도 함께 사라진다. 만약 서로 다른 종류의 객체가 훨씬 더 많았더라면 더욱 많은 코드들이 마법처럼 사라졌을 것이다. 마치 오케스트라 지휘자가 바이올린 연주하세요, 첼로 연주하세요, 플룻 연주하세요......와 같이 끝나지 않을 것 같은 XXX 연주하세요를 반복하지 않는 것과 같다. 부조리함이 많은 현실 세계에서도 이런 합리성을 추구하는데 매일 논리와 싸우는 우리 같은 사람들이야 어떠하겠는가?


간단한 예제였지만 상속의 진정한 가치와 의미를 이해하는데는 어려움이 없었을 것으로 생각된다. 상속 개념에는 분명 속성과 행위를 내려 받아 중복된 코드를 작성하는 것을 방지해 준다는 이점도 있다. 하지만 상속이 진정한 위력을 발휘 하는 것은 이러한 내부적 관점이 아니라 "타입" 상속이라는 외부적 관점이다. 이 관점이 위력적인 이유는 아무리 실질적으로는 다른 객체들이라도 동일하게 취급할 수 있고, 동일하게 취급되는 순간 유사하긴 하지만 반복적으로 구현되었던 수많은 중복 코드들을 사라지게 만들 수 있다는 점이다. 이러한 관점에서 보면 속성이나 행위 따위를 상속 받는 것은 매우 소소한 것으로 취급될 수 밖에 없다.


그래서 한 번 쯤 더 생각해 볼만한 부분이 바로 인터페이스와 추상 클래스의 개념이다. 인터페이스는 속성과 실제 구현부라는 실체는 눈 씻고 찾아봐도 없고 눈으로 보기에는 빈 껍데기에 불과한 API만을 제공한다. 하지만 이 빈껍데기는 "외부적 관점"을 제공해 주고, 이 관점을 이용하는 외부에서 그 객체를 다루는데는 전혀 어려움이 없다. 따라서 같은 인터페이스를 상속 받은 객체들에 대해서는 같은 관점을 유지할 수 있고, 같은 관점을 유지할 수 있다는 것은 동일하게 취급할 수 있다는 것이고, 이들 객체에 대해서 별도의 코드들을 작성할 필요가 없다는 것이다. 추상 클래스의 경우 인터페이스에 비해 조금 더 많은 것을 주긴 하지만 물려 받은 것에 비해 역시 더 중요한 것은 추상 클래스의 "타입"이다. 이 "타입" 이라는 외부적인 관점을 이용해서 인터페이스와 같은 위력을 발휘할 수 있다. 덤으로 내부적 관점에서의 중복 코드도 줄일 수 있다는 장점과 함께.


타입이 같다는 것이 주는 또 하나의 장점은 적절한 시기에 기존의 객체 대신 실질적으로는 다른 객체로 대체시킬 수 있다는 점이다. 이것은 마치 컴퓨터라는 하드웨어에 내부 소프트웨어만 바꿔 실행하면 여러 기능을 하는 범용 컴퓨터와 유사한 개념이다. 이것은 구조적으로 동일하게 구현된 코드 상에서 서로 다른 로직을 구현한 객체들을 바꾸어 가면서 사용함으로써 소스 코드의 양을 줄이면서도 다양한 기능을 수행하게 만들 수 있다는 말이다. 또한 기능의 확장을 위해서 다른 코드를 수정하지 않고 동일한 "타입"의 새로운 객체 하나만 만들어 넣으면 된다. 이것은 수정이나 기능의 확장을 위해서도 도움이 된다는 말이다. 


만약 상속을 통해 외부적 관점의 통일성, 즉 "타입"을 상속 받지 못하고 단순히 속성과 기능만을 상속할 수 있는 것이었다면 이러한 이점은 생각지도 못할 일이다. 아무리 속성과 기능을 동일한 클래스로부터 상속 받았다 해도 하위 클래스들은 서로 엄연히 다른 관점으로 봐야 했을 것이고, 그렇다면 어떤 방법으로도 이들을 다루는 코드들을 통합 시킬 수 없었을 것이다. 그리고 기존의 객체를 새로운 객체로 간단히 교체한다는 것은 상상하기도 힘들었을 것이다. 상속은, 특히 "타입"의 상속은 이처럼 위대한 개념이다.


혹시 이러한 개념을 잘 생각하지 않고 있었던 분들이라면 이 타입 상속이라는 개념을 염두해 두고 어떻게 하면 동일한 외부 관점을 유지시킬 수 있을 것인지 항상 생각하면서 프로그래밍을 했으면 좋겠다. 그것이 개발자로서의 수명(신체적인 수명까지도;)을 길게 가져가는 길이기도 하니까 말이다.


Posted by 이세영2
,

interface - abstract class - concrete class 패턴은 인터페이스 구현 시 자주 발생하게 되는 중복 구현을 방지하는 패턴이다.


해결하고자 하는 문제

- 구현해야 할 클래스에 대한 인터페이스가 이미 정해진 상태이다.

- 정해진 인터페이스를 통해 구현해야 할 클래스가 여러개이다.

- 인터페이스 API 중 일부가 모두 같은 구현부를 같게 된다. 이 구현부의 중복을 없애야 한다.


해결 방법

인터페이스 구현 시 구현해야 할 함수 중에서 중복되는 함수들을 abstract class에 넣음으로써 곧바로 인터페이스를 구현하려고 할 때 발생할 수 있는 중복 구현을 방지할 수 있다.


간단한 예제를 통해 interface - abstract class - concrete class가 어떻게 쓰이는지 살펴보자.


우선 각종 도형들을 그리는 소프트웨어를 개발한다고 하자. 삼각형 사각형 원 등 다양한 도형이 있는데 이들 도형은 모두 표면(surface)과 라인(line)으로 그려진다고 가정해 보자. 이런 경우 모든 도형의 공통 요소인 표면 색깔 지정, 라인 색깔 지정, 도형 그리기와 같은 API를 생각해 볼 수 있다. 이들은 모든 도형에 공통이므로 공통 인터페이스를 선언하는 것으로 구현을 시작해 보겠다.


interface IShape{

    public void setSurfaceColor(Color surfaceColor);

    public void setLineColor(Color lineColor);

    public void draw();

}



그러면 인터페이스가 정의 되었으니 도형을 구현해 볼 차례이다. 먼저 Rectangle을 만들어 보자.


class Rectangle implements IShape{

    private Color surfaceColor;

    private Color lineColor;

    public void setSurfaceColor(Color surfaceColor){

        this.surfaceColor = surfaceColor;

    }

    public void setLineColor(Color lineColor){

        this.lineColor = lineColor;

    }

    public void draw(){

        System.out.println("draw Rectangle with");

        System.out.println(surfaceColor.toString());

        System.out.println(lineColor.toString());

    }

}



현재까지는 크게 문제는 없어 보인다. 표면 색깔과 라인 색깔을 지정할 수 있는 인터페이스를 구현했고, 도형을 그리는 draw() 함수도 구현했으니 실제로 잘 그려지게 될 것이다. 이렇게 IShape 인터페이스가 제공하는 모든 API를 구현했으니 이제 다른 도형도 만들어 보겠다. Circle을 만들어 보자.


class Circle implements IShape{

    private Color surfaceColor;

    private Color lineColor;

    public void setSurfaceColor(Color surfaceColor){

        this.surfaceColor = surfaceColor;

    }

    public void setLineColor(Color lineColor){

        this.lineColor = lineColor;

    }

    public void draw(){

        System.out.println("draw Circle with");

        System.out.println(surfaceColor.toString());

        System.out.println(lineColor.toString());

    }

}


이제 문제점이 눈에 보일 것이다. draw() 함수는 각 도형이 다르겠지만 setSurfaceColor()와 setLineColor()는 서로 동일하다. 하지만 도형이라면 위의 두 인터페이스도 제공해야 하는 것이 맞다. 그러면 계속 중복된 코드들을 만들어 가면서 구현을 완료하는 것이 옳을까?


이런 문제점을 해결할 수 있는 방법이 인터페이스(interfac)와 구체 클래스(concrete class) 중간에 추상 클래스(abstract class)를 하나 두고 공통되는 부분을 모아 두는 것이다. 위의 예제에서 공통된 부분을 추상 클래스로 뽑아 내면 다음과 같아질 것이다.


abstract class Shape implements IShape{

    protected Color surfaceColor;

    protected Color lineColor;

    public void setSurfaceColor(Color surfaceColor){

        this.surfaceColor = surfaceColor;

    }

    public void setLineColor(Color lineColor){

        this.lineColor = lineColor;

    }

} 


우선 우리가 구현하고자 하는 구체 클래스를 외부에서 사용할 때는 IShape 타입이어야 한다. 따라서 일단 추상 클래스가 IShape을 구현하도록 선언한다. 그리고 구체 클래스에서 발생한 중복 코드들을 추상 클래스로 이동시킨다. 주의할 것은 private 변수들을 protected로 바꾸어 주어야 한다는 것이다. 그렇게 해야 구체 클래스들이 이 Shape 추상 클래스를 상속 받았을 때 그 변수들을 사용할 수 있게 된다.


그리고 한가지 주목할 것은 IShape이 제공하는 인터페이스 중에서 void draw() 인터페이스를 구현하지 않았다는 점이다. 추상 클래스의 경우 상속 받은 인터페이스의 일부만 구현해도 컴파일에러가 발생하지 않는다. 그 이유는 인터페이스에서 선언한 API의 타입은 항상 abstract public 타입이기 때문이다. 잠깐 옆길로 새서 interface의 실제 타입을 밝혀보면 다음과 같다.


interface Example{

    void api();

}

abstract class Example{

    abstract public void api();

}


위의 두 선언은 선언적으로는 동등하다. interface는 실체화 할 수 없는 추상 클래스(abstract class)와 같고, api()는 실제로는 abstract public 타입의 함수이다. 다만, 인터페이스는 다중 상속이 가능하지만 추상 클래스는 단 한 개의 클래스만 상속 가능하다는 점에서 실질적으로는 같지 않다. 어쨌든 개념적으로 보면 인터페이스는 추상 클래스의 "특수 케이스"라고 이해할 수 있다.


그러면 이제 본론으로 다시 넘어가서 draw() 함수를 추상 클래스에서 구현하지 않아도 에러가 나지 않은 이유를 알 수 있을 것이다. 추상 클래스는 추상 메소드를 선언할 수 있는 클래스이다. IShape에서 선언된 draw() 함수는 추상 메소드이고, Shape 클래스가 이를 상속 받았으므로 draw() 추상 메소드가 선언된 셈이다. 추상 클래스가 추상 메소드를 선언하는 것은 문법에 위배되지 않기 때문에 구현체가 없어도 전혀 문제가 없는 것이다.


그럼 이제 Rectangle 클래스와 Circle 클래스가 어떻게 바뀌었는지 보자.


class Rectangle extends Shape{

    public void draw(){

        System.out.println("draw Rectangle with");

        System.out.println(surfaceColor.toString());

        System.out.println(lineColor.toString());

    }

}


class Circle  extends Shape{

    public void draw(){

        System.out.println("draw Circle with");

        System.out.println(surfaceColor.toString());

        System.out.println(lineColor.toString());

    }

}


자 일단 중복된 부분이 모두 제거되었다. 그 이유는 IShape을 implements 하던 것을 Shape을 extends 하는 것으로 바꿈으로써 setSurfaceColor() 함수와 setLineColor() 함수의 구현부를 상속 받았기 때문이다. 이를 통해서 두 클래스는 서로 다른 부분인 draw() 함수만을 구현하도록 바뀌었다.


그러면 최종적인 모습이 어떤지 한번에 살펴보자.


구현 결과

interface IShape{

    public void setSurfaceColor(Color surfaceColor);

    public void setLineColor(Color lineColor);

    public void draw();

}


abstract class Shape implements IShape{

    protected Color surfaceColor;

    protected Color lineColor;

    public void setSurfaceColor(Color surfaceColor){

        this.surfaceColor = surfaceColor;

    }

    public void setLineColor(Color lineColor){

        this.lineColor = lineColor;

    }

}


class Rectangle extends Shape{

    public void draw(){

        System.out.println("draw Rectangle with");

        System.out.println(surfaceColor.toString());

        System.out.println(lineColor.toString());

    }

}


class Circle  extends Shape{

    public void draw(){

        System.out.println("draw Circle with");

        System.out.println(surfaceColor.toString());

        System.out.println(lineColor.toString());

    }

} 


위와 같이 되었다 중복 코드가 없는 깔끔한 모습이다. 그러면 사용 방법에 있어서는 어떨까? Rectangle 클래스와 Circle 클래스를 외부에서는 IShape 타입으로 잘 인식 할 수 있을까? 다음과 같이 테스트를 구현해 보겠다.


테스트 함수

public static void main(String[] args) {

   IShape shape = new Rectangle();

   shape.setSurfaceColor(Color.BLACK);

   shape.setLineColor(Color.WHITE);

   shape.draw();


   shape = new Circle();

   shape.setSurfaceColor(Color.WHITE);

   shape.setLineColor(Color.BLACK);

   shape.draw();

} 


모든 API를 한번씩 호출해보도록 작성했고, 각 구체 클래스들을 IShape 타입으로 지칭하도록 했다. 물론 오류 없이 잘 동작하고 다음과 같은 결과를 출력해 냈다.


출력 결과

draw Rectangle with

java.awt.Color[r=0,g=0,b=0]

java.awt.Color[r=255,g=255,b=255]

draw Circle with

java.awt.Color[r=255,g=255,b=255]

java.awt.Color[r=0,g=0,b=0]


이처럼 아주 잘 동작하는 것을 확인 할 수 있다.


실제로 외부에서 제공된 인터페이스를 이용하여 구현을 하다보면 중복 코드가 자주 발생하게 된다. 같은 인터페이스를 상속 받는다는 것은 상속 받아 구현될 구체 클래스들이 유사점을 많이 가지고 있다는 것을 암시한다. 따라서 구현을 진행하다 보면 자연스럽게 중복된 코드들이 자주 만들어지게 된다.


이런 경우에 이 패턴 처럼 중간에 추상 클래스 하나를 만들어 상속 받도록 하면 중복 코드들을 제거할 수 있다. 중복된 부분들이 제거된 구체 클래스들은 구체 클래스들 간에 서로 다른 부분들만 구현하여 가지고 있게 되므로 코드에 대한 이해 속도도 빨라진다는 장점이 있다.


혹시라도 인터페이스 구현으로 인해 중복이 많이 발생하게 되었다면 이 패턴을 이용해 보자.

'5.디자인패턴' 카테고리의 다른 글

Pluggable Selector 패턴  (1) 2016.08.21
State 패턴  (0) 2016.08.15
Strategy 패턴  (0) 2016.08.15
Telescoping Parameter 패턴  (0) 2016.08.13
Enum Factory Method 패턴  (0) 2016.08.07
Posted by 이세영2
,

Immutable(불변) 객체는 수학과 금융 등에서 자주 쓰이는 개념이다.

이 글에서는 켄트 벡의 저서 테스트 주도 개발(TDD)과 켄트 벡의 구현 패턴에 나오는 Money 객체를 예로 들어 설명하도록 하겠다.


정의

Immutable(불변) 객체는 객체 생성 이후 값이 변하지 않는 객체를 말한다. 이는 생성자를 이용한 값 설정 이외의 어떤 갱신 수단도 제공하지 않는다는 것을 의미한다.


특징

정의에서와 마찬가지로 객체 생성 이후에는 값을 변경시킬 수 없다.

또 한가지 특징은 같은 값을 가지는 객체 간에는 동일성(Equality)이 보장된다는 점이다.


이러한 개념이 나오게 된 배경은 바로 실생활에서 사용하는 값에 대한 개념이 바로 그러하기 때문이다. 금융이나 수학에서 자주 쓰이는 개념으로부터 나온 용례를 보면 보다 정확하게 알 수 있다.


용례

1. 100원은 (100원이 계속 존재하는 한) 100원이다. 100원짜리 동전들은 모두 100원이다.

2. PI는 항상 3.141592...이다. PI = PI이다.


Immutable 객체의 정의 및 특징과 마찬가지로 실 생활에서 어떤 값은 항상 그 값으로만 사용된다. 10이 10이었다가 어떤 때에는 20이 되거나 하지 않는다. 그리고 10은 항상 10과 같다. 이러한 동일성은 10이 10인 동안에는 항상 유지된다.

이러한 개념을 구현한 것이 바로 Immutable 객체이다.


실제 구현에 들어가 보자.

켄트 벡의 좋은 예제가 있으므로 그의 자취를 따라가보자. 우리는 Money 객체를 구현할 것이다. Money는 단위(unit)와 값(value)을 갖는다고 가정한다.


Money class 선언

class Money{

    private final String unit;

    private final int value;

    public String getUnit() {

        return unit;

    }

    public int getValue() {

        return value;

    }

}


객체의 외부에서 unit과 value를 확인할 수 있어야 하므로 getter 함수들도 함께 구현해 넣었다.


이제 정의를 만족하도록 더 구현해 넣을 차례다. 우선 생성 이후에는 값이 변경되지 않을 것이므로 생성자를 통해 값을 설정할 수 있도록 해주어야 한다.


class Money{

    public Money(String unit, int value){

        this.unit = unit;

        this.value = value;

    }


이렇게  unit과 value를 매개변수로 받아서 설정할 수 있도록 했다.


그 다음에는 동등성을 확보해야 한다. Java에서 동등성 비교는 equals() 함수를 통해 수행한다. 그런데 Java의 equals() 함수는 객체의 동일성을 비교하는 것이므로 이를 값을 비교하는 것으로 바꾸어 주어야 한다. 그래서 아래와 같이 equals() 함수를 재정의 한다.


class Money{

    @Override

    public boolean equals(Object obj) {

        Money extern = (Money) obj;

        return (extern != null) && unit.equals(extern.unit) && value == extern.value;

    }


여기까지 구현하면 이제 정의를 만족하는 Immutable 객체가 된 것이다. 


하지만 이것만으로 만족하기에는 뭔가 부족하다. 모든 값들은 연산을 할 수 있어야 한다. 5에 5를 더할 수 있어야 하고, 값의 의미에 따라 사칙연산이나 기타 여러 연산을 지원할 수 있어야 한다. 하지만 연산을 수행할 경우 값은 변경된다. 그런데 지금까지 구현한 Immutable 객체에서는 값을 변경할 방법이 없다.(그렇게 하면 값이 변경되므로 Immutable 객체의 정의에 어긋난다.) 그래서 값의 연산을 수행하고 그 결과를 새로운 Immutable 객체 생성을 통해 지원해 주는 함수를 구현해 주어야 한다.


아래 예제는 더하기를 구현한 것이다.


class Money{

    public Money plus(Money added){

        return new Money(unit, value + added.value);

    }


위와 같은 방법을 통해서 하나의 Immutable 객체와 다른 Immutable 객체의 덧셈을 구현한다.(편의상 unit에 대한 동일성 체크는 제외하였다.)


모두 다 구현했다면 아래와 같은 모양이 될 것이다.


최종 구현

class Money{

    private final String unit;

    private final int value;

    public String getUnit() {

        return unit;

    }

    public int getValue() {

        return value;

    }

   

    public Money(String unit, int value){

        this.unit = unit;

        this.value = value;

    }

   

    @Override

    public boolean equals(Object obj) {

        Money extern = (Money) obj;

        return (extern != null) && unit.equals(extern.unit) && value == extern.value;

    }

   

    public Money plus(Money added){

        return new Money(unit, value + added.value);

    }

}


이제 main() 함수를 이용하여 테스트를 해 볼 시간이다.


public static void main(String[] args) {

    Money five = new Money("KRW", 5);

    Money anotherFive = new Money("KRW", 5);

    Money ten = five.plus(five);

    System.out.println(five.equals(anotherFive));

    System.out.println(ten.getValue());

}


5원은 5원과 같아야 하고 5 + 5 = 10원이어야 한다.


용어와 개념은 모르고 있을 때는 전혀 쓸 수 없지만, 알고 있을 때는 매우 유용한 것이다.


Posted by 이세영2
,

Enum Factory Method 패턴은 Factory Method 패턴의 단점을 보완하기 위한 패턴이다.

기본적으로 Factory Method 패턴과 마찬가지로 객체의 생성을 담당하는 메소드를 구현하는 패턴이다. 이와 함께 Factory Method를 구현한 객체를 생성하기 위해 Singleton을 사용해야 하는 문제점을 Enum의 특성을 이용하여 해결한다.


Enum Factory Method 패턴(Java에서만 가능)

public enum EnumFactoryMethod {

    RECTANGLE{

        protected Shape createShape(){return new Rectangle();}

    }

    ,CIRCLE{

        protected Shape createShape(){return new Circle();}

    }

    ;

    public Shape create(Color color){

        Shape shape = createShape();

        shape.setColor(color);

        return shape;

    }

    abstract protected Shape createShape();

    public static void main(String[] args) {

        EnumFactoryMethod.RECTANGLE.create(Color.BLACK);

        EnumFactoryMethod.CIRCLE.create(Color.WHITE);

    }

}



Enum 타입 자체가 public static final 이기 때문에 생성을 위임 받은 객체에 대한 중복 생성이 불가하고, Singleton을 굳이 구현하지 않아도 단일한 객체만 생성됨이 보장된다.


Enum의 이러한 특성은 다른 패턴들에도 응용이 될 수 있는데 이는 이후 포스팅을 통해 살펴보도록 하겠다.

Posted by 이세영2
,

불완전성의 원리는 쿠르트 괴델이 증명한 원리이다.


쿠르트 괴델이 불완전성을 증명한 과정은 BBC에서 제작한 "Dangerous Knowledge" 라는 제목의 다큐멘터리에 잘 나와 있다.


게오르그 칸토어는 미지의 대상이던 무한에 관한 이론을 정립하고자 노력한 수학자였다. 그는 무한과 무한과의 관계를 정립하는 과정에서 연속체 가설을 증명하고자 하였다. 이전까지는 수학이 논리적으로 완전하다는 의식이 팽배해 있었는데, 칸토어가 연속체 가설을 내놓은 이후부터 수학에는 논리적 완전성을 증명하기 난해한(당대까지는) 문제가 있음을 알게 되었다. 즉, 단편적인 논리적 사실은 증명이 되지만 전체 체계를 놓고 보면 완전함이 증명되지 않는 것이다.

이후 많은 수학자들이 어떤 논리적인 체계가 완벽하게 증명되기를 기대하면서 연구를 진행했는데(이 기대는 수천년간 수학의 논리적인 완전성을 믿어왔던 수많은 수학자들의 바램이기도 했다) 쿠르트 괴델도 그 중 한 사람이다.

하지만 수많은 수학자들의 바램과는 달리 괴델은 "불완전성의 원리"로 불리우는 반대 증명을 해낸다.


불완전성의 원리

정리1. 자연수의 사칙연산을 포함하는 어떠한 공리계도 무모순인 동시에 완전할 수 없다. 어떤 체계가 무모순이라면, 그 체계에서는 참이면서도 증명할 수 없는 명제가 존재한다.


정리2. 자연수의 사칙연산을 포함하는 어떠한 공리계가 무모순일 경우, 그 공리계는 자기 자신의 무모순에 대한 정리를 포함할 수 없다.



불완전성의 원리를 해석해보면 다음과 같다. (실제가 아닌) 순수 논리의 세계에서 어떤 논리 체계가 완전하게 참과 거짓으로 증명될 수 있느냐는 물음에 불완전성의 원리는 "그럴 수 없다"는 답을 내놓은 것이다. 특히 이 과정에서 괴델은 "스스로 증명(계산)하는 논리적 체계"를 고안해 내고 이를 통해 불완전성 원리를 증명해 냈는데, 이 체계가 우리가 알고 있는 "알고리즘"의 시초이다. 알고리즘과 불완전성의 원리가 내포하는 관계를 명확히 해보면 다음과 같다.


대응관계

알고리즘 : 스스로 계산하는 논리적 체계

불완전성의 원리 : 알고리즘은 스스로 논리적으로 완전함을 증명할 수 없다.



컴퓨터, 특히 소프트웨어의 시작을 알리는 이 대응 관계는 비참하게도 커다른 악과 함께 탄생한 셈이다. 어떤 알고리즘도 스스로 완전하다고 증명할 수 없다. "스스로 할 수 없다면 다른 알고리즘으로 증명하면 되지 않을까?" 하는 생각도 금방 깨지게 된다. 다른 알고리즘의 완전함을 증명할 방법이 없기 때문이다. 그래서 소프트웨어 영역에는 "알고리즘은 자기 스스로 논리적 완결성을 증명할 수 없다"는 명제가 있는 것이다.

만약 불완전성이 없다면, 즉 논리적 체계가 스스로 완전함을 증명할 수 있다면 소프트웨어가 의도한 대로 동작하는지를 인간이 증명해야 할 필요가 없었을 것이다. 그렇다면 인간은 어떻게든 스스로 증명할 수 있는 알고리즘을 구현만 한다면 그 후에는 오류나 버그가 나오지 않는다는 것을 확신할 수 있게 될테고, 현대 개발자들이 가지고 있는 불안감이 모두 해결될 것이다. 반대로 이야기 하자면, 불완전성의 원리 때문에 자동으로 소프트웨어의 완전함을 증명할 방법이 없어지고, 소프트웨어의 완전함을 관리할 수 있는 도구는 오직 인간의 두뇌 밖에 없다는 말이 된다. 이것이 악의 근원이 아니면 무엇이겠는가?


불완전성과 소프트웨어

불완전성의 원리는 이후 소프트웨어의 발전 과정과 현대에 개발된 소프트웨어 기술에 대한 미래, 그리고 소프트웨어가 가진 본질적인 특성을 규정하게 되었다. 이 부분은 추후 포스팅을 통해서 계속 다룰 예정이다.


한가지 명료하게 이야기 해 두자면, 불완전성의 원리는 소프트웨어의 탄생이자 성격의 규정자라는 사실이다. 우리는 소프트웨어 및 프로그래밍, 소프트웨어 설계나 개발 방법론들을 접하는데, 이들은 모두 불완전성이라는 악을 다스리는 기술이다. 그리고 우리가 프로그래밍을 공부하면서 받아 들이는 수많은 격언, 설계 원칙, 변수나 함수의 명명 규칙

과 같은 사소한 규정까지도 사실 불완전성에 기인하는 것이다. 


이미 소프트웨어로 생계를 꾸려 나가기로 결정한 개인이나 소프트웨어를 이용해서 돈을 벌고자 하는 기업에게 한마디 하자면 다음과 같다.


"You have a big problem!"


'1.프로그래밍 일반' 카테고리의 다른 글

소프트웨어 기술 트리  (0) 2016.08.19
비 구조적 언어와 예견된 위기  (2) 2016.08.13
인간의 능력과 소프트웨어  (1) 2016.08.07
좋은 코드가 갖춰야 할 요소  (0) 2016.08.07
좋은 코드란?  (0) 2016.08.07
Posted by 이세영2
,

소프트웨어를 개발할 때는 다음과 같은 전제가 있음을 미리 알아 둘 필요가 있다. 아래의 전제들을 무시하고 개발에 임하면 개발자 간의 협력이 어려워지고, 투입되는 비용과 인력이 증가하며 개발 기간은 길어지게 된다. 개발자라면 언제나 효율성을 추구해야 한다. 그리고 효율성을 측정하는 단위는 프로젝트다. 개인 단위가 아님을 명심해야 한다.


소프트웨어를 관리 할 수 있는 도구는 인간의 두뇌 뿐이다 

잘 이해가 되지 않는다면 "불완전성의 원리"를 읽어 보기 바란다.

소프트웨어는 스스로 잘 동작함을 검증할 수 없다.

검증용 소프트웨어가 다른 소프트웨어를 검증할 수도 없다.


관리 가능한 소프트웨어의 크기가 기회의 크기이다

갈수록 소프트웨어의 규모는 커져가고, 큰 소프트웨어를 관리할 수 있는 기술력이 있어야 시장에서 기회를 잡을 수 있다.


개인의 능력으로는 부족하다

시장에서 요구하는 소프트웨어를 혼자서 모두 개발할 수 있는 사람은 없다.

다수의 개발자가 만든 소프트웨어를 통합해야만 큰 소프트웨어를 만들 수 있다.

따라서 커뮤니케이션을 통한 개발자간의 협력은 필수 요소이다.


언어를 통한 소통은 부정확하다

인간의 언어는 코드보다 부정확하다.(기능에 대한 설명은 개요 이상에 대한 설명을 기대하기 어렵다.)

코드는 가장 정확한 진실을 담고 있다.

원칙적으로 코드를 통한 커뮤니케이션이 가능해야 한다.


인지 능력에는 한계가 있다(인지능력의 한계를 시험하는 예)

명칭과 실제의 부조화

동등한 기능에 대한 서로 다른 구조의 구현

중복된 코드

추상 레벨의 편차

기능 흐름의 분산

잦은 제어 분기

암기할 것이 많은 코드

합리성의 결여

예측 불가능


인간의 인지 능력에 맞는 코드를 작성해야 한다

코드는 인간의 인지 능력 내에서 관리 될 수 있어야 한다.

개인의 능력을 넘는 소프트웨어 작성을 위해서 코드는 커뮤니케이션 도구 역할도 수행해야 한다.

따라서 코드는 모든 인간의 인지 능력의 효율에 맞게 작성되어야 한다.


Posted by 이세영2
,

좋은 코드는 제품, 소프트웨어, 커뮤니케이션 도구로서의 특성을 모두 만족시켜야 한다. 이들 특성으로부터 좋은 코드가 갖춰야 할 요소를 뽑아 낼 수가 있다.


좋은 코드의 정의로부터는

명료성, 간결성, 유연성


켄트 벡의 구현 패턴에서는

커뮤니케이션, 단순성, 유연성



많은 개발자들은 이미 코드가 커뮤니케이션 수단이라는 데 동의하고 있다.
커뮤니케이션을 위해서나, 좋은 품질의 제품으로서의 특성을 위해서나 간결함은 꼭 갖춰야 할 요소이다.
유연성은 소프트웨어의 본질적인 특성으로서, 언제든지 수정이 용이한 상태로 유지되어야 한다.(유연성이 요구되지 않는 경우라면 좋은 코드의 특성이 아니라 다른 요소를 더 만족시켜야 할 것이다.)




Posted by 이세영2
,

좋은 코드는 다음과 같은 특성을 만족시켜야 한다.


제품으로서의 특성

제품은 좋은 품질을 가져야 하고, 품질을 명료하게 확인할 수 있어야 한다.
결점(버그)이 있을 경우 빠르게 해결할 수 있어야 한다.

소프트웨어의 특성

기능의 수정, 추가, 제거가 용이해야 한다.
항상 수정이 가능한 상태를 유지해야 한다.

커뮤니케이션 도구로서의 특성

개발자 간의 커뮤니케이션 도구로서 코드만한 것이 없다.
논리적이고 명료해야 한다.(= 내용이 장황하거나 불필요한 부분이 없고 중복되지 않아야 한다.)
좋은 글의 요건*을 갖추고 있어야 한다.

* 좋은 글의 요건

가치 있는 내용(contents)을 담고 있을 것.

내용이 분명하고 논리 정연할 것.

쉽고 간결할 것.


"코드는 컴퓨터나 동료가 아닌 작성자 본인과 먼저 대화한다"


Posted by 이세영2
,