목차
Previous
1회차 진행으로 책의 내용을 거의 그대로 따라치는 중이며 요약 정리는 2회차부터 진행할 예정
데이터베이스는 세부사항이다
아키텍처 관점에서 데이터베이스는 엔티티가 아니다.
즉 데이터베이스는 세부사항이라서 아키텍처의 구성요소 수준으로 끌어올릴 수 없다. 소프트웨어 시스템의 아키텍처와 데이터베이스의 관계를 건물로 비교하면 건물의 아키텍처와 문 손잡이의 관계와 같다. 중요하지 않다고 하는게 아니다. 데이터베이스는 애플리케이션 내부 데이터의 구조는 시스템 아키텍처에서 대단히 중요하다. 하지만 데이터베이스는 데이터 모델이 아니다.
데이터베이스는 일개 소프트웨어일 뿐이다. 데이터베이스는 데이터에 접근할 방법을 제공하는 유틸리티다. 아키텍처 관점에서 이러한 유틸리티는 저수준의 세부사항(메커니즘)일 뿐이기에 아키텍처와는 관련이 없다. 그리고 뛰어난 아키텍트라면 저수준의 메커니즘이 시스템 아키텍처를 오염시키는 일을 용납하지 않는다.
관계형 데이터베이스
에드거 커드가 1970년에 정의한 관계형 데이터베이스 원칙은 지속적으로 성장했고, 1980년대 중반이되어 데이터 저장소의 주류 형태가 되었다.
하지만, 이 모델이 데이터를 저장하고 접근하는데 있어 얼마나 뛰어나건간게 결국 기술일 뿐이다.
그리고 이는 관계형 데이터베이스가 세부사항임을 의미한다.
관계형 테이블은 특정한 형식의 데이터에 접근하는 경우에는 편리하지만, 데이터를 테이블에 행 단위로 배치한다는 자체는 아키텍처적으로 볼 때 중요하지 않다. 애플리케이션의 유스케이스는 이런 방식을 알 필요도 관여해서도 안된다. 데이터가 테이블 구조를 가진다는 사실은 아키텍처의 외부 원에 위치한 최하위 수준의 유틸리티 함수만 알아야 한다.
많은 데이터 접근 프레임워크가 테이블과 행이 객체 형태로 시스템 여기저기 돌아다니도록 허용했는데, 이는 잘못된 아키텍처 설계로 유스케이스, 업무 규칙, UI조차도 관계형 데이터 구조에 결합되어 버린다.
데이터베이스 시스템은 왜 이렇게 널리 사용되는가?
한마디로 디스크때문이었다.
지난 반세기 동안 회전식 자기 디스크(magnetic disk)는 데이터 저장소의 중심으로 많은 프로그래머가 다른 형태의 데이터 저장소도 있다는 사실조차 모를 정도였다. 계속해서 기술이 발전하며 더 물리적인 크기는 작아지고, 데이터의 수용량은 1TB이상을 저장할정도로 커지는 기술의 발전이 있었지만, 느리다는 매우 치명적인 특성이 있었다.
이처럼 디스크 때문에 피해갈 수 없는 시간 지연이라는 문제를 완화하기 위해 색인, 캐시, 쿼리 계획 최적화가 필요해졌다. 그리고 데이터를 표현하는 일종의 표준적인 방식도 필요했는데, 이러한 색인, 캐시, 쿼리 계획에서 작업중인 대상이 어떤 데이터를 알 수 있어야 했기 때문이다.
즉, 데이터 접근 및 관리 시스템이 필요했다. 시간이 지나며 이러한 시스템은 두 가지 유형으로 분리되었는데, 하나는 파일시스템, 다른 하나는 관계형 데이터베이스 관리 시스템(RDBMS)였다.
파일 시스템은 문서(document)기반으로 문서 전체를 자연스럽고 편리하게 저장하는 방법을 제공한다. 문서를 이름으로 저장하거나 조회할 때는 잘 동작하지만, 내용을 기준으로 검색할때는 어려울 뿐 아니라 오래걸린다.
반면 데이터베이스 시스템은 내용기반이다. 그렇기에 내용을 기반으로 레코드를 자연스럽고 편리하게 찾는 방법을 제공한다. 레코드가 서로 공유하는 일부 내용에 기반해서 다수의 레코드를 연관 짓는 데 매우 탁월하다. 하지만 안타깝게도 정형화되지 않은 문서를 저장하고 검색하는 데이는 대체로 부적합하다.
이들 두 시스템은 데이터를 디스크에 체계화해서, 각 시스템에 특화된 방식으로 접근해야 할 때 가능한 한 효율적으로 데이터를 저장하고 검색할 수 있도록 한다. 각 시스템은 데이터를 색인하고 배치하는 고유한 전략을 활용한다. 덧붙혀서 데이터를 빠르게 조작할 수 있도록 결국엔 관련있는 데이터를 RAM으로 가져온다.
디스크가 없다면 어떻게 될까?
이제 디스크는 RAM으로 대체되며 사양길을 걷고 있다. 그럼 디스크가 모두 사라지고 모든 데이터가 RAM에 저장된다면 데이터를 어떻게 체계화해야 할까? 이 데이터들을 연걸리스트, 트리, 해시 테이블, 스택, 큐같은 수많은 자료구조로 체계화할 것이며, 데이터에 접근할 때는 포인터나 참조를 사용할 것이다. 이게 프로그래머가 하는 일이기 때문이다.
사실 우리는 이미 그렇게 사용을 하고 있다.
세부사항
데이터베이스는 비트를 담는 거대한 그릇이며, 데이터를 장기적으로 보관하는 공간에 지나지 않는다. 따라서 아키텍처 관점에서 본다면 회전식 자기 디스크에 데이터가 있기만 한다면, 데이터가 어떤 형태인지는 신경 쓸 필요도 없고 써서도 안된다.
하지만 성능은?
성능은 관심사가 맞지만 데이터 저장소 측면에서 성능은 캡슐화해서 업무 규칙과 분리할 수 있는 관심사다. 이는 저수준의 관심사로 저수준의 데이터 접근 메커니즘 단에서 다룰 수 있다.
즉 성능은 시스템의 전반적인 아키텍처와는 고나련이 없다.
결론
체계화된 데이터 구조와 데이터 모델은 아키텍처적으로 중요하다.
반면 그저 데이터를 자기 디스크 표면에서 이리저리 옮길 뿐인 기술과 시스템은 아키텍처적으로 중요치 않다. 데이터를 테이블 구조로 만들고 SQL로만 접근하도록 하는 관계형 데이터베이스 시스템은 전자보단 후자와 관련이 깊다. 데이터는 중요하다. 데이터베이스는 세부사항이다.
웹은 세부사항이다
끝없이 반복하는 추
연산 능력을 어디에 두느냐에 대한 방식은 웹이 등장하기 전에도 후에도 계속해서 변화해왔다. 가장 최근 등장한 웹을 기준으로 봐도 처음에는 연산 능력이 서버 팜에 위치하고 브라우저는 멍청해질 것이라 유추했다. 그러다가 브라우저에 애플릿을 추가하기 시작햇다. 하지만 이 방식이 내키지 ㅇ낳아 동적인 내요은 다시 서버로 이동시켰다.
하지만 또 이러한 방식이 마음에 안들어 웹2.0을 고안했고, Ajax와 자바스크립트를 이용해 처리 과정의 많은 부분을 브라우저로 옮겼다. 그리고 이제 노드(Node.js)를 이용해 자바스크림트를 다시 서버로 이동시키는 방식이 사용되고 있다.
하지만, 이렇게 반복되는 무게추의 이동은 웹으로부터 시작된 것은 아니다. 웹 이전에는 클라이언트-서버 아키텍처가 있었고, 그 전에는 중앙집중식 미니컴퓨터가 있었다, 그전에도 계속 해서 여러 기술들이 있었다. 연산 능력을 중앙에 집중하는 방식과 분산하는 방식 사이에서 끊임없이 무게추는 움직인다. 즉, IT역사 전체로 시야를 넓혀보면 웹은 무엇도 바꾸지 않았다.
아키텍트로서 우리는 멀리 내다봐야한다. 이 진동은 그저 핵심 업무 규칙의 중심에서 밀어내고 싶은 단기적인 문제일 뿐이다.
요약
GUI는 세부사항이다. 웹은 GUI다 따라서 웹은 세부사항이다.
그리고 아키텍트라면 이러한 세부사항을 핵심 업무 로직에서 분리된 경계 바깥에 두어야 한다.
이렇게 생각해 보자. 웹은 입출력 장치다. 다만, 웹과 같은 GUI는 너무 특이하고 다채롭기에 장치 독립적인 아키텍처를 추구하는 일이 힘들다고 할 수도 있다. 자바스크립트의 유효성 검증이나 드래그-앤-드롭 방식의 Ajax 호출, 그리고 웹 페이지에 넣을 수 있는 다른 무수한 위젯과 가젯으로 인한 복잡함을 생각해보면 웹에서 장치 독립성은 비현실적이라 생각할 수 있다.
이런 주장도 어느정도는 맞는 말인게 브라우저와 웹 애플리케이션이 함께 추는 춤은 데스크톱 GUI와 데스크톱 애플리케이션의 관계와는 차이가 난다 이러한 춤을 추상화하려는 시도는 유닉스로부터 장치를 추상화햇던 것과 다르게 성공할 가능성이 없어보인다.
하지만 UI와 애플리케이션 사이에는 추상화가 가능한 또 다른 경계가 존재한다.
업무 로직은 다수의 유스케이스로 구성되며, 각 유스케이스는 사용자를 대신해서 일부 함수를 수행하는 것으로 불 수 있다. 각 유스케이스는 입력 데이터, 수행할 처리 과정, 출력 데이터를 기반으로 기술할 수 있다.
UI와 애플리케이션이 함께 춤추는 동안, 어떤 시점이 되면 입력 데이터가 완전히 구성될 것이고, 그러면 유스케이스를 실행할 수 있게 된다. 유스케이스가 종료되면 해당 입력 데이터에 따른 결과 데이터를 UI와 애플리케이션이 함께 추는 춤으로 다시 되돌려줄 수 있다.
완전한 입력 데이터와 그에 따른 출력 데이터는 데이터 구조로 만들어 유스케이스를 실행하는 처리 과정의 입력 값과 출력 값으로 사용할 수 있다. 이 방식을 따르면 각 유스케이스가 장치 독립적인 방식으로 UI라는 입출력 장치를 동작시킨다고 간주할 수 있다.
결론
이러한 종류의 추상화는 만들기 쉽지 않고, 제대로 만들려면 수차례의 반복 과정을 거쳐야 한다.
하지만 중요한건 가능하다는 것이고, 이러한 추상화가 꼭 필요하다고 주장하는 마케터들은 넘친다.
프레임워크는 세부사항이다.
인기도 많을 뿐더러 무료인 데다 강력하고 유용한 프레임워크들이 많다.
하지만 아무리 해도 프레임워크는 아키텍처가 될 순 없다.
프레임워크 제작자
대다수의 프레임워크 제작자는 커뮤니티에 도움이 되고자 자신의 작업물을 무료로 제공한다.
훌륭한 행위다. 하지만 이런 동기와는 관계없이 이들은 우리가 풀어야 할 특별한 관심사를 염두해 두지 않는다. 프레임워크 제작자는 우리를 알지 못하고, 우리가 풀어야 할 문제도 모르기 때문이다.
물론, 우리의 문제와 프레임워크가 풀려는 문제는 겹치는 부분이 많을 것이다. 그렇기에 인기가 있을테니 말이다. 겹치는 영역이 크면 클수록 프레임워크는 실제로 더 유용해진다.
혼인 관계의 비대칭성
우리와 프레임워크 제작자 사이의 관계는 비대칭적이다. 우린 프레임워크를 위해 헌신을 해야 하지만, 프레임워크 제작자는 우리를 위해 딱히 어떠한 헌신을 하지 않는다.
그리고 이 부분을 잘 생각할 필요가 있다. 우리는 프레임워크를 사용하기 위해 프레임워크 제작자가 제공하는 문서를 꼼꼼히 읽는다. 이 문서에서 프레임워크 제작자와 그 프레임워크의 다른 사용자는 우리가 만들 소프트웨어와 프레임워크를 어떻게 통합할 수 있을지 조언한다.
대게의 경우 프레임워크를 중심에 두고 우리의 아키텍처는 그 바깥을 감싸야 한다고 말한다. 또한 이들은 프레임워크의 기반 클래스에서 직접 파생하거나, 프레임워크의 기능들을 업무 객체에 바로 임포트해서 사용하라고 권한다. 즉, 우리의 애플리케이션과 프레임워크가 공고하게 결합될 것을 강하게 역설한다.
프레임워크 제작자 입장에선 프레임워크와의 이러한 결합이 위험요소가 되지 않는다. 왜냐하면 제작자는 그 프레임워크에 대해 절대적인 제어권을 가지고 있기 때문이다. 그렇기에 사실상 프레임워크 제작자는 우리이게 프레임워크와 혼인하기를 요구하는 것이다. 이러한 관계는 일방적이고, 모든 위험과 부담은 우리만 감수해야 한다.
위험 요인
이러한 비대칭적 관계에서 우리가 고려해야 할 위험 요인들은 다음과 같다.
•
프레임워크 아키텍처는 그다지 깔끔하지 않은 경우가 많다. 의존성 규칙을 위반하는 경향도 있다. 자신의 프레임워크가 우리의 안쪽 원과 결합되기를 바라는데, 프레임워크가 한 번 안으로 들어오게 되면 다시는 원 밖으로 나오지 않을 것이다.
•
프레임워크는 애플리케이션의 초기 기능을 만드는 데는 도움이 될 것이다. 하지만 제품이 성숙해지면서 프레임워크가 제공하는 기능과 틀을 벗어나게 될 것이다. 시간이 지나면서 프레임워크와 계속 싸우고 있는 자신을 발견하게 될 것이다.
•
프레임워크는 우리이게 도움이 안되는 방향으로 진화할 수도 있다. 필요도 없는 방향으로 진화한 신규 버전에 맞게 업그레이드 하느라 높은 비용을 소모할수도 있다. 더 최악은 사용중이던 기능이 사라지거나 반영하기 힘든 형태로 변경될 수도 있다는 부분이다.
•
새롭고 더 나은 프레임워크가 등장해서 갈아타고 싶을 수도 있다.
해결책
해결책은 단순하다. 프레임워크와 적당히 거리를 두는 것이다. 프레임워크를 아키텍처의 바깥쪽 원에 속하는 세부사항으로 취급하자. 업무 객체를 만들 때 프레임워크가 자신의 기반 클래스로부터 파생하기를 요구한다면, 거절하자. 대신 프록시(Proxy)를 만들고, 업무 규칙에 플러그인 할 수 있는 컴포넌트에 이들 프록시 객체를 위치시키자. 그렇게 하여 프레임워크가 핵심 코드 안으로 들어오지 못하도록 하자. 대신 핵심 코드에 플러그인 할 수 있는 컴포넌트에 프레임워크를 통합하고, 의존성 규칙을 준수하자.
대표적으로 스프링(Spring)프레임워크가 있다. 스프링은 훌륭한 DI프레임워크로 의존성을 주입할 때 스프링의 오토 와이어링(auto-wiring)기능을 사용할 것이다. 물론 이 방법은 나쁘지 않지만 @Autowired 어노테이션이 업무 객체 도처에 산재해있어서는 안된다. 업무 객체는 절대로 스프링에 대해 알아선 안된다. 업무 객체보단 메인(Main) 컴포넌트에서 스프링을 사용해 의존성을 주입하는 편이 낫다. 메인은 아키텍처 내에서 가장 지저분한 최저 수준의 컴포넌트이기 때문에 스프링을 알아도 상관없다.
결론
프레임워크와 첫 만남부터 깊은 관계로 결합(결혼)하려 하지 말자.
가급적이면 프레임워크를 가능한 오래 아키텍처 경계 너머에 두자. 젖소를 사지 않고도 우유를 얻을 방법을 찾을 수 있을 것이다.
사례 연구: 비디오 판매
제품
이 소프트웨어의 기본적인 발상은 단순하다.
판매하길 원하는 비디오들을 개인 혹은 기업에게 웹을 통해 판매한다.
개인은 단품 가격을 지불해 스트리밍으로 보거나, 더 높은 가격을 내고 비디오를 다운로드해서 영구 소장할 수도 있다.
기업용 라이선스는 스트리밍 전용이며, 대량 구매르 하면 할인을 받을 수 있다. 일반적으로 개인은 시청자인 동시에 구매자다. 반면 기업은 다른 사람들이 시청할 비디오를 구매하는 사람이 따로 있다.
비디오 제작자는 비디오 파일과 비디오에 대한 설명서, 부속 파일을 제공해야 한다. 부속 파일에는 시험, 문제, 해법, 소스 코드 등이 포함된다.
관리자느 ㄴ신규 비디오 시리즈물을 추가하거나 기존 시리즈물에 비디오를 추가 또는 삭제하며, 다양한 라이선스에 맞춰 가격을 책정한다.
시스템의 초기 아키텍처를 결정하는 첫 단계는 액터와 유스케이스를 식별하는 일이다.
유스케이스 분석
전형적인 유스케이스 분석
네 개의 주요 액터는 분명하다. 단일 책임 원칙에 따르면 네 액터가 시스템이 변경되어야 할 네 가지 주요 근원이 된다. 신규 기능 추가 및 기존 기능 변경이 필요하다면, 그 이유는 반드시 이들 액터 중 하나에게 해당 기능을 제공하기 위해서다. 따라서 우리는 시스템을 분할하여, 특정 액터를 위한 변경이 나머지 액터에게는 전혀 영향을 미치지 않게 만들고자 한다.
위 유스케이스 목록은 완벽하지 않은데, 로그인이나 로그아웃관련 유스케이스도 찾을 수 없다.
이런 유스케이스를 뺀 이유는 문제의 범위를 이 번 챕터에서 다룰 수 있을정도로 줄이기 위해서다.
유스케이스 중 점선으로 된 유스케이스(라이선스 구매하기, 카탈로그 조회하기)가 있다.
이들은 추상 유스케이스 로 범용적인 정책을 담고 있으며, 다른 유스케이스에서 이를 더 구체화한다. 보다시피 시청자 입장에서 카탈로그 조회하기와 구매자 입장에서 카탈로그 조회하기 유스케이스는 모두 카탈로그 조회하기라는 추상 유스케이스를 상속받는다.
한편 이 추상화를 꼭 생성해야만 했던 것은 아니다. 이 추상 유스케이스를 다이어그램에서 없애더라도 전체 제품의 기능을 손상시키지 않는다. 하지만, 두 유스케이슨 너무 비슷하기에 유사성을 식별해서 분석 초기에 통합하는 방법을 찾는 편이 더 현명하다고 판단했다.
컴포넌트 아키텍처
이제 액터와 유스케이스를 식별했으니, 예비 단계의 컴포넌트 아키텍처를 만들어 볼 수 있다.
예비 단계의 컴포넌트 아키텍처
이중으로 된 선은 아키텍처 경계(Architecture Boundary)를 나타낸다.
뷰(View), 프레젠터(Presenter), 인터랙터(Interactor), 컨트롤러(Controller) 로 분리된 전형적인 분할 방법을 확인할 수 있다. 또한 대응하는 액터에 따라 카테고리를 분리했다는 사실도 확인할 수 있다. 위 그림에서 각 컴포넌트는 단일 jar 파일 또는 단일 dll 파일에 해당하며 각각 자신에게 할당된 뷰,프레젠터,인터랙터,컨트롤러를 포함한다.
특수한 컴포넌트인 Catalog View와 Catalog Presenter에 주목해보면 이는 카탈로그 조회하기라는 추상 유스케이스를 처리하는 저자만의 방식으로 뷰와 프레젠터는 컴포넌트 내부에 추상클래스로 코드화 될 것이며, 상속받는 컴포넌트에서는 이들 추상 클래스로부터 상속받은 뷰와 프레젠터 클래스들을 포함한다.
뷰와 프레젠터를 합쳐서 같은 jar에 두고, 인터랙터, 컨트롤러, 유틸리티는 그대로 개별 jar 파일에 두는 방식도 있고, 더 원시적으로 합치는 방법도 있는데 두 개의 jar파일을 생성해서 하나에는 뷰와 프레젠터를, 다른 하나에는 나머지 모두를 포함시키는 방법이다.
이처럼 선택지를 열어두면 나중에 시스템이 변경되는 양상에 맞춰서 시스템 배포 방식을 조정할 수 있다.
의존성 관리
위 다이어그램을 보면 제어흐름은 우측에서 좌측으로 흐른다.
입력이 컨트롤러에서 발생하면 인터랙터에 의해 처리되어 결과가 만들어지고, 프레젠터가 결과의 포맷을 변경하고 뷰가 화면에 표시한다.
모든 화살표가 우측에서 좌측을 가리키지는 않는다. 사실 대부분의 화살표는 좌측에서 우측으로 향하는데 이는 아케턱처가 의존성 규칙을 준수하기 때문이다. 모든 의존성은 경계선을 한 방향으로만 가로지르는데 항상 더 높은 수준의 정책을 포함하는 컴포넌트를 향한다.
그리고 사용 관계(열린 화살표)는 제어흐름과 같은 방향을 가리키며 상속 관계(닫힌 화살표)는 제어흐름과 반대 방향을 가리키는데 이는 개방 폐쇄 원칙을 적용했다는 점을 시사한다.
이를 통해 우리는 의존성이 올바른 방향으로 흐르고, 따라서 저수준의 세부사항에서 발생한 변경이 상위로 파급되어 상위 수준의 정책에 영향을 미치지는 않음을 보장할 수 있다.
결론
위 다이어그램은 두 가지 서로 다른 차원의 분리 개념을 포함하고 있다.
하나는 단일 책임 원칙에 기반한 액터의 분리이며, 두 번째는 의존성 규칙이다. 이 두 차원은 모두 서로 다른 이유로, 서로 다른 속도로 변경되는 컴포넌트를 분리하는데 목적이 있다. 서로 다른 이유라는 것은 액터와 관련이 있으며, 서로 다른 속도라는 것은 정책 수준과 관련이 있다.
이런 방식으로 코드를 구조화하고 나면 시스템을 실제로 배포하는 방식은 다양하게 선택할 수 있게 된다. 상황에 맞게 컴포넌트들을 배포 가능한 단위로 묶을 수도 있고, 상황이 변하면 변한 상황에 맞춰 묶는 단위를 바꾸기도 쉬워진다.