Skip to content
isdnetworks
Go back

보상은 한 번만 줘야 했다

앱에서 조건을 채우면 보상을 주는 기능을 맡았다. 조건을 확인하고 지급하고 기록하는 세 단계로 짰는데, 시험하다가 빠르게 두 번 누르니 보상이 두 번 들어갔다.

Table of contents

Open Table of contents

두 번 들어갔다

코드를 보니 확인과 기록 사이에 시간이 있었다. 두 요청이 거의 동시에 오면 둘 다 확인 단계를 통과한다.

요청 A: 확인 (없음) →
요청 B: 확인 (없음) →         둘 다 통과
요청 A: 지급·기록
요청 B: 지급·기록

확인한 시점에는 없었지만 지급하는 시점에는 이미 있을 수 있다는 것을 그때 처음 생각해 봤다.

화면에서 막으면 화면에서만 막힌다

먼저 화면에서 버튼을 한 번 누르면 잠기게 했다. 화면에서는 안 눌린다.

그런데 앱을 껐다 켜거나 요청을 직접 보내면 여전히 된다. 화면에서 막는 것은 화면을 거치는 경우에만 막는 것이었다. 뒤에서 막아야 했다.

순서를 바꿔서 기록을 먼저 하고 지급하게 해 봤는데 여전히 두 번 됐다. 확인과 기록 사이의 틈은 그대로이기 때문이다. 순서를 바꿔도 틈은 남았다.

DB가 막게 했다

찾아보니 중복을 MySQL 이 거부하게 하는 방법이 있었다. 사용자와 보상 조합에 UNIQUE 를 걸면 같은 조합이 두 번 안 들어간다.

ALTER TABLE reward_log ADD UNIQUE (user_id, reward_id);

이러니 확인 단계가 필요 없어졌다. 넣어 보고 실패하면 이미 준 것이다.

실패는 오류 번호로 가려냈다. MySQL 은 1062 를 내고 SQLState 는 23000 이다. iBatis 는 그것을 SQLException 으로 올려 준다. Connector/J 를 직접 쓰면 MySQLIntegrityConstraintViolationException 이 온다.

try {
    기록(userId, rewardId);   // 실패하면 이미 준 것
    지급(userId);
} catch (SQLException e) {
    if (e.getErrorCode() == 1062) return;   // 이미 줌
    throw e;
}

오류 번호를 안 보고 전부 삼키면 다른 오류까지 「이미 줌」이 된다. 그래서 1062 만 걸러 냈다.

왜 이게 되는지 생각해 보니 판정 시점이 다르기 때문이었다. 코드에서 확인하면 확인과 쓰기 사이에 틈이 생기는데, MySQL 의 UNIQUE 는 쓰는 그 순간에 판정하니 틈이 생길 자리가 없다.

다만 기록을 먼저 해야 한다. 지급을 먼저 하고 기록이 실패하면 준 것이 기록에 안 남고, 기록을 먼저 하고 지급이 실패하면 기록만 있고 안 준 상태가 된다. 둘 다 문제인데 앞쪽은 두 번 줄 수 있고 뒤쪽은 덜 준 것이다. 돈이 나가는 쪽이라 덜 주는 실수를 골랐다. 안 준 것은 문의가 오면 확인해서 줄 수 있지만 두 번 준 것은 회수가 어렵다.

외부 호출과 재시도

기록과 지급이 갈리는 것 자체가 안 좋아서 트랜잭션으로 묶었다. iBatis 는 startTransaction 으로 열고 commitTransaction 으로 닫는다. endTransactionfinally 에 두지 않으면 연결이 안 돌아온다.

묶기 전에 SHOW TABLE STATUS 로 엔진부터 봤다. reward_log 가 MyISAM 이면 트랜잭션을 열어도 오류 없이 그냥 무시된다. UNIQUE 는 MyISAM 에서도 듣지만 되돌리기는 안 된다. InnoDB 로 바꾸고 나서 묶었다.

다만 지급이 외부 시스템 호출이면 못 묶는다. InnoDB 안의 일은 같이 되돌릴 수 있지만 밖으로 나간 호출은 되돌릴 수 없다. 그런 경우는 기록만 먼저 커밋하고 외부 호출은 따로 하되 실패하면 재시도할 수 있게 상태를 남겼다.

여기서 또 걸린 것이 있다. 외부 호출이 성공했는데 응답을 받기 전에 끊기면 상태가 대기로 남고, 재시도하면 두 번 나갈 수 있다. 외부 시스템에 우리 기록 번호를 같이 넘겨서 같은 번호로 두 번 오면 저쪽이 한 번만 처리하게 했다. 우리 쪽에서만 막아서는 완전해지지 않는다.

정리


Share this post on:

Previous Post
같은 주소가 상태마다 다른 일을 했다
Next Post
지워도 되는지 아무도 몰랐다