[코딩인터뷰 완전분석] 면접 대비 정리 - 시스템 설계 및 규모 확장성

이 글은 게일 라크만 맥도웰 저자의 ‘코딩인터뷰 완전분석’을 읽으며, 깨달은 사실과 내용을 정리하는 글입니다.
자세한 내용이 궁금하다면 http://www.yes24.com/Product/Goods/44305533 에서 책을 구매하시면 좋겠습니다.

이번에는 면접 상황에서 마주할 수 있는 ‘시스템 설계 관련 문제’를 어떻게 접근하고 해결해야 하는지 정리하겠습니다.

저자가 말하길, 이 문제는 가장 쉬운 종류의 문제라고 합니다. (정말…?)

면접관이 이런 문제를 내는 이유는 ‘단순히 지원자가 실제 세계에서 어떻게 행동할지를 보기 위해 설계된 문제’ 이기 때문입니다.

따라서 이 문제를 해결할 때, 가장 중요한 것은 ‘질문을 하고’ , ‘면접관을 끌어들이고’ , ‘장단점을 토론’하는 것입니다.

본격적으로 문제를 어떻게 접근하고 해결해야 하는지 알아볼까요?



문제를 다루는 방법

시스템 설계 관련 문제를 다루기 위한 방법으로 아래 8개가 존재합니다.

  • 소통하라
    • 시스템 설계 문제를 출제하는 가장 큰 목적은 ‘지원자의 의사소통 능력을 평가하기 위함’ 임을 기억합시다.
    • 따라서 면접관과 끊임없이 의사소통하는 것이 중요합니다.
  • 처음에는 포괄적으로 접근하라
    • 바로 특정 부분에 뛰어들거나, 과도하게 파고들지 않고 포괄적으로 설계를 시작합니다.
  • 화이트보드를 사용하라
    • 화이트보드를 통해, 본인의 제안을 이해하기 쉽게 전달할 수 있습니다.
  • 면접관이 우려하는 부분을 인정하라
    • 자신의 답변에 대해, 면접관이 우려되는 부분을 파고들려고 할 것입니다.
      중요한 것은 이 우려를 인정하고, 적절하게 수정하는 것입니다.
  • 가정을 할 때 주의하라
    • 잘못된 가정은 문제를 완전히 다르게 바꿔버릴 가능성이 높습니다. 따라서 이 점을 주의해야 합니다.
  • 본인이 생각하는 가정을 명확히 언급하라
    • 이 가정을 면접관에게 알려 줘야, 실수를 했을 때 면접관이 바로잡아 줄 수 있고, 지원자가 어떤 가정을 하고 있는지 알 수 있습니다.
  • 필요하다면 어림잡아 보라
    • 출제된 문제에서 시스템 설계에 필요한 정보가 완벽히 제공되지 않을 가능성이 높습니다.
    • 따라서 이 경우, 합리적인 가정을 통해 설계를 진행할 수 있습니다.
  • 뛰어들라
    • 면접관과 이야기를 하며, 문제를 풀어내는 것이 핵심입니다.
    • 자신이 설계한 시스템의 장단점을 열린 마음으로 받아드리고, 깊이 파고드세요.

이런 문제들은 최고의 설계를 해내는 것보다, 대개 그 과정을 중요하게 본다는 것을 잘 새겨두시기 바랍니다!



시스템 설계 : 단계별 접근법

1 단계 : 문제의 범위를 한정하기

가장 먼저 주어진 문제를 잘 듣고, 어떤 시스템을 설계해야 하는지 파악해야 합니다.

’여러분이 만들고자 하는 시스템’과 ‘면접관이 원하는 것’이 같은지 확실히 해두어야 합니다.

면접관에게 질문을 던져서, 어떤 시스템을 설계해야 하는지 명확히 짚고 넘어갑시다.


2 단계 : 합리적인 가정 만들기

시스템 설계에 있어서 가정이 필요하다면 만들어내는 것도 괜찮습니다. 하지만 만들어낸 가정이 합리적이어야 합니다.

예를 들어, 시스템이 하루에 100명의 사용자만 처리할 수 있으면 된다는 가정은 다소 무리가 있습니다. 혹은 메모리 제약이 없다는 가정도 현실성이 없습니다.

하지만 하루에 10,000명의 사용자를 처리할 수 있다는 가정은 괜찮습니다.


3 단계 : 중요한 부분을 먼저 그리기

이제 화이트보드를 활용할 차례입니다.

시스템의 주요한 부분을 먼저 다이어그램으로 그리면 됩니다.

예를 들어, 백엔드 인프라를 설계한다면 핵심 객체들을 먼저 그려서 기본 틀을 잡을 수 있습니다.

현재 단계에서는 규모 확장성 문제를 무시해도 좋습니다. 그냥 단순 명백하게 동작한다고 접근해도 됩니다.


4 단계 : 핵심 문제점을 찾기

이제 기본적인 밑바탕은 설계가 완료되었습니다. 다음으로 수행해야 하는 절차는 해당 시스템의 핵심 문제점을 찾는 것입니다.

예를 든다면, 병목지점이나 사용량이 급증했을 때에 대한 문제가 있습니다.

아마도 이 단계에서 면접관이 어느 정도 가이드를 해줄 것이라고 저자가 말합니다. 그럴 땐 그 조언을 받아들이고 시스템에 적용하면 됩니다.


5 단계 : 핵심 문제점을 해결할 수 있도록 다시 설계하기

핵심 문제가 무엇인지 알아냈다면, 이제 그에 맞게 본인의 설계를 수정하면 됩니다.

시스템 전체를 갈아 엎어야 할 수도 있고, 몇 가지 자잘한 부분만 수정해도 될 수도 있습니다.

이렇게 개선한 시스템이라도, 수많은 제약사항이 있을 수 있습니다. 이때 중요한 것은 어떤 제약사항도 열린 마음으로 받아들일 수 있어야 하는 것입니다.

또한 본인이 알고 있는 제약사항들을 면접관과 이야기하는 것 또한 중요합니다.



규모 확장 : 단계별 접근법

간혹 시스템 전체를 설계하라는 문제 대신, 시스템의 한 부분이나 알고리즘을 설계해 보라는 요청을 받을 수도 있습니다.

이때 반드시 규모 확장성을 신경써야 합니다.

이와 관련한 문제를 어떻게 접근해서 해결해야하는지 알아보겠습니다.


1 단계 : 질문하기

역시 초반에는 질문을 통해, 문제를 제대로 이해했는지 확인하는 절차가 필요합니다.

이를 통해, 세부적인 사항들에 대해서도 알아보는 시간을 갖습니다.


2 단계 : 현실적 제약을 무시하기

문제를 파악했다면 바로 정답을 고민하는 것이 아니라, 가장 먼저 제약조건이 전혀 없다고 가정해서 일단 문제를 풀어보는 것이 중요합니다.

이를 통해, 실제 정답에 대한 윤곽을 그릴 수 있다고 저자가 설명합니다.

예를 들어, 메모리 제약이 없고, 컴퓨터 한 대에서 모든 데이터를 다 처리할 수 있다고 가정할 수 있습니다.


3 단계 : 현실로 돌아오기

이제 원래 문제로 돌아와서, 제약조건이 생겼을 때 어떤 문제가 발생할지 찾아봅니다.

규모 확장성과 관련된 문제 중, 가장 흔한 문제는 ‘거대한 양의 데이터를 어떻게 쪼개서 저장할 것인지’, ‘어떤 컴퓨터에 어떤 데이터가 저장되어 있는지 어떻게 알아낼 것인지’가 있습니다.


4 단계 : 문제 풀기

이제 마지막으로 3 단계에서 발견한 문제점들을 어떻게 해결할지 생각해봐야 합니다.

상황에 따라, 문제점 자체를 완전히 해결할 수도 있고, 그 수준을 완화시키는 데 그칠 수도 있습니다.

이때 순환적 접근법을 사용할 수 있습니다. 순환적 접근법이란, 어떤 문제를 해결하면 또 다른 문제가 발생하고, 그러면 그 문제를 다시 해결해 나가는 과정을 반복하는 작업을 말합니다.

이 문제들은 ‘지원자가 복잡한 시스템을 완벽히 설계해낼 수 있는지’를 확인하는 것이 아니라, ‘문제를 분석하고 풀 수 있는 능력을 입증’하려는 목적이 더 크다는 것을 기억해둡시다!



시스템 설계에 필요한 핵심 개념

시스템 설계 문제가 지원자가 무엇을 알고 있는지 확인하는 문제는 아닙니다. 하지만 특정 개념을 알고 있으면 문제를 더 쉽게 풀 수 있는 것도 사실입니다.

관련 개념들을 개략적으로 설명할 예정이므로, 보다 자세한 내용은 추가적인 학습을 하시면 됩니다!


수평적 vs 수직적 규모 확장 (Scale-Out vs Scale-Up)

시스템은 두 가지 방법으로 규모를 확장시킬 수 있습니다.

  • 수평적 규모 확장 (Scale-out)
    • 노드의 개수를 늘리는 방법을 말합니다.
    • Ex) 비슷한 서버를 1개 더 추가한다.
  • 수직적 규모 확장 (Scale-up)
    • 특정 노드의 자원을 늘리는 방법을 말합니다.
    • Ex) 특정 서버에 RAM을 추가한다. CPU를 업그레이드 한다.


서버 부하 분산 장치 (Load Balancer)

서버 부하 분산 장치, 즉 로드밸런서를 통해서 여러 서버에 작업을 분산시킬 수 있습니다.

예를 들어, /api/a 로 들어온 요청은 서버A 가 처리하도록 하고, /api/b 로 들어온 요청은 서버B 가 처리하도록 분산시킬 수 있습니다.

이를 통해서, 시스템에 걸리는 부하를 여러 대의 서버에 균일하게 분산시킬 수 있고, 서버 한 대 때문에 전체 시스템이 죽어버리는 문제를 방지할 수 있습니다.


데이터베이스 역정규화

관계형 데이터베이스는 JOIN 연산을 할 때 굉장히 느려질 수 있습니다. 따라서 JOIN 연산은 가능하면 피해야 합니다.

이때 역정규화를 통해 테이블에 특정 Column을 추가하거나, 테이블을 합쳐서 JOIN 연산을 최소화할 수 있습니다.

예를 들어, STUDENT 테이블과 TEACHER 테이블의 NAME 칼럼이 함께 조회되는 경우가 많다면, STUDENT 테이블에 TEACHER_NAME 이라는 칼럼을 추가하거나, STUDENT 테이블과 TEACHER 테이블을 합쳐버릴 수도 있습니다.
이를 통해, JOIN 연산을 최소화할 수 있습니다.


데이터베이스 분할 (샤딩)

샤딩이란, ‘데이터를 여러 컴퓨터에 나눠서 저장하고, 어떤 데이터가 어떤 컴퓨터에 저장되어 있는지 알 수 있는 방식’ 을 말합니다.

흔히 사용되는 샤딩 방식은 아래 3가지가 존재합니다.

  • 수직적 분할
    • 자료의 특성별로 분할하는 방식입니다.
    • Ex) 회원정보 / 상품정보 데이터를 따로 저장
    • 단점 : 특정 테이블의 크기가 너무 커지면, 다시 분할해야 할 수도 있습니다.
      예를 들어, 서버A 에 있는 상품정보 데이터 가 100GB를 넘어가면 재분할해야 합니다.
  • 키 or 해시 기반 분할
    • 가장 단순하게 구현한다면, mod(key, n) 연산을 통해 분할이 가능합니다.
    • mod 연산의 결과로 나온 0 ~ (n-1) 의 값으로 어떤 서버에 저장할지 판단할 수 있습니다.
    • 단점 : 하지만 서버가 1개 더 추가된다면 모든 데이터를 재분할해야 합니다.
      따라서 서버의 개수가 사실상 고정이라는 것입니다. (Ex. mod(key, 3)mod(key, 4) 로 변경시)
  • 디렉터리 기반 분할
    • 이 방법은 데이터를 찾을 때, ‘조회 테이블(Lookup Table)’을 사용하는 방식입니다.
    • 단점 1 : 조회 테이블이 단일 장애 지점이 될 수 있습니다.
    • 단점 2 : 지속적으로 테이블을 읽는 행위가 전체 성능에 영향을 미칠 수 있습니다. (오버헤드 발생)


캐싱

인메모리 캐시를 사용하면 결과를 매우 빠르게 가져올 수 있습니다.

보통 인메모리 캐시는 키-값을 쌍으로 갖는 간단한 구조로 이루어져있습니다.

애플리케이션과 데이터 저장소 사이에 위치시켜서, DB 대신 빠르게 동작할 수 있습니다.

캐시 운영 전략에는 아래 두 가지가 있습니다.

  • 캐시 읽기 전략
    • Look Aside
      • 데이터를 찾을때, 캐시에 저장된 데이터가 있는지 우선적으로 확인합니다.
        만일 캐시에 데이터가 없으면 DB에서 조회하고 캐시를 업데이트합니다.
      • 이때 캐시를 업데이트하는 주체는 애플리케이션 서버입니다.
      • 애플리케이션 서버 → (조회, 업데이트) → 캐시
      • 애플리케이션 서버 → (조회) → DB
      • 초기 조회 시 무조건 DB를 호출해야 하므로, 단건 호출 빈도가 높은 서비스에는 적합하지 않습니다.
    • Read Through
      • 캐시에서만 데이터를 읽어옵니다.
      • Look Aside와 비슷하지만, 데이터 동기화를 캐시가 수행한다는 점에서 차이가 있습니다.
      • 애플리케이션 서버 → (조회) → 캐시 → (조회, 업데이트) → DB
      • 애플리케이션 서버가 캐시만을 바라보기 때문에, 캐시가 죽었을 경우 전체 서비스가 중단될 수 있습니다.
  • 캐시 쓰기 전략
    • Write Back
      • 데이터를 캐시에만 저장하고, 일정 주기마다 캐시에 쌓인 변경사항을 모아서 DB에 반영합니다.
      • 장점 : 캐시에 모아두기 때문에, DB의 쓰기 쿼리 비용과 부하를 줄일 수 있습니다.
      • 단점 : 자주 사용되지 않는 리소스까지 캐싱되며, 캐시에 오류가 발생하면 데이터를 영구 소실할 수 있습니다.
    • Write Through
      • DB와 캐시에 동시에 데이터를 저장합니다.
      • DB 동기화 작업을 애플리케이션 서버가 아닌, 캐시가 수행합니다.
      • 장점 : 데이터 일관성을 유지할 수 있으며, 데이터 유실이 발생하지 않습니다.
      • 단점 : 매 요청마다 두 번의 쓰기 작업이 진행되기 때문에, 생성·수정이 자주 발생하는 서비스라면 성능 이슈가 발생할 수 있습니다.


비동기식 처리

속도가 느린 연산은 비동기식으로 처리해야 합니다. 동기식으로 동작한다면, 해당 연산이 끝날때까지 계속 기다려야 할 수도 있기 때문입니다.

혹은 미리 해당 연산을 처리해둘 수도 있습니다.


네트워크 성능 척도

네트워크의 성능을 측정할 때 사용되는 중요한 척도 3가지가 있습니다.

  • 대역폭 (Bandwidth)
    • 단위 시간에 전송할 수 있는 데이터의 최대치를 말합니다. 즉, 한번에 보낼 수 있는 데이터의 양을 의미합니다.
  • 처리량 (Throughput)
    • 단위 시간에 실제로 전송된 데이터의 양을 의미합니다.
  • 지연 속도 (Latency)
    • 데이터를 전송하는 데 걸리는 시간을 말합니다. 즉, 발신자가 데이터를 보내는 시점부터 수신자가 데이터를 받는 시점까지의 시간을 의미합니다.

이들간의 관계는 아래와 같습니다.

  • 대역폭 ↑ ⇒ 처리량 ↑
  • 대역폭 ↑ ⇒ 지연속도 (영향X)
  • 처리량 ↑ ⇒ 지연속도 (영향X)
  • 지연속도 ↑ ⇒ 처리량 ↑



시스템 설계 시 고려할 점

시스템을 설계할 때 위에서 살펴본 개념뿐만 아니라, 아래 사항들도 함께 고려하는 것이 좋습니다.


실패 대비책 준비

시스템의 어떤 부분이든 실패 가능성이 존재합니다.

따라서 각 부분이 실패했을 때를 대비한 대비책을 준비해야 합니다.


가용성 및 신뢰성 고려

  • 가용성 : 시스템의 사용 가능한 시간을 전체 시간으로 나누어 백분율로 나타낸 것
  • 신뢰성 : 특정 단위 시간에 시스템이 사용 가능할 확률


읽기 중심 vs 쓰기 중심

설계한 시스템에서 읽기 작업이 많은지, 쓰기 작업이 많은지에 대해 고민해야 합니다.

쓰는 연산이 많다면, 큐를 사용하는 방법을 고려해보는 것이 좋습니다.

읽는 연산이 많다면, 캐시를 사용하는 것이 좋을 수 있습니다.


보안

보안 위협은 시스템의 엄청난 위해를 가할 수 있습니다. 따라서 해당 시스템이 직면할 수 있는 문제점에 대해 생각해보고, 어떻게 해결할 수 있을지 고민해봐야 합니다.



완벽한 시스템은 없다

이 세상에 있는 어떤 시스템도 완벽하게 동작하는 것은 없습니다. 모든 시스템에는 장단점이 존재합니다.

따라서 이런 종류의 문제를 받았을 때 중요한 것은, ‘사례를 잘 이해하고’, ‘문제의 범위를 설정하고’, ‘합리적인 가정을 세우고’, ‘명확하게 설계한 시스템을 만드는 것’ 입니다.

그리고 설계한 시스템의 약점에 대해 열린 마음으로 받아드리고, 아주 완벽한 것을 기대하지 말아야 합니다.



마무리하며

이번 포스팅에서는 시스템을 설계하고 규모 확장성을 갖는 시스템을 만들라는 문제에 어떻게 접근해야 하고, 해결하면 좋은지 정리해봤습니다.

저는 아직 시스템을 설계해보라는 면접 질문을 받아본 적이 없지만, 다른 사람들의 면접 후기를 보며 유사 질문이 등장한다는 것을 알았습니다.

그리고 만약 내가 이 질문을 받으면 어떻게 답변을 해야할까? 고민을 하며 막막했던 기억이 있습니다.

이번 시간에 관련 내용을 정리하며, 어떻게 접근해서 답변을 시작해야 할지 조금 감이 잡히는 것 같습니다.

여러 문제들을 풀면서, 정리한 내용을 직접 적용해보고 스스로 답변해보면서 연습해야겠습니다.

면접을 준비하시는 모든 분들에게도 도움이 됐으면 좋겠습니다.