1️⃣ 업무 요청
특정 페이지를 그대로 복제할 수 있도록 해달라는 요청을 받았다.
해당 페이지에는 여러 개의 테이블이 존재하고, 그중 일부 테이블은 각 행마다 고유한 ID를 가지는 구조로 되어 있다.
기존 페이지에서는 + 버튼을 통해 행을 하나씩 추가하며,
이때마다 ID를 개별적으로 채번하는 방식을 사용하고 있었다.하지만 페이지 전체를 복제하는 기능에서는 상황이 달라진다.
한 번의 동작으로 n개의 행을 조회하고, 각 행마다 새로운 ID를 부여해야 하기 때문이다.만약 기존 방식처럼 행을 하나씩 추가하며 ID를 채번한다면,
통신 횟수가 급격히 증가하고, 결과적으로 페이지 복제 속도와 사용감이 크게 저하될 가능성이 있다.이런 문제를 해결하기 위해,
복제 대상 데이터를 한 번에 조회한 뒤, 서버 단에서 각 행에 새로운 ID를 부여하여 처리할 수 있다면,
페이지 복제 기능의 사용감은 훨씬 개선될 것이다.
2️⃣ 문제 접근
🔹 기존 방식의 한계
기존 구현 방식은 다음과 같은 흐름을 가진다.
- 사용자가 + 버튼 클릭
- 서버에 요청
- ID 채번
- 단일 행 INSERT
이 방식은 단건 처리에는 문제가 없지만,
다음과 같은 상황에서는 한계를 드러낸다.
- 복제 대상 행 수가 많을 경우
- 행마다 개별 통신이 필요한 경우
- 화면 응답 속도가 중요한 경우
특히 페이지 복제 기능에서는
**“한 번의 요청으로 여러 행을 생성해야 하는 요구사항”**과 맞지 않는다.
🔹 접근 전략: 한 번의 통신, 서버 단 처리
이 문제를 해결하기 위해 다음과 같은 전략을 선택했다.
- 복제 대상 데이터를 SELECT로 한 번에 조회
- 서버 단에서 데이터를 순회하며
- 각 행마다 새로운 ID를 채번
- 결과를 임시 테이블에 누적 저장
이 과정 전체를 하나의 SQL 실행 흐름 안에서 처리한다면,
- 네트워크 왕복 횟수를 줄일 수 있고
- 사용자 입장에서는 즉각적인 응답을 받을 수 있다.
🔹 CURSOR를 선택한 이유
일반적으로 SQL에서는 집합 기반 처리(Set-based) 가 권장된다.
하지만 이번 요구사항에서는 다음과 같은 제약이 있었다.
- 행마다 서로 다른 ID를 순차적으로 채번해야 함
- ID는 외부 프로시저 또는 시퀀스 로직을 통해 생성됨
- 단순 ROW_NUMBER() 로 대체 불가
이 경우,
각 행을 하나씩 읽으며 로직을 적용할 수 있는 CURSOR 방식이 오히려 명확한 해결책이 된다.
아래는 완성된 쿼리이다.
DECLARE @RESULT_TBL TABLE (
new_id VARCHAR(40),
src_id VARCHAR(40),
parent_id VARCHAR(40),
seq_no INT,
div_cd VARCHAR(20),
imp_dt VARCHAR(8),
dept_id INT,
user_id INT,
cnt_a INT,
cnt_b INT,
rate INT,
content NVARCHAR(MAX)
);
DECLARE
@v_new_id VARCHAR(40),
@v_src_id VARCHAR(40),
@v_parent_id VARCHAR(40),
@v_seq_no INT,
@v_div_cd VARCHAR(20),
@v_imp_dt VARCHAR(8),
@v_dept_id INT,
@v_user_id INT,
@v_cnt_a INT,
@v_cnt_b INT,
@v_rate INT,
@v_content NVARCHAR(MAX);
DECLARE cur_data CURSOR FOR
SELECT src_id, parent_id, seq_no, div_cd, imp_dt,
dept_id, user_id, cnt_a, cnt_b, rate, content
FROM SOURCE_TABLE
WHERE parent_id = @docseq
ORDER BY seq_no;
OPEN cur_data;
FETCH NEXT FROM cur_data INTO
@v_src_id, @v_parent_id, @v_seq_no, @v_div_cd, @v_imp_dt,
@v_dept_id, @v_user_id, @v_cnt_a, @v_cnt_b, @v_rate, @v_content;
WHILE @@FETCH_STATUS = 0
BEGIN
DECLARE @SEQ_TMP TABLE (val VARCHAR(50));
INSERT INTO @SEQ_TMP EXEC SP_GET_SEQ;
SELECT TOP 1 @v_new_id = val FROM @SEQ_TMP;
INSERT INTO @RESULT_TBL
VALUES (
@v_new_id, @v_src_id, @v_parent_id, @v_seq_no,
@v_div_cd, @v_imp_dt, @v_dept_id, @v_user_id,
@v_cnt_a, @v_cnt_b, @v_rate, @v_content
);
FETCH NEXT FROM cur_data INTO
@v_src_id, @v_parent_id, @v_seq_no, @v_div_cd, @v_imp_dt,
@v_dept_id, @v_user_id, @v_cnt_a, @v_cnt_b, @v_rate, @v_content;
END
CLOSE cur_data;
DEALLOCATE cur_data;
SELECT * FROM @RESULT_TBL ORDER BY seq_no;
조금더 자세하게 설명하자면,
1️⃣ 테이블 변수 (TABLE Variable)
🔹 테이블 변수란?
테이블 변수는 쿼리 또는 프로시저 내부에서만 사용하는 임시 테이블.
DECLARE @변수명 TABLE (...) 형태로 선언하며, 일반 테이블처럼 INSERT, SELECT가 가능.
DECLARE @RESULT_TBL TABLE (
new_id VARCHAR(40),
src_id VARCHAR(40),
parent_id VARCHAR(40),
seq_no INT,
...
);
🔹 이번 로직에서 테이블 변수를 사용한 이유
이 로직에서 @RESULT_TBL은 다음 역할을 한다.
- CURSOR를 통해 처리된 중간 결과를 누적 저장
- 각 행마다 새로 발급된 new_id를 포함한 결과 집합 유지
- 최종적으로 한 번에 결과를 반환
즉,
“행 단위 처리 결과를 모아두는 컨테이너” 역할.
임시 테이블을 쓰지 않은 이유는, 데이터 수가 많지 않기 때문이다.
2️⃣ CURSOR (커서)
🔹 CURSOR란?
CURSOR는 SELECT 결과를 한 행(row)씩 순차적으로 처리하기 위한 제어 구조
DECLARE cur_data CURSOR FOR
SELECT ...
FROM SOURCE_TABLE
🔹 이번 로직에서 CURSOR가 필요한 이유
이 로직의 핵심 요구사항은 다음과 같다.
- 각 행마다
- 새로운 ID를 채번
- 외부 프로시저(SP_GET_SEQ) 호출
- 행마다 서로 다른 결과값 필요
이는 아래와 같은 이유로 집합 처리로 해결하기 어렵다.
- 시퀀스가 단순 증가값이 아님
- 프로시저 호출 결과가 행 단위로 필요
- ROW_NUMBER() 등으로 대체 불가
🔹 CURSOR 처리 흐름
DECLARE → OPEN → FETCH → WHILE → CLOSE → DEALLOCATE
- DECLARE : 커서 정의
- OPEN : 메모리에 결과 적재
- FETCH : 한 행씩 변수에 바인딩
- CLOSE : 결과 집합 닫기
- DEALLOCATE : 메모리 해제
🔹 성능 관점에서의 CURSOR
- ❌ 대량 데이터 (수천~수만 행)
- ⭕ 행 단위 로직이 필요한 경우
- ⭕ 처리량보다 정확성과 명확성이 중요한 경우
이번 페이지 복제 기능은
사용자 액션 기반 + 제한된 행 수라는 점에서 CURSOR 사용이 합리적
3️⃣ FETCH
🔹 FETCH란?
FETCH는 CURSOR에서 현재 위치의 행을 읽어 변수에 할당하는 명령
FETCH NEXT FROM cur_data
INTO @v_src_id, @v_parent_id, ...
FETCH 실행 이후 시스템 변수 @@FETCH_STATUS 값으로 상태를 판단
@@FETCH_STATUS
| 0 | 정상적으로 행을 가져옴 |
| -1 | 더 이상 행 없음 |
| -2 | 행이 삭제됨 |
아래 패턴은 가장 표준적인 커서 반복 구조이다.
WHILE @@FETCH_STATUS = 0
BEGIN
...
END
🔹 FETCH의 역할 정리
- CURSOR 결과를 한 행씩 읽는다
- SQL을 절차적 코드처럼 다룰 수 있게 한다
- 각 행마다
- 시퀀스 채번
- 가공
- INSERT 수행 가능
🔹 마무리
아래 질문에 YES가 많을수록 CURSOR가 합리적
- 행마다 서로 다른 처리가 필요한가?
- 외부 프로시저 호출이 필요한가?
- 데이터 수가 제한적인가?
- 사용자 체감 성능이 중요한가?
- 집합 처리로 구현하면 SQL이 과도하게 복잡해지는가?
마구 남발하면 오히려 느려지니 잘 판단하여 사용하도록 하자
'CS > SQL' 카테고리의 다른 글
| 결재 검증 SQL의 실행 흐름 개선 (EXISTS, Short-Circuit 적용) (0) | 2026.02.03 |
|---|---|
| Index Merge (0) | 2026.01.06 |
| Covering Index (커버링 인덱스) (0) | 2026.01.06 |
| 인덱스 설계 사고방식 2 (0) | 2026.01.06 |
| 인덱스 설계 사고방식 1 (0) | 2026.01.06 |