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


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

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

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


객체지향(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
,