diff --git a/README.md b/README.md index af45e35..5a0e549 100644 --- a/README.md +++ b/README.md @@ -1 +1,149 @@ -# Bitly-Jungeun \ No newline at end of file +## Design Doc: 빠른 리다이렉트가 가능한 URL 단축기 + +본 문서는 구글식 디자인 문서 구조를 참고하여(URL 단축기의) 리다이렉트 경로를 초저지연으로 만드는 설계를 요약합니다. + +### 1. Context & Scope +- 문제 배경: URL 단축기는 극단적인 읽기 중심 워크로드를 갖습니다. 수백만~수억 건의 리다이렉트 트래픽에서 p95/p99 지연을 낮추는 것이 핵심입니다. +- 현재 가정 트래픽: 1억 DAU × 5회/일 ≈ 5억/일 → 평균 ~5,787 RPS, 피크 100× 가정 시 ~60만 RPS. +- 스코프: 단축 코드 → 원본 URL 매핑 조회 경로 최적화(데이터 레이어 및 엣지) +- 비스코프: 단축 URL 생성 알고리즘의 상세(충돌 방지, 키 공간 설계 등)는 별도 문서에서 다룸. + +### 2. Goals / Non-Goals +- Goals + - p95 리다이렉트 지연 단자리 ms(지역/엣지 히트 시)를 목표 + - 피크 시 ~60만 RPS 리다이렉트 처리 가능 + - 고가용성: 단일 장애 지점 제거, 캐시/DB/엣지 이중화 + - 캐시 히트율 90%+ (핫 코드 기준), 오리진 도달율 최소화 +- Non-Goals + - 단축 URL 생성 파이프라인 최적화 + - 광고/과금 로직 최적화 + - 정교한 AB 테스트/퍼스널라이제이션 + +### 3. System Context Diagram (고수준) +- 사용자는 `https://sho.rt/{code}` 로 접속 +- CDN(전세계 PoP) → 엣지 함수(코드 조회) → + - 엣지 캐시 hit 시: 301/302 즉시 응답 + - miss 시: 오리진 API → Redis →(miss)→ DB 조회 후 응답 및 상위 캐시 적재 + - 관측(로그/메트릭/트레이싱)은 엣지/오리진 모두에서 수집 + +### 4. APIs (스케치) +- GET `/{code}`: 코드로 리다이렉트 수행 + - 301/302 Location: `` + - 캐시 제어 헤더: 엣지 캐시 가능, 단 TTL/무효화 정책 고려 +- POST `/shorten` (참고): 원본 URL → 단축 코드 생성(본 문서 비스코프) + +### 5. Data Storage +- 테이블: `url_mapping` + - `code` (PK, 고정 길이 문자열) — 단축 코드, 기본 키 및 인덱스 + - `original_url` (text) + - `created_at`, `expires_at`(선택) +- 인덱싱 + - B-트리 인덱스(기본) 또는 해시 인덱스(정확 일치 최적화, PostgreSQL 등) + - 샤딩/파티셔닝: 코드 해시 기반 범용 샤딩, 리전별 리드 레플리카 + +### 6. Design +- 6.1 읽기 경로 레이어링 + - 엣지 캐시(및 엣지 키-밸류 저장) → 오리진 Redis/Memcached → RDBMS + - 캐시 키: `url:{code}` → value: `` + - TTL: 인기 코드 장기 캐시, 비인기 코드 짧은 TTL 또는 캐시 미적재 + + - 엣지(Cloudflare Workers, Lambda@Edge) + - 인기 코드 즉시 리다이렉트, 오리진 경유 차단 + - 캐시 무효화: 관리용 API 또는 tag 기반 purge + + - 오리진(애플리케이션) + - 캐시 미스 시 Redis 조회, 다시 미스면 DB 조회 후 301/302 + - 결과를 Redis 및 엣지에 업서트(upsert) + +- 6.2 캐시 정책 + - 에비션: LRU 우선, 크기 제한 엄수 + - TTL: 트래픽 기반 가변 TTL(핫 키는 길게) + - 프리워밍: 롤아웃 전 상위 N개 인기 코드 프리로드 + +- 6.3 일관성/무효화 + - 대부분 read-only. 삭제/만료/수정 이벤트 시 + - 오리진에서 Redis/엣지에 무효화 브로드캐스트 + - 지연 허용 시 TTL 자연 만료 + +### 7. Alternatives Considered +- 단일 DB만으로 버티기 + - 장점: 단순. 단일 신뢰 경로 + - 단점: 초고 RPS 피크 처리 어려움, 스케일/비용 한계 +- CDN만 사용(오리진 캐시 미활용) + - 장점: 사용자는 빠름(히트 시) + - 단점: 글로벌 무효화/미스 처리 비용 증가, 오리진 병목 발생 +- NoSQL(예: DynamoDB) 단독 + - 장점: 키-밸류 조회에 적합, 수평 확장 + - 단점: 기존 RDB 스키마/관계 활용 어려움, 마이그레이션 비용 + +### 8. Cross-Cutting Concerns +- 보안: 개방 리다이렉트 방지(허용 도메인 검증), 악용 방지 레이트 리밋 +- 프라이버시: 쿼리 파라미터/PII 로깅 최소화, 지역 규제 준수 +- 관측성: RPS, p50/95/99, 히트율, 오리진 도달율, 4xx/5xx, 엣지/오리진 트레이싱 +- 신뢰성: 멀티 리전, 다중 캐시 노드, 캐시 장애 시 페일오버 경로 확인 +- 비용: CDN/엣지/캐시/DB 별 단가 모니터링, 인기 키 집중 최적화 + +### 9. Rollout Plan +- 단계 1: DB 인덱싱/샤딩 정비, 읽기용 레플리카 확충 +- 단계 2: 오리진 Redis 도입, 캐시 키/TTL 설계, 프리워밍 +- 단계 3: CDN/엣지 함수 배포, 인기 코드 엣지 캐시 +- 단계 4: 글로벌 무효화/태그 purge 자동화, 히트율/지연 최적화 반복 + +### 10. Metrics & SLO +- SLO: p95 리다이렉트 < 50ms(리전 내), 가용성 ≥ 99.95% +- 핵심 지표: 캐시 히트율, 오리진 도달율, 엣지/오리진 p95/99, 에러율, 비용/요청 + +### 11. Risks & Mitigations +- 글로벌 캐시 불일치 → TTL/태그 purge, 변경 이벤트 브로드캐스트 +- 엣지 제한(메모리/런타임) → 경량 로직, 키 압축, 외부 의존 최소화 +- 핫 키 쏠림 → 레이트 리밋/스티키 캐시, 다중 리전 분산 + +### 12. Open Questions +- 만료 정책: per-code TTL vs 글로벌 TTL 최적 조합? +- 멀티 테넌트/도메인 지원 시 키 스키마 확장 방안? +- 퍼스널라이즈드 리다이렉트(기기/지역별) 요구가 생길 경우 엣지 로직 분기 전략? + +### 13. References +- Google 스타일 디자인 문서 개요 정리: GN 기사 요약 참고 (https://news.hada.io/topic?id=14704) + + +## Local Dev Setup (Docker Compose) + +### Prerequisites +- Docker Desktop (Compose v2) + +### Services +- PostgreSQL 16, Redis 7(alpine). Spring Boot 앱은 추후 `api` 서비스로 추가 예정. + +### Memory Limits in Compose +- `deploy.resources.limits.memory`는 Swarm 용 필드입니다. 로컬 Compose에서 강제하려면 `docker compose --compatibility up` 또는 서비스별 `mem_limit`(레거시) 사용을 권장합니다. +- JVM(스프링) 컨테이너는 `-XX:+UseContainerSupport -XX:MaxRAMPercentage=`로 컨테이너 메모리 한도를 인지시켜야 OOM을 피할 수 있습니다. + +### How to Run +```bash +docker compose --compatibility up -d +``` + +### Connection Info +- Postgres: `localhost:5432` (user: bitly, password: bitly, db: bitly) +- Redis: `localhost:6379` + +### Spring Boot (예시 설정) +- `application.yml` 예시 +```yaml +spring: + datasource: + url: jdbc:postgresql://postgres:5432/bitly + username: bitly + password: bitly + redis: + host: redis + port: 6379 +server: + port: 8080 +``` + +### Notes +- Redis는 `--maxmemory 1gb --maxmemory-policy allkeys-lru`로 설정되어 LRU 에비션 동작. +- Postgres/Redis 모두 healthcheck 포함. +- 추후 엣지/CDN 적용 시, 인기 코드 Top-N 프리워밍과 태그 기반 purge 전략을 고려하세요. diff --git a/TestJavaClass.java b/TestJavaClass.java deleted file mode 100644 index 25fb15f..0000000 --- a/TestJavaClass.java +++ /dev/null @@ -1,205 +0,0 @@ -import java.io.*; -import java.sql.*; -import java.util.*; -import java.net.*; -import javax.servlet.http.*; - -/** - * 여러 문제점이 있는 Java 클래스 - * 코드 리뷰 테스트용 - */ -public class TestJavaClass { - - // 전역 변수 사용 (나쁜 예) - private static String globalPassword = "123456"; - private static String apiKey = "sk-1234567890abcdef"; - private static List globalData = new ArrayList<>(); - - // 상수 대신 매직 넘버 사용 - private static final int MAGIC_NUMBER = 100; - - /** - * 보안 취약점이 있는 메서드 - */ - public String vulnerableMethod(String userInput) { - // SQL 인젝션 취약점 - String query = "SELECT * FROM users WHERE name = '" + userInput + "'"; - - // 명령어 인젝션 취약점 - try { - Runtime.getRuntime().exec("echo " + userInput); - } catch (IOException e) { - e.printStackTrace(); - } - - // XSS 취약점 (웹 환경에서) - String htmlOutput = "
" + userInput + "
"; - - // 하드코딩된 자격 증명 - String dbPassword = "mypassword123"; - String dbUrl = "jdbc:mysql://localhost:3306/mydb"; - - return query + htmlOutput; - } - - /** - * 예외 처리가 부족한 메서드 - */ - public void badExceptionHandling() { - // 예외 처리 없음 - File file = new File("test.txt"); - FileInputStream fis = new FileInputStream(file); - int data = fis.read(); - fis.close(); - - // Null 체크 없음 - String[] array = new String[10]; - String firstElement = array[0]; - System.out.println(firstElement.length()); - - // 리소스 누수 가능성 - Connection conn = null; - try { - conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "user", "pass"); - Statement stmt = conn.createStatement(); - ResultSet rs = stmt.executeQuery("SELECT * FROM users"); - while (rs.next()) { - System.out.println(rs.getString("name")); - } - } catch (SQLException e) { - e.printStackTrace(); - } - // conn.close() 호출 안함! - } - - /** - * 비효율적인 알고리즘 - */ - public List inefficientAlgorithm(List items) { - List result = new ArrayList<>(); - - // O(n²) 복잡도의 중첩 반복문 - for (int i = 0; i < items.size(); i++) { - for (int j = 0; j < items.size(); j++) { - if (items.get(i).equals(items.get(j))) { - result.add(items.get(i)); - } - } - } - - // 불필요한 객체 생성 - String temp = new String("temporary"); - String anotherTemp = new String("another"); - - // 중복 계산 - int sum1 = 0; - for (String item : items) { - sum1 += item.length(); - } - - int sum2 = 0; - for (String item : items) { - sum2 += item.length(); - } - - return result; - } - - /** - * 나쁜 클래스 설계 - */ - public static class BadInnerClass { - private List data; - private int counter; - - public BadInnerClass() { - this.data = new ArrayList<>(); - this.counter = 0; - } - - // 타입 체크 없음 - public void addItem(Object item) { - data.add(item.toString()); - counter++; - } - - // 참조 노출 (캡슐화 위반) - public List getData() { - return data; // 복사본 반환하지 않음 - } - - // 긴 메서드 (단일 책임 원칙 위반) - public void processData() { - List processed = new ArrayList<>(); - - for (String item : data) { - if (item != null) { - if (item.length() > 10) { - processed.add(item.toUpperCase()); - } else if (item.length() > 5) { - processed.add(item.toLowerCase()); - } else { - processed.add(item); - } - } - } - - // 중복 로직 - List result = new ArrayList<>(); - for (String item : processed) { - result.add(item); - } - - // 전역 변수 수정 - globalData.addAll(result); - } - } - - /** - * 메모리 누수 가능성이 있는 메서드 - */ - public void potentialMemoryLeak() { - List largeList = new ArrayList<>(); - - // 무한 루프 가능성 - for (int i = 0; i < 1000000; i++) { - largeList.add("item" + i); - - // 메모리 해제 안함 - if (i % 1000 == 0) { - System.out.println("Processed: " + i); - } - } - - // 스레드 안전성 문제 - globalData.addAll(largeList); - } - - /** - * 사용되지 않는 메서드 - */ - private void unusedMethod() { - System.out.println("이 메서드는 사용되지 않습니다."); - } - - /** - * 메인 메서드 - */ - public static void main(String[] args) { - TestJavaClass test = new TestJavaClass(); - - // 문제가 있는 메서드들 호출 - test.vulnerableMethod("'; DROP TABLE users; --"); - test.badExceptionHandling(); - test.inefficientAlgorithm(Arrays.asList("a", "b", "c", "a", "b")); - test.potentialMemoryLeak(); - - // 내부 클래스 사용 - BadInnerClass badClass = new BadInnerClass(); - badClass.addItem("test"); - badClass.addItem(123); - badClass.processData(); - - System.out.println("테스트 완료"); - } -} diff --git a/test_script.js b/test_script.js deleted file mode 100644 index ec90b70..0000000 --- a/test_script.js +++ /dev/null @@ -1,142 +0,0 @@ -// 전역 변수 사용 (나쁜 예) -var globalVariable = "test"; - -// 하드코딩된 값들 -const API_KEY = "sk-1234567890abcdef"; -const PASSWORD = "123456"; - -// 콜백 지옥 예제 -function callbackHell() { - fetch('/api/data', function (response) { - response.json().then(function (data) { - data.forEach(function (item) { - fetch('/api/process', function (processResponse) { - processResponse.json().then(function (result) { - console.log(result); - fetch('/api/save', function (saveResponse) { - saveResponse.json().then(function (saveResult) { - console.log('완료!'); - }); - }); - }); - }); - }); - }); - }); -} - -// 보안 취약점이 있는 함수 -function vulnerableFunction(userInput) { - // XSS 취약점 - document.getElementById('output').innerHTML = userInput; - - // eval 사용 (위험) - eval(userInput); - - // SQL 인젝션 취약점 - const query = `SELECT * FROM users WHERE name = '${userInput}'`; - - return query; -} - -// 비효율적인 함수 -function inefficientFunction() { - // 불필요한 반복문 - const array = [1, 2, 3, 4, 5]; - let result = []; - - for (let i = 0; i < array.length; i++) { - result.push(array[i] * 2); - } - - // 중복 계산 - const sum1 = array.reduce((a, b) => a + b, 0); - const sum2 = array.reduce((a, b) => a + b, 0); - - return { result, sum1, sum2 }; -} - -// 나쁜 클래스 설계 -class BadClass { - constructor() { - this.data = []; - this.counter = 0; - } - - // 타입 체크 없음 - addItem(item) { - this.data.push(item); - this.counter++; - } - - // 참조 노출 - getData() { - return this.data; - } - - // 긴 함수 (단일 책임 원칙 위반) - processData() { - const processed = []; - - for (let i = 0; i < this.data.length; i++) { - const item = this.data[i]; - - if (typeof item === 'string') { - processed.push(item.toUpperCase()); - } else if (typeof item === 'number') { - processed.push(item * 2); - } else { - processed.push(String(item)); - } - } - - // 중복 로직 - const result = []; - for (let i = 0; i < processed.length; i++) { - result.push(processed[i]); - } - - return result; - } -} - -// 예외 처리 없는 함수 -function noErrorHandling() { - const data = JSON.parse(localStorage.getItem('userData')); - const user = data.user; - const name = user.name; - - return name; -} - -// 메모리 누수 가능성 -function potentialMemoryLeak() { - const elements = document.querySelectorAll('.item'); - - elements.forEach(element => { - element.addEventListener('click', function () { - console.log('클릭됨!'); - }); - }); -} - -// 사용되지 않는 변수 -const unusedVariable = "사용되지 않음"; - -// 메인 실행 -document.addEventListener('DOMContentLoaded', function () { - console.log(globalVariable); - - // 문제가 있는 함수들 호출 - callbackHell(); - vulnerableFunction(''); - inefficientFunction(); - - const obj = new BadClass(); - obj.addItem("test"); - obj.addItem(123); - obj.processData(); - - noErrorHandling(); - potentialMemoryLeak(); -});