본문 바로가기

발로 그리는 분산 시스템

분산 시스템 설계 - 컴포넌트간 공통 기능 분산에 대한 생각 정리

요구 사항

지금 부터 상상을 해보자. 여러분은 게임 서버를 만들고 있는 개발자다. 여러분이 만들고 있는 서버 시스템은 간단한 캐주얼 게임으로써 유저들이 플레이를 하기전에 모여 있는 채널(채널 서버)이라는 곳과 실제 게임 플레이를 하는 게임룸(게임서버)이 있다고 가정하자.

플레이어들은 게임에 접속하게 되면 가장 먼저 채널 서버에 머무르게 된다. 채널서버에서 게임방을 만들고 다른 유저를 초대하거나 다른 유저가 방에 접속하길 기다리다 게임을 플레이 할 수 있는 조건이 완성되면 게임서버로 컨넥션을 옮기고 유저간 인터렉션하며 게임을 즐기게 된다.

지금까지는 "게임 서버"는 유저 인터렉션을 통한 게임 진행만 담당했었다. 아이템 사용과 골드 소모 같은 요소들은 모두 채널 서버에서 담당 했다. 한마디로 "채널 서버와 게임서버는 데이터와 로직이 완전히 분리 되어 있었다".

하지만 어느날 갑자기 기획자로 부터 이런 요구 사항이 도착했다고 하자.

"채널 서버와 게임 서버 모두에서 아이템을 사용 할 수 있게 해주세요"

개발자 관점에서 해석하면,

"서로 다른 분산 컴포넌트에서 같은 기능을 사용 할 수 있게 해주세요"

가 된다.

  • 용어 정의 : "컴포넌트 = 분산 시스템 구성 요소"

여기서 우리는 문제 상황에 봉착하게 된다.

앞에서 설명한 상상의 상황 처럼 "채널 서버와 게임 서버는 서로 다른 컴포넌트다". 즉, 채널 서버와 게임 서버가 가지고 있는 데이터와 데이터를 유지하는 자료 구조가 다르기 때문에 추상적으로는 '아이템 사용'이라는 같은 기능이지만 구현 방법은 서로 다르다.

예를 들면, 채널 서버는 데이터 A, B, C를 항상 유지하고 있고, 게임서버는 B 데이터만을 가지고 있다고 가정하자. 아이템 사용을 위해서는 A, B, C 데이터가 필요하다고 할 때, 채널 서버는 필요한 데이터들을 이미 가지고 있으므로 아이템 사용에 별다른 준비 과정이 필요 없지만 게임 서버는 A 만 가지고 있으므로 B, C를 얻어 오는 부분이 따로 구현 되어야 한다. 또한 채널 서버는 아이템 사용 결과만을 클라이언트에게 리턴하면 되지만 게임서버는 아이템 효과들을 캐릭터 정보에 반영 해야하는 이슈 처럼 아이템 사용 결과를 처리하는 방식도 두 컴포넌트간에 서로 다르다.

따라서 기능이 변경 되는 경우 그에 맞춰 항상 두 곳을 수정 해야 하는 이슈가 발생한다.

생각의 시작

기능 변경시 두 곳을 고치지 않고 공통 기능을 사용하는 방법이 없을까?

요점은 지금 까지 각 컴포넌트에 분리 되어 있던 로직과 데이터를 컴포넌트간 공유를 해야한다.

1. 코드 레벨에서의 공유

동일한 코드를 파일 링크 등을 통해 두 컴포넌트에서 공유

  • 장점 :
    동일한 코드를 재사용 할 수 있는 경우라면 직관적이며 구현이 간단 하다는 장점이 있다.
    위와 같이 동일 코드를 적용 할 수 없는 경우라면  코드 레벨에서 공유는 장점이 없다.
  • 단점 :
    하지만 대부분의 경우 기존 기능들과의 호환성을 위해 조금씩 코드가 달라지게 된다. 이는 기능 변경 이슈 발생 시 항상 두 부분을 동시에 수정해야 하는 유지 보수 비용을 크게 감소 시키진 못한다.
    #ifdef 등 컴파일 플래그를 이용한 공유는 코드 분석 및 유지 보수 비용 증가.
    개발자는 항상 하나의 기능이 두 컴포넌트에 구현 되어 있음을 알아야 하는 유지 보수 비용 증가

2. 모듈(dll, lib or so) 레벨에서 공유

공통 기능을 모듈화 시키고 각 컴포넌트에서 모듈을 import 하는 방식으로 공유

  • 장점 :
    추상화(eg. "아이템을 사용한다. 하지만 난 자세한 사항은 모르겠다")가 잘 정의되었다는 가정하에 중복 코드 개발 비용을 줄일 수 있다.
  • 단점 :
    추상화를 위해 모듈내에서 사용하는 데이터가 이미 컴포넌트에 존재하고 있는 데이터와 중복이 발생하는 경우 저장 공간 낭비, 성능 낭비등의 문제와 동일 데이터에 대한 동기화 문제가 발생 할 수 있다.
    성능이나 기존 컴포넌트 데이터들과의 호환성을 위해 각 컴포넌트마다 다른 추상화 모듈을 제공한다면 코드레벨 공유와 같은 단점을 가지게 된다.
    컨넥션이 변경 될때마다 데이터 로딩과 같은 초기화 작업들을 다시 해줘야 한다. 컨넥션이 자주 바뀌고 기능의 사용이 빈번하다면 성능 이슈가 발생 할수 있다.
    모듈에서의 장애가 서버 컴포넌트까지 확장 된다. 만일 장애 파급 효과를 제한하기 위해 중요한 기능들만 따로 모아 컴포넌트를 분리한 경우라면 분리의 의미가 없어진다

3. 컴포넌트 레벨에서 공유(back-end 방식)

공통 기능을 처리하는 새로운 컴포넌트 추가. 예를 들어 back end 서버를 두고 front-end 서버들이 질의 하는 방식

  • 장점 :
    - '모듈 레벨에서 공유'의 장점에 추가하여 컨넥션이 변경 되더라도 기존 데이터들을 유지 할 수도 있다.
    - 장애 파급 효과를 제한 할수 있으며 하나의 컴포넌트에서 장애가 나도 다른 컴포넌트에 질의 하는 형식으로 장애를 극복 할 수 있다.
  • 단점 :
    - 추가적인 메시지 트래픽 비용
    - front-end <-> back-end 컴포넌트간 메시지를 릴레이 하는 기능 추가 구현 비용. 메시지 내용이 변경 되는 경우 릴레이 부분도 항상 같이 수정해 줘야 하므로 새로운 유지 보수 포인트가 발생 할 수 있다.
    - 메시지 시퀀스가 길어짐으로써 시스템 복잡도 증가
    - 기존에 구축 되어 있는 시스템이 있다면 데이터와 로직의 처리가 새로운 컴포넌트로 옮겨져야 하므로 개발비용과 테스트 비용이 만만치 않게 든다.

4. 컴포넌트 레벨에서 공유(front-end 방식)

클라이언트와 직접 연결을 맺는 공통 기능 컴포넌트를 두고 클라이언트가 계속 연결을 유지하며 공통 기능이 필요할 때마다 질의하는 방식

  • 장점 :
    back-end 컴포넌트 레벨 공유 방식의 장점에 추가 하여 컨넥션이 변경 될 이슈가 없으므로 클라이언트 접속 동안 데이터를 계속 유지 할 수 있다.
  • 단점 :
    컨넥션 위치 관리 비용, 데이터 동기화 비용이 추가 된다.
    예를 들어 게임 서버에 위치한 사용자가 아이템을 사용하게되면 게임 서버에 접속한 사용자의 데이터를 업데이트 해줘야 한다. 공통기능 컴포넌트는 클라이언트의 다른 컨넥션이 어느 컴포넌트에 접속 중인지를 항상 파악하고 있다가 데이터가 변경 될 시 동기화 시켜 주어야만 한다.

마치며

이상 분산 시스템에서 데이터와 로직을 공유할 수 있는 방법에 대해 살펴 보았다. 시스템 설계에는 정답이 없다. 각각의 장점과 단점이 있고 각 상황에서 가장 적절한 방법을 사용하는 것이 핵심이다. 가장 적절한 방법을 찾는 것은 경험을 토대로일 수도 있지만, 그 경험 이전에 이론적으로도 해당 방법의 장단점을 알고 있어야 정확한 판단을 할 수 있는 것이다.

부록 1. 같이 읽으면 좋은 글들

유익한 글이었다면 공감(❤) 버튼 꾹!! 추가 문의 사항은 댓글로!!