어느 날 팀원분이 외부 서비스를 사내 서비스와 연동하는 도중에 도움을 요청하셨다.
api호출로 DB에 값을 넣고 외부 서비스에서 사내 서비스 api 호출로 데이터를 조회해보면 불규칙적으로 데이터 조회가 안되는 이슈가 발생했기 때문이었다.
해당 상황을 간략히 정리하면 아래와 같다.
1. A로직을 호출하면 a데이터를 저장한다.
2. 데이터 저장 후 외부 서비스를 HTTP 호출한다.
3. 외부 서버에서는 우리 서버의 B로직을 api 호출해서 a데이터를 조회한다.
4. 불규칙적으로 a데이터가 없다고 조회된다.
나는 원인을 파악하기 위해 우리 서버의 코드부터 확인해보았다.
문제가 된 자바 Spring 의사코드는 아래와 같다
import com.test.repository.DataRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
class AService {
@Autowired
DataRepository repository;
@Transactional
void insertData(Data a) {
repository.insertData(a); //0. 데이터 저장
http.apiCall(a.getUid()); //1. 외부 서버 호출 (해당 api 호출의 응답을 받아야 다음 코드로 넘어감)
//3. 이후의 비즈니스 로직 실행
다른 비즈니스 로직들~~~~
//4. 메소드가 끝나면 트랜잭션에 저장된 DB 변경사항이 DB에 반영
}
//2. 외부 서버에서 api를 통해 아래 메소드 호출
Data selectData(String uid) {
return repository.selectData(uid);
}
}
코드를 검토한 결과, Spring의 트랜잭션 관련 어노테이션(@Transactional)이 원인일 것으로 추정되었다.
외부 서버는 호출 받은 즉시 데이터를 조회한다고 가정했을 때
코드의 흐름을 정리해보면 아래와 같다.
insertData()
호출 후 데이터 저장selectData()
호출insertData()
비즈니스 로직 실행insertData()
메소드가 종료되고 트랜잭션에 저장된 DB 변경사항이 DB에 반영 위처럼 작성된 코드와 가정해둔 외부 서버동작으로 데이터를 조회하면 insertData()에서 저장한 데이터를 조회할 수 없다.
왜냐하면 @Transactional
어노테이션이 적용된 메소드는 메소드가 종료될 때까지 트랜잭션이 유지되며, 이로 인해 트랜잭션 외부에서 조회 시 아직 DB에 데이터가 반영되지 않아 조회가 되지 않기 때문이다.
그림으로 보자면 아래와 같은 상황이다.
하지만 위의 상황은 동료분에게 발생한 상황과 다르다.
위의 상황에선 무조건 조회가 안되지만 동료분은 불규칙적으로 조회가 안된다는 것
그러면 어떻게 해야 재현이 될까?
가정을 바꿔보자, 위에선 외부 서버가 즉시 데이터를 조회했지만
외부 서버가 또다른 외부 서버를 비동기적으로 호출한다면 코드의 흐름은 어떻게 될까?
아래 그림을 보자
아까와 달라진 점은 외부서버
가 외부서버2
를 비동기 호출하여 외부서버2
에서 데이터 조회를 한다는 점이다.
이렇게 되면 그림의 1번(외부서버 호출) 흐름 이후 2번 흐름부터는 거의 동일한 시점에 진행되게 된다.
외부서버
는 외부서버2
를 비동기 호출한다.외부서버
는 외부서버2
비동기 호출 후 1번 대한 응답을 우리서버
에 반환한다.외부서버2
는 우리서버
로 데이터 조회를 요청한다.우리서버
는 insertData()
메소드가 끝나고 트랜잭션이 커밋되어 데이터 저장이 DB에 반영된다.위와 같은 상황이면 3번 흐름에서 경쟁상태(Race Condition) 가 발생할 수 있다.
데이터베이스에서의 경쟁 상태란 여러 사용자나 프로세스가 동시에 같은 데이터에 조회하거나 수정을 시도할 때 데이터의 일관성이 깨질 수 있는 상황을 말한다.
동일한 행위를 했을 때 결과가 매번 달라지는 위와 같은 상황을 예시로 들 수 있다.
나는 동료분과 힘을 합쳐 아래의 해결방안을 생각했다.
@Transactional
어노테이션과 함께 하위 메소드로 내려서 외부서버 api호출전에 트랜잭션을 커밋하여 경쟁상태 해소해주기import com.test.repository.DataRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
class AService {
@Autowired
DataRepository repository;
void insertDataAndCallApi(Data a) {
insertData(a); //트랜잭션을 하위메소드로 내려서 처리
http.apiCall(a.getUid()); //데이터 반영 후 api 호출
다른 비즈니스 로직들~~~~
}
@Transactional
void insertData(Data a) {
repository.insertData(a); //메소드가 끝나면 데이터가 반영 됨
}
// 외부 서버에서 api를 통해 아래 메소드 호출
Data selectData(String uid) {
return repository.selectData(uid);
}
}
첫번째 방법은 기존 서버로직이 광대하여 어려웠고 두번째 방법을 토대로
동료분은 이슈를 원활히 해결하셔서 시간안에 개발을 마칠 수 있었다.
이번 문제를 통해 복잡한 백엔드 개발에서 발생할 수 있는 다양한 문제와
CS지식의 중요성을 다시 한번 느낄 수 있었다.
천천히 꾸준하게 더욱 정진해야겠다.