모든 가입자에게 블로그를 자동으로 만들어주기: 이메일에서 슬러그까지

"내 블로그는 어디에 있죠?"
FullStackFamily에는 블로그 기능이 있다. 그런데 실제로 블로그를 가진 사람은 극소수였다.
블로그를 만들려면 이런 과정을 거쳐야 했기 때문이다.
1. 사용자가 관리자에게 "블로그 만들고 싶어요" 요청 2. 관리자가 어드민 페이지에서 canCreateBlog = true 설정 3. 사용자가 직접 슬러그 입력하고 블로그 생성 4. 그제야 fullstackfamily.com/@my-slug 접속 가능
dev.to는 가입하면 바로 블로그가 있다. Medium도, velog도 마찬가지다. 2026년에 "관리자 승인 후 블로그 생성"은 좀 어색하다.
목표는 단순하다. 가입하면 블로그가 바로 생긴다. 슬러그도 자동으로 만들어지고, 나중에 마음에 안 들면 바꿀 수 있다.
이메일에서 슬러그 만들기
먼저 슬러그 자동 생성부터. 사용자가 직접 입력하지 않으니, 이메일 주소에서 뽑아내기로 했다.
@ 앞 부분을 가져와서 URL에 쓸 수 있도록 정리하면 된다.
toto@gmail.com → toto john.doe@company.com → john-doe Admin@test.com → admin
그런데 실제로 들어오는 이메일은 이렇게 깔끔하지 않다.
kakao_12345@kakao.user → kakao-12345 (언더스코어 → 하이픈) 123abc@test.com → u123abc (숫자 시작 → 'u' 접두사) ab@test.com → ab0 (2자 → 최소 3자 패딩) 한글닉네임@kakao.user → user (비ASCII → 폴백)
카카오 로그인 사용자는 이메일이 kakao_12345@kakao.user 형태로 들어온다. 네이버도 비슷하고, 한글 닉네임만으로 구성된 경우도 있다.
변환 파이프라인
입력: "Admin.Test@gmail.com" │ ├─ @ 앞 추출 → "Admin.Test" ├─ 소문자 변환 → "admin.test" ├─ 비영숫자 → '-' → "admin-test" ├─ 연속 하이픈 병합 → "admin-test" ├─ 앞뒤 하이픈 제거 → "admin-test" ├─ 숫자 시작? → 'u' 접두사 (해당 없음) ├─ 17자 초과? → truncate (해당 없음) └─ 3자 미만? → 패딩 (해당 없음) 결과: "admin-test"
17자에서 자르는 이유는 중복 시 -1, -2 같은 suffix를 붙여야 해서다. 슬러그 최대 길이가 20자이니 17 + -99 = 20자.
중복 처리
toto라는 슬러그가 이미 있으면?
toto → 이미 있음 toto-1 → 이미 있음 toto-2 → 사용 가능! ✓
예약어도 걸러야 한다. admin, api, login, settings 같은 슬러그는 URL 충돌이 생기니까. 40개 정도의 예약어 목록을 만들어 두고 같은 방식으로 처리한다.
admin → 예약어! → admin-1 api → 예약어! → api-1 blog → 예약어! → blog-1
로그인할 때 블로그 만들기
블로그를 "언제" 만들지도 고민이었다.
┌─────────────────────────────────────────────────────┐ │ 방법 A: 회원가입 직후 (eager) │ │ 장점: 가입 즉시 블로그 URL 존재 │ │ 단점: 기존 회원은? DB 마이그레이션 스크립트 필요 │ │ │ │ 방법 B: 로그인 시 없으면 생성 (lazy) │ │ 장점: 기존 회원도 다음 로그인 시 자동 생성 │ │ 단점: 첫 로그인이 아주 약간 느려질 수 있음 │ └─────────────────────────────────────────────────────┘
방법 B를 선택했다. 기존 회원 처리가 깔끔하기 때문이다.
OAuth2 로그인 플로우의 마지막 단계에 블로그 생성을 끼워 넣었다.
OAuth2 로그인 요청 │ ▼ ┌──────────────────────────────────────┐ │ CustomOAuth2UserService.loadUser() │ │ │ │ 1. provider + providerId로 사용자 조회│ │ 2. 없으면 email로 조회 │ │ 3. 그래도 없으면 신규 생성 │ │ │ │ ─── 여기까지가 기존 로직 ─── │ │ │ │ 4. 블로그 있는지 확인 │ │ 5. 없으면 자동 생성 ← NEW │ │ │ └──────────────────────────────────────┘
핵심은 4~5번이다. 신규 사용자든 기존 사용자든, 로그인할 때마다 블로그가 있는지 확인한다. 없으면 만들어 준다.
블로그 생성 실패가 로그인을 막으면 안 된다
여기서 중요한 결정이 있었다. 블로그 생성이 실패해도 로그인은 성공해야 한다.
try { blogAutoCreationService.createBlogForNewUser(user); } catch (Exception e) { log.warn("블로그 자동 생성 실패: userId={}", user.getId()); // 로그인은 정상 진행 }
슬러그 중복이 99개를 넘겼다든지, DB 트랜잭션 타임아웃이 걸렸다든지, 예상치 못한 상황이 발생할 수 있다. 블로그는 다음 로그인 때 다시 시도하면 되지만, 로그인이 안 되면 사용자는 서비스 자체를 쓸 수 없다.
부수 기능이 핵심 기능을 죽이면 안 된다.
블로그 생성 시 만들어지는 것들
블로그 하나가 만들어질 때 실제로는 3개의 엔티티가 함께 생성된다.
┌────────────────────────────────────────────┐ │ createBlogForNewUser(user) │ │ │ │ ┌──────────────┐ │ │ │ Blog │ slug: "toto" │ │ │ │ displayName: "toto의 블로그"│ │ └──────┬───────┘ │ │ │ │ │ ┌──────┴───────┐ ┌───────────────────┐ │ │ │LinkedAccount │ │ UnifiedBoard │ │ │ │ blogId ↔ │ │ slug: "blog-toto" │ │ │ │ userId │ │ preset: BLOG │ │ │ └──────────────┘ └───────────────────┘ │ │ │ └────────────────────────────────────────────┘
UnifiedBoard는 FullStackFamily의 통합 게시판 시스템이다. 블로그도 내부적으로는 게시판의 한 종류(BLOG preset)로 동작해서, 댓글, 투표, 태그 같은 기능을 그대로 재사용할 수 있다.
슬러그 변경: "toto 말고 다른 이름 쓰고 싶어요"
자동 생성된 슬러그가 마음에 안 들 수 있다. 블로그 설정 페이지에서 바꿀 수 있게 만들었다.
백엔드: 3중 검증
PATCH /api/blogs/{slug}/slug Body: { "newSlug": "my-awesome-blog" } 검증 순서: 1. 형식 검증 → ^[a-z][a-z0-9-]{1,18}[a-z0-9]$ (정규식) 2. 예약어 검증 → "admin", "api" 등 40개 3. 중복 검증 → DB 조회 3개 다 통과해야 변경 가능
슬러그가 바뀌면 연관된 UnifiedBoard의 슬러그도 함께 바꿔야 한다. "blog-toto" → "blog-my-awesome-blog". 이걸 빠뜨리면 기존 글의 게시판 연결이 깨진다.
실시간 가용성 체크 API
슬러그를 입력하는 동안 실시간으로 사용 가능 여부를 보여주도록 했다.
GET /api/blogs/check-slug?slug=toto → { "slug": "toto", "available": false, "reason": "already_taken" } → { "slug": "admin", "available": false, "reason": "reserved" } → { "slug": "my-blog", "available": true, "reason": null }
프론트엔드: 디바운스 + 상태 머신
타이핑할 때마다 API를 호출하면 서버에 부담이 되니 500ms 디바운스를 걸었다.
사용자 입력: m → my → my- → my-b → my-bl → my-blo → my-blog │ 500ms 대기 후 API 호출 │ ▼ ┌─────────────────┐ │ ✓ 사용 가능 │ └─────────────────┘
입력 필드의 상태는 5가지다.
┌──────────┬────────────────────────────┐ │ 상태 │ UI 표시 │ ├──────────┼────────────────────────────┤ │ idle │ (아무것도 안 보임) │ │ checking │ 🔄 확인 중... │ │ available│ ✅ 사용 가능한 슬러그입니다 │ │ unavail. │ ❌ 이미 사용 중인 슬러그입니다 │ │ invalid │ ❌ 형식이 올바르지 않습니다 │ └──────────┴────────────────────────────┘
변경 버튼을 누르면 확인 모달이 뜬다. "기존 URL로 접근이 불가능합니다"라는 경고와 함께. 다시 바꿀 수는 있지만, 이전 URL로 공유된 링크는 깨진다.
canCreateBlog는 이제 안녕
기존에는 User 엔티티에 canCreateBlog 필드가 있었다. 관리자가 이 값을 true로 바꿔줘야 블로그를 만들 수 있었다.
모든 가입자가 블로그를 가지게 되면서 이 필드는 의미가 없어졌다. @Deprecated 처리.
┌─────────────────────────────────────────┐ │ 기존 방식 │ │ │ │ 사용자 ──요청──▶ 관리자 ──승인──▶ 블로그 │ │ (canCreateBlog = true) │ │ │ │ 변경 후 │ │ │ │ 사용자 ──로그인──▶ 블로그 (자동 생성) │ │ (canCreateBlog 불필요) │ │ │ └─────────────────────────────────────────┘
DB 컬럼은 당장 삭제하지 않았다. 운영 중인 서비스에서 컬럼 삭제는 별도 마이그레이션으로 처리해야 하니까.
테스트: SlugGenerator 경계 케이스
이메일에서 슬러그를 만드는 과정은 경계 케이스가 꽤 많다. TDD로 먼저 테스트를 작성하고 구현했다.
┌──────────────────────────────────────────────────────┐ │ SlugGenerator 테스트 케이스 │ ├──────────────────────┬───────────────────────────────┤ │ 입력 │ 기대 결과 │ ├──────────────────────┼───────────────────────────────┤ │ toto@gmail.com │ "toto" │ │ John.Doe@co.kr │ "john-doe" │ │ kakao_12345@kakao.us │ "kakao-12345" │ │ 123abc@test.com │ "u123abc" (숫자 시작 → u) │ │ ab@test.com │ "ab0" (패딩) │ │ 한글@kakao.user │ "user" (폴백) │ │ null │ "user" (폴백) │ │ a.very.long.email. │ 17자 truncate + suffix 공간 │ │ address@example.com │ │ └──────────────────────┴───────────────────────────────┘
중복 처리 테스트도 빠뜨릴 수 없다. existsCheck와 reservedCheck를 Predicate로 주입받도록 설계해서, 실제 DB 없이 검증이 가능하다.
// base와 -1이 이미 사용 중인 상황 Predicate<String> exists = s -> s.equals("toto") || s.equals("toto-1"); String result = SlugGenerator.generateUniqueSlug("toto", exists, s -> false); // → "toto-2"
정리
| 항목 | 변경 전 | 변경 후 |
|---|---|---|
| 블로그 생성 | 관리자 승인 후 수동 | 로그인 시 자동 |
| 슬러그 생성 | 사용자 직접 입력 | 이메일 기반 자동 생성 |
| 슬러그 변경 | 불가 | 설정 페이지에서 가능 |
| canCreateBlog | 관리자가 제어 | deprecated |
| 기존 회원 처리 | 해당 없음 | 다음 로그인 시 lazy 생성 |
가장 신경 쓴 부분은 기존 회원 처리다. DB 마이그레이션 스크립트로 일괄 생성하는 대신, 로그인 시 lazy 생성 방식을 택했다. 안전하게 점진적으로 적용되고, 로그인하지 않는 휴면 계정에 불필요한 블로그를 만들 필요도 없다.
그리고 try-catch로 블로그 생성 실패를 감싸서 로그인을 보호한 것. 이건 자꾸 생각나는 교훈인데, 부가 기능 하나가 핵심 플로우를 죽이는 사고는 대부분 이런 식으로 예방할 수 있다.
슬러그 변경 이력을 남겨서 이전 URL에서 새 URL로 리다이렉트하는 건 아직 안 만들었다. 쓰는 사람이 좀 생기면 그때 해야겠다.






댓글
댓글을 작성하려면 이 필요합니다.