알림 시스템을 왜 별도로 분리했나

다양한 서비스에서 알림을 필요로 하지만, 단일 알림 서버가 모든 로직을 처리하면 병목이나 SPOF가 되기 쉽습니다. 그래서 알림 생성과 전송을 분리해 작업 서버가 담당하게 했고, 메시지 큐를 두어 비동기/확장 가능하도록 했습니다.

메시지 큐를 사용하는 이유와 이점은 무엇인가

알림 전송은 네트워크 상태나 제3자 서비스에 따라 지연될 수 있기 때문에, 작업 서버에서 느린 처리가 전체 서비스에 영향을 주지 않도록 메시지 큐를 사용해 비동기 처리합니다. 또한 큐는 일시적인 장애 시에도 재시도 처리가 가능해 안정성을 높입니다.

알림 중복 전송은 어떻게 방지했나

메시지 큐는 최소 1회 전송을 보장하므로 중복이 가능성상 존재합니다.

이를 막기 위해 Redis에 이벤트 ID 기반의 deduplication key를 두고 TTL을 설정하거나, DB에 (user_id, event_id) 조합으로 unique 키를 설정해 중복 전송을 방지했습니다.

꼬리질문: 알림 중복을 완전히 없앴다고 생각하나 아니라면 이유는?

완전히 없앨 수는 없습니다. 네트워크 장애나 작업 서버 재시작 시 동일 메시지를 여러 번 처리할 수 있기 때문입니다. 이 때문에 우리는 "완벽한 방지"보다 현실적인 수준의 멱등성(idempotency) 처리에 집중합니다.

알림 전송 실패 시 어떻게 처리되나

작업 서버는 전송 실패 시 재시도 큐에 넣고 exponential backoff를 적용합니다.

전송 로그는 알림 로그 DB에 기록되고, 일정 횟수 이상 실패 시 dead-letter queue(DLQ)로 이동시켜 수동 점검합니다.

알림 전송량이 폭주하면 어떻게 대응하나요?

큐의 backlog 수를 모니터링하고, 작업 서버를 수평 확장하도록 설계했습니다. K8s 환경에서는 HPA(Horizontal Pod Autoscaler)를 통해 자동 확장합니다.

특정 사용자에게 하루 1000개의 알림이 보내졌어요. 이걸 어떻게 막을 수 있나요?

Redis 기반의 사용자 알림 rate-limiting을 적용했습니다. 사용자 ID를 키로 하여 sliding window 방식으로 1일 기준 알림 개수를 체크합니다.

알림 순서를 보장해야 하는 경우 어떻게 설계할 건가요?

Kafka와 같은 메시지 큐를 사용하고, 특정 사용자 ID를 기준으로 같은 partition에 할당해 순서를 보장했습니다. 소비는 해당 partition을 단일 consumer로 처리해 순서 유지를 보장합니다.

알림 시스템은 서비스 개발할 때 많이 들어가는 기능인데, 생각해야하는 부분이 많은 것 같다.

하나에 대한 요청이 수 많은 다른 요청을 만드는 구조기 때문에 대규모에서도 잘 동작하게 설계해야 하는 부분 같다.

알림의 종류

  • 모바일 푸시 알림
  • SMS 메시지
  • 이메일

알림은 실시간/비실시간 가능

  • 실시간 알림
    • 이벤트 발생 직후 즉시 사용자에게 전달
    • 카카오톡 메시지 수신, 은행 OTP 등
  • 비실시간 알림
    • 일정 조건/시간 후 전달되거나, 큐에 적재되어 순차적으로 전송
    • 뉴스레터, 하루 요약 알림, 리마인더

연성(Soft) vs 경성(Hard) 특성도 있는데

  • 연성: 무시할 수 있는 정보성 알림
  • 경성: 무시할 수 없는 반드시 확인/처리해야하는 것

동기 vs 비동기 처리

  • 동기 처리
    • 이벤트 발생과 동시에 알림 발송
    • WebSocket, gRPC
  • 비동기 처리
    • 이벤트 큐에 적재 후 알림 발송
    • Kafka, RabbitMQ, AWS SNS+SQS 등 메시지 큐 사용

알림 유형 별 지원방식

알림에 유형 별 지원 방식이 다르다

  • iOS
    • APNs(Apple Push Notification service)
    • 앱에서 디바이스 토큰 받아서 서버에 전달해줘야함
      • 단말 토큰(알림 요청의 식별자)
      • 페이로드(알림 내용 담은 JSON 딕셔너리)
  • Android
    • FCM(Firebase Cloud Messaging)
    • 앱에서 FCM 토큰 받아서 서버에 저장
      • iOS 와 비슷함
  • SMS
    • SMS 게이트웨이(통신사별 서비스 혹은 Twilio)
    • 주의: ****발신번호 사전 등록, 요금 과금, 1일 건수 제한
  • Email
    • SMTP 운영 또는 SendGrid, Amazon SES, Mailgun 사용
    • SPF, DKIM, DMARC 설정 필수(스팸 필터 방지)
    • 자유도가 높은 반면 실시간성이 떨어짐

연락처 정보 수집 절차

이렇게 알림을 보내기 위해서는 사용자의 연락처 정보를 수집해야 한다.

  • 모바일 단말 토큰
  • 전화번호
  • 이메일 주소 등

이러한 정보를 받아 우리의 DB에 저장해 놓는다.

알람을 만들어 보낼 때 이때 저장한 DB에서 보낼 단말들을 찾아 전송해야한다.

알림 전송 및 수신 절차

  • 다양한 서비스(알림은 만드는 주체)는 알림을 보낼 필요 생김
  • 알림 시스템에 요청함
  • 알림 시스템은 각 단말에 맞게 알림을 만들어 제3자 제공 서비스로 보낸다.
  • 제3자 제공 서비스가 단말로 전송한다.

이 부분에서 부족한 부분이 알림 시스템이 너무 많은 역할을 수행한다는 것이다.

  • 많은 서비스가 알림 시스템으로 요청
  • 알림 시스템은 DB 조회가 필요
  • 제3자 제공 서비스로 알림을 보내기

이 경우 알림 시스템은 SPOF가 될 수 있고 병목되기도 쉽다. 거기다 확장하기도 힘들다.

이걸 분리해야한다.

  • 서비스에게 알림을 보내라는 요청을 받는 알림서버
  • 알림 서버에게 각 단말에 맞는 요청을 보내게 하는 작업서버
  • 그 사이에서 결합도를 낮추고 비동기적으로 수행할 수 있게 하는 메시지 큐
  • 알림을 보낼 단말 정보를 가져올 DB 와 캐시

알림 서버는

  • 알림 전송 API: 스팸방지를 위해 인증된 클라이언트의 알림만 허용
  • 알림 검증: 이메일 주소, 전화번호 등에 대한 기본적인 검증
  • 데이터베이스 또는 캐시 질의: 알림에 포함시킬 데이터 가져오는 기능
  • 알림 전송: 알림 데이터를 메시지 큐에 넣는다.

작업 서버는

  • 알림서버 메시지 큐에 넣은 알림을 꺼내 제3자 서비스로 전달하는 역할을 담당한다.
  • 이 알림이 잘 갔는지 중복은 없는지 판단하는 책임도 넣을 수 있다.

이렇게 작게 나눈 상태에서는 확장성도 높일 수 있고 iOS 알림 서비스가 안된다고 다른 곳에 영향을 미치지 못하게 되었다.

이 상황에서 추가적으로 고려해아하는 부분이 안정성, 추가 필요한 컴포넌트가 있다.

안정성

분산 환경에서 운영될 알림 시스템을 설계할 때는 안정성을 확보하기 위한 사항 몇 가지를 반드시 고려해야 한다.

데이터 손실 방지

알림에는 반드시 사용자에게 전달되어야 하는 알림이 있다. 이 경우는 늦게 가거나 순서가 바뀌는 등은 감내가능하다. 하지만 중간에 알림 데이터가 사라지는건 용납할 수 없다.

이러한 경우를 방지하기 위한 로직이 필요하다.

바로 작업 서버에 알림 로그 DB를 유지하는 것이다.

알림을 보냈을 때 로그를 작성해 손실 되었는지 확인할 수 있다.

알림 중복 전송 방지

동일한 알림을 다시 보내는 걸 완전히 막는 건 힘들다.

그 이유로는 메시지 큐는 최소 1회 전송 보장이기에 중복을 막기는 힘들고,

네트워크가 불안정해 전송 후 응답이 없을 경우 재시도 하는 로직이 있을 수 있다. 하지만 응답이 없던 알림이 느리게라도 도착할 수 있는 것이다. 이러한 경우 때문에 알림의 중복을 완전히 방지하는 건 힘들다.

그렇지만 최소화 하는 방법은 있다.

대표적인 방법이 보내야할 알림이 도착하면 그 이벤트 ID를 검사하여 이전에 본 적이 있는 이벤트인지 확인하는 것으로 중복된 건 처리안하는 방식으로 할 수 있을 것이다.

추가로 필요할 컴포넌트 및 고려사항

알림 템플릿

정해진 알림 템플릿으로 오류를 줄이고 시간을 아낄 수 있다.

알림 설정

사용자가 지정한 알림 설정(밤에는 안오게, 이메일만 허용)할 수 있게 해야한다.

전송률 제한

사용자에게 너무 많은 알림이 가지 않게 해야한다.

큐 모니터링

가장 중요한 지표가 메시지 큐의 쌓인 알림 수이다. 이걸 보고 작업 서버의 스케일 인아웃을 할 수 있을 것이다. 또한 왜 이렇게 쌓였는지 어디서 병목이 발생하는지 파악할 수 있다.

스터디에서 추가로 알게된 것

제3자 서비스로의 재시도 고려해야한다.

재요청 로직

  • 고정간격
  • 지수 백오프: 지수적으로 재요청 1, 2, 4, 8 … → 분산됨
  • 지수 백오프 + 지터: 지터는 네트워크에서 튀는 현상 이걸 일부로 넣는 것 그래서 더 분산될 수 있게

오늘 첫 면접에 다녀왔다.

45분 정도 면접이 진행됐고, 그렇게 길게 느껴지지는 않았다.

첫 면접인 만큼 합격한다는 마음보다 배우고 온다는 생각으로 임하자 다짐했는데,

막상 버벅이고 오니 조금 우울해지는 건 어쩔 수 없나보다.

질문들 다 기억하려 했는데, 긴장해서 몇 개 밖에 기억나지가 않는다.

일단 기억나는 것만이라도 적고

내가 부족했었던 질문은 기억나니 한 번 적어보자

 

  1. 자기소개 -> 준비한 거 함
  2. 포트폴리오 설명하기 -> 준비 부족, 핀트 놓침
  3. 클라우드와 온프레미스 사용자 측면에서 어떤부분이 다른지 -> 사용자가 그걸 구분하지는 않을 거 같고, 장애 없는 서비스 제공하는 거에서 클라우드 더 편리한 거 같다라는 식으로 대답
  4. 온프레미스 프로젝트를 클라우드로 이식하면서 가장 편리했던거? -> 완전관리형 서비스가 좋았다고 해야하는데 조금 버벅인 듯
  5. 비밀번호 암호화는 어떻게 했는지 이게 단방향인지 양방향인지 -> BcrytePasswordEncoder로 했다. 해시화 하는 거니까 단방향으로 알고 있다.
  6. 기술면접
    1. Java Spring 관련
      1.  MVC 구조 설명(어떤 역할인지) -> 각 책임을 분리해서, 나눈 것이다. 모델은 비지니스 로직, 뷰는 사용자에게 보여질거, 컨테이너는 앞단에 위치해서 ~~ 이런 식으로 대답함
      2. GC가 있는데 개발자가 왜 힙영역 졍리에 신경 써야하는지 -> GC가 참조가 없는 걸 반환하는 걸로 아는데 이게 완전하게 동작하는게 아니라고 알고있고, 개발자가 신경쓸 수 있는 부분에서 반환하는게 메모리 관리에서는 좋은 거 같다.
    2. DB 관련
      1. 인덱스가 무엇인지 -> 별도의 자료구조로 조회를 빠르게 하려는 목적을 가진 것인데, B-Tree나 B+Tree로 구현한다. 수정이 잦은 환경에서는 인덱스까지 수정해야 하기에 수정보다 조회가 더 많은 컬럼에 적용하는게 좋을 것 같다.
      2. 커넥션 풀이란(질문 리스트에 있었지만 질문 넘어감)
      3. SQL 인젝션 공격이란-> 서버로 들어오는 곳에 sql문을 넣어서 공격자가 원하는 방향으로 DB를 조작하는 공격이다.
    3. 코드 틀린거 찾기
      1. 회문 판별 코드에서 잘못된 점 -> 대충 맞음
      2. 배열 최대값 찾기 -> 제대로 못 찾아냄
      3. JSP에 java 코드로 DB 연결 후 SQL로 ID/PW 꺼내오는 코드 -> 제대로 못 찾아냄(SQL 인젝션 취약점, 커넥션 닫기)
  7. 왜 지원하게 됐는지 -> 준비한 거 말함
  8. 너가 개발자로서 어필할 수 있는 거 -> 무던함으로 스트레스 관리를 잘해서 개발자는 기한 관련해서 스트레스를 받거나 하는데 그거에서 괜찮다 이런식
  9. 받고 싶은 연봉 얼마인지 -> 딱히 생각한게 없었는데 그냥 대충 말함

기억 나는건 이 정도이다.

 

면접 분위기는 그렇게 딱딱하지 않았다. 면접관 3명과 나 이렇게 1대 3 면접이고,

내가 부족하다고 느꼈던 가장 큰 부분 두 개는 포트폴리오 발표랑 코드 틀린 거 찾기였다.

 

첫 번째 아쉬운 점

포트폴리오 기반해서 질문을 한다고 해서 포트폴리오에서 올 질문들을 많이 준비했는데

내 포트폴리오를 설명해보라고 해서 조금 당황했다.

포트폴리오에는 못 적은 기술적 내 성취를 말하는데 집중했어야 했는데,

단순히 포트폴리오에 나온 내용만 다시 말한 것 같다.

사실 이러면 불러서 듣는 의미가 없는데 말이다.

포폴 중 하나를 이야기하는 과정에서 기술적 내용이 아닌 프로젝트 자체에 대해서 설명하게 됐고,

바로 면접관님한테 컷 당했다.

기술적으로 뭘 얻었는지를 말해달라고 말이다.

여기서 뭔가 내가 잘못 접근했다는 걸 깨닿고 마지막 이야기할 때는 기술적으로 어떤 걸 했고 할건지 얘기하긴 했다.

 

두 번째 아쉬운 점

정말 아주 정말 간단한 코드였다.

고작 10줄 남짓한 코드였는데, 한 줄 읽고 다음 읽으면 이어지지가 않았다.

결국 최대값 구하는 간단한 코드에서 for문 인덱스 설정 잘못된 걸 못찾았다.

또 디비 커넥션 닫는 코드가 없는 것과 SQL문에 변수 그대로 사용해 SQL문에 사용해 SQL 인젝션에 취약하다는 점을 못 찾았다.

이게 긴장한 것 때문이기도 하지만 면접 준비가 하나도 안되어 있는게 느껴졌다.

 

마지막으로 MVC 구조 설명하고 무슨 역할인지 말해달라는 질문에

모델 뷰 컨트롤러로 책임을 분리한 것이고, 모델은 비지니스 로직, 뷰는 클라이언트에게 보여질 부분, 컨트롤러는 앞단에 위치에 연결한다. 이렇게만 설명했던게 가장 기본인데 대충 설명한 것 같아 아쉽다.

 

결과발표가 2~3주 정도 걸린다고 한다. 기대는 안되기 때문에 마음은 편한 것도 있다.

 

서류를 붙여주고 첫 면접 기회를 준 회사라서 뭔가 고맙고, 거기다 면접비까지 챙겨준거에서 좋았다.

면접 준비하면서 어떤 프로젝트들을 했는지 확인했는데 완성도가 높은 작업들이 많았다.

이런 점에서 여기서 일한다면, 재미있겠다라는 생각이 들었다.

만약 이번에 안되더라도 채용 공고가 올라오면 열심히 준비해서 한 번 더 도전해볼거 같다.

 

어제의 실습에서는 파드를 직접 생성하고 다루는 실습이었다.

실무에선 이러는 경우가 거의 없다고 한다.

Deployment 리소스를 통해 파드를 관리한다.

오늘의 실습은 Deployment를 다뤄보는 실습이다.

또한 지금까지의 실습환경은 VMware의 VM을 만들어 실습하고있다.

워밍업 CKA에서 나오는 문제 유형

kubectl get node -o wide --no-headers | awk '{print $6}'
172.16.0.128
172.16.0.133
172.16.0.136
172.16.0.137

awk 명령어

awk '{print $1}'
입력된 텍스트에서 첫 번째 필드만 출력
기본 구분자는 공백/탭

ps aux | awk '$2 < 10  {print $1, $2, $3, $11}
이건 pid가 10보다 작은 프로세스의 사용자이름 pid cpu 사용량, 실행 명령어 출력

삭제 명령어

[root@master ~]# kubectl delete pod nginx-pod --grace-period 0 --force
Warning: Immediate deletion does not wait for confirmation that the running resource has been terminated. The resource may continue to run on the cluster indefinitely.
pod "nginx-pod" force deleted

[root@master ~]# kubectl delete svc --all
service "clusterip" deleted
service "kubernetes" deleted
service "loadbalancer" deleted
service "loadbalancer8080-1" deleted
service "nodeport" deleted

Deployment 리소스 사용

[root@master ~]# kubectl create deployment nginx-app --replicas=3 --image=nginx
deployment.apps/nginx-app created

[root@master ~]# kubectl get all -n default
NAME                             READY   STATUS    RESTARTS   AGE
pod/nginx-app-7df7b66fb5-8qn6m   1/1     Running   0          16s
pod/nginx-app-7df7b66fb5-q65gl   1/1     Running   0          16s
pod/nginx-app-7df7b66fb5-xx8wv   1/1     Running   0          16s

NAME                 TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE
service/kubernetes   ClusterIP   10.96.0.1    <none>        443/TCP   3m27s

NAME                        READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/nginx-app   3/3     3            3           16s

NAME                                   DESIRED   CURRENT   READY   AGE
replicaset.apps/nginx-app-7df7b66fb5   3         3         3       16s

궁금증: Deployment 가 있을 때 ReplicaSet, Pod을 지웠을때는?

[root@master ~]# kubectl delete replicasets --all -n default
replicaset.apps "nginx-app-7df7b66fb5" deleted
[root@master ~]# kubectl get rs
NAME                   DESIRED   CURRENT   READY   AGE
nginx-app-7df7b66fb5   3         3         3       12s

다시 생성된다.

[root@master ~]# kubectl delete pods -n default --all
pod "nginx-app-7df7b66fb5-gdpb4" deleted
pod "nginx-app-7df7b66fb5-n7jqd" deleted
pod "nginx-app-7df7b66fb5-sk6qt" deleted

[root@master ~]# kubectl get Pods -n default
NAME                         READY   STATUS    RESTARTS   AGE
nginx-app-7df7b66fb5-9dqv9   1/1     Running   0          21s
nginx-app-7df7b66fb5-c5lqt   1/1     Running   0          21s
nginx-app-7df7b66fb5-lhkgl   1/1     Running   0          21s

다시 생성된다.

Deploymen와 Service 연결

Service --type ClusterIP

[root@master ~]# kubectl expose deployment nginx-app --name clusterip-app --type ClusterIP --port 8080 --target-port 80
service/clusterip-app exposed
  • nginx-app이라는 deployment에
  • clusterip-app이라는 Service를 만드는데
  • --type은 ClusterIP이고 → Pod끼리만 접속 가능한 고정된 IP 주소 할당하는 서비스
    • 내부용이므로 안전
    • 프론트 ↔ 백엔드
    • 백엔드 ↔ DB 또는 인증 서버
  • 서비스에서 들어올 때의 포트는 8080
  • 파드로는 80 포트로 간다.

Pod의 컨테이너에 직접 명령어를 통해 변경하기

[root@master ~]# kubectl exec -it nginx-app-7df7b66fb5-9dqv9 -- sh -c "echo 'worker' > /usr/share/nginx/html/index.html"

[root@master ~]# curl 10.101.49.221:8080
worker

Service --type Loadbalancer

외부 IP를 만들어서 그 곳으로 들어오는 요청을 Deployment의 Pod들에게 분배해주는 역할을 해줌

[root@master ~]# kubectl expose deployment nginx-app --name loadbalancer-app --type LoadBalancer --external-ip 172.16.0.128 --port 80
service/loadbalancer-app exposed
[root@master ~]# kubectl get svc
NAME               TYPE           CLUSTER-IP      EXTERNAL-IP    PORT(S)        AGE
clusterip-app      ClusterIP      10.101.49.221   <none>         8080/TCP       28m
kubernetes         ClusterIP      10.96.0.1       <none>         443/TCP        59m
loadbalancer-app   LoadBalancer   10.104.91.150   172.16.0.128   80:31224/TCP   10s

[root@master ~]# curl 172.16.0.128
worker2
[root@master ~]# curl 172.16.0.128
worker1
[root@master ~]# curl 172.16.0.128
worker1

롤링 업데이트 해보기

kubectl edit deployments nginx-app
-> 이미지 변경
--- 업데이트 진행 ---

[root@master ~]# kubectl get rs
NAME                   DESIRED   CURRENT   READY   AGE
nginx-app-7df7b66fb5   2         2         2       50m
nginx-app-864794d46b   2         2         1       9s

[root@master ~]# kubectl get rs
NAME                   DESIRED   CURRENT   READY   AGE
nginx-app-7df7b66fb5   0         0         0       52m
nginx-app-864794d46b   3         3         3       104s

[root@master ~]# kubectl get pods
NAME                         READY   STATUS              RESTARTS   AGE
nginx-app-7df7b66fb5-lhkgl   1/1     Running             0          49m
nginx-app-864794d46b-9t8ng   1/1     Running             0          28s
nginx-app-864794d46b-pxd7v   1/1     Running             0          21s
nginx-app-864794d46b-xbk7c   0/1     ContainerCreating   0          10s
[root@master ~]# kubectl get pods
NAME                         READY   STATUS      RESTARTS   AGE
nginx-app-7df7b66fb5-lhkgl   0/1     Completed   0          49m
nginx-app-864794d46b-9t8ng   1/1     Running     0          31s
nginx-app-864794d46b-pxd7v   1/1     Running     0          24s
nginx-app-864794d46b-xbk7c   1/1     Running     0          13s

ReplicaSet 교체

새로 ReplicaSet이 생성되고 교체됨

이전의 것은 그대로 파드가 0개로 유지를 해둠

Pod 교체

파드가 하나씩 새로운 ReplicaSet이 만드는 파드가 대체하고 있음

결국 전부 대체 되었다.

Scale-Out, In 해보기

Scale-Out

[root@master ~]# kubectl scale deployment nginx-app --replicas=6
deployment.apps/nginx-app scaled
[root@master ~]# kubectl get pod
NAME                         READY   STATUS    RESTARTS   AGE
nginx-app-864794d46b-9t8ng   1/1     Running   0          24m
nginx-app-864794d46b-fbgjt   1/1     Running   0          8s
nginx-app-864794d46b-p9j5z   1/1     Running   0          8s
nginx-app-864794d46b-pxd7v   1/1     Running   0          24m
nginx-app-864794d46b-xbk7c   1/1     Running   0          24m
nginx-app-864794d46b-z8qb8   1/1     Running   0          8s

Scale-In

[root@master ~]# kubectl scale deployment nginx-app --replicas=3
deployment.apps/nginx-app scaled

[root@master ~]# kubectl get pod
NAME                         READY   STATUS        RESTARTS   AGE
nginx-app-864794d46b-9t8ng   1/1     Running       0          25m
nginx-app-864794d46b-fbgjt   1/1     Terminating   0          88s
nginx-app-864794d46b-p9j5z   1/1     Terminating   0          88s
nginx-app-864794d46b-pxd7v   1/1     Running       0          25m
nginx-app-864794d46b-xbk7c   1/1     Running       0          25m
nginx-app-864794d46b-z8qb8   1/1     Terminating   0          88s

[root@master ~]# kubectl get pod
NAME                         READY   STATUS    RESTARTS   AGE
nginx-app-864794d46b-9t8ng   1/1     Running   0          26m
nginx-app-864794d46b-pxd7v   1/1     Running   0          26m
nginx-app-864794d46b-xbk7c   1/1     Running   0          25m

Deployment 삭제

[root@master ~]# kubectl delete deploy nginx-app
deployment.apps "nginx-app" deleted
[root@master ~]# kubectl get pod
NAME                         READY   STATUS        RESTARTS   AGE
nginx-app-864794d46b-9t8ng   1/1     Terminating   0          27m
nginx-app-864794d46b-pxd7v   1/1     Terminating   0          27m
nginx-app-864794d46b-xbk7c   1/1     Terminating   0          27m

[root@master ~]# kubectl get rs
No resources found in default namespace.
[root@master ~]# kubectl get pod
No resources found in default namespace.

Service 삭제

[root@master ~]# kubectl get svc
NAME               TYPE           CLUSTER-IP      EXTERNAL-IP    PORT(S)        AGE
clusterip-app      ClusterIP      10.101.49.221   <none>         8080/TCP       66m
kubernetes         ClusterIP      10.96.0.1       <none>         443/TCP        97m
loadbalancer-app   LoadBalancer   10.104.91.150   172.16.0.128   80:31224/TCP   37m
[root@master ~]# kubectl delete svc --all
service "clusterip-app" deleted
service "kubernetes" deleted
service "loadbalancer-app" deleted
[root@master ~]# kubectl get svc
NAME         TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE
kubernetes   ClusterIP   10.96.0.1    <none>        443/TCP   2s

kubernetes는 기본 서비스이므로 삭제되어도 자동으로 다시 만들어짐

'공부일지 > 클라우드 SA 교육' 카테고리의 다른 글

Kubernetes 설치 및 실습 on VMware  (1) 2025.07.07
Docker Swarm 실습해보기  (3) 2025.07.05
도커파일, 도커 컴포즈 실습 + cAdvisor 활용  (1) 2025.07.04
Ansible 실습2  (1) 2025.06.26
Ansible 실습  (1) 2025.06.25

웹크롤러가 무엇인가?

웹 크롤러는 인터넷 상의 웹 페이지를 자동으로 방문하고, 그 페이지의 콘텐츠와 하이퍼링크를 수집하는 프로그램입니다. 일반적으로 검색 엔진이 웹 전체를 색인할 때 사용되며, URL → HTML → 링크 추출 → URL 큐에 추가하는 과정을 반복하는 구조입니다.

꼬리질문: 그럼 크롤러와 스크래퍼는 어떻게 다르나요?

스크래퍼는 특정 페이지에서 필요한 정보만 추출하는 반면, 크롤러는 웹을 탐색하며 페이지를 순회한다는 점에서 다릅니다. 즉, 크롤러는 탐색 + 수집, 스크래퍼는 정보 추출에 초점이 있습니다.

BFS가 더 적합한 이유는?

웹은 유향 그래프 구조이기에 그래프 탐색 알고리즘이 필요 근데 여기서 DFS를 사용한다면 웹 페이지의 깊이는 예측할 수 없고 예의 없는 크롤링이 될 수 있습니다.

BFS는 FIFO 큐 기반이기 때문에 병렬 처리가 쉽습니다. 각 도메인에 대한 요청을 분산시키는 데 유리해 대규모 크롤러에는 BFS가 적합합니다.

꼬리질문: 그렇다면 BFS만 사용하면 모든 문제가 해결되나요?

아니요 BFS에는 우선순위가 없습니다. 하지만 웹페이지에는 중요한 정보나 필요성이 다 다를 것입니다. 따라서 우선순위를 나눠 큐를 사용하는 추가 설계가 필요할 수 있습니다.

크롤링에서의 예의란? 지키지 않는다면 어떻게 되나

크롤링에서의 예의란 대상 웹사이트 서버에 과도한 부하를 주지 않도록 조심하는 설계 원칙입니다.

이를 지키지 않으면 서버는 DDoS 공격으로 익식해 차단당합니다. 또한 robots.txt에 의해 법적, 윤리적 문제가 생길 수 있습니다.

꼬리질문: 예의를 지키기 위한 설계 방법에는 어떤 것이 있나요?

도메인별 큐를 별도로 두어 같은 사이트에 한 번에 하나의 요청만 보낼 수 있게 하고, 요청 간 간격을 설정해 부하를 적절히 유지합니다. 또한 robots.txt에 내용을 준수하는 것입니다.

크롤링 시스템을 설계해본 경험이 있나?

네, 저는 웹 기반 STT 시스템이나 검색 서비스 구축 중, 실시간 또는 주기적으로 정보를 수집하기 위해 웹 크롤러를 직접 설계하고 구현해본 경험이 있습니다. Seed URL부터 HTML Downloader, Parser, URL 필터, 중복 제거기까지 모듈화하여 설계했고, Spring 기반 RestTemplate과 Jsoup을 활용해 링크 추출과 텍스트 분석을 수행했습니다.

꼬리질문: 크롤러 설계 시 어떤 컴포넌트 간 분리를 가장 중요하게 생각했나요?

저는 URL 추출기와 콘텐츠 파서를 명확히 분리하는 것을 중요하게 생각했습니다. 콘텐츠 파서는 본문, 제목 등 의미 있는 정보를 처리하고, URL 추출기는 큐에 넣을 링크만 다뤄서 모듈화와 성능 최적화에 유리하도록 설계했습니다.

꼬리질문: 그중에서 가장 복잡하거나 어려웠던 부분은?

가장 어려웠던 부분은 중복 콘텐츠 제거와 거미덫(무한 URL 루프) 대응이었습니다. 단순 문자열 비교는 느려서 해시(SHA256) 기반으로 비교했지만, 완벽히 해결하기 어려운 케이스가 많았습니다. 그래서 URL 길이 제한, 비슷한 경로 패턴 필터링, 수집 이력 기반 TTL 설정 등으로 대응했습니다.

HTML 다운로더 최적화 방안은?

  • 분산 크롤링
  • DNS 캐싱
  • 짧은 타임아웃 설정
  • 지역성 고려

웹 크롤러란?

인터넷 상의 웹 페이지를 자동으로 방문하고, 그 내용을 수집하는 프로그램이다.

URL 접속 → 새로운 URL 획득 → 그 URL 접속

이걸 반복하는 것

단순한 것 같지만 이걸 위해서 많은 컴포넌트와 전략들이 필요하다

크롤러를 이용해서 다음과 같이 사용할 수 있다.

  • 검색 엔진 인덱싱: 크롤러가 웹 페이지의 내용을 수집하고, 이를 검색 엔진이 빠르게 찾을 수 있도록 인덱싱 하는 작업
  • 웹 아카이빙: 시간이 지나도 웹 페이지의 내용을 보존하기 위해 엡 전체 또는 특정 영역을 저장해두는 작업
  • 웹 마이닝: 웹에서 수집한 데이터에서 유용한 정보나 패턴을 추룰하는 데이터 마이닝 기법
  • 웹 모니터링: 웹사이트의 변경 사항, 상태, 콘텐츠 갱신 등을 실시간으로 또는 주기적으로 감시하는 용도

웹은 방대하기 때문에 만약 크롤링 규모에 따라 초대형 프로젝트가 될 수도 있다.

1단계 문제 이해 및 설계 범위 확정

기본 알고리즘

  1. URL 집합이 입력으로 주어지면 해당 URL들이 가르키는 모든 웹 페이지를 다운로드한다.
  2. 다운받은 웹 페이지에서 URL들을 추출한다
  3. 추출된 URL들이 다운로드할 URL 목록에 추가하고 위 과정 반복한다.

이러한 알고리즘을 기반으로 크롤러에 대해 명확히 해야한다.

  • 용도
  • 얼마나 많은 웹 사이트 수집해야하나
  • 수정된 것도 다시 고려해야하나
  • 중복된 컨텐츠는?
  • 규모 확장성: 병행성
  • 안정성: 잘못된 URL, 응답없음 등 대응
  • 예절: 한 번에 하나의 사이트에 많은 요청 X
  • 확장성: 다양한 컨텐츠를 고려해야할지
  • 등등

개략적 규모 추정

매달 10억 개의 웹 페이지 다운 ⇒ 약 400페이지/초

QPS = 400

피크 QPS = 400 * 2 = 800

웹 페이지 평균 용량 = 400k

10억 페이지니까 10억 * 400k = 500TB

5년 동안 유지해야하니까 500 * 12 * 5 = 30PB

이렇게 요구사항에 맞는 개략적 규모 추정이 들어가 줘야한다.

2단계 개략적 설계안 만들기

설계안에 포함된 컴포넌트

  • 시작 URL 집합
    • 웹 크롤러가 크로링을 시작하는 출발점
    • 크롤링 목적에 따라 적절히 골라야 함
  • 미수집 URL 저장소
    • 다운로드할 URL을 저장 관리하는 컴포넌트
    • FIFO 큐를 사용한다.
  • HTML 다운로더
    • 웹 페이지를 다운로드하는 컴포넌트이다.
    • 미수집 URL 저장소에서 꺼내서 저장
  • 도메인 이름 변환기
    • 웹 페이지 다운받을 때 URL을 IP로 변환하는 절차가 필요하다. 이 컴포넌트를 통해 변환한다.
  • 콘텐츠 파서
    • 다운로드한 걸 파싱과 검증하는 곳이다.
    • 이상한 컨텐츠를 제외하고 웹 페이지의 의미있는 내용을 JSON등의 형식으로 파싱
  • 중복 컨텐츠
    • 29% 가량의 웹 페이지 콘텐츠는 중복이다. 이런 걸 여러번 저장하면 성능에 악영향, 걸러주는 로직이 필요
    • 비교 가장 간단한건 문자열 비교, 하지만 느림
    • 두 문서의 해시값 비교가 가장 빠름
  • URL 추출기
    • 링크들을 골라내는 역할, 상대 경로를 전부 절대 경로로 변경
  • URL 필터
    • 특정한 콘텐츠 타입이나 파일 확장자를 갖는 URL, 접속 시 오류가 발생하는 URL, 접근 제외 목록에 포함된 URL 등을 크롤링 대상에서 배제하는 역할
  • URL 저장소
    • 이미 방문한 URL을 보관하는 저장소

웹 크롤러 작업 흐름

  1. 시작 URL들을 미수집 URL 저장소에 저장한다.
  2. HTML 다운로더는 미수집 URL 저장소에서 URL 목록을 가져온다.
  3. HTML 다운로더는 도메인 이름 변환기를 사용하여 URL의 IP주소를 알아내고, 해당 IP 주소로 접속하여 웹 페이지를 다운받는다.
  4. 콘텐츠 파서는 다운된 HTML 페이지를 파싱하여 올바른 형식을 갖춘 페이지인지 검증한다.
  5. 콘텐츠 파싱과 검즘이 끝나면 중복 콘텐츠인지 확인하는 절차를 개시
  6. 중복 콘텐츠인지 확인하기 위해서, 해당 페이지가 이미 저장소에 있는지 본다.
    1. 이미 저장소에 있는 콘텐츠는 처리하지 않는다
    2. 저장소에 없는 콘텐츠인 경우 저장소에 저장한 뒤 URL 추출기로 전달
  7. URL 추출기는 해당 HTML 페이지에서 링크를 골라낸다.
  8. 골라낸 링크를 URL 필터로 전달
  9. 필터링이 끝나고 남은 URL만 중복 URL 판별 단계로 전달
  10. 이미 처리한 URL인지 확인하기 위하여, URL 저장소에 보관된 URL인지 확인, 이미 있는 URL이면 버리기
  11. 저장소에 없는 URL은 URL저장소에 저장할 뿐 아니라 미수집 URL 저장소에도 전달해야한다.

상세 설계

이제 이러한 컴포넌트의 구현 기술에 대해서 이야기해야 한다.

DFS vs BFS

웹은 유향 그래프와 같다.

이 그래프를 탐색하는 알고리즘을 선택해야한다.

DFS의 경우 깊이 우선이다. 웹 페이지의 깊이를 예측할 수 없기에 DFS는 적절하지 않다.

BFS 즉 너비 우선 탐색을 사용한다. 이건 FIFO 큐를 통해 구현하는데 한 쪽에 URL 넣고 다른 쪽에서 꺼내서 URL을 탐색하면 됨

BFS를 사용해도 발생하는 문제점들이 있다.

  • 한 페이지에서 나오는 링크의 상당수는 같은 서버로 돌아간다. → 이 경우 반복적으로 하나의 서버에 부하를 줄 수 있는 예의 없는 크롤러가 된다.
  • 표준적 BFS는 우선순위가 없다. 하지만 모든 웹페이지는 동일한 수준의 품질, 중요성을 가진게 아니다. 사용자 트래픽, 업데이트 빈도 등으로 순위를 정해야 효율적이다.

미수집 URL 저장소

이러한 문제는 미수집 URL 저장소로 쉽게 해결할 수 있다.

문제 1 예의

동일 웹 사이트에는 한 번에 한 페이지만 요청한다.

이걸 보장하기 위해 하나의 호스트명의 하나의 큐를 두어 한 번에 한 페이지만 요청하도록 강제하는 것이다. 다음과 같은 컴포넌트로 구성한다.

  • 큐 라우터: URL이 같은 호스트 명에 맞는 큐로 가도록 한다.
  • 메핑 테이블: 호스트 명과 큐를 매핑
  • FIFO 큐: 호스트명이 같은 URL을 보관하는 큐
  • 큐 선택기: 큐들을 선회하면서 URL를 꺼내어 다운로드 작업 스레드로 보내는 역할
  • 작업 스레드: 전달된 URL을 다운로드하는 작업을 수행한다.

문제 2 우선순위

웹 페이지의 우선순위를 정하고, 우선순위에 맞게 처리하도록 하는 것

컴포넌트

  • 순위결정장치: URL을 입력받아 정해놓은 우선순위 기준으로 계산한다.
  • 우선순위 큐: 우선순위별로 큐가 하나씩 할당된다. 우선순위 높으면 선택될 확률도 높아야한다.
  • 큐 선택기: 우선순위가 높은 큐에서 더 자주 뽑도록하는 선택기

이 둘을 적용한다면 전면 큐에 우선순위 결정 과정을 진행하고,

후면 큐에서는 크롤러가 예의 있게 동작하도록 하면된다.

추가적으로 고려해야할 것이 있다.

추가 고려사항 1 신선도

웹 페이지는 수시로 변경된다. 한 번 수집하고 끝내면 안된다.

재수집을 위한 로직을 최적화 할 필요가 있다.

  • 웹 페이지의 변경 이력 활용
  • 우선순위에 따라 재수집 빈도 차등을 둠

추가 고려사항 2 미수집 URL 저장소를 위한 지속성 저장장치

URL의 수는 수억 개에 달한다. 그러니 그 모두를 메모리에 저장은 힘듬 또한 전부 디스크에 저장하는 것은 느려짐 → 병목 지점

대부분은 메모리와 디스크를 같이 사용한다.

HTML 다운로더

다운로더는 해당 사이트의 로봇 제외 프로토콜을 읽고 허락한 페이지만 다운 받아야 한다. 그 내용은 Robots.txt에 있다.

Robots.txt

로봇 제외 프로토콜이라고 불리며 이걸로 크롤러와 소통함

이 안에 허락하지 않는 페이지가 적혀있고, 그걸 제외하고 다운받아야한다.

다운로더 성능 최적화

  • 분산 크롤링: 성능을 높이기 위해 크롤링 작업을 여러 서버에 분산
  • 도메인 이름 변환 결과 캐시: DNS 변환은 동기적 작업, 그렇기에 병목지점이 되기 쉬움 그렇기에 조회 결과를 캐시해서 병목을 줄이도록 해야한다.
  • 지역성: 크롤링 대상 웹 페이지 서버와 크롤링 서버가 가깝다면 더욱 지연이 적을 것이다.
  • 짧은 타임아웃: 응답이 없음을 판단하는 타임아웃을 짧게 둬 빠르게 다음으로 넘어가 성능 최적화

다운로더 안정성

  • 안정 해시: 다운로더 서버에게 URL 분산할 때 사용
  • 크롤링 상태 및 수집 데이터 저장: 장애 발생 시에도 저장된 데이터를 통해 복구 가능
  • 예외 처리: 에러를 시스템 전반에 퍼지지 않게 설계
  • 데이터 검증: 데이터 검증을 통해 시스템 오류를 줄인다.

확장성

다양한 웹 사이트의 콘텐츠를 대응하기 위해 새롭게 설계하는 건 힘들다. 그렇기에 다양한 모듈을 통해 확정성을 높일 수 있게 설계해야한다.

PNG 다운로더, 영상 다운로더, 웹 모니터 등을 모듈화 해서 추가할 수 있을 것이다.

문제 있는 콘텐츠 감지 및 회피하기

  • 중복 콘텐츠: 중복 콘텐츠를 해시나 체크섬을 사용하여 탐지하고 무시
  • 거미 덫: 크롤러를 무한루프 빠지게 하려는 덫이다. 이는 URL의 최대 길이를 두면 어느 정도 해결 가능하지만 완벽하지는 않다. 수작업도 고려 대상이다.
  • 데이터 노이즈: 광고나 스크립트 코드, 스팸 URL 같은 것들이 노이즈이다. 이걸 잘 제외해야한다.

마무리

크롤러가 갖추어야 하는 특성들을 알아보고 설계해봤다.

규모확장성, 예의, 확장성, 안정성 등을 알아봤다.

추가적으로 봐야하는 부분은

  • 서버 측 렌더링
  • 원치 않는 페이지 필터링
  • 데이터베이스 다중화 및 샤딩
  • 수평적 규모 확장성
  • 가용성, 일관성, 안정성
  • 데이터 분석 솔루션

등이 있다.

단축 URL은 어떤 방식으로 생성되나요? (해시 기반인가요? ID 기반 + base62인가요?)

저는 ID 기반 + base62 방식을 사용하였습니다.

우선, 스노우플레이크 방식으로 유일 ID를 생성하고, 이를 62진법으로 인코딩하여 단축 URL로 사용했습니다.

이 방식은 충돌이 없고, 생성 속도가 빠르며, 단축된 URL 길이를 예측 가능하다는 장점이 있습니다.

꼬리질문: 왜 해시 기반 방식은 선택하지 않으셨나요?

해시 기반 방식은 충돌 가능성이 존재하고, 중복 체크를 위해 DB 조회가 필요하다는 점에서 성능상 불리하다고 판단했습니다. 또한 동일한 URL을 단축했을 때 일관성 유지도 어렵습니다. 반면, ID 기반은 충돌이 없고 DB 조회 없이도 바로 생성 가능합니다.

단축 URL 충돌은 어떻게 처리하셨나요?

ID 기반 + base62 방식에서는 충돌이 발생하지 않도록 설계되어 있기 때문에 일반적인 충돌 처리는 필요하지 않았습니다. 하지만 혹시 모를 중복 URL 입력 시 처리를 위해, longURL이 이미 존재하는 경우는 기존의 shortURL을 반환하도록 구현했습니다. 이를 위해 longURL에 대한 인덱스와 Unique 제약 조건을 걸었습니다.

단축 URL 접근 시 301과 302 중 어떤 리디렉션 방식을 사용했고, 그 이유는 무엇인가요?

저는 302(Found)응답을 사용했습니다.

302는 클라이언트가 매번 서버를 거치기 때문에 트래픽 로그 추적이 가능합니다. URL 단축기에서는 사용자 행동 분석, 클릭 수 측정이 중요하기 때문에 이 목적에 맞췄습니다. 반면 301은 캐시로 인해 로깅이 되지 않을 수 있어, 트래킹 측면에서는 불리합니다.

꼬리질문: 302는 서버 부하가 높아질 수 있는데, 그에 대한 대책은 있으셨나요?

맞습니다. 302 방식은 항상 서버에 도달해야 하므로, 캐시 레이어를 추가했습니다. Redis에 <shortURL, longURL>을 캐시해두고, 우선 조회하여 성능을 높였습니다. 또한 읽기 요청이 많기 때문에 read-heavy한 구조를 고려해 DB replica를 활용하거나, CDN 연동도 고려했습니다.

성능 최적화를 위해 어떤 기술(예: 캐시, DB 설계, rate limiting 등)을 적용하셨나요?

네 저는 캐싱과 DB 인덱싱, 처리량 제한 중 쓰기 제한, 비동기 로깅을 사용하였습니다. 각각 설명하자면

캐싱: Redis를 활용해 자주 요청되는 단축 URL을 캐싱했습니다.

DB 인덱싱: shortURL과 longURL 모두 인덱스를 적용해 빠른 검색을 지원했습니다.

쓰기 제한: Redis + Lua Script를 활용해 IP 기반 rate limiting을 구현했습니다.

비동기 로깅: 클릭 로그는 별도 로그 수집 시스템으로 비동기 전송하여 리디렉션 속도에 영향 없도록 했습니다.

꼬리질문: 캐시 무효화 전략은 어떻게 구성하셨나요?

shortURL은 대부분 불변성이 높아 캐시 TTL은 상대적으로 길게 설정했습니다. 그러나 관리자가 longURL을 변경하거나 삭제하는 경우에는 DB와 캐시를 함께 업데이트하도록 트랜잭션 후 캐시 갱신 또는 삭제 로직을 넣었습니다.

URL 단축기에서 확장성은 어떻게 고려하셨나요? (트래픽 증가, 서버/DB 확장 등)

웹 서버무상태(stateless) 로 설계하여 수평 확장이 용이하도록 했고, 로드밸런서를 통해 트래픽 분산이 가능합니다. DB는 읽기/쓰기 분리 구조를 사용하고, 읽기 부하는 replica DB를 통해 분산시켰습니다. 데이터가 커짐에 따라, longURL은 샤딩, click 로그는 로그 전용 스토리지로 분리해 운영했습니다.

꼬리질문: 트래픽이 몰릴 경우 병목이 발생할 수 있는 지점은 어디였고, 그에 대한 대응은 어떻게 하셨나요?

병목 가능성이 가장 큰 곳은 DB 조회 및 저장, 캐시 미스일 수 있습니다. 이를 위해 캐시 적중률을 높이고, DB 부하 분산을 위해 read replica비동기 처리 구조를 도입했습니다. 또한 로깅은 반드시 실시간일 필요가 없기 때문에 Queue(Kafka, SQS 등) 를 사용해 비동기로 처리했습니다.

URL 단축기란

길고 복잡한 URL을 짧고 간결한 형태로 바꿔주는 서비스 또는 시스템이다.

왜 필요한가

  • 공유 용이성
  • 통계 추적: 단축 URL을 통해 클릭 분석
  • 가독성 향상: 짧아졌으니
  • 리디렉션 제어: 단축 URL을 추후에 다른 곳으로 변경 가능

요구사항

쓰기 연산: 매일 1억 개의 단축 URL 생성 → 초당 1160

읽기 연산: 읽기: 쓰기 비율 10:1 로 가정하면 → 초당 11600

운영기간: 10년간 1억 * 365 * 10 → 3650억 개의 레코드

축약 전 URL 평균길이 100

총 저장 용량은 3650억 * 100 바이트 → 36.5TB

URL 단축기 구성요소

API 엔드포인트

클라이언트는 이 엔드포인트를 통해 서버와 통신한다.

REST API로 설계한다면

단축 URL 생성 (쓰기)

POST /api/v1/data/shorten

인자: {longURL: longURLstring}

응답: 단축 URL

단축 URL 접근(리디렉션 처리)

GET /api/v1/shortURL 302나 301 리디렉션

응답: 단축 URL

URL 리디렉션

단축 URL 접근 시 단축 URL로 리디렉션 해줘야한다.

이때 301, 302 응답의 차이가 발생한다.

301 Permanently Moved

이건 영구적으로 웹 페이지가 바뀌었다는 뜻이다. 따라서 클라이언트는 자신이 방문한 적있다면 단축 URL을 캐싱했다가 바로 원본 URL로 요청한다. → 서버에 부담 적지만 트래픽 로깅하지 못함

302 Found

항상 단축기 서버에 요청 후 리디렉션을 받아온다. 이 경우는 서버에 부담을 줄이진 못하지만 트래픽을 로깅할 수 있다.

URL 단축

URL을 단축하는 로직을 정해야한다.

하나의 원본 URL에 대응하는 단축 URL은 하나만 있어야한다.

다음 요구사항을 부합하는 해시 함수를 찾으면 된다.

  • 입력으로 주어지는 긴 URL이 다른 값이면 해시 값도 달라야한다.
  • 계산된 해시 값은 원래 입력으로 주어졌던 긴 URL로 복원될 수 있어야한다.

데이터 모델

하지만 여기서 모든 데이터를 해시 테이블에 넣는건 메모리에 부담

DB에 저장할 수 있어야 한다.

이때 <단축 URL, 원본 URL> 순서쌍으로 저장하면된다.

해시함수

해시 함수는 원래 URL을 단축 URL로 변환하는 데 쓰인다.

해시 값 길이

해시에 들어갈 수 있는 문자를 숫자 알파벳 대소문자라고 한다면 62개가 가능하다.

한글자 당 62개를 저장할 수 있다는 것

요구사항이 3650억개의 레코드기 때문에 해시 값 길이는 7개는 되어야 한다.

이제 이 해시 값을 얻어내는 방식인 해시 함수 구현 기술 2가지를 알아 볼 것이다.

해시 후 충돌 해소

해시 함수 중 잘알려진 함수를 사용하는 방식이 가장 편할 것이다.

CRC32, MD5, SHA-1 다음과 같은 함수들의 출력은 모두 7보다 긴 값을 출력한다.

이 때 앞의 7자리만 사용한다고 한다면 해결은 되겠지만 겹칠 확률이 올라 갈 것이다.

중복처리방법

이때 해시 함수를 통해 단축 URL을 만든 후 DB에서 조회를 해본다.

  • 있다. → 중복인 것이다. 중복을 피하기 위해 원본 URL에 미리 정한 문자열을 추가 후 다시 수행
  • 없다. → 바로 DB에 저장

추후에는 추가된 문자열을 제외하고 사용하면 될 것이다.

base-62 변환

진법 현환은 URL 단축기 구현에 흔히 쓰이는 방식이다.

여기서 62진법인 이유는 알파벳 대소문자 + 숫자(0~9) 가 62개이기 때문

base-62 변환에서는 변환할 ID를 만들어줄 생성기가 필수적이다.

여기서는 보통 트위터 스노우플레이크 방식의 유일성 보장 ID 생성기를 활용한다. → 정수이기 때문

이렇게 만들어진 ID를 62진법으로 변환하면 단축 URL이 되는 것이다.

이렇게 만들어진건 유일성이 보장되었기 때문에 별도의 중복 처리할 필요 없다.

또한 자리수가 고정되지 않을 수 있는 특징도 있다.

URL 단축기 상세 설계: 단축 URL 생성 과정

  1. 입력으로 원본 URL를 받는다.
  2. DB에 해당 URL이 있는지 검사
  3. 있다면 가져와서 제공
  4. 없다면 유일한 ID를 생성
  5. 62 진법 적용해 단축 URL로 만듦
  6. 새로운 레코드로 저장 후 단축 URL 제공

URL 리디렉션 상세 설계

먼저 DB에 매번 조회하는 것 병목지점이 된다.

여기에 <단축URL, 원본URL>로 된 쌍을 키벨류로 저장하는 캐시를 두어 성능을 높일 수 있다.

  1. 사용자가 단축 URL을 클릭
  2. 로드밸런서가 해당 클릭으로 발생한 요청을 웹 서버에 전달
  3. 단축 URL이 이미 캐시되어 있다면 원본을 바로 전달
  4. 없다면 DB에서 꺼내서 캐싱하고 단축 URL 전달

마무리

이번에는 URL단축기의 API, 데이터 모델, 해시 함수, URL 단축 및 리디렉션 절차를 알아봤다.

추가적으로 알아볼 내용으로는

  • 처리율 제한 장치: 엄청난 양의 단축 요청 시 문제가 될 수 있다. 이걸 방지하기 위해 쓰기요청이나 읽기 요청의 제한을 둘 수 있을 것이다.
    • IP 기반 제한
    • 토큰 버킷 알고리즘
    • Redis + Lua Script
  • 웹 서버 규모 확장: 설계를 무상태성으로 했다면, 웹 서버 확장은 쉬울 것이다.
  • DB 규모 확장: 다중화나 샤딩을 통해 확장 가능할 것이다.
  • 데이터 분석 솔루션: URL 단축기에 데이터 분석 솔루션을 두어 어떤 링크를 자주 사용하는지 등을 분석할 수 있을 것이다.
  • 가용성, 데이터 일관성, 안정성: 대규모 시스템이 성공적으로 운영되기 위해서는 반드시 갖추어야 할 속성들이다.

+ Recent posts