키 설계 기초: PRIMARY KEY/UNIQUE로 ‘중복 적립’과 ‘중복 차감’을 구조적으로 막는 방법

포인트 시스템에서 가장 골치 아픈 문제는 “한 번만 처리되어야 하는 요청이 두 번 처리되는 것”입니다. 네트워크 재시도, 클라이언트 중복 클릭, 서버 타임아웃 후 재요청은 운영에서 흔한 일이고, 이때 코드만으로 완벽하게 막는 건 생각보다 어렵습니다. 그래서 많은 서비스는 마지막 방어선으로 키(Primary Key/Unique Key) 를 활용합니다. 키 설계가 잘 되면, 애플리케이션이 실수하더라도 DB가 중복을 거절 합니다. 이번 글에서는 “키는 그냥 ID 하나”라는 수준을 넘어서, 포인트 원장(히스토리)에서 실제로 필요한 멱등성(idempotency) 을 어떻게 만들지에 초점을 맞춥니다. 이 단원의 목적: “중복이 생길 수밖에 없는 현실”을 전제로 설계하기 포인트 기능은 보통 결제, 이벤트, 쿠폰, 고객센터 보정 등 다양한 경로에서 호출됩니다. 호출 경로가 늘어날수록 중복 처리 가능성도 늘어납니다. 이번 글의 목표는 아래 3가지입니다. PK/UNIQUE의 역할을 정확히 이해하고 “어디에 걸어야 하는지” 결정한다 포인트 원장에서 중복을 막는 대표 전략(요청 ID/ref_id)을 설계한다 실제로 중복 요청이 들어오는 상황을 가정해, DB 레벨에서 안전하게 막는다 1) PRIMARY KEY는 “행을 식별하는 유일한 주소”다 PRIMARY KEY(PK)는 테이블에서 한 행을 유일하게 식별합니다. 흔히 AUTO_INCREMENT id를 PK로 두는 이유는 단순합니다. 쉽고, 빠르고, 안정적 이기 때문입니다. 하지만 포인트 시스템에서는 “id가 유일하다”만으로는 부족합니다. id는 단지 “기록의 번호”일 뿐, “같은 요청이 두 번 들어왔다”는 문제를 막아주지 않습니다. 즉, PK는 필요조건이지만 중복 방지의 충분조건은 아닙니다. -- 기록 자체의 식별자(기본) PRIMARY KEY (id) 2) UNIQUE는 “같은 의미의 기록이 두 번 들어오는 것”을 막는다 UNIQUE는 “비즈니스적으로 같은 의미”의 중복을 차단하는 도구입니다. ...

데이터 타입과 제약조건: 포인트 시스템에서 “틀린 데이터”는 애초에 못 들어오게 막아야 한다

포인트 시스템을 운영하다 보면, 조회 쿼리를 아무리 잘 만들어도 해결되지 않는 문제가 있습니다. 바로 데이터 자체가 잘못 들어오는 경우 입니다. 한 번 잘못 적립되거나 중복 차감이 발생하면, 그 이후부터는 “정상 데이터 위에 예외 처리를 덧칠하는 작업”이 계속 늘어납니다. 반대로, 초기에 데이터 타입과 제약조건을 제대로 잡아두면 많은 문제가 입력 단계에서 차단 됩니다. 이 단원의 목적: SQL을 “조회 언어”가 아니라 “데이터 품질 언어”로 쓰기 SQL을 배우는 초반에는 SELECT/WHERE가 눈에 들어오지만, 실제 서비스에서는 스키마 설계(타입/제약조건) 가 시스템의 신뢰도를 좌우합니다. 이번 글의 목표는 다음 3가지입니다. 포인트 시스템에 맞는 타입을 고르고, 오버플로우/범위 문제를 예방한다 NOT NULL, DEFAULT, CHECK(가능한 DBMS 한정)로 “불완전한 입력”을 막는다 UNIQUE/FK로 “중복 적립”과 “없는 사용자에 대한 적립” 같은 사고를 차단한다 1) 숫자 타입: INT로 충분한가, BIGINT가 필요한가 포인트는 결국 숫자입니다. 그런데 숫자는 시간이 지나면서 쌓이고, 서비스가 커지면 “상상보다 빨리” 범위를 넘길 수 있습니다. 그래서 초기에 아래를 구분해서 생각하는 편이 안전합니다. amount(변동량) : 한 번의 적립/차감 크기 balance(잔액) : 누적 결과 id(일련번호) : 내역이 쌓이는 속도에 따라 매우 빠르게 증가 amount는 INT가 흔히 충분하지만, balance는 BIGINT가 안전하다 단건 적립이 수천~수만 포인트 수준이라면 amount는 INT로 충분한 경우가 많습니다. 하지만 balance는 장기간 누적될 수 있고, 이벤트나 정책 변경으로 규모가 커질 수 있습니다. 그래서 잔액(balance)을 별도 테이블에 둔다면 BIGINT 를 권장하는 이유가 여기에 있습니다. -- 내역 일련번호는 빠르게 늘어날 수 있으므로 BIGINT 계열이 흔히 쓰입니다. id BIGI...

WHERE로 “조건별 포인트 내역”을 정확하게 필터링하기: 필터가 흔들리면 데이터도 신뢰를 잃는다

포인트 시스템에서 조회는 단순히 “보여주기”가 아닙니다. 포인트는 금액에 준하는 성격을 가지기 때문에, 내역 조회가 조금만 애매해져도 운영에서 바로 문제로 이어집니다. 특히 사용자가 가장 자주 쓰는 기능이 기간/유형/상태 같은 필터인데, 이 필터가 틀리면 “내 포인트가 왜 이래?”라는 불신으로 연결됩니다. 이 단원의 목적: “질문을 정확한 조건으로 번역하는 능력” 만들기 WHERE는 SQL의 필터링(조건) 문법이지만, 실제로는 사용자의 질문을 데이터베이스 언어로 번역 하는 과정입니다. 이번 글의 목표는 세 가지입니다. 포인트 조회 화면에서 자주 나오는 요구사항을 WHERE로 안정적으로 구현한다 AND/OR 조합에서 발생하는 실수를 “괄호”로 확실히 제어한다 운영 데이터가 커져도 느려지지 않게, 조건을 인덱스 친화적으로 작성하는 감각을 익힌다 준비: 실습 데이터(이전 글의 데이터가 있다면 생략 가능) -- uid 1명 기준 예시 SET @uid := '11111111-1111-1111-1111-111111111111'; -- 최근 30일 기준으로 몇 건 더 섞어두면 필터 연습이 쉬워집니다. INSERT INTO point_history (uid, point_type, action_type, amount, ref_id, created_at) VALUES (@uid, 'FREE', 'CHARGE', 150, 'EVENT_20251201', NOW() - INTERVAL 13 DAY), (@uid, 'PAID', 'USE', -80, 'USE_7010', NOW() - INTERVAL 12 DAY), (@uid, 'FREE', 'ADJUST', 30, 'ADMIN_0001', NOW() - INTERVAL 11 DAY); 1) WHERE의 이론: “조건식은 참/거짓을 만든다” WHERE 뒤에 오는...

SELECT 기초로 “내 포인트 잔액/내역” 조회하기: 문법보다 ‘질문’을 먼저 만든다

SQL을 처음 배울 때 가장 빠른 길은 문법을 외우는 게 아니라, “데이터베이스에 어떤 질문을 던질 것인지”를 먼저 정하는 것입니다. 포인트 시스템에서 가장 자주 나오는 질문은 딱 2개입니다. 지금 잔액이 얼마인가? 어떤 내역 때문에 이렇게 되었는가? 이번 글은 SELECT의 핵심 문법(컬럼 선택, 별칭, 정렬, 제한)을 익히면서 위 2개 질문을 실제 쿼리로 만드는 데 집중합니다. 준비: 예시 데이터(작게 넣고 바로 확인하기) 실습을 위해 사용자 1명과 포인트 내역 몇 건을 넣습니다. 부호 정책은 “적립 +, 차감 -”로 통일합니다. -- 사용자 1명 INSERT INTO users (uid, created_at) VALUES ('11111111-1111-1111-1111-111111111111', NOW()); -- 내역 5건 (적립 3, 사용 1, 만료 1) INSERT INTO point_history (uid, point_type, action_type, amount, ref_id, created_at) VALUES ('11111111-1111-1111-1111-111111111111', 'FREE', 'CHARGE', 1000, 'EVENT_20251214', NOW() - INTERVAL 10 DAY), ('11111111-1111-1111-1111-111111111111', 'PAID', 'CHARGE', 500, 'ORDER_9001', NOW() - INTERVAL 7 DAY), ('11111111-1111-1111-1111-111111111111', 'FREE', 'USE', -300, 'USE_7001', NOW() - INTERVAL 5 DAY), ('11111111-1111-1111-1111-111111111111', ...

포인트 시스템에 필요한 최소 테이블 목록 설계: “나중에 고치기 어려운 것”부터 잡는다

포인트 기능을 만들 때 가장 흔한 시행착오는 “일단 구현하고 나중에 정리하자”입니다. 그런데 포인트는 금액에 준하는 데이터라, 설계가 흔들리면 운영에서 바로 비용으로 돌아옵니다. 이번 글에서는 SQL 문법보다 한 단계 앞에서, 포인트 시스템에 필요한 최소 테이블 을 확정하고, 각 테이블이 어떤 질문(조회/검증)에 답해야 하는지까지 정리합니다. 포인트 데이터는 2가지 질문을 반드시 견뎌야 한다 왜 잔액이 이 숫자인가? (원장/히스토리로 추적 가능해야 함) 지금 잔액이 얼마인가? (빠르게 조회되어야 함) 첫 번째 질문은 “감사(추적)”의 영역이고, 두 번째는 “성능”의 영역입니다. 이 둘을 동시에 만족시키는 가장 단단한 출발점은 원장(내역) 테이블을 중심 으로 두고, 필요하면 잔액 테이블을 추가하는 방식입니다. 최소 테이블 2개 + 선택 테이블 1개 처음부터 많은 테이블을 만들 필요는 없습니다. 아래 2개는 필수, 1개는 성능 목적의 선택입니다. 구분 테이블 필요 이유 대표 조회 필수 users uid(사용자 식별) 기준을 고정 회원별 잔액/내역 조회 필수 point_history (원장) 적립/차감/만료/취소의 “사실”을 남김 기간별 내역, 합계, 감사 추적 선택 point_balance “지금 잔액”을 즉시 응답하기 위한 캐시성 테이블 uid로 잔액 1건 조회 1) users: 최소 컬럼만으로 시작해도 된다 여기서 중요한 건 회원정보가 아니라 uid가 일관되게 유지되는 것 입니다. uid는 문자열(UUID)일 수도 있고 정수형 id일 수도 있습니다. 어떤 것을 쓰든 “한 번 정하면 바꾸기 어렵다”는 점만 기억하면 됩니다. CREATE TABLE users ( uid CHAR(36) PRIMARY KEY, -- UUID 가정 created_at DATETIME NOT NULL ); 2) point_history(원장): 포인트 시스템의 중심 포인트는 잔액보다 “내역”이 핵심입...

포인트 시스템으로 배우는 SQL 로드맵: 적립·차감·만료를 끝까지 완성하는 20단계 계획

SQL을 “문법”으로만 배우면, 실제 서비스에 붙일 때 어디서부터 막히는 경우가 많습니다. 반대로 작은 기능을 끝까지 완성 해 보면 SELECT 하나를 배우더라도 “왜 이 쿼리가 필요한지”가 분명해집니다. 이 연재는 포인트(적립/사용/만료/취소) 시스템을 목표로, SQL 기초를 실전 감각으로 쌓아가는 로드맵입니다. 왜 하필 ‘포인트 시스템’인가 포인트는 단순히 숫자 하나를 더하고 빼는 문제가 아닙니다. 운영에서 반드시 부딪히는 이슈가 다 들어 있습니다. 중복 적립 을 어떻게 막을까? 동시에 여러 요청 이 들어오면 잔액이 꼬이지 않을까? 만료(Expire) 는 언제/어떤 기준으로 처리할까? 사용 취소 나 환불 이 생기면 어떻게 되돌릴까? 데이터가 쌓이면 조회 성능 은 어떻게 유지할까? 결국 이 주제 하나로 테이블 설계 , 쿼리 작성 , 인덱스 , 트랜잭션 , 운영 검증 까지 자연스럽게 연결됩니다. 이번 연재에서 만들 결과물 연재가 끝나면 아래를 스스로 설명하고 구현할 수 있는 상태가 목표입니다. 포인트 히스토리 기반 으로 잔액과 내역을 정확하게 조회한다 중복/오류 를 막는 제약조건과 키 설계를 적용한다 동시성 에서 안전하게 적립/차감을 처리한다(트랜잭션/잠금) 만료 배치 쿼리를 만들고 운영 점검 쿼리로 검증한다 성능을 위해 인덱스 와 조회 패턴을 설계한다 포인트 시스템 최소 모델(처음엔 이것만으로 충분) 포인트는 “현재 잔액”보다 왜 그렇게 되었는지(내역) 가 더 중요합니다. 그래서 기본은 ‘히스토리(원장)’ 테이블입니다. -- 예시: point_history (가장 단순한 형태) -- DBMS에 따라 타입/문법은 조금씩 다를 수 있습니다. CREATE TABLE point_history ( id BIGINT PRIMARY KEY AUTO_INCREMENT, uid CHAR(36) NOT NULL, -- 사용자 식별자(UUID 가정) point_type ENUM('PAID','FREE'...