본문 바로가기
에러 핸들링

UUID 로 설계한 댓가

by kdohyeon (김대니) 2023. 2. 23.
반응형

https://www.uuidgenerator.net/

예전에 신규 프로젝트를 맡아 설계부터 진행한 적이 있다. DB 테이블 설계를 할 때 평소같았으면 PK 값을 기본 Integer 를 사용했을텐데, 새로운 방식을 사용해보고 싶어서 UUID 로 적용해보고자 했다. 그 땐 몰랐지만 운영을 해오며 겪었던 문제점과 힘들었던 점에 대해 정리하고자 한다.

PK 를 UUID 로 설정한 이유

당시 생성해야 했던 테이블이 7개 정도 되었고, 각 테이블의 PK 를 UUID 로 설정했다. 보안상의 이유가 가장 큰 부분을 차지했는데, 이 서비스가 외부로 노출이 되어야 했기 때문인데, PK 가 Auto-increment 하게 설정되어 있으면 외부로 데이터가 쉽게 노출될 리스크가 크다. 트렌비 홈 화면에 노출될 데이터였기 때문에 좀 더 보안에 신경을 쓰려고 했다. 괜히 ID 값이 노출되어서 다른 회사에서 지표로 활용해버리면 안되기 때문이다.

또한 이 방법을 한번도 사용해본적이 없어서 한번 사용해보고 싶다는 도전 정신이 있기도 했다. (지금은 꽤나 후회를 하고 있지만..)

데이터

잘 운영을 해오다가.. 생성했던 7개의 테이블 중 하나에서 운영상 이슈가 문제가 발생했다. 먼저 문제가 되는 테이블에 대한 소개를 하자면 이력이 쌓이는 형식의 데이터이다. 하나의 Group에 대해 상태값이 변경되면서 새로운 Row 가 생성될 수 있다. 예를 들어, Group 100번에 대해 2023-01-01 에 UUID_1 으로 row 가 생성되었다. Group 100 의 상태가 CREATED -> COMPLETED 로 변경되면서 새로운 row (id: UUID_2) 가 추가되었다.

ID GROUP ID Status CreatedAt ...
UUID_1 100 CREATED 2023-01-01 ...
UUID_2 100 COMPLETED 2023-01-02 ...
UUID_3 100 DELETED 2023-01-04 ...
UUID_4 200 CREATED 2023-01-04 ...
... ... ... ... ...

Group 별 최신 데이터 가져오기

순서가 없는 랜덤한 UUID 를 PK 로 적용해두었기 때문에 최신값을 가져오는 쿼리의 경우에는 createdAt 을 기반으로 설정해두었다. 각 Group 별 최신 Row 를 가져오기 위해 기존에는 아래 쿼리를 활용했다. 서브 쿼리를 활용하여 각 group 의 최신 created_at 을 가져오고, createdAt 으로 맵핑을 시켜주었다. 이 쿼리를 작성할 때의 전제 조건은 중복 created_at 이 없을 것이란 전제가 있었다. (created_at 을 timestamp 형식으로 만들어두었기 때문에 설마 millisecond 까지 똑같은 데이터가 생길까 라는 생각으로...)

SELECT *
FROM table AS a
WHERE
    a.created_at IN (
        SELECT MAX(b.created_at)
        FROM table AS b
        GROUP BY group_id
    )

데이터가 적을 때는 문제가 되지 않아서 테스트 케이스도 통과하고 몇 개월 운영이 잘 되었다. 하지만 갑자기 이 쿼리를 활용하는 배치 시스템이 중단되었고, 그 원인을 찾아가보았다.

역시 전제를 깔아두면 안된다

위 쿼리의 전제를 깨는 데이터가 생성되었다. created_at 이 완전, 완전 똑같은 row 가 발생한 것이다. 서브 쿼리에서 각 group 별 최신 created_at 의 데이터를 가져오긴 했지만 본 쿼리에서 created_at 을 기준으로만 쿼리를 하기 때문에 원하는 group id 에 해당하는 row 를 가져오지 못했다.

서브 쿼리의 결과로 아래와 같은 groupBy 데이터가 추출되었고 원하지 않게 SUB_QUERY_UUID_1 의 Row 가 선택되어 비즈니스 로직을 제대로 처리하지 못했다. 

ID GROUP ID Status CreatedAt
SUB_QUERY_UUID_1 100 DELETED 2023-01-04
SUB_QUERY_UUID_2 200 CREATED 2023-01-04

고쳐보자

결국 문제는 created_at 을 기준으로 쿼리를 했기 때문이고 incremental 한 PK ID 였다면 쿼리를 작성하기 훨씬 편했을 것이고 이런 문제도 발생하지 않았을 것이다 (물론 쿼리를 더 자세하게, 세세하게 작성했다면 이런 문제가 발생하지 않았을 수도 있다).

쿼리를 아래처럼 수정하여 이 문제를 해결했다. (결국 ID 비교가 필요하다.)

SELECT *
FROM table AS a
WHERE created_at = (
    SELECT MAX(created_at)
    FROM table AS b
    WHERE a.id = b.id
)

원래는 이렇게 고치려고 했다

최신 데이터를 가져온다는 의미를 좀 더 직관적으로 전달할 수 있도록 orderBy 와 limit 의 조합으로 문제를 해결하고자 했다. 하지만 사용하고 있는 QueryDSL 에 한계가 있었다. 서브쿼리 내 LIMIT 을 걸게되면 제대로 작동이 안된다는 것이었다.

SELECT *
FROM table AS a
WHERE a.id = (
    SELECT b.id
    FROM table AS b
    WHERE a.id = b.id
    ORDER BY created_at DESC
    LIMIT 1
)

위와 같이 QueryDSL 에 작성을 하고 동작을 해보면 아래와 같은 에러가 발생한다.

Caused by: org.h2.jdbc.JdbcSQLDataException: Scalar subquery contains more than one row

다른 곳에서 찾은 UUID 의 장점과 단점

UUID 를 사용하는 이유로는 대부분 보안상의 이유가 가장 크다고 말하고 있고 그 외에도 장점과 단점에 대해 정리하는 내용을 가져왔다.

회고를 하며

  • 이런 리서치를 프로젝트 전에 좀 더 했더라면 좋았을텐데 라는 아쉬움이 남는다.
반응형

댓글