왜 여기까지 왔는가?

궁금증이 있었다. 우선 회사에서 가르치는 아키텍트(Architect) 관련 과정이 무엇을 가르치는 것일까? 그리고, 거기서 말하는 소프트웨어 아키텍트(Software Architecture)라는 것은 어떤 것일까? 그 과정을 들었떤 지인들에게 이야기 들었던 것들은 나에게 속시원한 답을 주지는 않았다. 그래서, 우선은 과정에 참여하였고, 일단락을 맺었다. 하지만, 이제 시작이라는 느낌이다. 그래서, 이글을 시작으로 또 공부하게되는 내용들을 정리하려고 한다.

 

교육 과정을 통해서 알게 된 소프트웨어 아키텍처 설계(Software Architecture Design)은 소프트웨어 공학(Software Engineering) 관점에서 최적 설계에 대한 것이었다. 단계별로 보면, 요구사항 분석(Requirement Analysis)에서 시작하여 시스템의 범위를 정하고, 기능적 요구 사항(Functional Requirement)를 도출한다. 그리고, 이를 분석하여 품질 시나리오(Quality Scenario)를 도출한다. 여기서, 비기능적 요구 사항(Non-Functional Requirement)와 품질 속성(Quality Attribute)를 선정한다. 이 때, 비지니스 동인(Business Driver)를 고려하여 우선순위 선정이 필요하다. 그리고, 이를 기반으로 시스템 범위 내에 중요한 내용을 기반으로 하는 후보 구조(Candidate Architecture)들을 도출하고 이를 비교 분석하며 아키텍처 디시젼(Architectural Decision)을 해가면서 최종적 아키텍처를 완성해 간다. 이 내용들은 시스템의 이해관계자(Stakeholder)과 공유 되어야 하면서 장점과 단점이 지속적으로 언급이 되면서, 의사결정이 되는 과정인 것이다.

 

아주 오래전에 소프트웨어 공학(Software Engineering) 교수님께서 설명하신 객체 지향 패러다임(Object Oriented Paradigm)관련한 강의에서 들은 내용이 있다. 소프트웨어 공학(Software Engineering)이 무엇인지에 대한 것인데, 이는 소프트웨어 개발자가 가져야 하는 상식(Common Knowledge)이다라는 것이 있다. 나도 소프트웨어를 개발하는 사람이 가져야 하는 기본적 소양이라는 관점에 지금까지도 동의 하는 부분이다. 하지만, 많은 소프트웨어 공학 책에서 강조하는 정식 프로세스(Formal Process) 관점에 대해서는 나는 수긍하지 못하고 있다. 많은 회사들이 이를 채용하고 있다고는 하지만, 직간접적으로 경헌한 나의 경우는 아직 보지 못했기 때문이다. 물론 많은 회사들이 제품을 만들기 때문에 프로세스가 있을 것이다. 

 

가장 큰 이유 중에 하나는 사람이다. 프로세스 중에 사람이 없다. 내 경험으로 여기에 가장 가까운 것은 애자일 소프트웨어 개발(Agile Software Development)이다. 나는 이것도 결국은 소프트웨어 공학의 한 분야로 이해하고 있다. 사람이 하는 일은 사람을 떠나서 생각하면 공허해 진다.

 

문서화에 대한 거부감

내가 SW 개발 실무를 처음 시작할 때에는 통신 프로토콜 스택의 설계 및 구현에 참여 하였다. 요구 사항(Requirement)이 사양서(Specification)이었다. 나의 상사는 소프트웨어 공학을 했던 분으로 내가 받았던 업무 지시는 요구 사항 분석을 통해서 상세 설계를 하는 것이었다. 코딩은 중요하지 않다(Trivial하다는 의미로 이해된다)고 이야기를 계속 들었다. 이러한 작업은 내가 구현해야 하는 분야의 도메인 지식(Domain Knowledge)을 높여 주는데는 성공했지만, 최종적으로 코딩을 하는데는 실패하게 되었다. 이유는 나의 기술적인 미숙, 하드웨어 제약에 대한 이해 부족, 목표한 테스트의 실패, 충분하지 못한 일정 그리고는 지속적은 목표 변경 등 여러 가지가 복합적이었다.

 

이후 내가 경험했던 여러 성공한 프로젝트들은 문서화보다는 실질적으로 동작하는 제품에 맞춰졌다. 내부 혹은 외부의 고객이 원하는 제품 혹은 우리가 만들 수 있는 제품을 고객에게 보여주면서 정제(Refine)해가는 절차를 통해서였다. 즉, 동작하는 솔루션을 만들어 놓고, 그를 바탕으로 확장해 나아가는 방식이다. 여기에서의 우리가 하는 문서화는 점점 줄어 들었다. 이는 읽지 않는 문서는 만들지 않는다는 중요한 원칙이 있었던 것이다.

 

성공적이라 할 수 있는 프로젝트는 많았지만, 늘 지적 받던 것은 소프트웨어의 품질이었다. 과제가 끝났지만, 여전히 소프트웨어에는 문제점(Defect)이 많고, 변경이 용이하지 않으며, 성능이 좋지 않은 경우가 많았다. 여러 가지 개선 방법들을 논의 하였지만, 우선적으로 소개되며 고민되었던 방법은 프러덕트 기반의 애자일 개발 접근 방법이었다. 여기서도 문서는 그렇게 중요한 것은 아니었다. 그 보다는 진행 사항을 가시화(Visualize)하고 제품을 점진적으로 개발하여 실재 제품에 적용가능한지를 평가하면서 진행하는 방식으로 프로젝트가 변경되었다.

 

이렇듯, 소프트웨어 공학에서 이야기 하는 설계서와 같은 문서화에 대해서는 여러 거부감이 있다. 하지만, 아이젠하워의 말대로 "계획대로 승리한 전투는 없지만, 계획없이 승리한 전투도 없다. No battle was ever won according to plan, but no battle was ever won without one" 인 것이다. 결국은 읽힐 문서, 다이어그램(Diagram)을 만들어야 하는 것이다.

 

소프트웨어 공학에 대한 비판

많은 아키텍처 책이나 글 그리고 프로세스는 소프트웨어 공학을 가정한 내용이다. 아래 글은 Wikipedia의 소프트웨어 공학(Software Engineering)[1]에 있는 내용이다. 여러가지 관점에서 살펴 보기 위해서 이러한 의견도 고려해 보려 한다.

 

소프트웨어 공학에서는 개발자를 전문가(Practitioner)로 본다. 즉, 문제 해결에 대해 잘 정의된 엔지니어링 방식을 따르는 개인, 즉 전문가로 본다. 이는 마치 의사, 변호사와 마찬가지로 보는 것이다. 이러한 접근법은 다양한 소프트웨어 공학 서적 및 연구 논문에 명시되어 있으며 항상 예측 가능성, 정확성, 리스크의 완화 및 전문성을 함축한다. 이러한 관점은 엔지니어링 지식을 전파하고 현장을 성숙시키는 메커니즘으로서 라이센스, 인증 및 체계화 된 지식 체계를 요구하게 되었다.

 

하지만, 소프트웨어 공학에 대한 핵심 쟁점 중 하나는 일반적으로 실제 접근 방식의 검증이 없거나 매우 제한적이어서 접근 방식이 충분히 경험적이지 않다는 것이다. 그래서, 소프트웨어 엔지니어링이 종종 "이론적 환경"에서만 가능한 것으로 오인된다.  

 

에츠허르 데이크스트라(Edsger Dijkstra)는 오늘날 소프트웨어 개발에서 사용 된 여러 개념을 만든 사람으로서 2002 년 사망 할 때까지 소프트웨어 엔지니어링에 대해 컴퓨터 과학의 "급진적 참신성(Radical Novelty)"라고 반박했다.[2]

 

"이러한 현상은 소프트웨어 공학이라는 이름으로 포장되었다. 경제학이 비참한 과학(The Miserable Science)으로 알려짐에 따라 소프트웨어 공학은 종말에 이른 체계(Doomed Discipline)으로 알려 져야한다. 목표가 자기 모순적이어서 목표에 다가 갈 수 없기 때문이다. 물론 소프트웨어 공학은 또 다른 가치있는 이유를 제시하지만, 눈속임이다. 글을 주의 깊게 읽고 그것이 실제로 무엇을 하려고 헌신하는지 분석하면 프로그래밍을 할줄 모르면 어떻게 하는가(How to program if you can not)가 소프트웨어 공학의 진정한 근본 원리(Charter)로 받아 들여 졌음을 알게 될 것이다."

 

그럼에도 불고하고, 소프트웨어 공학은 이러한 비판에 대해서 닫혀 있지 않았다. 여러 경험적이고 공통적으로 받아 지는 것들을 쌓아 왔다. 특히, 애자일 소프트웨어 개발(Agile Software Development)의 성공적인 부분을 받아 들였따. 특히, 반복적 접근 방법(Iteratvive Development)를 받아 들이고 있다.

 

정리해 보면...

받아 들이기 어려운 극단적인 비판적 입장일 필요도 없다. 간단하고 명료하게, 우리는 실용적(Practical)일 필요가 있다. 즉, 결과를 낼 수 있고, 논리적으로 명백하다면 받아 들이기는 더욱 쉬울 것이다. 단지, 늘 비판에 대해서 열려 있어야 한다. 하지만, 목표는 분명하고 단단해야 할 것이다. 즉, 소프트웨어는 완성되어야 한다. 그리고, 소프트웨어 개발이 어려 사람들과 함께 만들어 내야 하는 것일 것이다.

 

이러한 실용적이라는 관점에서는 목표를 이야기 하고 싶다. 우리는 최상의 결과물을 만들어 내야 한다. 그런 입장에서 결과물을 처음 부터 그려 가는 것이 필요하다. 처음에 그렸던 최종 결과물이 마지막에도 꼭 그런 모습을 가지지 않을 수 있다. 아이젠하워의 말처럼 "Plan is nothing, but planning is indispensable"이 생각난다. 결국, 전쟁에서는 승리가 최종 목표 이듯이, 설계서에 함몰하지 말고 최종 결과물을 목표로 하는 설계에 집중해야 한다고 생각한다.

 

그래서, 나는 소프트웨어 공학 관점에서의 아키텍처에 관해서 고민을 시작하여 여러 관점에서의 실용적인 소프트웨어 아키텍처의 설계에 대해서 살펴 보고자 한다.

 

참고 문서

[1] Software Engineering, https://en.wikipedia.org/wiki/Software_engineering 

[2] On the cruelty of really teaching computing science, https://www.cs.utexas.edu/~EWD/transcriptions/EWD10xx/EWD1036.html

 

 

개요


Factory Pattern은 Creational Pattern 중의 하나이다. 아래은 GoF의 Design Pattern Categorization이다. 여기서 Purpose는 Design Pattern의 목적과 관련된 것이다. 즉, Creation은 Class의 생성에 관련된 것이고 Structure는 Object 구조에 대한 것이고, Behavior는 Object의 동작에 관한 것이다. Scope은 Design Pattern이 적용되는 범위이다. 즉, Class의 경우는 class들 같으 관계에 관한 것으로 static하고 compile-time에 결정이 된다. 하지만, Object의 경우는 Object간의 관계에 관한 것이고 더 Dynamic하다.





여기서는 Factory Method에 대해서 살펴 보자. Factory Method은 Creational 이면셔 Class에 적용되는 패턴이다. 


Head First의 이야기

Head First[1]에서는 Pizza Restaurant를 예를 들어 Factory Method를 설명한다.


이야기 한데로, Head First에서는 Pizza 가게의 예를 들어 Design Pattern을 설명한다. 이야기의 흐름은 다음과 같다. 우선 Pizza를 주문하는 Method(orderPizza)를 생각해 보자. Pizza Instance를 생성하고, 준비(prepare), 굽고(bake), 자르고(cut), 포장하고(box) 그리고 완성된 Pizz를 제공하는 것이라고 하자. 그런데,리고, Pizza의 종류 또한 여러 가지가 될 수 있다. 여기서는 cheese, pepperoni, veggie와 같이 Pizza가 늘어 날 수록 orderPizza에서는 종류에 따라 다르게 생성하고 이를 추가해야 한다. 즉, Pizza가 추가 될 때마다 orderPizza가 계속 영향을 받게 된다. 그렇게 하기 위해서 다음 2가지를 수행해야 한다.

1. Pizza를 Abstraction한다.  (Pizza Abstract Class)

2. prepare, bake, cut, box를 지원하는 concrete pizza를 구현한다. (Concrete Pizza Classes)

3. Creation하는 부분을 분리한다. (Factory Method Class)


Class Diagram으로 표시하면 아래와 같은 구조가 된다. 여기까지가 Simple Factory이다. Factory까지 추상화 해야 Factory Method가 된다.



이제 이 장사 잘 되는 Pizza가게를 프렌차이즈로 만들어 보자. 하지만, 이 피자가게가 모두 같지 않고 지역별로 조금씩 특징이 있을 수 있다. 예를 들자면, New York, Chicago, California등이 그 예이다. 그러면, 이 Factory의 경우도 Abstraction이 필요하다 


여기까지 구현한 것이 Factory Method이다. 그래서, 아래와 같은 Class Diagram을 가질 수 있다.




GoF DP의 요약 과 Quick Reference

GoF 책[2]에서 설명하는 의도는 "하나의 object를 생성하려는 interface를 정의한다. 그리고, subclass가 어떤 class가 instantiate될지 결정하게 한다. 즉, Factory Method는 하나의class가 subclass들의 생성을 분리(defer)한다."라고 되어 있다. [3],에서 해당하는 class diagram은 아래와 같다. 사실 같은 내용이다.




관련 Object-Oriented Principle

Factory Method가 관련된 SOLID Principle은 Dependency Inversion Principle라고 할 수 있다. DIP에서 많이 드는 예는 Layered Architecture에서 하위 Interface를 상위에스 Reference하게 만드는 것이다. 아래의 예에서 상위 Layer는 하위 Layer에 Dependency를 가진다. 즉, 하위 Layer가 변경되면 상위 Layer도 영향을 받게 된다. 어떻게 이 Dependency를 끊을 수 있을까? 아래 그립과 같이 하위 Layer의 Interface를 만들어 상위 Layer에 두는 것이다. 즉, 하위 Layer가 상위 Layer에 Dependency가 생겨서 Inversion이 생겼다. 이 구조에서는 하위 Layer의 구조가 상위 Layer의 Interface에 제약되므로 이를 벗어날 수 없다. 아이러니하게도 상호 종속되어 나빠질 것 같지만, 서로에 약속에 의해서 제약된 것 이외에는 자유로워 졌다.



Factory Method에서는 Product와 Creator가 하위 Layer로 볼 수 있다. 그러므로 이것들의 Abstract인 Interface를 활용하면 이를 사용하는Client와 Product, Creator를 Loosely Coupling하게 할 수 있다.


References

[1] Eric Freeman and Elsabeth Freeman, "Head First Design Patterns," OReilly

[2] Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides, "Design Patterns: Elements of Reusable Object-Oriented Software," Adisson-Wesley

[3] Design Pattern Quick Reference: http://www.mcdonaldland.info/2007/11/28/40/

시작하며

Refactoring은 1999년의 Martin Fowler의 책[1]에서 소개 된 후, 계속 이야기 되고 있는 개념이다. Design Pattern과 같이 하나 하나 필요한 사례를 익히는 것도 시간이 걸리고 이를 실제로 활용하는데도는 더 많은 시간이 필요하다. 크게는 간단한 정의 및 Bad Smell에 대해서 살펴 본다. 실제 Technique은 옆에 두고 참고하기 위해서 따로 정리하였다.


정의

Refactoring은 Martin Fowler[1]가 제안한 코드 품질 향상 혹은 재설계의 방법이라고 할수 있다. Refactor라는 단의의 정의는 명사 및 동사의 의미를 가진다고 한다.


명사: a change made to the internal structure of software to make it easier to understand and cheaper to modify without changing its observable behavior.

동사: to reconstruct software by applying a series of refactoring without changing its observable behavior


즉, 존재하는 코드를 기능의 변화 없이 안전하게 디자인을 향상 시키는 기법이다. 기능을 추가하면서도 Refactoring을 할 수 있기는 한다. 이는 구조가 변화 해야 하므로 기능을 변화 시키지 않고 변형을 하여 테스트를 하여 문제가 없다는 것을 확인한 후에 거기에 기능을 추가하는 방법이 적절하다.


어떻게 수행하는 하는가? 우선 Band Smell을 찾는다. 이것이 Refactor해야 하는 부분으로 인지하고 Refactoring을 적용하는 것이다.


[1]의 2nd Author인 Kent Beck의 Test Driven Development에서 Test를 수행하면서 개선하는 방법을 제안한다. 이것에 대한 이야기는 다음에 기회를 보아 살펴 봐야겠다.


Bad Smell

Refactoring을 해야 하는 시작점으로 볼 수 있다. 즉, 설계의 문제점이나 심각한 문제점의 조짐이라고 볼 수 있다. 이것들은 Martin Fowler 책에서 소개 된 것들이지만, 분류는 [2], [3]에 따른 것이다. (M)은 Martin Fowler의 책에 소개된 항목들이고 나머지들은 다른 항목들이다.



Bloaters

부풀어 오름이라고 번역이 가능할 것 같다. 즉, 일반적이거나 정상적이지 못하게 커지거나 길어진 상태라고 보면 되겠다.

. Long Method: 말 그대로 Method가 너무 길어진 형태이다. 이를 나누거나 줄이는 작업이 필요하다.

. Large Class (M): 큰 Class이다. Responsibility가 여러 개가 들어 있을 수도 있다. 또한 중복 코드가 포함되어 있을 수 있다.

. Primitive Obsession (M): Object를 사용하기 보다는 Primitive Data를 사용하는 경우가 있다. 이를 Object/Class로 만드는 것이다. Class를 추출("Extract Class")하거나 Parameter 객체를 도입("Introduce Parameter Object")하거나 어레이를 객체로 치환("Replace Array with Object") 할 수 있다.

. Long Parameter List (M): 매개변수(Parameter)가 간결한 것은 이해하는데 큰 장점이다. 만일 데이터가 객체로 부터 나오는 것이면 객체를 전달("Preserve Whole Object")를 수행하거나 Parameter 객체를 도입("Introduce Parameter Object")를 수행할 수 있다.

. Data Clumps (M): [1]의 한국어 버전에는 데이터 뭉치로 번역이 되어 있다. Class 내에서 3~4개 이상의 데이터 항목이 뭉쳐 다니는 경우가 있다. 이런 경우는 Class 추출("Extract Class")을 수행하거나 Parameter 객체 도입("Introduce Parameter Object")를 수행할 수 있다.


Object-Oriented Abusers

Object-Oriented 개념이 잘 적용되지 못한 경우들이다.

. Switch Statements (M): switch문이 꼭 나쁜 것은 아니지만, Object-Oriented에서 확연히 줄어드는 것이 특징이다. switch문은 코드 중복을 만들기 쉬우므로 이를 Polymorphism등으로 제거하는 방법을 고려할 수 있다. 또한 긴 if 문도 코드 중복등이 있을 수 있다. Method 추출("Extract Method") 혹은 Method 이동 ("Move Method") 또는 복잡한 형태로 Subclass로 Type Code를 치환("Replace Type Code wit Subclass)하거나 State/Strategy로 Type Code를 치환("Replace Type Code with State/Strategy)하는 방법이 있을 수 있다.

. Temporary Field (M): 임시 변수로 중간 결과를 가지고 다른 동작이나 판단을 하는 코드의 경우이다. 이는 Method 추출(Extract Method)를 통해서 임시 변수를 대체 할 수 있다.

. Refused Bequest (M): 거부된 유산. Martin Fowler 책의 번역본에는 방치된 상속물이라고 번역되어 있다. 즉, Inheritance를 했지만, 실제로 Superclass의 Method들이 사용되지 않는 경우이다. 즉, 잘못된 계층 구조 때문일 수 있다. 이때는, Superclass의 변형을 고려하거나 Delegation 관계로 바꾸는(Replace Inheritance with Delegation)을 고려할 수 있다.

. Alternative Classes with Different Interfaces: 비슷한 역할의 Class이지만 제공하는 Interface가 다른 Class들인 경우이다. 같은 기능을 하는 Method들의 이름을 통일 시키고, 피요한 경우에는 Superclass를 추출해 내는 것이 필요하다.

Change Preventers

변화 방해 요인으로 볼 수 있다. 

. Divergent Change (M): 하나의 Class가 다양한 원인으로 인해 수정되는 경우이다. 이는 Single Responsibility Principle을 위배했을 가능성이 높다. 그러므로 Class를 분리("Extract Class")를 고려하는 것이 좋다. 

. Shotgun Surgery (M): Divergent Change와 유사해 보인다. 하지만, 반대의 개념으로 하나를 수정하려고 하는데 수정이 산발적으로 여러 곳에서 일어나는 경우이다. 이는 Method나 Member 변수의 위치가 적절하지 못하다고 볼 수 있다. Method를 이동("Move Method")하거나 Field를 이동("Move Field")를 수행할 수 있다.

. Parallel Inheritance Hierarchies (M): 한 Class의 Subclass를 만들 때 마다, 매번 다른 Class의 Subclass를 만들어야 하는 상황이다. Shotgun Surgery의 특수항 경우라고 볼 수 있다. Shotgun Surgery와 유사하게 Move Method와 Move Field를 활용한다.

Dispensables

말 그대로 불필요한 것들이다. 

. Comments (M): Method에 설명을 위한 코멘트가 많이 달려 있는 것이다. 코멘트 보다는 코드가 자실을 잘 설명하도록
Refactoring나는 것이 좋다. 코멘트는 코드의 원리 혹은 무엇을 해야 할지 확실하지 않은 경우에 표시하는 정도가 좋다.

. Duplicate Code: 중복된 코드는 공통 부분으로 추출하여 Method로 만드는 것이 좋다.

. Lazy Class (M): Lazy Class는 결국 사용되지 않는 Class이다 관리를 위해서라도 이런 Class는 삭제하는 것이 좋다.

. Data Class (M): Getter, Setter만 있는 Class를 가리킨다. Class가 독립적으로 동작하지 않을 경우가 있으니, 어떻게 분리하여 다른 Class로 배분할지 고민하는 것이 좋다.

. Dead Code: 한마디로 죽은 코드로 삭제하는 방향이 맞다.

. Speculative Generality (M): 이는 미래 지향 코드로 필요하다고 생각해서 미리 넣어 둔 코드들인데, 실제 동작하지 않는 경우이다. 이도 삭제하는 편이 좋다.

Couplers

Coupling에서 온 개념으로 Class간의 잘못된 Dependency에 의한 Bad Smell이라고 볼 수 있다.

. Feature Envy (M): Method가 자신이 포함된 Class의 Data보다 다른 Class의 데이터를 계속 접근하려 하는 것이다. 이렇다면, Method를 옮기는 것을 고려하는 것이 좋다.

. Inappropriate Intimacy (M): 이 것은 Class가 다른 Class의 Field와 Method를 사용하려고 하는 경우이다.

. Message Chains (M): a->b()->c()->d()와 같이 연속해서 참조하여 Method를 호출하는 경우이다. 이런 경우는 Refacotring을 통해서 한번에 호출 되도록 Method를 옮기는 등의 방법을 고민해야 한다.

. Middle Man (M): 어떤 Class가 다른 한 Class의 Delagation 역할만 하는 경우이다. 이 때, 삭제하는 것을 고민해 볼 필요가 있다. 이 때, OCP를 위해서 Interface로 변경하거나 확장에 큰 도움이 되지 않는다면 삭제하는 것도 고민할 수 있겠다.

. Incomplete Library Class (M): library에서 제공하는 method들이 완전하지 않은 경우이다. 이 때도, 원작자가 그 기능을 추가해주면 좋지만, 그렇지 않다면 어떻게 필요한 기능을 추가할지에 대한 Bad Smell이다.


Refactoring Techniques

실제 Refactoring Techniques들은 계속 참조 할 수 있도록 Quick Reference Sheet를 만들어 보았다. 다른 분들과도 나누기 위해서 첨부하여 둔다.



돌아 보며

서두에서 이야기 한 것과 같이 이 내용도 계속해서 살펴 봐야 하는 내용이다. 특히, Refactoring은 설계를 한 후에 설계에 대해서 개선하는 작업이기 때문에 점진적인 작업과 반복 작업에 도움이 된다. 늘 주변을 깨끗하게 하는 것은 도움이 된다.



References

[1] Martin Fowler, John Brant, William Opdyke, and Don Roberts, "Refactoring: Improving the Design of Existing Code," Addison Wesely
[2] Refacotoring Guru, https://refactoring.guru/
[3] Source Making, https://sourcemaking.com/




+ Recent posts