지금까지 클래스 단위의 모듈화, 컴포넌트 단위의 모듈화 방법을 정리하였다.
이제 단계를 높여서 서브 시스템 아키텍처를 설계해보자.
소프트웨어를 만들 때 requirement 를 모두 찾았다면 제일 먼저 아키텍처를 그려야 한다.
'아키텍처' 라고 하면 big picture 를 생각하면 된다.
그리고 제일 먼저 서브시스템과 컴포넌트를 찾아서 이들 사이의 관계 (인터페이스) 를 정의해야 한다.
서브 시스템을 정의해보면 같은 특성을 공유하는 element 의 그룹으로 정의한다.
예를 들어 UI 와 같이 사람과 컴퓨터 사이의 인터페이스 역할을 하는 element 를 모으면 'HCI 서브 시스템' 과 같이 분류할 수 있다.
이렇게 서브 시스템을 나누면 몇 가지 장점이 있다.
'독립적인' 개발 단위 크기가 줄어들고 complexity 가 작아진다.
그 결과 재사용이 쉬워지고 유지보수가 용이해지며, 이식성도 좋아진다.
소스코드를 수정하거나 새로운 기능을 추가할 때 어디에 추가하면 될 지, 어디를 고치면 될 지 파악하기가 쉽고,
그렇게 수정하더라도 다른 모듈에 영향이 거의 없기 때문이다.
이식성이 좋은 것도, 하드웨어에 의존적인 코드를 별도로 분리해두고 해당 코드만 하드웨어에 맞춰 수정하기만 하면
나머지 계층은 동일한 인터페이스로 같은 기능을 수행할 수 있기 때문에 이식성이 좋다.
결국 중요한 것은 변하지 않는 최소 개수의 인터페이스를 잘 정의하는 것이다.
서브 시스템을 나누는 방법은 크게 layering & partitioning 과 MVC 2가지가 있다.
Layering & Partitioning
먼저 Layering 은 서로 다른 서브 시스템을 서로 다른 추상화 레벨로 나누어 표현하는 것이다.
간단하게 서브시스템을 수직으로 나누는 것이 layering 이다.
partitioning 은 같은 레이어 안에서 기능 별로 서브시스템을 나누는 것을 partitioning 이라고 말한다.
소프트웨어를 설계할 때는 layering 과 partitioning 을 통해서 아키텍처를 먼저 설계해야 한다.
대표적인 예시는 안드로이드 시스템의 구조를 생각할 수 있다.
안드로이드 시스템은 커널 - 라이브러리 - 어플리케이션 프레임워크 - 어플레케이션 계층으로 나누어져있으며
밑으로 갈수록 하드웨어와 가깝고 위로 갈수록 어플리케이션에 가까워진다.
각 레이어 안에는 기능별로 여러 서브시스템이 나누어져 있으며, 각 서브시스템은 자신과 인접한 밑 계층의 인터페이스를 사용하여 자신의 기능을 구현한다.
Layered Architecture 는 크게 closed architecture 와 open architecture 로 나눌 수 있다.
closed architecture 는 각 레이어가 자신과 인접한 바로 밑 계층에만 메세지를 보낼 수 있는 구조이고
open architecture 는 각 레이어가 인접 여부와 상관없이 모든 밑 계층에 메세지를 보낼 수 있는 구조이다.
이 두 구조의 특징을 비교해보자.
open architecture 는 밑 계층에게 직접 메세지를 전달할 수 있는 반면
closed architecture 는 밑 계층에게 메세지를 전달하려면 인접 계층을 여러번 경유해서 전달해야 한다.
레이어가 N 개라면, 최대 N-1번의 메세지를 전달하는 과정이 필요한 것이다
따라서 성능면에서는 open architecture 가 더 좋다.
또한 closed architecture 는 같은 메세지를 여러 계층을 경유하여 전달해야 하기 때문에
각 계층에서 같은 메세지를 받을 수 있는 메서드를 중복해서 작성해야 한다.
즉, 같은 기능을 수행하는 코드가 중복된다는 단점이 있다.
따라서 작성하는 코드의 양과 컴팩트한 정도도 open architecture 가 더 좋다.
하지만 encapsulation 관점에서는 closed architecture 가 더 좋다.
closed architecture 는 각 레이어가 자신의 기능을 자신과 인접한 상위 계층에게만 보여주기 때문에 레이어간 의존성을 최소화할 수 있기 때문이다.
덕분에 한 레이어가 수정되어도, 그 수정사항이 미치는 영향 범위가 작다.
open architecture 는 특정 레이어의 기능을 그 상위 모든 계층의 레이어에게 공개해야 하기 때문에 encapsulation 관점에서는 좋지 않다.
정리하면 closed architecture 는 encapsulation 을 지킬 수 있어 레이어간 의존성을 줄이고 수정사항의 영향 범위를 줄일 수 있지만 중복 코드를 작성해야 하며, 메세지를 여러번 전달해야 하므로 성능이 비교적 좋지 않다.
open architecture 는 성능이 더 좋고, 중복되는 코드가 없어 작성하는 코드의 양을 줄일 수 있지만 encapsulation 원칙을 지킬 수 없다는 단점이 있다.
closed architecture 의 예시로는 네트워크의 OSI 7계층을 생각할 수 있다.
OSI 의 각 계층은 인접 계층과만 소통하며, 그 외 다른 계층의 기능과 동작은 전혀 모른다.
MVC
하지만 아키텍처를 그리다보면 Layering, Partitioning 만으로 구조 설계를 다 할 수 없는 경우가 있다.
예를 들면 같은 데이터를 화면에 따라 서로 다른 포맷으로 보여주어야 하는 경우를 생각할 수 있다.
특히 이때 한 화면에서 데이터를 수정했을 때 다른 화면에도 그 수정된 정보가 바로 반영이 되어야하는 상황은 자주 있다.
이 과정에서 그럼에도 데이터를 다루는 코어 기능은 화면을 보여주는 기능과 분리가 되어있어야 한다.
예를 들어 윈도우 탐색기에서 같은 폴더를 보여주는 윈도우 3개를 띄우고, 하나의 윈도우에서 새로운 폴더를 만들면 나머지 2개 윈도우에도 동시에 새로운 폴더가 보인다. (맥 파인더에서도 똑같이 동작했다)
이렇게 하나의 뷰에서의 변화가 다른 뷰에도 동시에 즉각적으로 반영되어야 하는 상황이 있을 때, 위의 Layering, Partitioning 만으로는 해결할 수 없다.
이를 해결할 수 있는 대표적인 아키텍처가 바로 MVC 아키텍처이다.
MVC 는 Model, View, Controller 의 줄임말이다.
이름 그대로 시스템을 Model, View, Controller 역할로 나눠 구성하는 것을 말한다.
Model 은 어플리케이션의 핵심 기능을 제공하며, 자신이 의존하는 view, controller 를 알고 있다.
별도의 데이터베이스가 없다면 Model 이 데이터를 갖는다. (엔티티 클래스의 역할과 동일)
따라서 자신의 데이터가 변경되면 자신이 알고있는 모든 view, controller 에 이를 알려주고 업데이트한다. (propagation mechanism)
View 는 데이터를 사용자에게 보여주는 역할을 수행한다.
이때 데이터를 model 로부터 받으므로, 모델의 데이터가 수정되면 View 에서 보여지는 데이터도 수정된다.
View 는 자신과 관련된 controller 를 만든다.
따라서 하나의 윈도우에 대해서는 하나의 뷰와 컨트롤러가 쌍으로 함께 존재한다.
Controller 는 사용자의 입력을 event 형태로 받고, 이벤트에 따라 model 에서 제공하는 기능을 트리거하는 역할을 수행한다.
이 관계를 그림으로는 위와 같이 그릴 수 있다.
Model 은 자신의 변경사항을 View 와 Controller 에게 전파한다.
View, Controller 는 서로서로에게 접근할 수 있고, Model 에도 접근할 수 있다.
이제 MVC 아키텍처의 동작 과정을 식당 정보를 수정하는 use case 를 MVC 아키텍처로 표현해보며 정리해보자.
먼저 각 컴포넌트 클래스를 다이어그램으로 나타낸 모습은 위와 같다.
Restaurant Model 클래스를 보면 view 에 보여줄 restaurant core data 를 가지고 있다.
그리고 자신의 데이터가 수정되면 view, controller 에게 알려주어야 하기 때문에
이들의 참조를 observers 리스트로 갖고 있다.
이 모델을 사용하는 새로운 뷰, 컨트롤러 쌍이 추가되면 attach() 메서드를, 삭제되면 detach() 메서드를 호출해서 추가/삭제한다.
자신의 데이터가 바뀌면 notify() 메서드를 호출하여 연결된 view, controller 에게 알려준다.
View 클래스는 자신이 화면에 보여주기 위한 데이터를 attribute 로 갖고 있다.
Controller 클래스는 식당 수정 이벤트를 받으면 changeRestaurant() 메서드를 호출하여 식당 정보를 수정한다.
multiplicity 를 보면 Model 과 View, Controller 사이는 일대다 관계이고,
뷰와 컨트롤러 사이는 일대일 관계임을 알 수 이다.
하나의 모델에 대해서 여러 뷰가 존재할 수 있고, 각각의 뷰에는 그 뷰에 대응하는 컨트롤러 클래스가 1개씩 존재한다.
이제 구체적으로 어떻게 식당 수정 작업을 수행하는지 시퀀스 다이어그램으로 살펴보자.
사용자가 식당 정보를 수정하고, '수정' 버튼을 클릭하면
그 이벤트를 기반으로 컨트롤러 클래스의 changeRestaurant() 메서드가 먼저 호출된다.
메서드 내에서는 Model 의 modifyRestaurant() 메서드를 호출한다.
모델은 헤당 메서드 activation 내에서 식당 정보를 수정한 뒤, 데이터가 수정되었음을 등록된 view와 controller 에게 알린다.
이를 위해 notify() 메서드를 호출하면 그 안에서 view, controller 의 update() 메서드를 각각 호출한다.
view 의 update() 메서드를 호출하면 view 는 displayRestaurant() 메서드를 호출하여 화면을 업데이트한다.
이때 자신이 갖고 있는 view data 를 수정하기 위해 내부적으로 Model 의 getRestaurantData() 메서드로 자신이 가진 식당 정보를 업데이트한다.
이 과정은 컨트롤러 클래스에서도 동일하게 일어난다.
즉, 컨트롤러 클래스도 자신의 attribute 로 식당 데이터를 갖고 있다는 것이다.
컨트롤러가 식당 데이터를 갖는 이유는, 컨트롤러는 '데이터의 변경을 모델에게 알리는' 역할을 수행하기 때문이다.
이때 데이터가 변경되었음을 감지하기 위해서는 컨트롤러 역시 가장 최근의 데이터를 갖고 있어야 한다.
따라서 컨트롤러의 update 함수를 호출하면 컨트롤러가 보유한 최근 데이터를 갱신해준다.
(컨트롤러가 최근 데이터를 가질 필요가 왜 있을까? 컨트롤러의 메서드 동작은 유저 event 에 의해서 호출되는 것 아닌가? 시스템 성능을 높이기 위해서 유저가 아무런 수정 없이 '수정' 버튼을 클릭했을 때는 화면을 갱신하지 않기 위해서 최근 데이터를 갖고 있는 것이라고 하면 또 납득이 되기는 한다..)
정리하면 처음에는 유저가 이벤트를 발생시킨 그 윈도우의 컨트롤러 메서드가 호출되어 변경을 모델에게 알리면
모델은 데이터를 변경한 뒤, notify 를 호출하여 자신이 알고 있는 모든 컨트롤러, 뷰에 이 변경사항을 알리고 업데이트를 지시한다.
'CS > 소프트웨어공학' 카테고리의 다른 글
[소프트웨어공학] 18. Refining the Requirements Model (1) | 2025.06.07 |
---|---|
[소프트웨어공학] 17. State Machine 예제 (수정예정) (0) | 2025.06.07 |
[소프트웨어공학] 16. Designing Boundary Class (0) | 2025.06.07 |
[소프트웨어공학] 15. Detailed Design (2) | 2025.06.05 |
[소프트웨어공학] 14. Object Interaction (0) | 2025.06.04 |