Cheondi
개발 · 알고리즘·CS

첫 번째 계좌만 불러오던 버그

여러 계좌를 초기화하는 비동기 반복문이 첫 항목에서 끝난 문제를 작업 단위로 정리한 기록입니다.

  • #async
  • #collection
  • #debugging

로그인 뒤 계좌가 여러 개인데 첫 번째 계좌 데이터만 화면에 나타나는 문제가 있었다. 목록 API는 모든 계좌를 반환했고 UI에도 탭은 만들어졌다. 첫 탭을 누르면 정상, 나머지는 로딩 상태에 머물렀다. 초기화 반복문 안에서 첫 비동기 호출 이후 함수가 종료되고 있었다.

단순한 return 한 줄이 문제였지만 왜 그 코드가 들어갔고 다시 생기지 않게 할지가 더 중요했다.

반복문의 종료 조건을 읽었다

첫 계좌의 데이터가 없으면 함수를 끝내려던 코드가 전체 초기화 메서드의 return으로 작성돼 있었다.

foreach (var account in accounts)
{
    var data = await LoadAsync(account.Id);
    if (data == null) return; // 나머지 계좌도 중단
    store.Set(account.Id, data);
}

해당 계좌만 건너뛸 상황이라면 continue, 전체를 멈춰야 한다면 명확한 실패 결과가 필요했다.

실패 단위를 계좌별로 만들었다

한 계좌 조회가 실패했다고 다른 계좌까지 보이지 않는 건 사용자 경험에 맞지 않았다. 결과를 계좌별 성공과 실패로 모았다.

var tasks = accounts.Select(async account =>
{
    try { return AccountLoad.Success(account.Id, await LoadAsync(account.Id)); }
    catch (Exception ex) { return AccountLoad.Failed(account.Id, Classify(ex)); }
});

var results = await Task.WhenAll(tasks);

동시 요청 수 제한이 필요하면 무작정 전부 실행하지 않고 작은 배치나 semaphore를 사용했다.

화면도 부분 성공을 표현했다

성공한 계좌 탭은 바로 사용하고 실패한 탭에는 재시도 상태를 표시했다. 전체 로딩 스피너 하나로는 일부 실패를 설명할 수 없었다. 권한 없음과 일시 오류도 구분했다.

로그에는 전체 계좌 수, 성공 수, 실패 분류를 남기고 실제 계좌 식별값은 가렸다. 첫 항목만 처리됐다는 사실을 개수 비교로 빠르게 발견할 수 있었다.

컬렉션 코드는 개수로 검증했다

첫 데이터가 정상이라 화면이 얼핏 완성돼 보여 버그가 늦게 발견됐다. 계좌 1개 테스트만으로는 반복 처리 문제를 볼 수 없었다. 0개, 1개, 여러 개, 중간 항목 실패를 시나리오로 추가했다.

이후 컬렉션 초기화 코드를 수정하면 첫 항목의 값뿐 아니라 입력 개수와 결과 개수를 확인한다. 작은 return 문제를 통해 실패 범위와 부분 성공을 설계하는 법을 배웠다.