PHpullh
개발자 면접 준비/시스템 디자인

시스템 디자인

확장 가능한 서비스 설계 면접 답변

트래픽 추정에서 캐시, 큐, 데이터베이스, 장애 대응까지 확장성 면접 답변을 구성하는 체크리스트입니다.

확장성 질문에서 감점되는 첫 문장

확장성 면접에서 가장 자주 나오는 나쁜 시작은 “로드밸런서를 두고 서버를 여러 대로 늘린 다음 캐시와 큐를 붙이겠습니다”입니다. 이 문장에는 문제가 없습니다. 문제는 이 문장이 어떤 요구사항에서도 똑같이 나온다는 데 있습니다. 문제를 듣기 전에도 말할 수 있는 답은 그 문제에 대한 답이 아닙니다.

확장성 설계에서 실제로 평가되는 것은 구성요소 목록이 아니라 순서입니다. 무엇이 먼저 무너질지 예측하고, 그 지점에만 손을 대고, 그 변경이 새로 만드는 실패 모드를 인지하고 있는지를 봅니다. 그래서 좋은 답변은 언제나 “가장 단순한 구조”에서 출발해 “여기가 먼저 깨집니다”를 근거로 한 단계씩 이동합니다.

연습 문제: 이미지 업로드·조회 서비스

사용자가 이미지를 올리고, 올린 이미지는 여러 크기의 썸네일로 변환되어 다른 사용자에게 노출됩니다. 이 서비스를 설계하십시오.

0단계 · 무엇을 물어야 하는가

숫자를 받기 전에는 설계를 확정할 수 없습니다. 읽기와 쓰기 중 어느 쪽이 압도적인지, 이미지 하나의 평균 크기와 하루 업로드 건수는 어느 정도인지, 업로드 직후 즉시 보여야 하는지 잠시 뒤에 보여도 되는지, 삭제와 비공개 전환이 있는지, 지역이 여러 곳인지 정도는 확인해야 합니다.

여기서는 이렇게 받았다고 하겠습니다. 조회가 쓰기보다 압도적으로 많고, 업로드 직후에는 원본이 바로 보이면 되며 썸네일은 잠시 뒤에 완성되어도 됩니다. 삭제가 있고, 비공개 이미지가 존재합니다. 정확한 숫자는 면접관이 주지 않으면 스스로 가정을 말하고 진행하되, 그 가정이 어떤 결정을 바꾸는지 반드시 연결해야 합니다.

1단계 · 가장 단순한 구조부터 그립니다

애플리케이션 서버 한 대, 데이터베이스 한 대, 이미지 파일은 오브젝트 스토리지에 두고 데이터베이스에는 키와 메타데이터만 저장합니다. 원본 바이트를 관계형 데이터베이스에 넣는 선택은 여기서 미리 배제해야 합니다. 백업 시간이 이미지 용량과 함께 늘고, 복제 대역폭을 잡아먹고, 버퍼 캐시가 메타데이터 대신 이미지로 채워져 모든 질의가 함께 느려지기 때문입니다.

이 단순한 구조를 먼저 말하는 이유는 겸손해 보이려는 것이 아닙니다. 다음 단계마다 “무엇이 여기서 깨져서 이걸 추가했는가”를 말할 수 있는 기준선을 만들기 위해서입니다.

2단계 · 읽기 경로를 먼저 분리합니다

조회가 압도적이면 읽기 경로부터 손대는 것이 비용 대비 효과가 가장 큽니다. 이미지 바이트는 애플리케이션 서버를 거치지 않고 CDN에서 나가게 하고, 애플리케이션은 권한 확인과 URL 발급만 담당합니다. 업로드도 마찬가지로 서버를 통과시키지 않고, 서명된 URL을 발급해 클라이언트가 스토리지로 직접 올리게 합니다. 이렇게 하면 애플리케이션 서버의 대역폭과 메모리가 파일 크기에 영향받지 않습니다.

REQUEST FLOW · 업로드와 조회

[업로드]
client -> API   POST /v1/images  {content_type, size}
                └ 권한 확인, images 행 생성(status=UPLOADING), 서명 URL 발급
client -> STORE PUT <signed-url>            (바이트는 API를 통과하지 않음)
client -> API   POST /v1/images/{id}/commit  (Idempotency-Key 포함)
                └ status=READY, 변환 작업을 큐에 발행
                  * 큐 발행은 DB 커밋과 원자적이지 않다 →
                    아웃박스 테이블에 함께 기록하고 별도 발행기가 읽어 보낸다

[변환]
worker <- QUEUE  {image_id, sizes:[128,512,1024]}
worker -> STORE  변환본 저장 (키: {image_id}/{size}.webp)
worker -> API/DB variants 행 추가, derived_status=READY
                 * 실패 시 지수 백오프 재시도, 한도 초과분은 DLQ로 격리

[조회]
client -> CDN   GET /img/{image_id}/512.webp
CDN(miss) -> STORE  원본 조회 후 캐시
                 * 비공개 이미지는 CDN 공개 캐시 대신
                   짧은 만료의 서명 URL을 발급하고 캐시 키에 그 사실을 반영

[삭제]
client -> API   DELETE /v1/images/{id}
                └ DB에 삭제 표시(즉시 응답) → CDN 무효화 → 스토리지 정리 작업
                  * 스토리지 삭제를 동기로 하면 응답이 외부 장애에 묶인다

3단계 · 쓰기 경로에서 느린 일을 떼어냅니다

썸네일 변환은 CPU를 오래 쓰고 실패할 수 있는 작업입니다. 이것을 업로드 응답 경로에 두면 업로드 지연이 이미지 크기와 서버 부하에 좌우되고, 변환기 하나가 죽으면 업로드 전체가 실패합니다. 요구사항에서 “썸네일은 잠시 뒤에 완성되어도 된다”는 답을 받았으므로 큐로 분리하는 것이 정당화됩니다.

여기서 중요한 것은 분리했다고 끝이 아니라는 점입니다. 비동기로 미룬 작업에는 세 가지가 따라붙어야 합니다. 사용자에게 보이는 상태값, 실패 시 재시도 정책, 그리고 끝내 실패한 작업을 격리할 곳입니다. 상태값이 없으면 클라이언트는 썸네일이 없는 이유를 알 수 없고, 재시도가 없으면 일시적 장애가 영구 손실이 되며, 격리 장소가 없으면 독성 메시지 하나가 큐 전체를 막습니다.

그리고 데이터베이스 커밋과 큐 발행이 원자적이지 않다는 점을 짚어야 합니다. 커밋 후 발행 직전에 프로세스가 죽으면 이미지는 있는데 변환 작업이 영원히 오지 않습니다. 해법은 같은 트랜잭션 안에서 발행할 메시지를 테이블에 기록하고, 별도 발행기가 그 테이블을 읽어 보내는 방식입니다. 이때 메시지가 두 번 전달될 수 있으므로 소비자는 멱등해야 합니다. 같은 이미지에 대한 변환을 두 번 수행해도 결과 키가 같으므로 이 작업은 자연스럽게 멱등하다는 점도 함께 말할 수 있습니다.

약한 답과 강한 답

약한 답 — “로드밸런서 뒤에 서버를 여러 대 두고, 이미지는 S3 같은 오브젝트 스토리지에 넣고, 앞에 CDN을 붙이고, 썸네일은 큐로 비동기 처리하고, DB는 읽기 복제본을 두고 필요하면 샤딩합니다. 캐시로 레디스를 붙이면 성능이 좋아집니다.”

강한 답 — “조회가 쓰기보다 압도적이고 썸네일 지연이 허용된다고 확인했으므로 그 두 가지를 근거로 순서를 잡겠습니다.

먼저 서버 한 대와 DB 한 대, 파일은 오브젝트 스토리지에 두는 구조에서 시작합니다. 이 구조에서 가장 먼저 포화되는 곳은 이미지 바이트가 애플리케이션 서버를 통과하는 지점입니다. 그래서 업로드는 서명 URL로 클라이언트가 스토리지에 직접 올리게 하고, 조회는 CDN에서 나가게 해 서버를 대역폭 경로에서 빼겠습니다. 이 변경으로 얻는 것은 서버 증설 없이 조회량을 감당하는 것이고, 지불하는 것은 권한 검사를 URL 서명과 만료로 옮기면서 생기는 복잡도입니다. 비공개 이미지는 공개 CDN 캐시에 올라가면 안 되므로 짧은 만료의 서명 URL을 쓰고, 이 경우 캐시 적중률이 낮아진다는 점을 감수합니다.

다음으로 느린 것은 썸네일 변환입니다. 이건 응답 경로에서 떼어 큐로 보내고, 이미지 상태에 변환 진행 여부를 명시해 클라이언트가 원본이나 자리표시자를 먼저 보여 주게 하겠습니다. 재시도는 지수 백오프에 무작위 지연을 섞고, 반복 실패한 메시지는 별도 큐로 격리해 나머지 처리를 막지 않게 합니다. DB 커밋과 큐 발행 사이의 구멍은 아웃박스 테이블로 막습니다.

데이터베이스는 아직 나누지 않겠습니다. 이 시점에서 DB가 하는 일은 메타데이터 조회이므로, 읽기 부하는 복제본으로 분산하고 쓰기는 인덱스 정리로 먼저 대응합니다. 샤딩은 조인과 트랜잭션 경계를 잃는 대가가 크므로, 단일 쓰기 노드가 실제로 포화되고 샤드 키를 접근 패턴에서 도출할 수 있을 때 꺼내겠습니다.

마지막으로 이 설계가 어떻게 실패하는지 말씀드리겠습니다. 인기 이미지 하나에 트래픽이 몰리면 서버 증설로는 해결되지 않는 핫 키 문제가 되고, CDN 만료 순간 요청이 한꺼번에 원본으로 몰릴 수 있어 만료 시각 분산과 단일 재생성 잠금이 필요합니다. 변환 워커가 밀리면 썸네일 없는 이미지가 쌓이므로 큐 적체 길이를 알림 지표로 두겠습니다.”

차이의 정체

두 답에 등장하는 부품은 거의 같습니다. 다른 것은 네 가지입니다. 첫째, 강한 답은 모든 추가에 그 추가를 강제한 관측을 붙였습니다. “조회가 많다” 때문에 CDN이, “썸네일 지연이 허용된다” 때문에 큐가 들어왔습니다. 둘째, 강한 답은 하지 않을 것을 명시했습니다. 샤딩을 지금 하지 않는다고 말하고 조건까지 붙인 것이 무작정 샤딩을 나열하는 것보다 훨씬 강합니다. 셋째, 강한 답은 각 결정이 지불하는 비용을 말했습니다. 서명 URL은 권한 로직을 복잡하게 만들고 비공개 이미지의 캐시 적중률을 떨어뜨립니다. 넷째, 강한 답은 자기 설계가 어떻게 실패하는지 스스로 말했습니다. 확장성 면접에서 이 마지막 항목을 하는 사람은 많지 않고, 하면 확실히 남습니다.

TIP

구성요소를 추가할 때마다 “이걸 빼면 무엇이 먼저 깨집니까”를 스스로 물어보십시오. 답이 안 나오면 그 부품은 아직 필요 없습니다. 면접관이 실제로 던지는 질문도 대부분 이 형태입니다.

캐시를 말할 때 반드시 함께 말할 것

캐시는 성능 도구가 아니라 계약입니다. 캐시를 도입하겠다고 말했으면 네 가지가 따라와야 합니다. 무엇을 키로 삼는가, 얼마나 살려 두는가, 원본이 바뀌면 어떻게 무효화하는가, 캐시가 죽으면 어떻게 되는가입니다.

특히 마지막이 중요합니다. 캐시가 통째로 비면 모든 요청이 원본으로 몰리고, 평소 캐시에 기대던 시스템은 그 순간 원본이 감당하지 못해 무너집니다. 그래서 캐시는 있으면 빠른 것이지 없으면 죽는 것이어서는 안 되고, 그렇게 만들려면 원본 쪽에 동시 요청 제한이나 단일 재생성 잠금이 필요합니다. 만료 시각을 전부 같게 두면 다 같이 만료되어 같은 상황이 주기적으로 재현되므로 만료에 무작위 폭을 주는 것도 기본입니다.

관측과 되돌리기

확장 설계에서 자주 빠지는 마지막 조각은 “이게 잘 돌고 있는지 어떻게 아는가”입니다. 지표는 평균이 아니라 상위 백분위로 봐야 합니다. 평균 응답 시간은 소수의 아주 느린 요청을 숨기는데, 사용자가 이탈하는 것은 정확히 그 소수 구간입니다. 오류율, 상위 백분위 지연, 큐 적체 길이, 워커 실패율 정도가 이 서비스의 기본 지표가 됩니다.

그리고 배포 전에 “무엇이 나빠지면 되돌릴 것인가”를 숫자로 정해 두어야 합니다. 되돌리기가 한 번의 동작으로 끝나려면 스키마 변경과 코드 변경을 같은 배포에 섞지 않아야 합니다. 이 부분은 데이터 모델링의 확장·전환·정리 절차와 이어집니다. 웹 요청이 실제로 어떤 경로를 지나는지 감이 흐릿하다면 웹사이트를 열면 무슨 일이 일어나는가를 먼저 보시기 바랍니다.

WARNING

“나중에 트래픽이 늘면 확장하면 됩니다”는 계획이 아닙니다. 확장은 대부분 코드 변경이 아니라 데이터 배치 변경을 요구하고, 그건 서비스가 커진 뒤에는 훨씬 비쌉니다. 지금 하지 않기로 한 결정에도 “어떤 신호가 보이면 착수한다”는 조건을 붙여 두어야 합니다.

이어지는 질문들

특정 이미지 하나에 트래픽이 몰리면요?

핫 키 문제이므로 샤딩이나 인스턴스 증설로는 해결되지 않습니다. 같은 키가 어느 샤드에 있든 그 하나로 몰리기 때문입니다. CDN 캐시 적중이 첫 번째 방어선이고, 만료 순간 원본으로 몰리지 않도록 만료 분산과 단일 재생성 잠금을 둡니다. 이 질문에 증설로 답하면 부하의 성질을 구분하지 못한 것으로 읽힙니다.

썸네일 생성이 밀리면 사용자에게 무엇을 보여줍니까?

처리 중 상태를 응답에 명시적으로 넣고 클라이언트가 원본이나 자리표시자를 먼저 보여 주게 합니다. 비동기로 미룬 작업은 사용자에게 보이는 상태값을 반드시 동반해야 하며, 최종 실패도 상태로 표현되어야 합니다. “나중에 처리됩니다”로 끝내면 클라이언트가 무한정 다시 조회하게 됩니다.

데이터베이스를 나눠야 할 시점은 언제입니까?

읽기는 복제본으로, 쓰기는 인덱스와 쿼리 정리로 먼저 해결하고 그래도 단일 쓰기 노드가 포화될 때 나눕니다. 샤딩은 조인과 트랜잭션 경계를 잃는 대가를 치르므로 샤드 키를 접근 패턴에서 도출할 수 있을 때만 꺼냅니다. 나누기 전에 오래된 데이터를 분리해 활성 테이블을 작게 만드는 방법이 대개 더 쌉니다.

복제본에서 읽으면 방금 올린 이미지가 안 보일 수 있지 않나요?

그렇습니다. 복제 지연 때문에 자기가 방금 한 쓰기를 자기가 못 읽는 상황이 생깁니다. 해법은 그 사용자에 한해 일정 시간 동안 쓰기 노드에서 읽거나, 클라이언트가 방금 만든 결과를 로컬에서 보여 주는 것입니다. 어느 일관성 수준을 사용자에게 약속할지 먼저 정해야 하는 문제입니다.

배포한 변경이 잘못됐다는 것을 어떻게 압니까?

배포 전에 무엇이 나빠지면 되돌릴지 지표와 임계값을 미리 정합니다. 오류율과 상위 백분위 지연을 배포 단위로 비교하고, 되돌리기가 한 번의 동작으로 끝나도록 스키마와 코드 변경을 분리해 배포합니다. “모니터링하겠습니다”만으로는 부족하고 어떤 값이 얼마나 나빠지면 되돌리는지가 있어야 합니다.

이 주제에서 자주 나오는 실수

첫째, 부품 나열입니다. 요구사항을 듣기 전에 답이 정해져 있으면 무엇을 물어도 같은 그림이 나옵니다.

둘째, 실패 모드를 말하지 않는 것입니다. 캐시가 죽으면, 큐가 밀리면, 외부 스토리지가 느려지면 어떻게 되는지가 없으면 그건 그림이지 설계가 아닙니다.

셋째, 숫자 없이 확장을 논하는 것입니다. 정확한 수치가 없어도 “업로드가 하루 수만 건 수준이라면 이 단계까지는 필요 없습니다”처럼 가정을 세워 범위를 좁혀야 합니다.

넷째, 비동기 도입을 만능으로 여기는 것입니다. 큐를 넣으면 응답은 빨라지지만 순서 보장, 중복 전달, 적체, 최종 실패 처리라는 새 문제가 생깁니다. 얻은 것과 새로 생긴 것을 같이 말해야 합니다.

다섯째, 되돌리기를 생각하지 않는 것입니다. 확장 작업은 대부분 데이터 이동을 포함하고, 중간에 멈췄을 때 어느 상태에 있게 되는지 설명하지 못하면 운영 경험이 없는 것으로 읽힙니다.

설계를 말하기 전 점검

  • 읽기와 쓰기 비율, 데이터 크기를 가정으로라도 말했는가
  • 가장 단순한 구조에서 시작했는가
  • 추가한 부품마다 그것을 강제한 관측이 붙어 있는가
  • 지금 하지 않기로 한 것과 그 착수 조건을 말했는가
  • 각 결정이 지불하는 비용을 스스로 언급했는가
  • 캐시를 넣었다면 무효화와 장애 시 동작까지 말했는가
  • 비동기로 미룬 작업에 상태·재시도·격리를 붙였는가
  • 지표와 되돌리기 조건을 숫자로 정했는가

연습 방법

같은 문제를 조건만 바꿔 다시 설명하는 방식이 가장 효율적입니다. 이미지가 아니라 동영상이면 무엇이 달라지는지, 업로드 직후 즉시 썸네일이 보여야 한다면 큐 분리가 여전히 가능한지, 이미지가 전부 비공개라면 CDN 전략이 어떻게 바뀌는지 스스로 답해 보십시오. 조건 하나가 어떤 구성요소를 무너뜨리는지 알면 그 구성요소를 이해한 것입니다.

그리고 반드시 시간을 재고 말해 보십시오. 요구사항 확인, 기본 구조, 병목 이동 두 번, 실패 모드, 지표 순서로 10분 안에 끝나야 합니다. 이어서 볼 주제는 API 설계이고, 전체 학습 경로는 백엔드 개발 로드맵과 심층 가이드에 정리되어 있습니다.

관련 언어 학습으로 복습하기