데브콘 활동 후기

[Review] 시스템 디자인 스터디 8주차 후기

dev-jonghoonpark 2025. 11. 10. 20:00

안녕하세요!

K-DEVCON (이하 “데브콘”) 그로스 매니저 박종훈입니다.

 

이 글에서는 11/06 (목)에 진행된 스터디에서 나눈 이야기를 정리합니다.

 

이번 시간에는 우리가 매일 사용하는 “구글 드라이브” 에 대한 시스템 설계를 해보았습니다.

 

 

구글 드라이브와 같은 클라우드 저장소 서비스는 파일을 언제 어디서나 업로드 및 공유할 수 있도록 합니다. 아울러 업로드된 파일은, 다른 사용자들과 손쉽게 공유될 수 있어야 합니다.

비슷한 서비스로는 Microsoft onedrive, Dropbox, Apple iCloud, Naver Mybox 등이 있습니다. 

덕분에 USB 없이도 편하게 파일을 옮길 수 있는 세상이 되었습니다.

[1] 기본적인 시스템 설계

이번 장에서는 파일을 업로드/다운로드하는 기능에 집중하여 논의 합니다.

 

이에 따른 기초 설계안은 다음과 같습니다.

책에서는 ‘클라우드 저장소 → 로드밸런서’ 로 표현되어있지만 ‘블록 저장소 서버 → 로드밸런서’가 더 적절한 연결로 보여 위 이미지 에서는 수정하여 표현하였습니다.

 

사실 구조는 굉장히 단순해 보입니다. 하지만, 실제로 대규모 서비스로 운영하게 된다면 수많은 문제들과 부딪히게 될 것입니다.

 

챕터의 참고 문헌에 있는 Dropbox 시스템 디자인도 비슷한 구조로 설계되어있습니다.

2012년 Dropbox 시스템 디자인 (출처: How We've Scaled Dropbox)

 

[2] 블록 저장소 서버의 필요성

블록 저장소 서버는 파일 업로드 및 관리를 담당하는 서버입니다. 이때 클라이언트가 보낸 파일은 블록단위로 나누어야 하고 (혹은 애초에 업로드할 때부터 나눠서 올리고) 각 블록에 압축 알고리즘을 적용하고 암호화 처리까지 담당합니다.

 

지난 유튜브 시스템 설계 때에도 파일을 블록 단위로 나누어 처리하는 것을 볼 수 있었습니다.

아무래도 큰 파일을 업로드/다운로드 할 때 발생될 수 있는 다양한 상황에 대해서 유연하게 대처할 수 있기 때문이 첫번째 이유일 것 같고 또 파일 저장소의 특성상 파일이 변동되는 경우도 있어, 전체 파일을 업데이트 하는 것이 아닌, 변동된 블록만 동기화 할 수 있다는 것도 장점이 될 수 있습니다.

 

이때 Dropbox는 한 블록당 4MB로 잡았다고 하는데, 그 이유는 그냥 정해보았는데 꽤 잘 동작하여서, 따로 변경할 생각을 하지 않았다고 합니다. (it's completely arbitrary, worked pretty well so far, not like we, I don't know if we have the option to change it.)

 

그리고 또 재밌게 느꼈던 부분은, 서로 다른 사용자가 같은 파일을 올렸다면, 해당 파일을 별도의 블록으로 나눠서 관리하는 것이 아니라, 같은 블록을 매핑하도록 한다고 합니다. 이를 통해 실제 스토리지에 저장되는 데이터의 양을 줄일 수 있었다 합니다.

처음에는 이러한 방식이 보안적으로 문제가 되지 않을까 생각했는데, 같은 파일을 보유중인 사용자 이기 때문에 큰 문제가 없을것 같다는 생각이 들었습니다.

 

[3] 클라우드 저장소의 필요성

구글 드라이브와 같은 동기화 서비스에서는 고객의 파일이 손상없이 관리되는것이 중요합니다.

 

하드디스크는 수명을 가지고 있는 소모품입니다. 수명이 다 된다면, 운이 나쁠 경우 데이터를 복구하지 못할 수 있습니다.

따라서 파일 데이터를 적절하게 분산처리를 해야합니다.

 

파일 데이터에 대한 분산처리를 직접 할 수도 있었겠지만, 이는 현실적으로 쉽지 않은 일입니다.

따라서 S3와 같은 별도의 객체 스토리지 서비스를 사용하여, 책임을 분리합니다.

 

[4] 롱폴링

롱폴링은 주기적으로 요청하는 폴링 방식과 다르게, 요청을 오래 유지한 채 서버에서 응답할 때까지 기다리는 방식입니다. 서버에 즉시 보낼 데이터가 없다면 요청을 일정 시간 동안 유지하고, 데이터가 준비되는 순간 응답을 보냅니다. 타임아웃이 나면 클라이언트는 다시 요청을 보내어 연결을 갱신합니다.

 

개인적으로 롱폴링을 사용하여 기능을 구현해본적이 없었기 때문에, 현재 회사에서 롱폴링을 사용중인 부분이 있으신지 스터디원분들에게 질문을 드렸습니다.

 

그 중 한 스터디원께서 공유해주신 사례가 저에게는 재밌게 느껴졌는데요.

현재 Amazon SQS 를 사용중이신데 SQS 는 요청당 비용이 발생된다고 합니다. 그래서 이에 대한 비용을 줄이고자, 롱폴링을 이용하여 요청횟수를 줄이고 계신다고 사례를 공유해주셨습니다.

서비스의 특성에 따라 실용적인 전략을 취하신것 같다는 생각이 들었습니다.

 

[5] 동기화 알고리즘

책에서는 아주 간단하게만 다루고 넘어간 내용입니다.

 

파일이 충돌될 경우 어떻게 처리할 것인가. 이는 전략에 따라 다르게 처리할 수 있습니다.

책에서는 먼저 처리되는 변경을 성공한 것으로 보고, 나중에 처리되는 변경은 충돌이 발생한 것으로 처리하는 전략을 소개하고 있습니다. (first write win)

dynamodb 는 반대로 last write win 전략을 취하고 있는 것으로 유명합니다.

 

그러면 google docs 처럼 실시간성이 필요한 서비스의 경우 어떤 전략을 취할 수 있을까요?
이 내용에 대해서 책의 참고문헌으로 나와 있는 Differential Synchronization (차분 동기화) 이라는 논문에서 다루고 있습니다. (이에 대한 유튜브 영상도 관심 있으신 분을 위해 남겨둡니다.)

 

이 논문의 내용 중 일부를 소개드립니다.

 

이 논문에서는 점점 범위를 확장해나가며 동기화 알고리즘을 소개를 하고 있습니다.

 

<1> Pessimistic

먼저 동기화를 하기 가장 쉬운 방법은 방법은 락을 사용하는 것입니다.

이는 충돌은 적을 수 있겠지만, 이는 실시간에 어울리지 않습니다.

 

<2> Edit-based

사용자의 모든 입력/수정 이벤트를 브로드 캐스트 합니다.

이는 일반적인 경우에는 잘 동작할 수 있습니다. 하지만, 중간에 네트워크 장애와 같은 이유로 동기화가 깨졌을 경우, 이를 바로잡기 위해 많은 비용이 발생됩니다.

 

<3> Three-way merges

버전 관리 시스템에서 주로 사용하는 방식입니다. (Git, Svn …)

각 상태의 의미는 다음과 같습니다.

  • Base: 공통 조상 (공통 commit)
  • client text: 현재 사용자 로컬 브랜치의 최신 버전
  • server text: 병합하려는 다른 브랜치의 최신 버전

이 방식도 좋은 방식이지만, 내가 수정중일때, 서버의 수정 사항이 실시간으로 반영되지는 않습니다.

서버의 수정 사항을 받아오려 할 때, 충돌이 발생된다면, 사용자가 이를 수정해야 서버에 반영할 수 있습니다. (Git 에서 pull 을 하였을 때, conflict 가 나는 상황을 생각하면 됩니다.)

 

<4> 차분 동기화(DIFFERENTIAL SYNCHRONIZATION)

이 알고리즘이 저자가 소개하는 알고리즘 입니다.

 

이 알고리즘의 특징은 다음과 같습니다.

  • 대칭적입니다.
  • 수렴됩니다.
    • 패치가 실패하면, 실패한 변경 사항이 역으로 반영됩니다.

한 번에 이해하기 어렵기 때문에, 저자도 상황을 확장하며 알로리즘을 설명합니다.

 

1. offline 에서 진행 가정

먼저 오프라인 상태에서 서버클라이언트를 나눠 생각해보겠습니다.

여기서 Shadow 는 Three-way merges 방식에서의 base 와 같다고 생각하면 됩니다. 어떤 버전을 기준으로 수정하고 있는지를 나타냅니다.

  • 내 수정 버전과 Common Shadow 의 차이를 Diff 로 만듭니다. (1, 2)
  • Diff 를 생성해두었기 때문에, 내 변경사항을 Shadow 에 반영합니다. (3)
  • Diff 를 서버에도 반영합니다. (4, 5)

오프라인 환경에서는 나 밖에 수정하고 있지 않습니다. 따라서 중앙의 Common Shadow 한 개로 충분히 동기화를 할  수 있습니다.

 

2. online 에서 진행 가정

이제 좀 더 복잡해집니다. 나 말고도 다른 사람이 server 와 작업하고 있을 수 있기 때문에, shadow 가 Client Shadow 와 Server Shadow 로 나눠서 관리합니다.

 

Client Shadow 는 클라이언트 입장에서 마지막 공통 버전을 의미하며, Server Shadow 는 서버 입장에서 마지막 공통 버전을 의미합니다.

이를 통해 다른 사람들과 함께 동기화를 할 수 있는 구조를 갖추었습니다.

 

만약 컨플릭트가 발생된다면 어떻게 될까요?

이 경우에는 서버쪽에서 diff를 생성하여 클라이언트 쪽에 patch를 적용하기 때문에, 해당 conflict 를 유저쪽에서 핸들링 할 수 있게 됩니다. 해당 부분을 수정한 뒤 다시 patch 를 보내면 됩니다.

 

3. online 에서 네트워크 장애가 발생되어도 문제 없도록 개선

2번 구조로도 네트워크 장애가 발생한다면 어떻게 될까요?

예를 들어, 서버는 클라이언트의 응답을 받아 이미 적용하였지만, 클라이언트가 ack를 받지 못하면 어떻게 될까요? 이는 분산 환경에서 쉽게 발생될 수 있는 이슈입니다. 로직에 따르면 클라이언트는 변경되지 않았다고 보고, 변경사항을 다시 서버에 적용하고자 할 것입니다.

이를 방지하기 위해 아래와 같이 패치 여러개를 보낼 수 있는 구조로 변경하고 동시에 backup shadow 를 추가합니다.

백업 shadow 는 클라이언트가 동일한 응답을 보냈을 때 동작합니다. 서버에 잘못된 변경사항이 반영되는것을 방지하기 위해, 이전 상태로 되돌린 후, 다시 patch를 적용하는데 사용됩니다.

 

논문에서는 server 쪽에만 backup shadow 를 적용하였지만, 유튜브 영상을 보면 client 쪽에도 backup shadow를 두어, 서버쪽에서 동기화 할 때에도 client 에서 동일한 일이 발생되지 않도록 방지하는 것으로 보입니다.

 

[6] 번외: 오버엔지니어링

How We've Scaled Dropbox 영상에는 Dropbox 와 관련된 많은 재밌는 내용들이 나오는데요

 

저는 아래 내용이 기억에 남았습니다.

Dropbox의 경우 처음부터 위와 같은 아키텍처로 시스템을 구성하지는 않았다고 합니다. 최초 디자인은 단일 서버로 구성된 아주 단순한 구조였습니다.

 

2007년 Dropbox 시스템 디자인 (출처: How We've Scaled Dropbox)

 

이 구조는 곧 한계를 만나게 됩니다. 사용자가 늘어나면서 서버의 디스크 공간이 부족해지고 서버 과부하가 발생했습니다.

그래서 그 이후부터 S3 와 별도의 MySQL 서버를 이용하여 처리를 분산하기 시작하였습니다.

Dropbox는 시스템에 한계가 생길때마다, 적절히 시스템을 개선해 나갔습니다.

 

이렇게 순차적으로 개선해 나간 이유는 창업자들이 더 나은 구조를 몰라서가 아니라, 가장 중요한 것(서비스의 존재 증명)에 집중하기 위한 의도된 선택이였다고 합니다.

개발자들은 오버엔지니어링 하기 쉽습니다. 우리가 하고 있는 작업은 오버엔지니어링이 아닐지 항상 고민해 봐야겠습니다.

 


스터디에서 더 많은 이야기를 나눴지만, 오늘은 여기서 마쳐보겠습니다.

 

우리 스터디에서는 실제 사례들을 통해 문제를 어떻게 해결해 나갔을까 많은 고민을 해보고 이야기를 나눠보고 있습니다. 이번주도 다들 열심히 참여해주셔서 더 풍성한 스터디가 될 수 있었습니다.

 

 

어느새 계획했던 8회의 스터디를 마쳤습니다. 🎉🥰

잠시 쉬었다가 다시 더 유익한 스터디로 돌아올 수 있도록 하겠습니다. 😎


직접 스터디를 개설해보고 싶은 분이 계시다면, K-DEVCON에서 운영을  도와드리겠습니다. 데브콘의 '랩짱'에 도전하여 커뮤니티 성장을 함께 이끌어주세요! 슬랙을 통해 운영진에게 DM 부탁드리겠습니다😉