포스트

운영자가 끌어 정한 노출 순서를 저장하는 법

운영자가 직접 정한 노출 순서를 정수 sort_order 컬럼으로 다룬다. 중간 삽입 비용, 간격 띄우기, 타이브레이커, 재정렬을 한 트랜잭션으로 묶는 설계를 정리한다.

운영자가 끌어 정한 노출 순서를 저장하는 법

그 주엔 운영자가 항목과 이미지의 노출 순서를 직접 정해 저장하는 기능을 만들었다. 정렬이라고 하면 보통 “이름 오름차순” “최신순”처럼 데이터 자체의 속성으로 자동 정렬하는 걸 떠올린다. 하지만 배너·홍보 영역처럼 사람이 “이건 맨 앞, 저건 그 다음”이라고 임의로 정한 순서는 데이터 어디에도 자연적으로 존재하지 않는다. 이 순서는 별도의 컬럼으로 명시적으로 저장해야 한다.

순서를 어떻게 표현하나

가장 직관적인 방법은 정수 sort_order 컬럼이다. 작은 값이 앞에 온다.

1
2
3
4
5
6
7
8
CREATE TABLE banner (
    id          BIGINT PRIMARY KEY,
    title       VARCHAR(200),
    sort_order  INT NOT NULL,
    created_at  DATETIME
);

SELECT * FROM banner ORDER BY sort_order ASC, id ASC;

조회는 단순하다. 문제는 재정렬이다. 순서를 1, 2, 3, 4로 촘촘히 매겼다고 하자. 운영자가 “새 배너를 2번과 3번 사이에 넣어줘”라고 하면, 기존 3번·4번을 각각 4번·5번으로 밀고 새 배너를 3번에 넣어야 한다. 항목이 100개면 삽입 한 번에 수십 개 행을 UPDATE한다. 이게 누적되면 비싸다.

간격을 띄우면 재배치가 줄어든다

해법은 순서 값을 1씩 주지 않고 10, 20, 30처럼 간격을 띄우는 것이다. 그러면 20과 30 사이에 끼울 때 새 값을 25로 주면 끝이다. 다른 행을 건드리지 않는다.

1
2
3
-- 20과 30 사이에 삽입: 중간값만 계산하면 됨
INSERT INTO banner (id, title, sort_order)
VALUES (101, 'New Banner', 25);

이건 데이터 구조에서 흔히 쓰는 트릭이다. 미리 여유 공간을 둬서 평균적인 삽입을 O(1)로 만든다. 다만 간격은 유한한 자원이다. 20·25 사이에 또 넣으면 22, 그 사이에 또 넣으면 21… 결국 간격이 1까지 좁아진다. 그때는 전체를 다시 10, 20, 30…으로 재배치(rebalance)한다. 평소엔 싸게 끼우고, 가끔 한 번 비싸게 정리하는 분할상환(amortized) 전략이다.

같은 순위가 나오면 — 타이브레이커

sort_order만으로 정렬하면 값이 같은 행들의 순서는 DB가 보장하지 않는다. 똑같은 값이 두 개면 매번 다른 순서로 나올 수 있다. 그래서 항상 2차 정렬 키를 둔다. 보통 변하지 않는 id나 created_at을 붙인다.

1
ORDER BY sort_order ASC, id ASC

이렇게 하면 순서 충돌이 나도 결과가 안정적(stable)이다.

재정렬은 한 트랜잭션으로

운영자가 화면에서 드래그로 전체 순서를 바꾼 뒤 “저장”을 누르면, 보통 여러 행의 sort_order를 한꺼번에 갱신한다. 이걸 행마다 따로 커밋하면 중간에 실패했을 때 절반만 바뀐 순서가 화면에 노출된다. 1번이 둘이거나 빠진 번호가 생긴다.

1
2
3
4
5
6
7
8
@Transactional
public void reorder(List<Long> orderedIds) {
    int seq = 10;
    for (Long id : orderedIds) {
        bannerMapper.updateSortOrder(id, seq);
        seq += 10;
    }
}

전체 갱신을 한 트랜잭션으로 묶으면, 실패 시 전부 롤백되어 이전의 일관된 순서로 돌아간다. 부분 적용된 순서가 노출될 일이 없다.

운영 함정

고유 제약을 걸면 재정렬이 막힌다. sort_order에 UNIQUE 제약을 거는 건 직관적이지만, 1과 2를 맞바꾸려고 둘 다 갱신하는 도중에 일시적으로 값이 충돌해 제약 위반이 난다. 순서 컬럼엔 보통 UNIQUE를 걸지 않고, 타이브레이커로 결정성을 확보한다.

전체 +1 방식을 그대로 두면 항목이 늘수록 느려진다. 처음 몇 개일 땐 중간 삽입 시 전부 미는 방식도 멀쩡히 돈다. 항목이 수백 개가 되면 삽입마다 수백 행 갱신이 쌓여 락 경합과 지연이 생긴다. 간격 띄우기로 미리 전환해 두는 게 안전하다.

핵심 요약

  • 사람이 정한 임의 순서는 정수 sort_order 컬럼으로 명시 저장한다.
  • 값을 10, 20, 30으로 띄우면 중간 삽입이 다른 행을 안 건드리고 O(1)에 가깝다. 간격이 마르면 재배치한다.
  • 항상 id·created_at 같은 타이브레이커로 정렬을 안정화한다.
  • 재정렬은 한 트랜잭션으로 묶어 부분 적용을 막는다.

면접 한 줄 Q&A Q. sort_order에 간격을 왜 띄우나? A. 1씩 매기면 중간 삽입마다 뒤 행을 전부 +1 해야 한다. 10·20·30으로 띄우면 중간값만 넣어 한 행만 쓰고, 간격이 마르면 가끔 재배치하는 분할상환 전략이다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.