상황 :
저는 최근에 스프링과 마이바티스 구조의 어떤 프로젝트를 맡게 되었습니다.
이 프로젝트에는 온라인 접수 시스템이 있는데,
신청인들의 신상정보와 기타 옵션정보를 번갈아 가며 한번씩 저장하는 단순한 로직의 시스템이었습니다.
이 곳의 온라인 접수는 별로 인기가 없었기 때문에 그동안은 문제가 없었으나
최근에 인기인는 온라인 접수가 늘면서 문제가 발생했다고 합니다.
문제 :
여러 사용자가 동타임에 접수를 시도하면 꼭 접수 실패가 생기고,
아무 정보도 저장되지 않는데 접수가 성공했다고 나온다는 것이었습니다.
그 코드를 살펴보니 아래와 같은 형태였습니다.
//자바
public void 프로세스메서드 () {
...
try{
저장메서드();
}
catch{
(실패페이지로이동)
}
...
(성공페이지로이동)
}
public void 저장메서드() {
...
try{
신청자정보저장();
옵션정보저장();
}catch{
log(저장이 실패되었습니다)
}
...
}
//마이바티스 매퍼
<insert id="신청자정보저장" parameterType="맵">
<selectKey resultType="long" order="BEFORE" keyProperty="id">
SELECT To_Number('0' || Max(id))+1 FROM 신청자정보
</selectKey>
INSERT INTO
...
</insert>
<insert id="옵션정보저장" parameterType="맵">
<selectKey resultType="long" order="BEFORE" keyProperty="id">
SELECT To_Number('0' || Max(id))+1 FROM 옵션정보저장
</selectKey>
INSERT INTO
...
</insert>
해결 :
시스템의 형태를 보면, 상위메서드(프로세스)가 여러 분기를 통해 하위메서드(저장)를 실행시키는 모양이었습니다.
접수에 실패했는데 접수성공 페이지로 들어간다는게 제일 급한 문제이기 때문에 그 부분을 먼저 수정했습니다.
현재 코드에서는 당연하게도 오류페이지로 이동하지 않습니다.
위의 코드 예시에서 저장메서드() 내부에서 신청자정보저장() 또는 옵션정보저장() 실행 중 예외가 발생하면 해당 예외는 저장메서드() 내부의 catch 구문에서 잡힙니다. 이때, 예외가 로그로만 기록되고 있으며, 이 예외를 다시 상위 메서드인 프로세스메서드()로 전파하지 않고 있습니다. 따라서, 프로세스메서드()의 catch 구문에서는 이 예외를 잡을 수 없게 됩니다.
그래서 다음과 같이 수정해줍니다.
public void 저장메서드() {
...
try {
신청자정보저장();
옵션정보저장();
} catch (Exception e) { // 구체적인 예외 타입으로 변경하는 것이 좋습니다.
log("저장이 실패되었습니다");
throw e; // 여기서 예외를 다시 throw하여 상위 메서드로 전파
}
...
}
그리고 이제 동시삽입 문제를 봅니다.
현재의 코드는 마이바티스에서 저장을 원하는 테이블의 max(id)를 하여 그 값을 저장 하려고 시도하고 있습니다.
저도 그렇지만 초보 개발자 분들이 DB삽입 로직을 짜실때 이러한 로직으로 작성하시는 분들이 있을 것 같습니다.
제가 첨부한 코드형태도 그렇고, 변수에 max(id)값을 먼저 조회후 거기에 +1 등을 해서 저장을 시도한다거나 하는.
하지만 당연하게도 이런 형태는 동타임 삽입에 취약할 수 밖에 없습니다 (기본키 제약조건)
저는 이 문제를 해결하고자 시퀀스를 사용한 방법으로 바꿨습니다.
시퀀스를 사용하는 방식이 도움이 되는 이유
1. 고유성 보장: 시퀀스는 데이터베이스에서 고유한 값을 순차적으로 생성하는 객체입니다. 이는 동시에 여러 트랜잭션이 발생하더라도 각 트랜잭션이 고유한 ID를 받게 보장하므로, ID 충돌 문제를 예방할 수 있습니다.
2. 성능 향상: selectKey나 count로 최대 ID를 찾는 방식은 테이블을 조회해야 하므로 오버헤드가 발생할 수 있습니다. 반면, 시퀀스는 메모리에서 값을 생성하기 때문에 이러한 오버헤드가 없으며, 따라서 더 빠른 성능을 제공합니다.
3. 동시성 관리: 시퀀스는 여러 사용자가 동시에 접근하더라도 각각에게 고유한 값을 제공할 수 있으므로, 동시성 문제를 효과적으로 관리할 수 있습니다.
오라클(티베로)에서 시퀀스를 만드는 방법
CREATE SEQUENCE 신청자정보_seq START WITH 1(현재 테이블에 값이 있다면,
이 값은 최대id의 값보다 크게 설정해주는 게 좋습니다)
INCREMENT BY 1;
CREATE SEQUENCE 옵션정보_seq START WITH 1 INCREMENT BY 1;
마이바티스 코드 수정
<insert id="신청자정보저장" parameterType="map">
INSERT INTO 신청자정보
(id, /* 다른 컬럼들 */)
VALUES
(신청자정보_seq.NEXTVAL, /* 다른 컬럼 값들 */)
</insert>
<insert id="옵션정보저장" parameterType="map">
INSERT INTO 옵션정보
(id, /* 다른 컬럼들 */)
VALUES
(옵션정보_seq.NEXTVAL, /* 다른 컬럼 값들 */)
</insert>
이 방식으로, 각 삽입 시 시퀀스에서 다음 값을 가져와 id로 사용함으로써 동시 삽입 시에도 고유한 id를 보장받을 수 있습니다. 이는 데이터베이스가 자동으로 고유한 값을 생성해주기 때문에, <selectKey>를 사용하여 최대 id를 찾는 방식보다 더 효율적이고 안전합니다. 시퀀스 사용은 특히 고성능이 요구되는 환경에서 동시 삽입 문제를 해결하는 데 유용합니다.
후기 :
현재는 이 정도 처리로도 어느정도 요청사항을 만족할 수 있었습니다. (정보저장에 대한 롤백처리도 진행하였음)
참고로 jpa를 이용하신다면 @GeneratedValue 어노테이션을 사용해 ID 생성함으로 동시 삽입에서 발생할 수 있는 ID 충돌 문제를 피할 수 있습니다.
'웹개발 > 오류해결' 카테고리의 다른 글
could not prepare statement [Table ""] (0) | 2024.03.22 |
---|---|
Error executing DDL "create table user" (0) | 2024.03.22 |
Cannot access script base class 'org.gradle.kotlin.dsl.KotlinBuildScript' 해결 (0) | 2023.11.22 |
No primary or default constructor found for interface org.springframework.data.domain.Pageable 오류해결 (0) | 2023.03.15 |
using constructor NO_CONSTRUCTOR with arguments 오류해결 (0) | 2023.03.03 |