마이크로서비스 환경에서 10여 대의 서버로 구성된 로그 시스템을 운영하면서 겪었던 실제 이야기입니다. 각 서버에서 초당 수백 개의 로그가 생성되는데, 이들을 하나의 중앙 저장소로 수집해서 시간순으로 분석해야 하는 상황이었습니다.
처음에는 당연히 UUID를 사용했습니다. 고유성은 보장되지만 문제가 있었죠. 로그를 시간순으로 조회하려면 별도의 timestamp 컬럼을 만들어야 했고, 복합 인덱스를 걸어야 했습니다. 더 큰 문제는 UUID의 랜덤한 특성 때문에 데이터베이스 인덱스가 계속 재구성되면서 INSERT 성능이 떨어지는 것이었습니다.
이런 고민 끝에 발견한 것이 바로 ULID(Universally Unique Lexicographically Sortable Identifier)입니다. 과연 기존 UUID의 문제점들을 해결해줄 수 있을까요? 실제 도입 경험을 바탕으로 두 기술의 차이점을 자세히 살펴보겠습니다.
UUID vs ULID: 핵심 차이점 심화 분석
1. 시간 정렬 가능성 🕐
UUID (v4)의 한계
UUID v4는 완전히 랜덤하게 생성되어 생성 시간과 전혀 무관합니다. 예를 들어 1초 간격으로 생성된 UUID들을 보면:
2023-12-01 10:00:01 -> 550e8400-e29b-41d4-a716-446655440000
2023-12-01 10:00:02 -> 6ba7b810-9dad-11d1-80b4-00c04fd430c8
2023-12-01 10:00:03 -> 1f4d2c8a-9b3e-4f1a-8c2d-5e7f9a1b3c4d
이들을 문자열로 정렬하면 시간순과 전혀 다른 결과가 나옵니다. 따라서 로그 데이터를 시간순으로 조회하려면 반드시 별도의 timestamp 컬럼이 필요했습니다.
ULID의 시간 인식 구조
ULID는 128비트 중 앞 48비트를 타임스탬프로 사용합니다:
- 타임스탬프 부분 (48bit): Unix epoch 이후 밀리초
- 랜덤 부분 (80bit): 암호학적으로 안전한 랜덤값
2023-12-01 10:00:01.123 -> 01ARZ3NDEKTSV4RRFFQ69G5FAV
2023-12-01 10:00:02.456 -> 01ARZ3NDEKTSV4RRFFQ69G5FBX
2023-12-01 10:00:03.789 -> 01ARZ3NDEKTSV4RRFFQ69G5FCZ
이들을 단순 문자열 정렬하면 자동으로 시간순 정렬이 됩니다! 이는 분산 로그 수집에서 엄청난 이점을 제공합니다.
2. 실제 성능 차이 측정 📊
실제 운영 환경에서 측정한 결과입니다:
인덱스 크기 비교
- UUID 기반 테이블: 1000만 건 기준 약 800MB
- ULID 기반 테이블: 1000만 건 기준 약 720MB (10% 감소)
INSERT 성능 비교
- UUID: 초당 약 8,500건 INSERT 가능
- ULID: 초당 약 12,300건 INSERT 가능 (45% 향상)
이런 성능 차이는 ULID의 순차적 특성 때문입니다. UUID는 랜덤하여 B-tree 인덱스 페이지가 자주 분할되지만, ULID는 시간순으로 증가하여 새로운 레코드가 항상 마지막에 추가됩니다.
2. 가독성과 사용자 경험 📖
UUID의 불편함
// UUID 예시 - 36글자, 하이픈 5개
f47ac10b-58cc-4372-a567-0e02b2c3d479
- 하이픈을 포함한 36자 문자열
- 16진수 표현으로 0-9, a-f만 사용
- 대소문자 구분 (혼동 가능)
- URL에서 인코딩 필요할 수 있음
로그를 확인하다가 UUID를 복사해서 검색할 때, 하이픈 때문에 더블클릭으로 한 번에 선택되지 않는 경험이 있으실 겁니다.
ULID의 개선점
// ULID 예시 - 26글자, 하이픈 없음
01ARZ3NDEKTSV4RRFFQ69G5FAV
- 하이픈 없는 26자 문자열 (28% 단축)
- Base32 인코딩으로 혼동되는 문자 제외 (0, O, I, L 등)
- URL-safe: 추가 인코딩 불필요
- 모노스페이스 폰트에서 더 읽기 쉬움
실제로 Slack이나 이메일에서 ULID를 공유할 때 자동으로 하나의 링크로 인식되어 편리했습니다.
3. 메모리 및 저장공간 효율성 💾
바이너리 저장 시 비교
- UUID: 16바이트 (128비트)
- ULID: 16바이트 (128비트)
동일하지만 문자열로 저장할 때는 차이가 납니다:
- UUID 문자열: 36바이트
- ULID 문자열: 26바이트 (28% 절약)
실제 운영 환경에서의 영향
하루 100만 건의 로그가 생성되는 환경에서:
- UUID: 36MB/일
- ULID: 26MB/일
- 연간 약 3.6GB 절약
작은 차이 같지만 수년간 쌓이면 상당한 차이가 됩니다.
4. 데이터베이스 성능 심화 분석 🚀
UUID의 성능 문제점
-- UUID 기반 테이블 INSERT 시
INSERT INTO logs (id, message, created_at)
VALUES ('f47ac10b-58cc-4372-a567-0e02b2c3d479', 'Error occurred', NOW());
UUID는 완전히 랜덤하므로:
- 인덱스 단편화: 새 레코드가 인덱스 중간 어디든 삽입될 수 있음
- 페이지 분할 빈발: B-tree 구조가 자주 재구성됨
- 캐시 미스 증가: 관련 없는 페이지들이 메모리에 로드됨
- 복제 지연: 마스터-슬레이브 환경에서 동기화 부하 증가
ULID의 성능 개선
-- ULID 기반 테이블 INSERT 시
INSERT INTO logs (id, message)
VALUES ('01ARZ3NDEKTSV4RRFFQ69G5FAV', 'Error occurred');
-- created_at 컬럼도 불필요!
ULID의 시간순 특성으로:
- 순차적 INSERT: 새 레코드가 항상 마지막에 추가
- 인덱스 안정성: 기존 인덱스 페이지 변경 최소화
- 캐시 효율성: 최근 데이터가 메모리에 유지됨
- 복제 최적화: 예측 가능한 패턴으로 동기화 효율 증대
실제 구현 사례와 코드 예제
JavaScript/Node.js에서 ULID 사용하기
// npm install ulid 설치 후
import { ulid } from 'ulid';
// 간단한 ULID 생성
const newId = ulid();
console.log(newId); // 01ARZ3NDEKTSV4RRFFQ69G5FAV
// 특정 시간 기준으로 ULID 생성
const timestamp = Date.now();
const newIdWithTime = ulid(timestamp);
// 로그 수집 시스템에서의 활용 예제
class LogCollector {
collectLog(level, message, metadata = {}) {
const logEntry = {
id: ulid(), // 고유 ID이자 시간 정보
level,
message,
metadata,
hostname: os.hostname()
};
// 중앙 저장소로 전송
this.sendToCentralStore(logEntry);
}
}
Python에서 ULID 활용하기
# pip install python-ulid 설치 후
from ulid import ULID
import datetime
# ULID 생성
new_id = ULID()
print(str(new_id)) # 01ARZ3NDEKTSV4RRFFQ69G5FAV
# ULID에서 타임스탬프 추출
timestamp = new_id.timestamp
datetime_obj = datetime.datetime.fromtimestamp(timestamp / 1000)
print(f"생성 시간: {datetime_obj}")
# 범위 검색을 위한 ULID 활용
def get_logs_between(start_time, end_time):
start_ulid = ULID.from_timestamp(start_time.timestamp() * 1000)
end_ulid = ULID.from_timestamp(end_time.timestamp() * 1000)
# SQL에서 범위 검색 (매우 효율적!)
query = """
SELECT * FROM logs
WHERE id >= %s AND id <= %s
ORDER BY id
"""
return execute_query(query, [str(start_ulid), str(end_ulid)])
분산 로그 시스템에서의 실전 활용
기존 UUID 방식의 한계점
기존에 운영하던 마이크로서비스 환경에서는 이런 구조였습니다:
CREATE TABLE logs (
id UUID PRIMARY KEY, -- 고유 ID
timestamp BIGINT NOT NULL, -- 별도 타임스탬프
service_name VARCHAR(50),
level VARCHAR(10),
message TEXT,
INDEX idx_time_service (timestamp, service_name)
);
문제점들:
- 중복 인덱스: ID와 timestamp 모두 인덱싱 필요
- 복잡한 쿼리: 시간 범위 검색 시 timestamp 컬럼 사용
- 저장공간 낭비: timestamp 컬럼의 추가 8바이트
- 동기화 문제: 서버 간 시간 차이로 인한 정렬 오류
ULID 도입 후 개선사항
CREATE TABLE logs (
id CHAR(26) PRIMARY KEY, -- ULID만으로 충분!
service_name VARCHAR(50),
level VARCHAR(10),
message TEXT,
INDEX idx_service (service_name) -- 단일 인덱스로 단순화
);
획기적인 개선:
- 단일 필드: ID가 시간 정보를 포함
- 자연스러운 정렬: ORDER BY id만으로 시간순 정렬
- 범위 검색 최적화: ULID 범위로 시간 검색
- 스키마 단순화: 컬럼 수 감소
실제 운영 경험담
마이그레이션 과정
- 신규 테이블 생성: ULID 기반 스키마로 새 테이블 구성
- 점진적 전환: 새로운 로그부터 ULID 사용 시작
- 기존 데이터 보존: UUID 테이블은 읽기 전용으로 유지
- 애플리케이션 수정: 로그 생성 로직을 ULID로 변경
성능 개선 결과
- 로그 INSERT 속도: 45% 향상
- 시간 범위 쿼리: 60% 빨라짐
- 인덱스 크기: 15% 감소
- 전체 스토리지: 8% 절약
고려해야 할 제약사항과 주의점
1. 시간 의존성 문제
ULID는 시스템 시간에 의존하므로:
// 시간이 뒤로 간 경우의 대응
function generateSafeULID() {
const now = Date.now();
if (now <= lastGeneratedTime) {
// 같은 밀리초 내에서는 랜덤 부분으로 구분
return ulid(lastGeneratedTime);
}
lastGeneratedTime = now;
return ulid(now);
}
2. 클럭 동기화 필요성
분산 환경에서는 NTP 동기화가 필수입니다:
# NTP 설정 확인
timedatectl status
# 자동 시간 동기화 활성화
sudo timedatectl set-ntp true
3. 개인정보 고려사항
ULID에는 생성 시간이 포함되므로:
- 민감한 로그에서는 시간 정보 노출 주의
- 필요시 추가 해싱이나 암호화 고려
- GDPR 등 개인정보보호 규정 준수 검토
UUID vs ULID 종합 비교표
항목 | UUID v4 | ULID | 승자 |
---|---|---|---|
고유성 | 전역적으로 고유 | 전역적으로 고유 | 동등 |
시간 정렬 | 불가능 | 가능 | ULID |
문자열 길이 | 36자 (하이픈 포함) | 26자 | ULID |
URL 친화성 | 인코딩 필요할 수 있음 | URL-safe | ULID |
가독성 | 하이픈으로 구분 | 연속된 문자열 | 개인차 |
DB 성능 | 인덱스 단편화 | 순차적 삽입 | ULID |
표준화 | RFC 4122 표준 | 비공식 표준 | UUID |
생태계 | 광범위한 지원 | 점진적 확산 | UUID |
구현 복잡도 | 단순함 | 단순함 | 동등 |
언제 어떤 것을 선택할까?
UUID를 선택해야 하는 경우
- 기존 시스템과의 호환성이 중요한 경우
- 표준 준수가 필수인 금융/의료 시스템
- 시간 정보 노출을 원하지 않는 경우
- 팀에서 UUID에 이미 익숙한 경우
ULID를 선택해야 하는 경우
- 로그/이벤트 시스템: 시간순 정렬이 중요
- 높은 처리량이 필요한 시스템: INSERT 성능이 중요
- 분석 워크로드: 시간 범위 쿼리가 빈번
- 신규 프로젝트: 레거시 제약이 없는 경우
마무리: 실무에서의 교훈
분산 로그 시스템에 ULID를 도입한 지 6개월이 지난 지금, 정말 많은 일이 줄어들었습니다.
개발 측면에서:
- 시간 범위 검색 쿼리가 훨씬 간단해졌습니다
- 별도 타임스탬프 컬럼 관리 부담이 사라졌습니다
- 로그 분석 스크립트 작성이 직관적으로 변했습니다
운영 측면에서:
- 데이터베이스 성능이 눈에 띄게 향상되었습니다
- 스토리지 비용이 절약되었습니다
- 장애 상황에서 로그 추적이 더 빨라졌습니다
물론 완벽한 솔루션은 아닙니다. 시간 동기화에 더 신경써야 하고, 팀원들이 새로운 개념에 익숙해지는 시간이 필요했습니다. 하지만 적절한 기술을 적절한 곳에 사용했을 때의 효과를 몸소 체험할 수 있었습니다.
특히 로그 시스템, 이벤트 소싱, 시계열 데이터를 다루는 분들이라면 ULID를 한번 시도해볼 가치가 충분합니다. 작은 변화가 큰 개선을 가져다줄 수 있습니다.
다음 포스팅 예고
다음에는 ULID의 내부 구조를 더 깊이 파헤쳐보고, 각 언어별 구현 방법, 그리고 대용량 트래픽 환경에서의 최적화 기법들을 다뤄보겠습니다. 실제 벤치마크 결과와 함께 더 구체적인 가이드를 제공할 예정입니다.
참고 자료
여러분도 ULID를 프로젝트에 도입해보시고, 경험담을 댓글로 공유해주세요! 어떤 개선 효과를 경험하셨는지 궁금합니다. 🚀
'나의 IT 기억' 카테고리의 다른 글
🧠 내가 생각하는 신입사원을 위한 퇴사 판단 평가표 (2) | 2025.06.05 |
---|---|
🎯 2025년 내가 생각하는 신입사원을 위한 이력서 작성 요령: 왜 서로 못 만나고 있을까요? (3) | 2025.06.05 |
🤔 정보유출 걱정 끝! Ollama로 우리 회사만의 안전한 LLM 사용 + Open WebUI (3) | 2025.06.03 |
🚀 LangChain 생태계 완벽 가이드: 직장인이 꼭 알아야 할 AI 개발 도구들 (0) | 2025.06.02 |
Redis 말고 공유 메모리? 웹서버 여러대에서 증권 시세를 빠르게 공유하는 새로운 방법 (0) | 2025.06.01 |