공부하는 블로그

#11 - 아키텍쳐 본문

design patterns

#11 - 아키텍쳐

devtimothy 2019. 4. 15. 09:05

#11 - 아키텍쳐

우리는 큰 문제를 작은 기능들의 조각으로 나누었고, 작은 조각에 대해서는 해결 방법을 알고 있다. 그러나 유스케이스 다이어그램과 특징 리스트, 그 외에도 고려해야 할 사항이 많다. 처음에 우리는 무엇을 해야할까? 당연하게도 요번 챕터 주제와 같이 아키텍쳐가 필요하다.

아키텍쳐는 시스템의 분할, 나뉜 부분들 사이의 연결과 상호 작용 메커니즘, 그리고 시스템의 디자인에 사용된 원리와 결정 사항들을 담고 있는 시스템의 구조를 말한다.

아키텍쳐는 디자인의 구조이고, 프로그램의 가장 중요한 부분들과 그들 사이의 관계를 명확히 보여주며, 뒤죽박죽 복잡한 내용을 잘 정리된 프로그램으로 바꾸는데 도움을 준다.

우리가 1장에서 말했던 <쉬운 3단계로 좋은 소프트웨어 만들기>를 여기서도 적용 가능하다.

기능

첫째 단계는 <고객이 원하는 기능을 하도록 하라> 이다. 특징 리스트를 잠시 살펴보자

게임 시스템 프레임웍 특징 리스트
1. 다양한 타입의 지형을 지원한다.
2. SF소설이나 판타지 소설에 등장하는 가상의 시대를 포함한 다양한 시대를 지원한다.
3. 게임 특성에 맞는 여러 타입의 부대 또는 유닛들을 지원한다.
4. 새로운 작전 수행이나 전쟁 시나리오를 위한 추가 모듈을 지원한다.
5. 사각형의 타일들로 구성된 보드를 제공하고 각 타일은 지형 타입을 가지고 있는다.
6. 누구 차례인지에 대한 정보를 유지한다.
7. 기본적인 이동을 관장한다.

이 중에서 뭐가 가장 중요할까? 어디서부턴가는 먼저 시작해야 한다. 구성요소가 없는데 시스템 구성 요소 사이의 관계에 대해 논하기란 어렵다. Board와 Units 모듈이 어떻게 상호작용하는지 알고 싶다면? 먼저는 기본적인 내용을 먼저 알아야 할 것이다. )물론 관계에 대해 알아내는 것만이 가장 중요한 일은 아니다.)

아키텍쳐에 대한 세 가지 질문

  1. 시스템의 본질이 되는 부분인가?
    • 그 특징이 시스템의 핵심인가?
  2. 이것은 "도대체" 무슨 의미인가?
    • 무슨 뜻인지 확신이 서지 않는다면 그 특징에 대해 관심을 기울이라.
    • 확실치 않은 일은 오래걸리고, 시스템의 나머지 부분을 발목을 잡을수 있다.
  3. 도대체 어떻게 해야 하나?
    • 구현하기 까다로운 문제가 있다면, 시간을 들여 문제 발생을 막는다.

위 세가지 질문을 보았다면, 무엇이 중요한지 살펴보자.

  • 게임을 위한 보드 - 1
  • 게임 특유의 유닛 - 1, 2
  • 프레임웍은 기본적인 이동을 관장한다. - 3

시스템의 본질

시스템의 본질이란 가장 기본적인 수준이 완성되었을 때를 말한다. 우리가 부가적으로 넣은 화려한 기능이나 아이디어가 본질은 아니다. 특징을 찾을때는 "이 특징이 구현되지 않으면 시스템이 해야 할 일을 정말 하는걸까?"라는 답에 "아니요"라고 답한다면 본질적인 특징을 찾은 것이다. 쉽게 말하자면, "잘 돌아 가는가?"부터 확인하는 것이다.

나의 개발 이야기를 잠깐 하자면, 나는 레스토랑 예약 서비스를 운영하는 스타트업에서 일하고 있다. 어느날 음식을 먹으러 갔다가 음식점 앞에 대기 리스트 적는 판이 있는 것을 보고 우리 서비스에 적용하면 좋겠다는 생각이 들었다. 가장 먼저 했던 일은 대기등록용 앱을 만드는 일이었는데, 정말 간단하게 앱을 만들었다.

  1. 손님이 대기 등록을 하게 한다. (이름, 전화번호, 인원 수)
  2. 매장 관리자 앱과 대기등록 앱이 실시간으로 목록이 갱신되게 한다.

차후에는 여러가지 기능이 무궁무진하게 많아졌지만… 최소한의 조건을 맞추어 개발을 했던 기억이 있다. 초기 단계에서는 시스템이 어떻게 발전할지 몰라 세부적인 것을 만드는 것을 자제하기도 했다. 시스템의 전체를 보기 위함이다.

특징 리스트가 있다고 하더라도 결정하기 애매한 것들이 많이 있을 것이다. 비록 시스템을 직접 작성해보진 않았더라도 우리가 어떻게 해야할지 모르는 것은 아니다. 새로운 세부사항이 있는 것 뿐이다. 그러나 가령 멀티 쓰레드를 이용한 채팅 서버 구축을 해야하는데, 쓰레드와 네트워크 프로그래밍에 대해 잘 모른다면, 이는 어떻게 해야할지 모르는 것이다.

애매한 것들이 많이 있겠지만, 어찌됐건 우리가 가장 먼저 해야할 일은 익숙한 부분부터 찾아서 시작하는 것이다.

위험 요소를 줄여라

  • 게임을 위한 보드
  • 게임 특유의 유닛
  • 프레임웍은 기본적인 이동을 관장한다.

여러 과정을 거쳐서 우리는 위 세가지가 중요한 특징임을 확인했다. 그렇다면 뭐부터 만들어야 할까. 다 중요해보이면 어쩌지? 혹은 사람들마다 중요하게 여기는 게 다르다면 어떻게 해야할까?

중요한 것은 위험 요소를 줄이는 방향으로 진행하는 것이다. 위험 요소를 최소화 하려면 Board 인터페이스부터 작성하는 게 좋겠다.

문제점

보드의 높이와 너비는 게임 설계자가 제공한다.

보드는 주어진 위치에 있는 타일을 반환한다.

유닛들을 타일에 추가하고, 주어진 x, y 위치에 있는 모든 유닛들을 반환한다.

할 일

  1. Board.ts 클래스를 새로 만든다.
  2. Board에서 너비와 높이를 받아서 새로운 보드를 만드는 생성자를 추가한다. 생성자는 보드를 사각형 타일들로 x,y 위치에 하나씩 채운다.
  3. 타일의 x, y 위치가 주어지면 주어진 위치에 있는 타일을 반환할 메소드를 작성한다.
  4. 타일의 x,y 위치에 맞게 유닛을 추가하는 메소드를 작성한다.
  5. 주어진 x, y 위치에 있는 타일 위의 모든 유닛들을 반환하는 메소드를 작성하라.

위 요구사항은 상식선에서 보강한 내용이다. 그러나 경계해야 할 것은 너무 구체적인 내용까지는 생각하지 않는 것이다. 너무 깊이 세세한 내용까지 파고들면 좋은 유스케이스를 뽑아내는데 도움이 되질 않는다.

시나리오들이 위험요소를 줄이는 데 도움이 많이 된다. 사실 많은 사람들이 유스케이스 작성에 그렇게 공을 들이지는 않는다. 예약 서비스를 개발한다고 생각해보면, 간단하게 생각할 때는 고객이 예약을 한다. 예약을 할때는 이름, 전화번호, 인원 수를 입력한다. 정도이지만, 사실 예약 경로가 다른 루트로 들어올 수도 있다. 인스타그램 DM, 전화, 블로그, 카톡 등… 전화번호를 모르지만 예약은 등록해야 하는 경우가 생길수 있다.

지금 우리는 보드를 만들고 있고, 여기서는 보드가 어떻게 사용되는지를 생각할 수 있다면 더 좋을 것이다. 가령 8 X 10 칸의 보드에서 1,3 위치에 있는 부대가 4,5 위치에 있는 부대를 친다 등의 내용들을 나타낼 수 있겠다. 아마 지금 내가 생각지 못한 다양한 이슈들이 있을 것이다.

시나리오를 작성하면 유스케이스는 필요가 없을까? 그렇지는 않다. 시나리오는 빠르게 문제를 해결 할 때 보편적인 요구사항을 찾는데 도움이 된다. 그러나 이는 유스케이스를 이용할 하나의 경로일 뿐이다. 위험요소를 줄이는데는 고객에게 좋은 요구사항을 뽑아내는 것이다.

우리는 시나리오를 이용해서 모듈이나 코드 조각의 기초적인 내용을 이해하고, 앱의 기본적인 부분들을 채워나갈 수 있다. 자, 그러면 시나리오를 한번 작성해보자.

보드 시나리오

  1. 게임 설계자가 높이와 너비를 가지고 보드를 생성한다.
  2. 플레이어 2가 탱크들을 4,5로 옮긴다.
  3. 플레이어 2가 군대를 4,5로 이동한다.
  4. 플레이어 1이 포병을 4,5로 이동한다.
  5. 게임이 4,5의 유닛들을 요구한다.
  6. 플레이어 1과 플레이어2가 전쟁을 치른다.
  7. 게임이 4,5의 지형을 요구한다.
  8. 플레이어 2의 유닛이 전쟁을 승리한다.
  9. 플레이어 1의 유닛들이 4,5에서 제거된다.
  10. 플레이어 1이 잠수함을 2,2로 이동한다.
  11. 게임이 2,2에 있는 지형을 요구한다.

코드 구현

코드를 여기에 놓자

배열의 배열을 사용하면 사각형 보드판만 써야 하는거 아니냐? 그래프 자료구조나 좌표계 클래스는 왜 안사용하냐? 라고 생각할 수도 있겠다. 유연성을 생각한다면 좋은 생각이지만, 요구사항에 맞춰서 작성할 했을 뿐이며, 또한 필요 이상의 복잡한 방법을 쓰지는 않았다.

코드를 여기에 놓자

Tile과 Unit을 너무 완벽하게 만들고자 하지 말자. 우리가 고민할 내용은 Board 클래스를 만들어 주요 특징들이 작동하게 하는 일이다.

Tile 클래스가 유닛 추가/삭제를, Board가 getTile() 메소드를 이용해 타일을 반환할 수 있는데 왜 addUnit, removeUnit 메소드를 Board에 추가할까? getTile을 호출해서 Tile이 알아서 하면 안될까?

Unit 관련 오퍼레이션이 getTile()을 통해 반환되는 Tile 객체들을 통해 해결할 수 있다.

우리는 Unit 관련 메소드들을 Board에 추가하기로 하고, Board가 게임 설계자의 시작점이 되게 하기로 했다.

Tile 메소드들을 protected로 선언했고, Tile과 같은 패키지의 클래스들만이 addUnit, removeUnit 메소드를 직접 호출할 수 있다는 것을 알게 될 것이다. 그래서 Board 객체를 이용해서 타일, 유닛, 지형을 조작하게 됐다.

지금은 시스템 프레임웍 전체를 코딩하려고 하는 것이 아니다. 핵심 특징을 나누고, 프로젝트 주요 위험 요소를 줄이는 것을 선행한다. Unit 클래스와 Tile 클래스를 작성하는데 시간 들이는 것이 위험을 줄이는 데 도움이 되지 않을 것이다. 시스템의 핵심이자 프로젝트 성패가 달려있는 곳이라고 결정한 부분이 Board 클래스니까, Board가 동작하는 데 충분한 만큼만 Unit과 Tile에 신경쓰자.

큰 위험 요소를 줄이거나 없애고 난 후에는 Unit 클래스와 같은 작은 특징에 신경을 쓸 시간이 많아진다.

좀 더 질서있게, 덜 혼란스럽게

게임 시스템 프레임웍 핵심 특징
1. 게임읠 위한 보드 - 시스템의 본질
2. 게임 특유의 유닛 - 본질, 이것은 무슨 의미지?
3. 이동 관장 - 이것은 뭐고, 어떻게 하지?

이제 구조도 갖추었겠다, 클래스 간에 어떻게 조화되는지 생각해보자.

Board는 Tiles 다차원 배열을 통해 Tile을 소유하고, Tile을 Unit을 소유한다. Board는 Unit 타입에 대한 변수는 가지고 있지 않지만, Unit 객체를 받아들이는 메소드들이 연결되어 있어서 Unit 클래스와 연결된다.

Unit 클래스도 준비되었고, 게임 특유의 유닛들에 대해 해결해보는 건 어떨까? Board와 Unit 간 상호작용이 일어나는지도 볼 수 있다.

가능하면 이미 만들어진 내용을 기초로 만들라. 요구사항과 다이어그램만 있을 때는 무엇부터 할지 선택해야한다. 그러나 코드와 클래스가 있으니, 이미 만든 것과 관련된 핵심 특징을 선택하는 것이 쉬운 선택일 것이다.

아키텍쳐는 디자인의 구조이고, 프로그램의 가장 중요한 부분들과 그들 사이의 관계를 명확히 보여준다.

유닛에 대한 정의

게임 특유의 유닛들에 대해 이해하려면, 게임 설계자와 이야기하는 것이 가장 좋겠다. 유닛에 대해 다음 같은 내용들이 피드백되었다.

  • 공격치
  • 방어치
  • 경험치
  • 무기 착용
  • 캐릭터 간의 관계
  • 군대, 우주선, 비행기….

프레임웍에 기반한 각 게임은 다른 종류의 유닛들이 있고, 유닛들은 다양한 속성과 능력을 갖추고 있다. 그래서 각 게임마다 유닛에 대해 속성들을 가질 수 있어야하고, 그러한 속성들에 대한 여러 데이터 타입을 지원할 수 있어야 한다.

그렇다면 다양한 타입의 유닛들 중에 무엇이 공통점일까? 모든 유닛 타입에 적용되는 기본적인 내용에는 무엇이 있을까?

유닛들도 다양한 속성을 가지고 있을 것이다. 그렇다면, 앞서서 기타 상점 프로그램을 짤 때처럼, 기타, 만돌린 등으로 여러개의 클래스로 쪼개는 것이 좋은 디자인일까? 유닛 수가 100개, 200개 늘어난다면? 관리가 어려움이 있을 것이다. (스타크래프트만 해도 유닛 수가 엄청 많지 않는가.)

좋은 디자인은 항상 위험을 줄여준다. Unit 클래스에 디자인하는 가장 좋은 방법을 찾음으로써, 처음부터 잘 만들 수 있다. 전체 시스템에 깊숙히 들어가 작업해서 타 코드에 영향 주는 Unit 클래스에 수정을 가해야 하기 전에, 게임 특유의 유닛들이 무엇인지 알아냈을 뿐 아니라, 기본적인 Unit 클래스를 정의하고, 프로젝트 진행 중에 별다른 변경 없이 Board 클래스와 같은 다른 클래스들과 연결 될 수 있다.

아키텍쳐 핵심은 위험을 줄이고, 질서를 잡는 일이다.

(최근에 작업했던 알림톡 템플릿 전송 기능의 경우에도 기존에 잘 설계했더라면 주말에 개고생 하는 일이 없었을텐데… 정말 최신 유행하는 기술서 보는 것보다 중요한게 이런 기본서들을 찾아 읽는 것인듯 하다. 왜 늘 나는 사고가 터진 후에 이런 깨달음을 얻게 되는건가 ㅠㅠ)

우리는 코딩을 한 부분도 있고, 안 한 부분도 있다. 시스템의 기본적인 이해가 필요해서 코딩이 필요한 경우가 있다. 그러나 모든 것들의 우선순위는 프로젝트의 위험도를 줄이기 위함임을 기억하자.

고객과 이야기하는 것이 능사는 아닐 수 있지만, 우리가 개발하고자 하는 것이 무엇인지 목표를 명확히 한다면 우리를 혼란스럽게 하는 것은 잘 걸러낼 수 있다.

객체지향 분석 설계는 많은 코드가 관련된 것은 아니다. 가끔은 가장 좋은 소프트웨어 작성은 코드 작성을 피하는 일일 수도 있다.

(개발하면서 기존에 있는 기능들을 운영적으로 풀어내면서 문제를 해결하게 되는 경우가 있다.)

이동 관장

특징에 대해 무슨 말인지 이해가 가지 않을 때는 고객에게 직접 물어보는 게 최고다. 각 유닛이 얼마나 많은 칸을 움직일 수 있는지에 대한 이동 속성을 가지고 있고, 게임은 지형을 확인해서 이동이 가능한지 확인한다. 여기서 더 복잡한 계산으로 나아간다면, 비행기가 건물을 통과해서 날아간다던지, 비행기의 속도가 바람의 영향을 받는다던지 하는 것도 생각할 수 있다.

이동에 대해서 기본적으로 느껴지는 공통점이 있는가? 그 특징을 어떻게 시스템에 녹여낼 것인가?

  • 유닛들은 한 타일에서 다른 타일로 이동할 수 있어야 한다.
  • 이동은 계산이나, 각 게임 특유의 알고리즘을 근거로 하며, 가끔 유닛의 게임 특유의 속성들과 관련된다.

공통점

  • 이동 전에, 움직임이 가능한지를 확인한다.
  • 유닛의 속성들은 유닛이 얼마나 멀리 갈 수 있는지를 확인하는 데 이용된다.
  • 유닛 이외의 요소도 이동에 영향을 준다.

차이점

  • 이동의 합법성을 확인하는 알고리즘은 게임마다 다르다.
  • 사용되는 속성의 수나 이동에 영향을 주는 요소들은 게임마다 다르다.
  • 이동에 영향을 주는 요소는 게임마다 다르다.

결국은 게임마다 다르다라는 결론이 섰다. 이동과 관련된 내용은 게임 설계자에게 맡겨야 하는 듯 하다. 불확실성이 높기 때문이다. 공통점으로는 이론상 Movement 인터페이스를 만들어 MovementAlgorithm과 LegalMovement 같은 객체를 받아들이는 move() 메소드를 정의할 수 있다. 그러면 각 설계자는 이를 상속받아 확장할 수 있다.

좋은 소프트웨어 != 좋은 코드

좋은 코드는 잘 설계되고, 의도된 대로 작동한다. 좋은 소프트웨어는 잘 설계되고, 스케쥴에 맞게 제작되며, 고객이 원하는 기능을 한다. 이게 아키텍쳐가 필요한 이유다.

우리는 먼저 Board를 위한 기본 클래스들을 도출했고, 고객에게 잘못된 보드를 만들어주는 위험을 줄이기에 충분한 코드를 작성했다. 그 다음으로는 유닛에 대해 정의하고, 클래스 다이어그램을 이용해 어떻게 문제를 해결할지 계획했다. 마지막으로, 이동을 다룸으로서 게임 설계자가 고민할 내용이 무엇인지 생각해냈다.

많은 코드를 작성하진 않았지만, 제 때에 제대로 된 기능의 소프트웨어를 만들 수 있다고 확신하는 프로젝트가 되었다.

다음 장에서는 디자인 원리들에 대해서 알아보자.

Comments