URLcut

[개요]

  • 긴 URL을 단축하고 QR 코드로 공유할 수 있는 사이드 프로젝트
  • 실제 집중 개발은 수일간 진행했으며, 이후 시간을 내어 배포 및 기능 개선을 비정기적으로 진행
  • 서비스명은 URLcut으로, 회원에게 커스텀 주소, 만료 시각, 최대 클릭 수 설정 및 클릭 통계 제공
  • URL 생성부터 리다이렉트, 통계 조회, 실제 서버 배포까지 구현하고 이후 캐시 정합성 및 동시성 문제를 지속적으로 개선
  • 담당
    • 백엔드 전반 설계 및 구현
    • 미니 PC 서버 구축, 도메인 연결 및 HTTPS 적용
    • GitHub Actions CI/CD 파이프라인 작성
    • 프론트엔드 구현 및 인증·통계 오류 수정, 컴포넌트 분리와 리디자인

[사용 기술 스택 및 선정 이유]

  • BE
    • Java 17 / Spring Boot 3.5.3
      • URL과 사용자 도메인을 분리하고 Controller → Service → Repository 계층으로 구성
      • 데이터 변경은 서비스 계층의 트랜잭션으로 관리하고, 요청 검증 및 예외 응답을 공통 처리
    • Spring Data JPA / PostgreSQL 14
      • 사용자, URL, 클릭 이벤트 간 관계와 영속 데이터 관리
      • 통계 조회는 엔티티 전체 로딩 대신 집계 쿼리를 사용하도록 개선
    • Spring Security / JWT
      • 비회원 단축 기능과 회원 전용 관리 기능의 접근 권한 분리
      • Access Token과 Refresh Token을 분리하고 Redis로 재발급 및 로그아웃 상태 관리
    • ZXing
      • 생성한 단축 주소를 PNG QR 코드로 변환하여 함께 제공
  • Cache
    • Redis 7
      • 반복되는 슬러그 조회를 캐시하고, 클릭 제한이 설정된 URL의 카운터 관리
      • URL 캐시에는 리다이렉트에 필요한 필드만 담은 전용 DTO 사용
    • Lua
      • 클릭 한도 확인과 증가 사이에 다른 요청이 끼어들지 않도록 Redis 내부에서 하나의 연산으로 처리
  • FE
    • Vue 3 / JavaScript / Vite
      • 단축 폼, 마이페이지, 통계 패널을 제공하는 SPA 구현
      • 리디자인 과정에서 Composition API와 컴포넌트 단위로 구조 정리
      • 현재 화면 규모에서는 별도 상태 관리 라이브러리를 추가하지 않고 composable로 인증 및 마이페이지 상태 공유
    • Axios / Chart.js
      • 인증 헤더 주입과 토큰 재발급을 공통 처리하고 일별 클릭 통계 시각화
  • Infra / DevOps
    • 미니 PC / Ubuntu Server 26.04 LTS
      • 미니 PC에 Ubuntu Server를 직접 설치하고 SSH로 원격 관리
      • 포트 포워딩, 프로세스 재시작, 도메인 연결 등 서비스 공개에 필요한 운영 환경 구축
    • Docker Compose / Nginx / Certbot
      • 백엔드, DB, Redis를 내부 네트워크로 연결하고 Nginx의 80·443 포트만 외부에 공개
      • 프론트엔드 정적 파일 제공, API 프록시, HTTPS 인증서 발급 및 갱신 구성
    • GitHub Actions
      • 백엔드 빌드·테스트와 프론트엔드 빌드·린트를 자동 검증하고, main 반영 시 SSH 배포 단계로 연결
  • Testing
    • JUnit 5 / Mockito
      • URL 검증, 슬러그 충돌, 링크 만료, 클릭 한도 및 소유권 검증에 대한 단위 테스트 작성

[주요 담당 기능]

  1. URL 단축 및 커스텀 주소 생성
    • Snowflake ID를 Base62로 인코딩하여 자동 슬러그 생성
    • 회원별 동일 원본 URL 재사용 및 비회원 URL 별도 재사용 처리
    • 커스텀 슬러그 중복, 허용 문자, 길이 및 애플리케이션 예약 경로 검증
    • 단축 주소와 QR 코드 생성
  2. 리다이렉트 및 만료 조건 처리
    • 유효한 단축 주소 접근 시 원본 URL로 302 리다이렉트
    • 만료 시각 또는 최대 클릭 수에 도달한 경우 안내 화면으로 이동
    • Redis 캐시 조회 후 캐시가 없을 때 DB를 조회하는 구조 적용
    • 클릭 한도 검사와 카운터 증가를 Lua 스크립트로 통합
  3. 인증 및 URL 관리
    • 회원가입, 로그인, 토큰 재발급, 로그아웃 및 회원 탈퇴 구현
    • Refresh Token 저장과 Access Token 블랙리스트를 Redis로 관리
    • 본인 URL만 상세 조회 및 삭제할 수 있도록 소유권 검증
  4. 클릭 통계 조회 개선
    • 클릭 시각, 유입 경로, User-Agent 등 클릭 이벤트 저장
    • 전체 클릭 이벤트를 반환하던 API를 총 클릭 수와 일별 집계 결과를 반환하는 형태로 변경
    • 마이페이지 URL별 클릭 수를 한 번의 집계 쿼리로 조회하여 반복 쿼리 제거
  5. 배포 환경 구성 및 자동화
    • 미니 PC에 Docker 기반 실행 환경을 구축하고 urlcut.kr 도메인과 HTTPS 적용
    • 프론트엔드 빌드 컨테이너와 Nginx를 분리하여 정적 파일을 공유 볼륨으로 전달
    • GitHub Actions에서 PostgreSQL·Redis 서비스 컨테이너를 사용한 백엔드 검증 구성
    • 백엔드와 프론트엔드 검증이 모두 성공한 main 변경에 대해 서버의 Docker 이미지를 재빌드하도록 배포 작업 작성

[트러블슈팅 & 문제 해결]

  • BE
    • 클릭 수 제한을 넘기는 동시성 문제
      • 기존에는 Redis에서 클릭 수를 읽고 Java에서 한도를 확인한 뒤, 클릭 이벤트 저장 이후 별도로 카운터 증가
      • 최대 5회인 링크의 현재 클릭 수가 4일 때, 두 요청이 동시에 4를 읽으면 모두 통과하여 6회까지 허용될 수 있는 구조
      • Redis의 개별 명령이 원자적이어도 GET → 조건 검사 → INCR 전체가 원자적인 것은 아니라는 점을 확인
      • 한도 확인과 증가를 하나의 Lua 스크립트로 묶어, 카운터가 존재하는 경우 두 연산 사이의 경쟁 조건 제거
      • 카운터가 없으면 DB에서 기존 클릭 수를 조회하고 setIfAbsent로 초기화한 뒤 스크립트를 재실행
      • 한도 도달, 정상 허용, 카운터 초기화 후 재시도 경로를 단위 테스트로 확인
      • 다만 Redis 카운터 증가와 DB 클릭 이벤트 저장은 하나의 트랜잭션이 아니므로, DB 저장 실패나 카운터 재초기화 시점의 정합성은 추가 과제로 남김
    • DB 커밋 전 캐시 적재 문제
      • URL 저장 직후 Redis 캐시를 채우면 이후 DB 트랜잭션이 롤백되어도 캐시에 URL이 남을 수 있음
      • 캐시 적중률뿐 아니라 DB에 확정된 데이터가 캐시에 반영되는 순서를 먼저 보장해야 한다고 판단
      • URL 생성 시 캐시 적재와 개별 URL 삭제 시 캐시 무효화를 TransactionSynchronization#afterCommit으로 이동
      • DB 커밋 이후 Redis 작업 자체가 실패하는 경우의 복구까지 해결한 것은 아니므로 재시도 및 장애 대응은 별도로 고려할 필요가 있음
    • 클릭 통계 조회 비용 증가
      • 처음에는 클릭 이벤트 목록을 응답으로 전달하여 프론트엔드에서 통계 처리
      • 클릭이 쌓일수록 전송 데이터와 메모리 사용이 함께 늘어나는 구조라, 총 클릭 수와 일별 집계 결과만 반환하도록 변경
      • 이후 백엔드에서 이벤트 전체를 읽어 집계하던 방식도 DB의 GROUP BY와 COUNT를 사용하는 방식으로 개선
      • 마이페이지에서 URL마다 클릭 수를 조회하던 방식은 사용자 소유 URL 전체를 한 번에 집계하여 개선
      • 클릭 제한이 없는 URL은 Redis 카운터를 만들 필요가 없어, 제한이 설정된 URL만 카운터를 관리하도록 범위 축소
    • Snowflake ID의 전달 및 슬러그 충돌 처리
      • Java의 Long ID를 JSON 숫자로 전달했을 때 JavaScript에서 정밀도가 유지되지 않는 문제 발생
      • ID는 계산용 값이 아닌 식별자이므로 응답 DTO에서 String으로 변환하여 전달
      • 자동 생성 슬러그가 커스텀 주소와 겹쳤을 때 같은 ID를 다시 인코딩하면 결과가 바뀌지 않아 반복문을 벗어날 수 없는 문제 확인
      • 충돌 시 ID부터 새로 발급하도록 수정하고 해당 경로에 대한 테스트 추가
    • 캐시 직렬화 및 긴 URL 인덱스 재검토
      • 캐시에는 URLCacheDto만 저장하는데 모든 Object 하위 타입의 다형적 역직렬화를 허용하고 있어 설정 범위가 과도하다고 판단
      • Jackson2JsonRedisSerializer<URLCacheDto>로 저장 타입 명시
      • 긴 원본 URL의 TEXT 컬럼에 B-tree 인덱스를 두면 인덱스 항목 크기 제한으로 저장이 실패할 수 있는 위험 확인
      • 해당 인덱스를 제거하여 긴 URL 저장 제약을 피하고, 원본 URL 중복 조회의 인덱스 설계는 후속 개선 대상으로 남김
    • URL 검증 및 인증 경계 보완
      • URL 전체 문자열에 차단 도메인이 포함되는지만 검사하면, 정상 URL의 경로에 해당 문자열이 있어도 거부할 수 있음
      • URI 파싱 후 호스트를 비교하고 자체 서비스 도메인도 환경 설정에서 추출하도록 변경
      • 커스텀 슬러그가 auth, me, urls 등 API 경로와 충돌하지 않도록 예약어 차단
      • 보호할 API 경로를 SecurityConfig에 명시하고, 잘못된 인증 헤더에 대해 500 대신 401 응답을 반환하도록 개선
  • FE
    • 동시에 발생한 토큰 재발급 요청의 경쟁 조건
      • 여러 API가 동시에 401을 받으면 각각 Refresh Token 재발급을 시도하여 인증 상태가 불안정해지는 문제
      • 진행 중인 재발급 Promise를 공유하여 한 번만 갱신하고, 나머지 요청은 같은 결과를 기다린 뒤 재시도하도록 변경
    • 통계 패널 비동기 처리 및 차트 렌더링 문제
      • 다른 URL을 빠르게 선택하면 이전 요청이 늦게 도착하여 현재 선택한 URL의 통계를 덮어쓰는 문제
      • 요청 버전 번호를 비교하여 최신 요청 결과만 반영
      • 패널 애니메이션 종료 시점을 고정 타이머로 추측하던 차트 생성을 transition 완료 훅으로 변경하여 폭이 확정된 뒤 렌더링하도록 개선
    • 프론트엔드 리디자인 시 기존 동작 보존
      • 2,371줄의 App.vue에 화면, 인증, 통계 로직이 모여 있어 변경 영향 범위를 파악하기 어려운 상태
      • UI 컴포넌트, API 클라이언트, 인증·마이페이지 composable로 분리
      • 재작성 전에 엔드포인트와 응답 필드명을 문서화하고, originUrloriginURL처럼 API마다 다른 대소문자까지 대조
      • 이미 수정한 토큰 재발급 및 통계 요청 경쟁 조건 처리 로직을 유지하며 화면 구조와 스타일 변경
  • Infra
    • 서버 OS 교체 및 원격 관리 환경 구성
      • 초기에는 Windows의 WSL2에 Docker를 설치했으나, 서버 용도로 사용하기에 무겁고 원격 관리가 불편했음
      • 미니 PC의 OS를 Ubuntu Server 26.04 LTS로 교체하고 SSH로 관리하도록 변경
      • OS 재설치로 SSH 호스트 키가 바뀌어 접속이 차단된 문제는 기존 known_hosts 항목을 제거한 뒤 재접속하여 해결
      • Netplan 고정 IP 설정과 공유기 포트 포워딩을 적용하여 외부에서도 서버에 접속할 수 있도록 구성
    • 배포 후 코드 변경이 반영되지 않는 문제
      • docker compose up -d만 실행하면 기존 이미지를 재사용하여 최신 코드가 반영되지 않음
      • 프론트엔드 빌드 결과 복사 시각이 바뀌어도 실제 내용은 이전 빌드일 수 있어, 파일 시각만으로 배포 성공을 판단할 수 없었음
      • 배포 명령에 --build를 명시하고 문서에 재빌드 절차를 정리한 뒤 CI/CD 배포 작업에도 반영

[성과]

  • URL 단축, 회원 인증, 만료 조건, 클릭 통계부터 도메인·HTTPS 배포까지 서비스 전체 흐름 구현
  • Redis Lua 스크립트로 클릭 한도 확인과 증가를 원자적으로 처리하고, DB와 캐시 간 반영 순서 개선
  • 클릭 통계를 원본 이벤트 전달 → 백엔드 집계 → DB 집계로 단계적으로 개선하고 마이페이지 반복 조회 제거
  • 단일 프론트엔드 파일을 역할별로 분리하고 기존 API 계약을 유지하며 리디자인
  • 백엔드 테스트와 프론트엔드 빌드·린트를 배포 전 검증하는 GitHub Actions 파이프라인 작성
  • 다만 실제 사용자 확보와 충분한 운영 트래픽을 바탕으로 개선 효과를 검증하는 단계까지는 이어가지 못함

[아키텍처 구조]

사용자
  │ HTTPS

미니 PC / Ubuntu Server 26.04 LTS (직접 설치)
  └─ Docker Compose
      ├─ Nginx : 80 / 443
      │   ├─ Vue 정적 파일 제공 ← fe-build → 공유 볼륨
      │   └─ API / 단축 슬러그 요청 → Spring Boot
      │                                ├─ PostgreSQL : 사용자, URL, 클릭 이벤트
      │                                └─ Redis : URL 캐시, 클릭 제한 카운터, JWT 상태
      └─ Certbot : 인증서 발급 및 갱신

GitHub Actions
  ├─ Backend : Gradle 빌드·테스트 + PostgreSQL / Redis
  ├─ Frontend : ESLint + Vite 빌드
  └─ main 변경 및 검증 성공 → SSH → Docker Compose 재빌드
  • 외부 요청은 Nginx를 통해서만 들어오며 백엔드, PostgreSQL, Redis는 Docker 내부 네트워크에서 통신
  • 리다이렉트는 Redis 캐시를 먼저 확인하고, 캐시 미스 시 PostgreSQL 조회 후 캐시 적재
  • 캐시에 적중하더라도 클릭 이벤트는 PostgreSQL에 저장하므로 리다이렉트 전체가 DB 접근 없이 끝나는 구조는 아님

[ERD]

  • users : 사용자 ID, 이메일, 닉네임, 비밀번호 해시
  • url : 원본 URL, 단축 슬러그, QR 코드, 생성 시각, 만료 시각, 최대 클릭 수
    • 사용자와 1:N 관계이며 비회원 URL은 사용자 참조 없이 저장
    • 단축 슬러그에 UNIQUE 제약 적용
  • click_event : 클릭 시각, 유입 경로, User-Agent, IP 주소
    • URL과 1:N 관계로 연결하여 클릭 이력 및 통계 관리

[회고]

  • Best Practice
    • AI를 활용한 바이브 코딩으로 개발 생산성을 크게 높일 수 있었음
      • 원하는 기능과 동작을 설명하고 AI와 구현 결과를 확인하며 수정하는 방식으로 개발 진행
      • 화면 구성, 반복 코드 작성, 리팩터링, 오류 원인 분석에 AI를 활용하면서 혼자 개발할 때 드는 구현 부담과 시간을 크게 줄였음
      • 특히 프론트엔드 컴포넌트 분리와 리디자인, 백엔드 개선을 짧은 시간 안에 함께 진행할 수 있었고, 생각한 개선안을 실제 코드로 확인하는 속도에서 큰 이득을 봄
      • 줄어든 구현 시간을 기술 선택과 변경 범위 검토에 활용할 수 있어, 1인 개발에서도 여러 영역을 다루는 데 도움이 되었음
    • 단축 주소를 만드는 기능에서 시작했지만, 배포 후 코드를 다시 보면서 동시성, 캐시 정합성, 조회 비용처럼 처음에는 놓쳤던 문제를 찾아 개선할 수 있었음
    • Redis를 사용하면 동시성 문제가 해결되는 것으로 생각하기 쉬웠는데, 개별 명령의 원자성과 여러 명령으로 구성된 업무 로직의 원자성은 다르다는 점을 배움
    • AI 도구와 함께 코드 진단 및 리디자인을 진행하면서도, 기존 API 계약과 보존할 동작을 먼저 문서화하고 변경 범위를 정하는 과정이 중요했음
    • 악성 URL 탐지나 공개 API 같은 새 기능도 검토했지만, 이미 확인한 클릭 제한 오류를 먼저 해결하는 쪽으로 우선순위를 정함
    • 미니 PC에서 직접 서비스를 공개하면서 애플리케이션 구현 외에도 네트워크, 인증서, 컨테이너 재시작 및 배포 절차를 함께 관리해야 한다는 것을 경험함
  • Lesson Learned
    • 홍보와 사용자 모집까지 이어가지 못한 점이 가장 아쉬움
      • 도메인을 연결하고 검색 노출을 위한 메타 태그, 사이트맵, 구조화 데이터를 추가했지만 적극적인 홍보와 사용자 모집은 충분히 진행하지 못했음
      • 서비스를 공개하는 것과 실제 사용자를 확보하는 것은 다른 과제였고, 구현과 고도화에 비해 누구에게 어떤 이유로 필요한 서비스인지 확인하는 활동이 부족했음
      • 사용자 피드백과 반복 사용 데이터를 바탕으로 기능의 필요성을 검증하지 못해, 기술적으로 개선한 내용이 실제 사용 경험에 얼마나 도움이 되었는지 확인하는 데 한계가 있었음
      • 다음에는 기능 개발과 함께 대상 사용자를 정하고, 소규모 사용자 모집과 피드백 수집까지 개발 일정에 포함할 필요가 있다고 느낌
    • 캐시를 추가하면 조회는 줄일 수 있지만 DB와 Redis라는 두 저장소의 실패 시점까지 고려해야 함을 배움. 커밋 이후 캐시 반영이나 Lua 적용만으로 모든 정합성 문제가 해결되는 것은 아니었음
    • 단위 테스트로 분기와 예외 처리를 확인했지만, 실제 Redis·DB를 연결한 동시 요청 및 장애 상황 검증은 더 필요함. 성능 개선도 부하 테스트 수치로 확인하는 과제가 남아 있음
    • 인덱스를 추가할 때 조회 속도만 고려하기보다 저장할 데이터의 길이와 쓰기 실패 가능성도 함께 검토해야 한다는 점을 배움
    • 서비스 규모에 맞는 구현 범위를 정하는 것이 중요했음. 다크 모드나 새 상태 관리 라이브러리를 추가하기보다 기존 구조를 정리하고 오류를 수정하는 데 집중함