이전 시간에서 랭킹 차트를 구현하면서 쿼리문을 최적화하는 방법을 고민해 보았다. 그렇게 해서 완성된 코드는 각 과목별로 first score, last score 를 각각 두 번 요청하는 방식으로 구성되었다. NCA 를 만들 때 까지는 나쁘지 않아 보였는데… NCP 구현에 들어가면서 Axios 요청을 좀 더 줄일 수 있지 않나? 하는 생각이 들었다. 웹 사이트를 최적화 함에 있어서 HTTP 요청 최소화는 기본 중의 기본이다. 자 지금부터 NCA 때 구현했던 코드를 리팩토링 해보자 !

1️⃣ 기존 코드 분석

🔯 FrontEnd

아래 코드가 문제의 Axios 호출 부분이다. getRankingData 메서드 내부에서 if 문으로 tableName 에 따라 Axios 호출을 분기하고 있는 것처럼 보이지만, 실상은 렌더링 될 때 마다 두 테이블에서 데이터를 전부 가져와야 한다. 다시 말해 분기할 필요가 전혀 없는 로직이다…

const [firstData, setFirstData] = useState([]);
const [lastData, setLastData] = useState([]);
const table = ["first_score", "last_score"];

useEffect(() => {
  if (firstData.length === 0) {
    getRankingData(table[0]);
    getRankingData(table[1]);
  }
}, []);

const getRankingData = async (tableName) => {
  if (tableName === "first_score") {
    const response = await axios
      .post(`/ranking/v2`, {
        title: "NCA",
        table: tableName,
      })
      .catch((err) => {
        console.log(err);
      });

    setFirstData(response.data);
  } else {
    const response = await axios
      .post(`/ranking/v2`, {
        title: "NCA",
        table: tableName,
      })
      .catch((err) => {
        console.log(err);
      });

    setLastData(response.data);
  }
};

☕️ BackEnd

단순한 Read 작업으로, 프론트에서 title 과 table 정보를 받아서 조회하는 코드

// 컨트롤러
@GetMapping("/ranking/v2")
public ResponseEntity<List<RankDTO>> getRankingV2(@RequestBody RankDTO rankDTO) {
    if(rankDTO == null || rankDTO.getTitle() == null || rankDTO.getTable() == null) {
        throw new CustomException(
                "title 및 table 정보는 필수입니다.",
                "INVALID_RANK_INFO",
                HttpStatus.BAD_REQUEST
        );
    }
    return ResponseEntity.ok(rankService.getRankingV2(rankDTO));
}

// 서비스
public List<RankDTO> getRankingV2(RankDTO rankDTO) {
    return rankMapper.findTop5V2(rankDTO);
}

2️⃣ 리팩토링

리팩토링 작업은 생각 외로 간단했다. 프론트에서는 중복 코드를 삭제했고, 백에서는 필요한 데이터를 한 번에 리턴하도록 바꿔주었다.

추가로, NCA 와 달리 다른 과목들은 ‘과목 코드’ 라는 것이 있어서 과목별로 랭킹을 집계해야 한다. 이 부분도 한 번의 통신으로 모든 데이터를 가져올 수 있도록 리팩토링 했다.

🔯 FrontEnd

  • 프론트에서는 랭킹을 조회할 과목이 무엇인지만 전달한다. (NCA or NCP)
useEffect(() => {
  if (rankingData.length === 0) {
    getRankingData();
  }
}, []);

const getRankingData = async () => {
  const response = await axios
    .post(`/ranking/v2`, {
      title: "NCA",
    })
    .catch((err) => {
      console.log(err);
    });

  console.log(response.data);
  setRankingData(response.data);
};

☕️ BackEnd

  • 프론트에서 요청이 들어오면 first_score, last_score 두 개의 테이블에서 랭킹 데이터를 조회하는 로직을 수행한다.

  • NCP 의 경우, ‘과목 코드’ 별로 first_score, last_score 를 조회해야 하므로, 서비스 단계에서 이를 분기했다.

  • 추가로, 데이터를 조회하는 로직을 private 메서드로 따로 정의하여 코드를 간결하게 정리했다.

// 컨트롤러
@PostMapping("/ranking/v2")
public ResponseEntity<Map<String, List>> getRankingV2(@RequestBody RankDTO rankDTO) {
    if(rankDTO == null || rankDTO.getTitle() == null) {
        throw new CustomException(
                "title 정보는 필수입니다.",
                "INVALID_RANK_INFO",
                HttpStatus.BAD_REQUEST
        );
    }
    return ResponseEntity.ok(rankService.getRankingV2(rankDTO));
}

// 서비스
@Service
@RequiredArgsConstructor
@Slf4j
public class RankService {

    private final RankMapper rankMapper;
    private Map<String, List> results = new ConcurrentHashMap<>();

    /**
     * 랭킹 조회 메서드
     */
    private void findRanker(RankDTO rankDTO) {
        // 1회차 랭킹 조회
        rankDTO.setTable("first_score");
        List<RankDTO> firstDTO = rankMapper.findTop5V2(rankDTO);

        // 다회차 랭킹 조회
        rankDTO.setTable("last_score");
        List<RankDTO> lastDTO = rankMapper.findTop5V2(rankDTO);

        // 리턴값 세팅
        String first = rankDTO.getTitle() + "first"; // EX) NCAfirst, NCP200first
        String last = rankDTO.getTitle() + "last"; // EX) NCAlast, NCP200last
        results.put(first, firstDTO);
        results.put(last, lastDTO);
    }

    /**
     * V2 : join 쿼리로 한번에 검색
     */
    public Map<String, List> getRankingV2(RankDTO rankDTO) {
        String title = rankDTO.getTitle();
        log.info("Ranking title: {}", title);

        if(title.equals("NCA")) {
            findRanker(rankDTO);
        }

        if(title.equals("NCP")) {
            // NCP200
            rankDTO.setTitle("NCP200");
            findRanker(rankDTO);

            // NCP202
            rankDTO.setTitle("NCP202");
            findRanker(rankDTO);

            // NCP207
            rankDTO.setTitle("NCP207");
            findRanker(rankDTO);
        }

        return results;
    }

}

}

🤓 느낀 점

  • 과거의 나는 바보였나? 어떻게든 화면을 구현하는 것에만 급급하다 보니 일단 화면에 잘 보이기만 하면 자잘한 것은 넘어가 버리는 안일주의 부실공사가 불편한 코드를 만든 것 같다.

  • 최적화를 고민하자 ! 개발을 하면서 굳어지는 생각 하나. 단순히 코드만 짤 수 있다면 코더와 다를 바 없다. 효율, 성능 측면에서 더 나은 로직을 구현할 수 없을까 고민하는 과정이 즐겁다.