서비스를 개발하다 보면 종종 DB의 PK 값을 직접 생성해야 하는 상황이 생긴다. 예를 들어 오라클에서는 sequence.nextval을 통해 유일한 값을 뽑아 쓰는 방식이 흔하다. MyBatis에서도 이런 요구를 처리하기 위해 <selectKey> 태그를 제공한다. 하지만 사용량이 많아질수록 selectKey 방식은 심각한 문제를 일으킬 수 있다는 사실을 경험하면서, 이 글을 정리해 본다.
🔧 selectKey란?
MyBatis의 <selectKey>는 insert 쿼리 실행 전에 별도의 쿼리를 먼저 실행해, 생성된 키 값을 객체에 세팅하는 방식이다. 예를 들면 다음과 같다.
<insert id="insertUser" parameterType="User">
<selectKey keyProperty="id" resultType="long" order="BEFORE">
SELECT USER_SEQ.NEXTVAL FROM DUAL
</selectKey>
INSERT INTO USERS (ID, NAME, EMAIL)
VALUES (#{id}, #{name}, #{email})
</insert>
이 코드를 보면 insert 전에 먼저 시퀀스를 조회해서 객체의 id에 넣고, 그 값을 insert에 사용한다. 얼핏 보면 깔끔하고 실용적이지만, 실제 트래픽이 많아지는 서비스에서는 예상치 못한 문제가 생긴다.
❗ 왜 selectKey를 지양해야 할까?
1. 동시성 이슈: 중복 채번 가능성
selectKey는 트랜잭션 내에서 실행되는 쿼리가 아니다. 즉, selectKey → insert가 하나의 트랜잭션으로 묶이지 않기 때문에, 다중 스레드 환경에서 selectKey로 채번 한 값이 아직 insert 되기 전에 다른 트랜잭션이 동일한 selectKey를 수행할 수 있다.
이런 상황이 반복되면 PK 충돌 에러가 발생하거나, 예상과 다른 값이 insert 되는 현상이 생길 수 있다. 특히 insert 쿼리 자체가 실패할 경우, selectKey로 뽑은 시퀀스 값은 그냥 날아가고 gap이 생기게 된다.
실무에서는 장애 원인이 이런 selectKey 충돌로 밝혀지는 경우도 종종 있다.
2. 멀티 DB 환경에서의 비일관성
selectKey는 DBMS 의존적인 로직이다. Oracle의 DUAL 테이블이나 NEXTVAL 같은 문법은 다른 DBMS와 호환되지 않는다. 시스템이 커지고 마이크로서비스나 DBMS 이중화 환경으로 넘어갈수록, selectKey 방식은 유지보수성을 심각하게 해친다.
3. 대체 가능한 방식이 존재한다
현대의 대부분 DB에서는 auto-increment 또는 identity column을 제공하고, MyBatis에서도 useGeneratedKeys="true"로 처리할 수 있다. 아래와 같이 작성하면 DB가 PK를 자동으로 생성하고, 그 값을 객체에 자동으로 매핑해 준다.
<insert id="insertUser" parameterType="User" useGeneratedKeys="true" keyProperty="id">
INSERT INTO USERS (NAME, EMAIL)
VALUES (#{name}, #{email})
</insert>
이 방식은 트랜잭션 안에서 안정적으로 처리되며, 중복 채번이나 gap 문제를 피할 수 있다.
✅ 결론
MyBatis에서 제공하는 selectKey는 간편한 기능처럼 보이지만, 실무에서는 지양해야 할 방식이다. 특히 서비스 규모가 커질수록 데이터 무결성과 시스템 안정성에 치명적인 영향을 줄 수 있다. 가능한 한 DB의 자동 생성 기능을 활용하고, 비즈니스 로직에서 직접 채번이 필요한 경우에도 트랜잭션 단위로 처리할 수 있는 로직을 구성해야 한다.
'개발 > 좋은 개발 습관' 카테고리의 다른 글
순환참조(Circular Reference)란? 개념부터 해결 방법까지 (0) | 2025.05.12 |
---|---|
데이터 비교는 어떻게 해야 할까? – 성능을 살리는 DB 쿼리 작성법 (0) | 2025.05.08 |