async/await을 통한 비동기 loop 처리 하기 (feat. 테스팅)

필영·2021년 1월 28일
0

최근 코로나 확진자 수 정보를 받아오는 크롤러 코드를 작성하였다.
로컬 환경에서는 별 문제 없이 작동하고 구상했던대로 동작했기 때문에 문제없는 코드라고 판단하였다.
따라서 테스트 코드 작성하고 테스트를 실행 함에 있어서 문제 없겠구나 생각하였는데, 테스트는 실패하였다.

처음엔 테스트 실패의 이유를 도통 이해할 수 없어, 처음으로 StackOverflow에 질문까지 올리게 되었다. StackOverflow 질문 글

크롤러 코드

export const getCoronaData = async () => {
  try {
    // ... 크롤링 코드 생략
    
    // fake 데이터 추가
    const covidStatus = [
      {
        city: 'city',
        totalCases: '100',
        increasedCases: '100',
        date: 'time',
      },
    ];
    const covidRepository = getRepository(Covid);
    covidStatus.forEach(async (status) => {
      const exist = await covidRepository.findOne({
        where: { city: status.city },
      });
      if (!exist) {
        // expecting save() method at this part to be called, but it fails
        return await covidRepository.save(
          covidRepository.create({
            city: status.city,
            totalCases: status.totalCases,
            increasedCases: status.increasedCases,
            date: status.time,
          })
        );
      } else {
        return await covidRepository.save([
          {
            id: exist.id,
            totalCases: status.totalCases,
            increasedCases: status.increasedCases,
            date: status.time,
          },
        ]);
      }
    });
  } catch (err) {
    console.error(err);
    return;
  }
};

코로나 확진자 수 정보를 크롤링 하여, covidStatus 배열에 담아, 순차적으로 DB에 저장 혹은 수정을 반영하는 코드이다.

테스트 코드

describe('getCoronaData', () => {
  it('should get a Covid Status data and return it', async () => {
    typeorm.getRepository = jest.fn().mockReturnValue({
      findOne: jest.fn().mockResolvedValue(null),
      create: jest.fn(),
      save: jest.fn(),
    });

    await getCoronaData();

    expect(typeorm.getRepository).toHaveBeenCalledWith(Covid);
    expect(typeorm.getRepository(Covid).findOne).toHaveBeenCalled();
    expect(typeorm.getRepository(Covid).save).toHaveBeenCalled(); // this expect result is making an error
                                                                  // Expected number of calls: >= 1
                                                                  // Received number of calls:    0
  });
});

테스트 실행 시, 테스트는 실패한다.
이유는 expect(typeorm.getRepository(Covid).save).toHaveBeenCalled(); 부분이 호출 되지 않고 있다고 한다.

"음 ? 분명 실제로 잘 동작하던 코드인데 왜 안되지?"

코드를 뜯어 여러 시도를 해본 결과, 테스트 실패의 원인은 forEach()부분이었다.

  1. async 함수는 프로미스를 return 한다.
  2. 반면 forEach()는 return 값이 존재 하지 않는다.
  3. 그에 따라 forEach()에서의 async 콜백은 await가 보장되지 않는다고 한다.
    이전 루프에 대한 결과 여부와 상관없이 로직들이 실행 된다.

즉, forEach()문 내의 로직들의 대한 처리가 return 됨을 기다려 주지 않고 다음 로직들에 대한 처리가 이루어진다.

이러한 이유로 Jestsave() 메서드에 대한 호출을 인지 할 수 없던 것이다.


문제의 원인은 파악하였고, 따라서 잘못 사용 하고 있던 forEach() 부분들에 대한 수정을 진행하였다.

  1. Promise.all()을 활용하는 방법
    await Promise.all(
      covidStatus.map(async (status) => {
        const exist = await covidRepository.findOne({
          where: { city: status.city },
        });
        if (!exist) {
          const newData = covidRepository.create({
            city: status.city,
            totalCases: status.totalCases,
            increasedCases: status.increasedCases,
            date: $standardTime,
          });
          return covidRepository.save(newData);
        } else {
          return covidRepository.save([
            {
              id: exist.id,
              totalCases: status.totalCases,
              increasedCases: status.increasedCases,
              date: $standardTime,
            },
          ]);
        }
      })
    );
  1. for ... of를 활용하는 방법
    for (const status of covidStatus) {
      const exist = await covidRepository.findOne({
        where: { city: status.city },
      });
      if (!exist) {
        const newData = covidRepository.create({
          city: status.city,
          totalCases: status.totalCases,
          increasedCases: status.increasedCases,
          date: $standardTime,
        });
        return covidRepository.save(newData);
      } else {
        return covidRepository.save([
          {
            id: exist.id,
            totalCases: status.totalCases,
            increasedCases: status.increasedCases,
            date: $standardTime,
          },
        ]);
      }
    }

이 두가지 방법도 차이점이 있는데, Promise.all()은 내부 로직에 대한 처리가 병렬로 이루어져 순차적 동작하지 않는다.
반면 for ... of문에서는 순차적으로 처리가 이루어 진다는 점이다.


참고

https://stackoverflow.com/questions/54751181/using-foreach-and-async-await-behaves-different-for-node-and-jest

https://stackoverflow.com/questions/37576685/using-async-await-with-a-foreach-loop

0개의 댓글