Develop/Trouble Shooting
API 동시성 문제 개선하기 (MAX + 1 Key 채번)😡
알 수 없는 사용자
2025. 2. 4. 11:33
반응형
결제 API 동시성 이슈..
전자락카 매출정보를 연동하는 API 동시성 문제를 개선해보자!
작업 할 당시에 개발 환경은 jdk 17 , spring boot , mybatis , oracle 이다.
키오스크에서 결제를 하면
VAN 결제정보 데이터를 API를 이용하여 저장하는 단순한 구조이다.
api 응답시간 0.3초
키오스크를 이용하는 손님도 적고 문제가 생길거라고는...미처 생각하지 못했다 😒
이러고 시간이 한참 지나고나서.....
이용자수가 엄청 늘었다...
혹시? 동시성 문제가 생기진 않았을까.. 로그를 보니
중복키 로그가........ 😓
개선하고자
테스트코드를 수정하고 디버깅을 진행 하였다.
문제점 찾기
예시 코드
AS-IS
@Transactional(rollbackFor = Exception.class)
public ResponseEntity<?> payment(paymentDto dto){
// ... 코드
coinLockerMapper.insertA(dto);
coinLockerMapper.insertB(dto);
coinLockerMapper.insertC(dto);
coinLockerMapper.insertD(dto);
return ResponseEntity.ok("성공");
}
INSERT INTO TABLE_A
(
SALE_DATE,
UPJANG_CODE,
UPJANG_SEQ,
SEQ_NO1,
UPDATE_SYS_DATE,
UPDATE_EMP_NO,
UPDATE_IP
)
VALUES(
'영업일자',
'영업장코드',
'영업장순번',
(SELECT NVL(MAX(SEQ_NO1), '0') + 1 FROM TABLE_A
WHERE jm.UPJANG_CODE ='영업장코드'
AND jm.UPJANG_SEQ ='업장순번'
AND jm.SEQ_NO2 = '?'
AND jm.SALE_DATE = '영업일자'
AND jm.MENU_CODE = '특정메뉴코드'),
sysdate,
'손님',
'IP'
)
;
...
...
...
TO-BE
@Transactional(rollbackFor = Exception.class)
public ResponseEntity<?> payment(paymentDto dto){
// ... 코드
coinLockerMapper.insertABCD(dto);
return ResponseEntity.ok("성공");
}
...
V_SEQ_NO1 NUMBER(4,0) := 1;
BEGIN
SELECT (SEQ_NO1 + 1)
INTO V_SEQ_NO1
FROM TABILE_SEQ
WHERE SALE_DATE = P_SALE_DATE
AND UPJANG_CODE = P_UPJANG_CODE
AND TABLE_TYPE = P_TABLE_TYPE
FOR UPDATE WAIT 3;
EXCEPTION
WHEN NO_DATA_FOUND THEN
V_SEQ_NO1 := 1;
END;
INSERT INTO TABLE_A
(
SALE_DATE,
UPJANG_CODE,
UPJANG_SEQ,
SEQ_NO1,
UPDATE_SYS_DATE,
UPDATE_EMP_NO,
UPDATE_IP
)
VALUES(
'영업일자',
'영업장코드',
'영업장순번',
V_SEQ_NO1,
sysdate,
'손님',
'IP'
);
MERGE INTO TABLE_Z target
USING (
SELECT '영업일자' AS SALE_DATE,
'업장코드' AS UPJANG_CODE,
'테이블타입' AS TABLE_TYPE,
sysdate AS UPDATE_SYS_DATE,
'사용자' AS UPDATE_EMP_NO,
'아이디' AS UPDATE_IP
FROM DUAL
) source
ON (target.SALE_DATE = source.SALE_DATE AND target.UPJANG_CODE = source.UPJANG_CODE AND target.TABLE_TYPE = source.TABLE_TYPE )
WHEN MATCHED THEN
UPDATE SET SEQ_NO1 = SEQ_NO1 + 1,
UPDATE_SYS_DATE = SOURCE.UPDATE_SYS_DATE,
UPDATE_EMP_NO = SOURCE.UPDATE_EMP_NO,
UPDATE_IP = SOURCE.UPDATE_IP
WHEN NOT MATCHED THEN
INSERT (SALE_DATE, UPJANG_CODE,UPJANG_SEQ, SEQ_NO1,TABLE_TYPE,UPDATE_SYS_DATE,UPDATE_EMP_NO,UPDATE_IP)
VALUES (source.SALE_DATE, source.UPJANG_CODE,'40', 1,source.TABLE_TYPE,SOURCE.UPDATE_SYS_DATE,SOURCE.UPDATE_EMP_NO,SOURCE.UPDATE_IP);
문제가 되는 건
채번을 시퀀스로 관리하는게 아닌
컬럼 조합에 따라 순번을 max + 1을 이용하여 채번하고 있었기 때문에 발생하였다... 🤔
이용자수가 적으면 문제가 되지 않지만 급증했을 경우에는
api 처리량이 증가함에 따라 unique constraints violation(유일성 제약조건 위배) 발생한다.
해결 방법
1. 시퀀스를 이용
✅ 장점
- 동시성(Concurrency)에 강함
- 원자적(Atomic) 연산으로 여러 트랜잭션이 동시에 시퀀스를 요청해도 충돌 없이 동작.
- SELECT SEQ_NAME.NEXTVAL FROM DUAL; 실행 시 별도의 락(Lock) 없이 안전하게 증가.
- 대량 삽입(INSERT) 시에도 병목 없이 빠른 키 할당 가능.
- 빠른 키 생성 (Full Table Scan 없음)
- 인덱스를 조회하거나 별도의 테이블을 조회할 필요 없이 즉시 새로운 키 반환.
- 채번 테이블을 사용할 경우 발생하는 SELECT MAX(ID) + 1 같은 연산이 필요 없음.
- Rollback 영향을 받지 않음
- 트랜잭션이 ROLLBACK 되더라도 이미 할당된 시퀀스 값은 그대로 유지됨.
- 동일한 값을 두 번 사용하는 일이 없으므로 유니크한 키를 보장.
- 성능 최적화 옵션 제공
- CACHE 옵션을 사용하면 일정 개수의 값을 미리 메모리에 올려두어 DB I/O를 줄이고 성능 향상 가능.
- NOORDER 옵션을 사용하면 RAC 환경에서도 성능을 높일 수 있음.
- 단순한 자동 증가 값이 필요한 경우 최적
- 시퀀스를 사용하면 자동으로 증가하는 값(ID) 생성이 가능하여 별도의 로직이 필요 없음.
- UUID나 복합 키보다 간단하게 고유한 값을 생성할 수 있음.
❌ 단점
- 시퀀스 값이 중간에 건너뛸 수 있음
- CACHE 옵션을 사용하면 DB 장애나 재시작 시 미리 할당된 시퀀스 값이 유실될 수 있음.
- 트랜잭션 롤백 시에도 이미 할당된 시퀀스 값은 되돌려지지 않음.
- 비즈니스 로직에 맞춘 복합 키 생성 불가
- 단순 숫자 증가 방식이라 YEAR + DEPT + SEQ 같은 조합 키를 생성할 수 없음.
- 특정 그룹(부서별, 연도별)로 별도의 시퀀스를 운영해야 할 경우 관리가 복잡해질 수 있음.
- 초기화(Reset) 어려움
- 특정 조건(예: 연도가 바뀔 때마다 1부터 시작)에서 시퀀스를 초기화하려면 DROP 후 다시 생성해야 함.
- 일반적으로 ALTER SEQUENCE로 현재 값을 변경할 수 없으며, 임시 테이블을 활용하는 우회 방법이 필요함.
- 트랜잭션과 독립적이므로 Rollback 시 순차적 증가가 깨질 수 있음
- 예를 들어, INSERT INTO ... VALUES (SEQ.NEXTVAL) 후 ROLLBACK 하면 해당 값이 유실되며 건너뜀.
- 순차적인 숫자가 필요할 경우 시퀀스 대신 채번 테이블을 고려해야 함.
- 일부 데이터베이스(MySQL, SQL Server)에서는 지원되지 않음
- 오라클에서는 기본적으로 지원되지만, 다른 DBMS에서는 AUTO_INCREMENT나 채번 테이블을 사용해야 할 수도 있음.
2. 채번 테이블 만들어서 관리
✅ 장점
- 조합된 키 관리 가능
- 특정 컬럼 조합(예: YEAR + DEPT + SEQ)으로 원하는 방식의 키를 생성할 수 있음.
- 단순한 숫자 증가 방식의 시퀀스로는 불가능한 복합 키를 유연하게 생성 가능.
- 비즈니스 로직 적용 가능
- 특정 조건에 따라 다른 규칙을 적용하여 키 생성 가능.
- 예를 들어, 특정 부서(DEPT)별로 연도별(YEAR) 독립적인 증가값 유지 가능.
- 시퀀스와 다르게 응용 프로그램에서 제어 가능
- 시퀀스는 DB 내부에서 자동 증가하지만, 채번 테이블은 직접 SQL을 통해 제어 가능.
- 특정 조건에서 번호를 다시 초기화하는 등의 유연한 관리 가능.
- 트랜잭션 내에서 조정 가능
- 시퀀스는 트랜잭션과 독립적으로 동작하므로 Rollback 시 복구 불가능하지만,
채번 테이블은 트랜잭션 롤백 시 다시 원래 값으로 되돌릴 수 있음.
- 시퀀스는 트랜잭션과 독립적으로 동작하므로 Rollback 시 복구 불가능하지만,
- 다른 데이터베이스에서도 동일한 방식 사용 가능
- 일부 데이터베이스(예: MySQL)에서는 시퀀스 기능이 제한적이므로,
채번 테이블을 사용하면 DBMS에 상관없이 일관된 방식으로 키를 관리할 수 있음.
- 일부 데이터베이스(예: MySQL)에서는 시퀀스 기능이 제한적이므로,
❌ 단점
- 동시성 이슈 (Locking 필요)
- 다수의 트랜잭션이 동시에 같은 키를 가져가려 하면 충돌 가능.
- 이를 방지하기 위해 SELECT ... FOR UPDATE 또는 LOCK을 사용해야 함.
- 성능이 중요한 환경에서는 병목이 될 가능성이 있음.
- Full Table Scan 발생 가능
- 특정 조건(YEAR + DEPT)의 최신 키 값을 조회하려면,
인덱스가 적절히 설정되지 않으면 Full Table Scan이 발생할 가능성이 있음.
- 특정 조건(YEAR + DEPT)의 최신 키 값을 조회하려면,
- 트랜잭션 롤백 시 키 값 관리 필요
- 시퀀스는 자동 증가하여 롤백해도 번호가 건너뛰지만,
채번 테이블은 롤백 시 값을 직접 관리해야 일관성 유지 가능. - 예를 들어, 트랜잭션 실패 시 증가된 키를 다시 조정해야 함.
- 시퀀스는 자동 증가하여 롤백해도 번호가 건너뛰지만,
- 추가적인 I/O 비용 발생
- 시퀀스는 메모리에서 관리되지만, 채번 테이블은 매번 DB를 조회해야 함.
- 따라서 동시 요청이 많을 경우, 성능 저하 가능성이 있음.
- 이를 방지하려면 미리 일정 개수의 키를 캐싱하는 방식 고려 가능.
- 응용 프로그램에서 추가 로직 필요
- 단순한 자동 증가 ID보다 관리할 로직이 많아짐.
- 키를 가져오는 별도의 서비스/DAO 로직이 필요하며,
복잡한 비즈니스 로직이 얽힐 가능성이 있음.
🔹 결론 (언제 채번 테이블을 선택하면 좋을까?)
✅ 채번 테이블이 적합한 경우
- 컬럼 조합(YEAR + DEPT + SEQ) 기반의 복합 키를 사용해야 할 때
- 비즈니스 로직에 따라 키를 다르게 생성해야 할 때
- 특정 조건에서 시퀀스를 초기화하거나 재사용해야 할 때
- 시퀀스를 지원하지 않는 DB(MySQL 등)에서도 동일한 방식 유지가 필요할 때
❌ 시퀀스가 더 나은 경우
- 단순 숫자 증가 방식이면 시퀀스가 훨씬 빠르고 효율적
- 동시 트랜잭션이 많은 경우 Locking 문제 발생 가능
- 채번 테이블 조회 시 추가적인 I/O 부담이 발생하는 것이 문제될 때
💡 🚀 성능을 개선하려면?
채번 테이블을 쓰더라도 성능을 개선하는 방법을 고려해야 합니다.
- SELECT ... FOR UPDATE를 사용하여 충돌을 방지하면서 최신 값을 가져오기
- 일정 개수(BATCH_SIZE)를 미리 캐싱하여 DB I/O를 줄이기
- 인덱스를 잘 설계하여 Full Table Scan 방지
반응형