🚀 URL 단축 서비스 개발 로드맵

Phase 1: 기본 기능 구현 (모놀리식 MVP)

이 단계에서는 완벽함보다는 ‘동작하는 것’에 초점을 맞춥니다.

  1. 기술 스택 선택:

    • Backend: Spring Boot(Java), Node.js/Express(JavaScript), Django/Flask(Python) 등 본인에게 가장 익숙한 기술을 선택하세요.
    • Database: MySQL, PostgreSQL 같은 RDBMS가 시작하기에 좋습니다. 구조가 명확하기 때문입니다.
    • Frontend: 아주 간단한 HTML Form이면 충분합니다. (URL 입력창, 제출 버튼)
  2. 핵심 기능 구현:

    • URL 단축:
      • 사용자가 긴 URL을 입력하면, 고유한 짧은 키(key)를 생성하여 DB에 저장합니다.
      • 원본 URL단축 키를 함께 저장합니다.
    • URL 리다이렉션:
      • 사용자가 http://내도메인.com/{단축키}로 접속하면,
      • DB에서 {단축키}에 해당하는 원본 URL을 찾아 301 Moved Permanently 또는 302 Found 로 리다이렉트 시킵니다.
  3. DB 테이블 설계:

    • 아주 간단하게 시작합니다.
    CREATE TABLE urls (
        id BIGINT AUTO_INCREMENT PRIMARY KEY,
        original_url VARCHAR(2048) NOT NULL,
        short_key VARCHAR(10) NOT NULL UNIQUE, -- **여기에 인덱스 거는게 중요!**
        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    );
  4. ‘단축 키’ 생성 전략 (⭐ 꿀잼 포인트 1):

    • 방법 1 (비추): 그냥 랜덤 문자열 생성. 중복 가능성이 있고, 중복 체크를 위해 매번 DB를 조회해야 해서 비효율적입니다.
    • 방법 2 (강력 추천): Base62 인코딩.
      1. DB에 URL을 저장하면 AUTO_INCREMENT로 고유한 숫자 ID가 생성됩니다. (예: 1, 2, 3, …)
      2. 이 숫자 ID를 62진수(0-9, a-z, A-Z)로 변환합니다.
      3. 예: ID: 1 1, ID: 10 a, ID: 62 10
      4. 이 변환된 값이 바로 short_key가 됩니다. 이 방법은 중복이 절대 발생하지 않고, ID가 커져도 URL 길이가 완만하게 늘어나는 장점이 있습니다.

Phase 2: 성능 개선 (본격적인 경험 쌓기)

MVP가 완성되고 나면, 이제부터가 진짜 실력을 늘리는 구간입니다.

  1. 부하 테스트 (성능 측정의 시작):

    • 개선 전 성능을 측정해야 개선 후 얼마나 좋아졌는지 알 수 있습니다.
    • nGrinder, JMeter, k6 같은 부하 테스트 툴을 사용해서 현재 시스템의 초당 처리량(TPS), 응답 시간 등을 측정합니다. (예: 초당 100개의 리다이렉션 요청 보내보기)
  2. 캐싱 도입 (⭐ 꿀잼 포인트 2):

    • 문제점: 모든 리다이렉션 요청마다 DB를 조회하는 것은 매우 비효율적입니다. DB는 디스크 I/O를 사용하므로 가장 느린 구간입니다.
    • 해결책: RedisCaffeine 같은 캐시를 도입합니다.
    • 로직 변경:
      1. 리다이렉션 요청({단축키})이 들어온다.
      2. 먼저 캐시(Redis)를 확인한다. (short_key를 key로 original_url을 value로)
      3. Cache Hit (캐시에 데이터가 있음): DB 조회 없이 바로 캐시에서 원본 URL을 꺼내 리다이렉트한다. (매우 빠름)
      4. Cache Miss (캐시에 데이터가 없음): DB를 조회해서 원본 URL을 가져온다. 그리고 이 데이터를 캐시에 저장한 뒤 리다이렉트한다. (다음 요청부터는 빨라짐)
    • 결과: 캐시를 적용하고 다시 부하 테스트를 해보세요. TPS가 말도 안 되게 치솟는 것을 경험할 수 있습니다.
  3. DB 최적화:

    • short_key 컬럼에 **인덱스(Index)**를 걸었는지 다시 한번 확인하세요. 인덱스가 없으면 데이터가 수백만 건 쌓였을 때 조회 속도가 급격히 느려집니다.
    • 커넥션 풀(Connection Pool) 설정을 최적화합니다. (대부분의 프레임워크가 기본적으로 잘해주지만, 원리를 이해하는 것이 중요합니다.)

Phase 3: 더 나아가기 (고급 기능 및 아키텍처 고민)

성능 개선에 자신감이 붙었다면, 이제 기능을 확장하며 새로운 문제들을 마주해볼 수 있습니다.

  1. 분석 기능 추가:

    • 단축 URL이 몇 번 클릭되었는지 카운트하는 기능을 추가합니다.
    • 고민거리: 클릭마다 DB에 UPDATE 쿼리를 날리면 다시 성능이 저하될 수 있습니다. 어떻게 효율적으로 처리할 수 있을까요? (ex. 메시지 큐에 쌓아두고 배치 처리, 별도의 통계 서버…)
  2. API 서버 분리 (MSA 맛보기):

    • 지금은 모놀리식이지만, 만약 서비스가 커진다면?
    • URL을 단축하는 서버(Write)와 리다이렉트하는 서버(Read)의 역할이 다릅니다. 리다이렉션 요청이 훨씬 많겠죠.
    • 이 두 서버를 분리하고, 로드 밸런서를 앞에 두어 트래픽을 분산시키는 구조를 구상해볼 수 있습니다.
  3. 안정성을 위한 장치:

    • Rate Limiting (요청 수 제한): 악의적인 사용자가 무한정 URL을 생성하는 것을 막기 위해 IP당 1분에 10번만 생성 가능하도록 제한을 걸어봅니다. (Token Bucket 알고리즘 등을 학습해볼 기회)
    • Custom URL: 사용자가 직접 단축 키를 지정하는 기능을 추가합니다.

결론

“URL 단축 서비스를 모놀리식으로 만들어서 성능 개선까지 해보는 경험”은 정말 훌륭한 학습 경로입니다.

  • 1단계 (MVP): 웹 개발의 기본기를 다질 수 있습니다.
  • 2단계 (성능 개선): 캐시, DB 인덱싱 등 백엔드 개발자의 핵심 역량인 성능 최적화를 직접 체험하고 데이터로 증명할 수 있습니다.
  • 3단계 (고급 기능): 대용량 트래픽과 서비스 확장에 대한 고민을 시작하며 아키텍처 설계 능력을 키울 수 있습니다.

이 프로젝트 하나만 제대로 파고들어도 이력서에 쓸 멋진 스토리가 생길 겁니다. 망설이지 마시고 지금 바로 시작해보세요! 응원하겠습니다


결론부터 말씀드리면, JVM 튜닝 요소는 분명히 존재합니다. 하지만 중요한 것은, 앞서 언급한 캐싱이나 DB 인덱싱 같은 아키텍처 개선이 100배는 더 효과적이라는 점입니다. JVM 튜닝은 그 다음 단계의 ‘마이크로 최적화’에 가깝습니다.

이 프로젝트의 목표가 ‘학습’이므로, JVM 튜닝을 통해 어떤 효과를 볼 수 있고, 어떤 원리로 동작하는지 경험하는 것 자체에 큰 의미가 있습니다.

URL 단축 서비스의 특성과 연관 지어 JVM 튜닝 포인트를 짚어보겠습니다.

URL 단축 서비스의 JVM 관점에서의 특징

  1. 매우 짧은 생명주기를 가진 객체가 대량으로 생성됩니다.

    • HTTP 요청이 들어올 때마다 Request, Response 객체, 컨트롤러, 서비스 레이어의 객체 등이 생성되었다가 응답이 끝나면 바로 버려집니다.
    • 이는 JVM의 Young Generation 영역과 Minor GC에 직접적인 영향을 줍니다.
  2. 응답 시간(Latency)이 매우 중요합니다.

    • 사용자는 리다이렉션이 1초라도 지연되면 바로 알아차립니다.
    • GC로 인해 발생하는 “Stop-the-World” (STW) 시간을 최소화하는 것이 관건입니다. STW 동안에는 애플리케이션의 모든 스레드가 멈추기 때문에 응답이 지연됩니다.

학습을 위한 JVM 튜닝 시나리오

0단계: 모니터링 (가장 중요!)

튜닝의 시작은 측정입니다. 내 애플리케이션이 어떻게 동작하는지 모르면 튜닝할 수 없습니다.

  • 툴: VisualVM 또는 JConsole (JDK에 기본 포함)을 사용하세요.
  • 방법:
    1. 애플리케이션을 실행합니다. (java -jar my-app.jar)
    2. VisualVM을 실행하여 실행 중인 내 애플리케이션 프로세스에 연결합니다.
    3. 부하 테스트 툴(nGrinder, JMeter 등)로 트래픽을 발생시킵니다.
    4. VisualVM의 Monitor 탭과 Visual GC 플러그인 탭에서 Heap 메모리 사용량, CPU 사용량, GC 활동을 실시간으로 관찰합니다.

1단계: Heap 메모리 사이즈 튜닝 (-Xms, -Xmx)

가장 기본적이고 필수적인 튜닝입니다.

  • 옵션:
    • -Xms<size>: 시작 힙(Heap) 크기 (e.g., -Xms512m)
    • -Xmx<size>: 최대 힙 크기 (e.g., -Xmx512m)
  • 튜닝 포인트:
    1. -Xms-Xmx를 동일한 값으로 설정합니다.
      • 이유: 애플리케이션 실행 중에 JVM이 동적으로 힙 크기를 조절(늘리거나 줄이는)하느라 불필요한 오버헤드를 사용하는 것을 방지합니다. 서버 환경에서는 거의 표준처럼 사용되는 설정입니다.
    2. 적절한 크기 찾기:
      • 부하 테스트를 실행하면서 VisualVM으로 최대 힙 사용량을 관찰합니다.
      • 관찰된 최대 사용량보다 30~50% 정도 여유를 두어 힙 크기를 설정합니다. 너무 작으면 OutOfMemoryError가 발생하고, 너무 크면 메모리를 낭비하고 Full GC 시 STW 시간이 길어집니다.

2단계: Garbage Collector (GC) 튜닝

URL 단축기의 성능과 가장 밀접한 튜닝 포인트입니다. 목표는 STW 시간 줄이기입니다.

  • Java 11 이상을 사용한다면 기본 GC는 G1GC(Garbage-First GC)입니다. G1GC는 대부분의 웹 애플리케이션 환경에서 아주 훌륭한 성능을 보여줍니다.

  • 튜닝 시나리오 (학습용):

    1. 기본 G1GC로 성능 측정: 아무 옵션 없이 실행하고 부하 테스트 후 평균 응답시간, GC 횟수 및 총 GC 시간을 기록합니다.
    2. Parallel GC로 변경 후 성능 측정:
      • 옵션: -XX:+UseParallelGC
      • 특징: Throughput(처리량)에 중점을 둔 GC. Minor GC는 빠르지만, Full GC 발생 시 STW가 길 수 있습니다. G1GC와 비교하여 어떤 차이가 있는지 직접 확인해보세요.
    3. (심화) ZGC로 변경 후 성능 측정:
      • 옵션: -XX:+UseZGC (Java 11+ 에서 실험적 기능으로, 15+ 부터 정식 지원)
      • 특징: STW를 수 밀리초(ms) 단위로 극단적으로 줄이는 데 초점을 맞춘 최신 GC. URL 단축기처럼 latency가 중요한 애플리케이션에 어떤 영향을 주는지 비교해보는 것은 좋은 경험입니다.
  • G1GC 튜닝 옵션 (경험해볼 만한 것):

    • -XX:MaxGCPauseMillis=100: “GC로 인한 최대 멈춤 시간을 100ms 정도로 맞춰줘” 라고 JVM에게 ‘목표’를 제시하는 옵션입니다. JVM이 이 목표를 달성하기 위해 내부 파라미터를 스스로 조절합니다. 이 값을 조절하며 응답시간의 변화를 측정해보세요.

실습 로드맵 요약

  1. 준비: URL 단축 서비스, 부하 테스트 툴(nGrinder 추천), VisualVM을 준비합니다.
  2. 기준선 측정: 아무 JVM 옵션 없이 서비스를 실행하고, 부하 테스트를 통해 TPS와 평균 응답 시간을 측정합니다. VisualVM으로 힙 메모리 변화와 GC 로그를 관찰합니다.
  3. 힙 튜닝: -Xms512m -Xmx512m 처럼 힙 크기를 고정하고 다시 측정합니다. (크기는 모니터링 결과에 따라 조절)
  4. GC 튜닝: -XX:+UseParallelGC 또는 -XX:+UseZGC로 변경해보거나, G1GC의 -XX:MaxGCPauseMillis 값을 조절해보며 각각의 성능 변화를 측정하고 ‘왜’ 이런 결과가 나왔는지 분석하고 블로그 등에 정리하면 최고의 포트폴리오가 됩니다.

이 과정을 통해 단순히 코드를 짜는 개발자를 넘어, 자신이 만든 애플리케이션이 동작하는 런타임 환경(JVM)까지 이해하고 제어할 수 있는 개발자로 성장하는 경험을 하실 수 있을 겁니다. 정말 재밌는 여정이 될 거예요


네, 아주 예리한 질문입니다! URL 단축 서비스는 동시성(Concurrency) 문제를 학습하고 해결책을 적용하기에 정말 좋은 예제입니다. 겉보기에는 간단해 보이지만, 수많은 사용자가 동시에 요청을 보낼 때 예상치 못한 문제들이 발생할 수 있습니다.

동시성 관련 처리 요소들을 “문제 상황”과 “해결 방안” 중심으로 설명해 드릴게요.

1. 단축 키(Short Key) 생성 시의 동시성 문제

가장 직관적으로 마주할 수 있는 동시성 문제입니다.

문제 상황 A: “Custom URL” 기능

만약 사용자가 직접 단축 URL을 지정하는 기능(http://my.short/my-custom-url)을 제공한다고 가정해 봅시다.

  1. 사용자 A가 my-custom-url 이라는 키를 사용하고 싶어서 ‘생성’ 버튼을 누릅니다.
  2. 애플리케이션은 DB에서 my-custom-url 키가 이미 존재하는지 확인합니다. (SELECT count(*) FROM urls WHERE short_key = 'my-custom-url')
  3. 동시에, 사용자 B도 my-custom-url 키를 사용하고 싶어서 ‘생성’ 버튼을 누릅니다.
  4. 애플리케이션은 B의 요청에 대해서도 DB를 확인합니다. 아직 A의 데이터가 커밋되지 않았으므로, B에게도 my-custom-url 키가 “사용 가능”하다고 판단합니다.
  5. A의 요청이 DB에 INSERT 됩니다.
  6. B의 요청이 DB에 INSERT 하려고 시도하지만, short_key 컬럼에 UNIQUE 제약 조건이 걸려 있으므로 DB 에러(Duplicate entry)가 발생하고, 사용자 B는 500 에러 페이지를 보게 됩니다.

해결 방안

  • 방법 1: DB의 UNIQUE 제약 조건 활용 (기본적이면서도 확실함)

    • short_key 컬럼에 UNIQUE 제약 조건을 반드시 설정합니다.
    • 애플리케이션 코드에서는 INSERT 시 발생하는 DataIntegrityViolationException (Spring 기준) 같은 예외를 try-catch로 잡습니다.
    • 이 예외가 발생하면, “이미 사용 중인 URL입니다. 다른 URL을 입력해주세요.” 와 같이 사용자 친화적인 메시지를 반환합니다.
    • 이것이 가장 간단하고 일반적인 해결책입니다.
  • 방법 2: 비관적 락 (Pessimistic Lock)

    • SELECT ... FOR UPDATE 구문을 사용하여, 특정 row를 조회할 때 다른 트랜잭션이 접근하지 못하도록 lock을 거는 방식입니다. Custom URL 기능에는 과할 수 있지만, 학습용으로는 좋습니다.
    • 단점: DB에 부하를 줄 수 있고, 데드락(Deadlock)의 위험이 있습니다.
  • 방법 3: 분산 락 (Distributed Lock) 사용

    • 서버가 여러 대인 분산 환경에서는 JVM 레벨의 락은 소용이 없습니다.
    • Redis의 SETNX (SET if Not eXists) 명령어를 사용하여 분산 락을 구현할 수 있습니다.
    • 로직:
      1. 사용자가 my-custom-url을 요청한다.
      2. Redis에 SETNX lock:my-custom-url "locked" 를 시도한다.
      3. 성공하면(1을 반환), DB 작업을 수행하고 작업이 끝나면 Redis의 키를 삭제(DEL)한다.
      4. 실패하면(0을 반환), 다른 스레드/서버가 작업 중이므로 “이미 사용 중”이라고 응답하거나 잠시 대기 후 재시도한다.

2. 클릭 수(조회수) 카운트 기능의 동시성 문제

단축 URL이 클릭될 때마다 조회수를 1씩 증가시키는 기능을 추가한다고 가정해 봅시다.

문제 상황 B: Race Condition (경쟁 상태)

매우 인기 있는 단축 URL이 있어서, 1초에 수백 번의 클릭이 발생합니다.

  1. 스레드 A가 DB에서 해당 URL의 현재 조회수(count)를 읽어옵니다. (예: count = 100)
  2. 동시에, 스레드 B도 DB에서 조회수를 읽어옵니다. (B도 count = 100을 읽음)
  3. 스레드 A가 count + 1 (101)을 계산하여 DB에 UPDATE 합니다.
  4. 스레드 B도 count + 1 (101)을 계산하여 DB에 UPDATE 합니다.
  5. 결과: 실제 클릭은 2번 발생했지만, DB의 조회수는 100에서 101로, 단 1만 증가했습니다. (조회수 유실 발생)

해결 방안

  • 방법 1: DB Update 쿼리 개선 (가장 간단)

    • Read-Modify-Write 패턴을 피하고, UPDATE 쿼리 내에서 계산을 완료합니다.
    • UPDATE urls SET click_count = click_count + 1 WHERE short_key = ?
    • 이 쿼리는 Atomic(원자적)하게 동작하여 여러 스레드가 동시에 실행해도 DB가 순서대로 처리해주므로 데이터 정합성이 보장됩니다. 대부분의 경우 이 방법으로 충분합니다.
  • 방법 2: 성능 저하 문제와 그 해결 (더 심화된 고민)

    • 위 방법은 정확하지만, 클릭이 있을 때마다 DB에 UPDATE 쿼리가 발생하여 DB에 큰 부하를 줍니다. 특히 캐싱을 사용 중이라면 리다이렉션 자체는 빠른데, 조회수 업데이트 때문에 느려지는 주객전도 현상이 발생합니다.
    • 해결책: Write-Back 캐싱 또는 버퍼링
      1. 클릭이 발생하면 DB를 직접 업데이트하지 않고, Redis 같은 인메모리 저장소의 카운터를 1 증가시킵니다. (Redis의 INCR 명령어는 Atomic 합니다.)
      2. 별도의 스케줄러(Scheduler) 또는 배치(Batch) 잡이 주기적으로 (예: 1분에 한 번) Redis에 쌓인 카운트 값을 읽어와 DB에 한 번에 업데이트(Bulk Update)합니다.
      3. 이렇게 하면 DB UPDATE 부하를 수천, 수만 분의 일로 줄일 수 있습니다. (리다이렉션 성능 확보 + 데이터 정합성 유지)
      4. 단점: 실시간 조회수와 약간의 차이가 발생할 수 있지만, 대부분의 서비스에서 허용 가능한 수준입니다.

학습 로드맵 제안

  1. 기본 구현: 먼저 UNIQUE 제약조건과 UPDATE ... SET count = count + 1 쿼리를 사용하여 동시성 문제를 해결하는 기본 버전을 만듭니다.
  2. 부하 테스트로 문제 재현: 부하 테스트 툴을 사용하여 동시에 많은 요청을 보내 위에서 설명한 문제(데이터 유실 등)가 정말 발생하는지 눈으로 확인합니다.
  3. 고급 해결책 적용: Redis를 도입하여 분산 락을 구현해보거나, 클릭 카운트를 비동기 배치 처리하는 방식으로 리팩토링합니다.
  4. 성능 비교: 개선 전/후의 성능(TPS, 응답시간, DB 부하)을 측정하고 비교하여 “나는 이런 동시성 문제를 발견했고, 이런 기술을 사용해 이렇게 개선했다”는 자신만의 스토리를 만드세요.

이러한 동시성 문제들을 직접 마주하고 해결하는 경험은 백엔드 개발자로서의 역량을 한 단계 끌어올리는 귀중한 자산이 될 것입니다.