목록으로

Programming Notes

Car-ffeine, 23만 잔의 데이터를 담는 여정: 데이터 저장 최적화 일지

안녕하세요! 우테코 Car-ffeine 팀의 제이입니다. 저희 서비스는 사용자들에게 최고의 카페 경험을 제공하기 위해 끊임없이 노력하고 있습니다. 그 노력의 일환으로, 방대한 양의 카페 데이터를 수집하고 관리하는 것이 필수적이죠. 특히 공공 API를 통해 얻는 정보는 저희...

안녕하세요! 우테코 Car-ffeine 팀의 제이입니다. 저희 서비스는 사용자들에게 최고의 카페 경험을 제공하기 위해 끊임없이 노력하고 있습니다. 그 노력의 일환으로, 방대한 양의 카페 데이터를 수집하고 관리하는 것이 필수적이죠. 특히 공공 API를 통해 얻는 정보는 저희 서비스의 핵심 자산입니다.

하지만, API에서 한 번에 받아올 수 있는 데이터 양은 제한적입니다. 페이지당 최대 9,999개의 데이터를 제공받을 수 있어서, 약 23만 건의 데이터를 확보하려면 23번이나 API 호출을 반복해야 했습니다. 처음에는 단순히 데이터를 받아 즉시 DB에 저장하는 방식으로 구현했었는데, 예상대로 속도가 너무 느렸습니다. 마치 에스프레소를 한 방울씩 추출하는 것처럼 답답했죠.

저희 팀(누누, 박스터, 키아라, 그리고 저 제이)은 이 문제를 해결하기 위해 머리를 맞대고 고민했습니다. 단순히 '빨리' 저장하는 것을 넘어, 효율적이고 안정적인 데이터 저장 파이프라인을 구축하는 것을 목표로 삼았습니다.

느림의 미학은 없다: 데이터 저장 속도 개선 프로젝트

본격적인 최적화 작업에 들어가기 전에, 먼저 기존 방식의 문제점을 명확히 파악해야 했습니다. 프로파일링 도구를 사용하여 분석한 결과, 병목 지점은 크게 두 가지였습니다.

  • 잦은 DB 연결 및 트랜잭션: 데이터를 한 건씩 DB에 삽입하면서 매번 연결을 열고 닫는 과정이 과도한 오버헤드를 발생시켰습니다. 마치 카페에 손님이 한 명 들어올 때마다 문을 활짝 열었다 닫는 것과 같았습니다.
  • 단순한 삽입 쿼리의 반복: 각 데이터 레코드를 개별적으로 삽입하는 방식은 DB 서버에 부담을 주었고, 전체 처리 시간을 늘리는 주범이었습니다.

이러한 문제점을 해결하기 위해, 저희는 다음과 같은 전략을 세웠습니다.

1. 벌크 연산의 도입:

가장 먼저 적용한 방법은 벌크 연산입니다. 데이터를 하나씩 삽입하는 대신, 일정량의 데이터를 묶어서 한 번에 DB에 전달하는 방식입니다. 예를 들어, JDBC의 PreparedStatement를 사용하여 여러 개의 삽입 쿼리를 하나의 배치로 만들어 실행했습니다.

// PreparedStatement를 사용하여 벌크 삽입
String sql = "INSERT INTO cafes (name, address, latitude, longitude) VALUES (?, ?, ?, ?)";
try (Connection connection = dataSource.getConnection();
     PreparedStatement preparedStatement = connection.prepareStatement(sql)) {

    connection.setAutoCommit(false); // 트랜잭션 시작

    for (Cafe cafe : cafeList) {
        preparedStatement.setString(1, cafe.getName());
        preparedStatement.setString(2, cafe.getAddress());
        preparedStatement.setDouble(3, cafe.getLatitude());
        preparedStatement.setDouble(4, cafe.getLongitude());
        preparedStatement.addBatch();
    }

    preparedStatement.executeBatch();
    connection.commit(); // 트랜잭션 커밋

} catch (SQLException e) {
    connection.rollback(); // 트랜잭션 롤백
    // 예외 처리
}

이렇게 벌크 연산을 적용하자, DB 연결 횟수를 줄이고 쿼리 실행 횟수를 획기적으로 감소시킬 수 있었습니다. 마치 여러 잔의 커피를 한 번에 내리는 것과 같은 효과였죠.

2. Connection Pool 활용:

DB 연결을 재사용하기 위해 Connection Pool을 도입했습니다. Apache Commons DBCP나 HikariCP와 같은 라이브러리를 사용하여 Connection Pool을 구성하면, DB 연결을 미리 생성해두고 필요할 때마다 가져다 쓸 수 있습니다. 이렇게 하면, 매번 새로운 연결을 생성하는 데 드는 비용을 줄일 수 있습니다.

3. 비동기 처리 도입:

API 호출과 데이터 저장 작업을 분리하기 위해 비동기 처리를 도입했습니다. Spring의 @Async 어노테이션을 사용하여 API 호출 결과를 별도의 스레드에서 처리하도록 했습니다. 이렇게 하면, API 호출이 완료될 때까지 기다리지 않고 다음 API 호출을 바로 시작할 수 있어 전체 처리 시간을 단축할 수 있습니다.

4. 데이터 정합성 검증:

데이터를 저장하기 전에 필수적인 데이터 정합성 검증 과정을 추가했습니다. API에서 받아온 데이터 중 누락된 정보가 있거나 형식에 맞지 않는 데이터는 사전에 필터링하여 DB에 저장되는 데이터의 품질을 높였습니다.

5. 인덱스 최적화:

자주 사용되는 검색 조건에 대한 인덱스를 추가하고, 불필요한 인덱스를 제거하여 쿼리 성능을 향상시켰습니다. 마치 카페 메뉴판을 효율적으로 정리하여 손님들이 원하는 메뉴를 빠르게 찾을 수 있도록 하는 것과 같았습니다.

이러한 노력들을 통해, 저희는 데이터 저장 속도를 눈에 띄게 향상시킬 수 있었습니다. 초기에는 23만 건의 데이터를 저장하는 데 몇 시간이 걸렸지만, 최적화 후에는 몇 분 만에 완료할 수 있게 되었습니다.

Car-ffeine, 더 맛있는 커피를 향하여

이번 데이터 저장 최적화 프로젝트를 통해, 저희는 단순히 속도를 높이는 것을 넘어 효율적인 데이터 관리의 중요성을 깨달았습니다. 또한, 팀원들과 함께 문제를 분석하고 해결하는 과정에서 협업 능력을 향상시킬 수 있었습니다.

물론, 아직 개선해야 할 부분은 남아있습니다. 앞으로도 지속적인 모니터링과 프로파일링을 통해 성능을 개선하고, 더 나아가 데이터 파이프라인의 안정성과 확장성을 확보하기 위해 노력할 것입니다.

저희 Car-ffeine 팀은 앞으로도 사용자들에게 최고의 카페 경험을 제공하기 위해 끊임없이 배우고 성장하며, 더 맛있는 커피를 만들어 나가겠습니다. 감사합니다!