모아온 조회 API 성능 개선기 (1) - 700만 건 테스트 데이터 생성

2025. 10. 6. 06:34·개발/기타

들어가며

모아온은 프로젝트와 관련 아티클을 함께 제공하며 인사이트를 전달하는 플랫폼 서비스다. 탐색이 주가 되는 서비스인 만큼 조회 성능이 매우 중요하다.

 

모아온의 기능 구현을 어느 정도 끝낸 후, 조회 성능 개선을 위한 작업들을 시작했다. 개선 과정은 생각보다 녹록치 않았고, 여러 단계를 거쳐 현재도 진행 중이다. 단계별 개선 과정을 글로 남겨보려고 한다.

 

그 중 첫 번째로, 성능 테스트를 위한 데이터베이스 세팅 과정을 정리해본다.

DataFaker, JdbcTemplate, SSH 터널링을 사용해 개발 서버에 700만 건의 데이터를 생성했다.

이 글에서 다루는 내용

  • DataFaker를 이용한 대량 테스트 데이터 생성
  • JdbcTemplate Bulk Insert 성능 최적화 (40배 개선)
  • SSH 터널링을 통한 안전한 원격 DB 접근

예상 독자

Spring Boot와 MySQL을 사용하는 백엔드 개발자


1️⃣ 현실적인 테스트 데이터 설계하기 

실제 운영 서버에는 프로젝트 80개, 아티클 1,000개 정도의 데이터가 있다. 하지만 이 정도 규모로는 실질적인 병목 지점을 파악하기 어려웠다. 성능 이슈가 명확하게 드러날 만큼 충분히 많은 데이터가 필요했다.

 

데이터베이스로 MySQL을 사용 중이고, project, article 두 개의 테이블을 중심으로 다양한 연관 관계 테이블들이 있다. 실제 운영 환경과 최대한 비슷한 비율을 유지하면서 데이터를 생성하기 위해 다음과 같은 전략을 세웠다.

 

데이터 생성 목표

  • project는 10만 건, article은 100만 건 생성
  • tech_stack, category 같은 상수 테이블은 실제 값과 동일하게 유지

실제 비율을 반영한 데이터 분포

  • project는 10~30개의 tech_stack을 가짐
  • project는 1~3개의 category를 가짐
  • article은 0~3개의 tech_stack을 가짐
  • article은 1~3개의 topic을 가짐
  • article의 sector는 50%가 BE, 20%는 FE로 구성

데이터 분포는 생각보다 중요했다. 같은 쿼리라도 데이터가 어떻게 분포되어 있느냐에 따라 성능이 크게 달라지기 때문이다.


2️⃣ DataFaker로 다양한 테스트 데이터 생성하기

데이터베이스 세팅을 위해 모든 팀원이 익숙한 Java 코드를 사용하기로 했다. 프로덕션 코드에 영향을 주지 않도록 테스트 코드 내에서 데이터를 생성하고 DB에 삽입하는 방식으로 구현했다. 테스트 데이터에 랜덤 값을 생성하기 위해서는 DataFaker 라이브러리를 사용했다.

DataFaker란?

DataFaker는 다양한 유형의 랜덤 데이터를 쉽게 생성할 수 있는 라이브러리다. 사용법도 간단하다.

Faker 객체를 생성하고 메서드를 호출하면 랜덤 값이 생성된다. Faker를 생성할 때 Locale 값을 넘겨주면 특정 지역에 특화된 데이터도 만들 수 있다. 숫자, 문자, datetime 같은 자주 사용하는 타입에 대해서는 다양한 편의 메서드를 제공한다.

faker.number().numberBetween(0, 100);// 랜덤 문자 생성
faker.lorem().character();
faker.lorem().word();
faker.lorem().sentence(5);
faker.lorem().paragraph(10);// 랜덤 일시 생성
faker.date().past(10, TimeUnit.DAYS);
faker.date().future(10, TimeUnit.DAYS);

 

또한 특정 도메인에 관련된 재밌는 기능들도 제공한다. aws, baseball, basketball 등등.. 심지어 Kpop에 관련된 기능도 제공한다.. (펄럭)

배치 처리 전략

100만 건의 데이터를 한 번에 생성하면 메모리에 무리가 올 수 있다. 그래서 5,000개씩 나눠서 생성하는 배치 처리 방식을 사용했다.

실제 구현 코드

다음은 article 테이블에 데이터를 추가하는 코드다. 앞 섹션에서 설계한 데이터 분포 전략을 그대로 구현했다.

for (int i = 0; i < ARTICLE_COUNT; i += BATCH_SIZE) {
    List<object[]> articleBatchArgs = new ArrayList<>(); // 다음 단계에서 이 배열을 DB에 삽입한다
    int end = Math.min(i + BATCH_SIZE, ARTICLE_COUNT);
    
    for (int j = i; j < end; j++) {
        // sector 분포: 50% BE, 20% FE, 나머지 랜덤
        String sector =
                (j < ARTICLE_COUNT * 0.5) ? "BE"
                        : (j < ARTICLE_COUNT * 0.7) ? "FE"
                                : sectors[faker.number().numberBetween(0, sectors.length)];
        
        articleBatchArgs.add(new Object[]{
                projectIds.get(faker.number().numberBetween(0, projectIds.size())),
                faker.lorem().sentence(5),
                faker.lorem().sentence(15),
                faker.lorem().paragraph(10),
                "<https://example.com/article/>" + (j + 1),
                faker.number().numberBetween(0, 100_000),
                new Timestamp(faker.date().past(3 * 365, TimeUnit.DAYS).getTime()),
                new Timestamp(faker.date().past(3 * 365, TimeUnit.DAYS).getTime()),
                sector
        });
    }    
}

 

코드를 보면 sector 분포를 의도적으로 조정한 부분이 눈에 띈다. 전체 데이터의 50%는 BE, 20%는 FE로 설정하고, 나머지 30%만 랜덤하게 배정했다. 이렇게 실제 운영 환경의 데이터 분포를 최대한 반영하려고 노력했다.


3️⃣ JdbcTemplate을 이용해 MySQL에 INSERT 하기

앞 섹션에서 만든 데이터를 실제 DB에 삽입해야 한다. Spring 환경이니 JdbcTemplate을 사용했다.

첫 번째 시도: batchUpdate()

가장 먼저 시도한 방법은 JdbcTemplate의 batchUpdate() 메서드였다.

String articleSql = "INSERT INTO article (project_id, title, summary, content, article_url, clicks, created_at, updated_at, sector) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)";
jdbcTemplate.batchUpdate(articleSql, articleBatchArgs);

 

코드는 간결했지만 문제가 있었다. 프로젝트 10,000개, 아티클 10,000개를 삽입하는데 5분이나 걸렸다. 목표인 100만 건을 이 속도로 삽입하면 몇 시간이 걸릴지 알 수 없었다.

 

두 번째 시도: StringBuilder로 Bulk Insert 쿼리 직접 작성

속도를 개선하기 위해 Bulk Insert 쿼리를 직접 만들어보기로 했다. StringBuilder로 VALUES를 여러 개 이어붙인 하나의 큰 INSERT 문을 만들었다.

String sql = "INSERT INTO article (project_id, title, summary, content, article_url, clicks, created_at, updated_at, sector) VALUES ";
StringBuilder sb = new StringBuilder(sql);

for (Object[] batchArg : articleBatchArgs) {
    sb.append("(")
            .append(batchArg[0]).append(", ")
            .append("'").append(batchArg[1]).append("', ")
            .append("'").append(batchArg[2]).append("', ")
            .append("'").append(batchArg[3]).append("', ")
            .append("'").append(batchArg[4]).append("', ")
            .append(batchArg[5]).append(", ")
            .append("'").append(batchArg[6]).append("', ")
            .append("'").append(batchArg[7]).append("', ")
            .append("'").append(batchArg[8]).append("'")
            .append("), ");
}

sb.delete(sb.length() - 2, sb.length());  // 마지막 ", " 제거
sb.append(";");

jdbcTemplate.execute(sb.toString());

 

같은 데이터를 삽입하는데 7초밖에 걸리지 않았다. 40배 이상 빨라진 것이다.

 

속도가 차이나는 이유는?

이 글을 작성하면서 batchUpdate()가 왜 느렸는지 찾아봤다.

원인은 MySQL JDBC 드라이버의 기본 설정 때문이었다. MySQL JDBC 드라이버는 기본적으로 batchUpdate() 요청을 받아도 각 INSERT 문을 하나씩 서버로 보낸다. 즉, 배치가 아니라 사실상 반복문으로 하나씩 실행하는 것과 다름없었다.

 

이 문제는 JDBC 연결 URL에 옵션 하나만 추가하면 해결된다.

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/your_db?rewriteBatchedStatements=true

rewriteBatchedStatements=true 옵션을 켜면 드라이버가 여러 개의 INSERT 문을 하나의 Bulk Insert 쿼리로 자동 변환해준다. batchUpdate()가 가독성도 좋고 SQL Injection도 방지할 수 있어서, 다음에는 이 옵션을 적용해서 시도해볼 생각이다.


4️⃣ SSH 터널링을 이용해 DB 업데이트하기

지금까지 DataFaker로 데이터를 생성하고, JdbcTemplate로 DB에 삽입하는 코드를 완성했다. 이 코드를 어떻게 실행할지가 문제였다.

배포 없이 개발 서버 DB에 접근하기

데이터 생성 코드를 개발 서버에 배포하는 건 비효율적이었다. 한 번만 실행할 코드를 배포하고, 실행하고, 다시 제거하는 과정이 번거로웠다. 로컬에서 테스트를 실행해서 개발 서버의 DB를 직접 업데이트할 수 있다면 훨씬 간편할 것 같았다.

 

하지만 개발 서버는 보안 정책상 MySQL 포트(3306)를 외부에 열어두지 않았다. 인바운드로 열린 포트는 80(HTTP), 443(HTTPS), 22(SSH) 뿐이었다. 이 문제를 해결하기 위해 SSH 터널링을 사용했다.

SSH 터널링 설정하기

SSH 터널링은 SSH 연결을 통해 안전하게 원격 서버의 내부 포트에 접근할 수 있게 해준다. 다음 3단계로 설정했다.

 

1. SSH 터널 연결

로컬의 3307 포트를 EC2 내부의 13306 포트(MySQL)로 연결한다.

# ssh -N -L [로컬 포트]:127.0.0.1:[EC2 내부 DB 포트] [유저]@[EC2 IP] -i [SSH 키]
ssh -N -L 3307:127.0.0.1:13306 ubuntu@15.xxx.xxx.xxx -i key-dev.pem

 

터미널에 아무것도 출력되지 않는 게 정상이다. 연결이 잘 됐는지 확인하려면 다음 명령어를 실행하면 된다.

mysql -u root -p --host 127.0.0.1 --port 3307

 

2. 테스트 코드의 데이터소스 설정 변경

로컬의 3307 포트로 접속하도록 설정을 변경한다.

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3307/moaon
    username: root
  password: root

 

3. 테스트 실행

이제 로컬에서 테스트를 실행하면 SSH 터널을 통해 개발 서버의 DB에 데이터가 삽입된다. 실제 코드는 다음과 같이 작성했다. @Disabled 어노테이션을 달아서 일반 테스트 실행 시 함께 돌아가지 않도록 했고, 필요할 때만 수동으로 실행할 수 있게 했다.

@SpringBootTest
@Disabled
public class DataGeneratorTest {
    
    @Autowired
    private JdbcTemplate jdbcTemplate;
    
    private final Faker faker = new Faker(Locale.KOREAN);
    
    public static final int PROJECT_COUNT = 100_000;
    public static final int ARTICLE_COUNT = 1_000_000;
    public static final int BATCH_SIZE = 5_000;
    
    @Test
    void generateLargeData() {
        // DataFaker로 데이터 생성
        // StringBuilder로 Bulk Insert 쿼리 작성
        // jdbcTemplate.execute()로 실행
    }
}

 

이렇게 로컬 환경에서 안전하게 개발 서버의 데이터를 생성할 수 있었다.


5️⃣ 결과: 700만 건 생성 완료, 그리고 처참한 성능

위 과정을 거쳐 개발 서버에 총 700만 건의 데이터를 생성했다. 소요 시간은 24분, 디스크 사용량은 5.3GB 증가했다.

생성 데이터 현황

각 테이블별 데이터 개수는 다음과 같다.

그리고 드러난 성능 문제

데이터를 추가하고 Postman으로 API 응답 속도를 측정했다. 결과는 정말 처참했다.

프로젝트 도메인 (10만 건)

  • 모든 조회 API의 응답 시간이 5초 이상

아티클 도메인 (100만 건)

  • 기본적으로 수십 초 소요
  • 대부분의 API가 300초 타임아웃 초과
  • 일부 API에서는 Out Of Memory 예외 발생

80개, 1,000개 규모에서는 전혀 문제없던 API들이 10만 건, 100만 건 규모에서는 완전히 무너졌다. 이제 본격적으로 병목 지점을 찾아 개선해야 할 시간이다.

 

다음 글에서는 이런 처참한 성능 문제가 발생한 주요 병목 지점을 분석하고, 어떻게 개선했는지 정리해보겠다.

 

이번 글에서 작성한 코드는 GitHub에서 확인할 수 있다.

👉 DataGeneratorTest.java

 

2025-moaon/backend/src/test/java/moaon/backend/db/DataGeneratorTest.java at faker · woowacourse-teams/2025-moaon

프로젝트를 모아모아, 모아온 📦. Contribute to woowacourse-teams/2025-moaon development by creating an account on GitHub.

github.com

'개발 > 기타' 카테고리의 다른 글

모아온 조회 API 성능 개선기 (3) - 쿼리 튜닝의 한계  (3) 2025.12.01
모아온 조회 API 성능 개선기 (2) - OOM 문제 해결  (0) 2025.11.24
'개발/기타' 카테고리의 다른 글
  • 모아온 조회 API 성능 개선기 (3) - 쿼리 튜닝의 한계
  • 모아온 조회 API 성능 개선기 (2) - OOM 문제 해결
yesjuhee
yesjuhee
Dopamine Driven Developer
  • yesjuhee
    나랑 노랑
    yesjuhee
  • 전체
    오늘
    어제
    • 분류 전체보기 (29)
      • 개발 (11)
        • DevOps (2)
        • Java & Spring (4)
        • AI (1)
        • DB (1)
        • 기타 (3)
      • 후기 or 회고 (15)
        • 우아한테크코스 (11)
        • 기타 (4)
      • 독서 (2)
      • 기타 (1)
      • 초록 스터디 (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    coderabbit
    mysql
    우테코
    레벨4
    초록 스터디
    spring
    바킹독
    초록 밋업
    레벨3
    QueryDSL
    소프티어 부트캠프
    SCG
    모아온
    우아콘
    후기
    독서
    레벨2
    Ai
    claude code
    DispatcherServlet
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
yesjuhee
모아온 조회 API 성능 개선기 (1) - 700만 건 테스트 데이터 생성
상단으로

티스토리툴바