본문 바로가기
프로젝트 관련

스프링부트 With Mysql - easyRandom을 통한 bulk Insert 및 Index 적용

by khds 2023. 8. 3.

 

이 글은 개인적인 생각을 작성한 것이다.

 

들어가기

 

데이터베이스에 대해 공부하다 보면 마주치는 것 중 인덱스라는 것을 들어보았을 것이다. 필자 또한 인덱스라는 개념을 책 및 인터넷을 통해 많이 접하았다. 쿼리 최적화에 대해 공부하면서 인덱스 또한 공부도 하였다. 하지만 인덱스를 통해 충분한 성능을 보기 위해서는 충분한 데이터가 있어야 한다고 들었다. 그래서 데이터가 많지 않으니까 다음에 적용하자~! 라는 핑계로 계속 인덱스를 직접 적용해 보는 것을 미뤘었다. 

그러던 중 최근에 EasyRandom을 통한 랜덤한 객체를 수백만 건을 생성해 주는 방법을 접하게 되었다.

지금까지 진행하던 테스트에서는 일일이 값을 집어넣어서 객체를 생성하고 테스트를 진행하였다. EasyRandom은 이런 나에게 매우 매혹적으로 보였고 테스트 코드로서 활용해 보자고 생각하게 되었다. 그리고 수백만 건의 데이터를 생성하는 것은 곧, 인덱스를 사용해 보기에 적기라고 생각하여 인덱스 또한 적용해 보기로 하였다.

마침 지금 진행하는 프로젝트 중 데이터를 한번에 수백만 건 이상 조회할 가능성이 있는 쿼리가 존재하여 이곳에 적용해보고자 한다. 쿼리에 대한 자세한 내용은 아래 인덱스를 다룰 때 작성하겠다.

참고로 필자는 springboot, mybatis, mysql을 사용하였다.

 

 

EasyRandom을 통해 BulkInsert를 해보자!

 

필자는 프로젝트를 하면 테스트 할때마다 테스트에 사용되는 객체를 생성하는 bulid 메서드를 항상 생성하여 테스트에 사용하였다. 아래는 Article 객체를 생성하는 메서드로 memberId를 받아 memberId가 작성한 Article를 반환하도록 구현한 예시이다.

public static Article buildArticle(Long memberId) {
        return Article.builder()
            .content("test")
            .latitude(10.0)
            .longitude(10.0)
            .public_map(true)
            .private_map(false)
            .title("test")
            .create_date(new Date())
            .member_id(memberId).build();
    }

 

위와 같이 구현하여 테스트를 진행하였었다. 하지만 최근에 이 메서드에는 치명적인 문제가 하나 있다는 것을 알았다. 바로 필드의 값들이 고정되어 있다는 것이다..!

content는 "test"로, title도 "test"로, latitude도 longitude 등 memberId를 제외한 모든 필드 값이 고정되어 있다. 

이는 내가 아는 범위에서만 테스트가 진행되는 것인데, 테스트란 것은 모든 경우에 대해서 통과를 해야 하는 것이라고 생각한다. 즉, 모든 값들도 외부에서 접근하는 이용자가 어떤 값을 넣을 지도 전부 모르는 상태여야 하는 것이다. 

그렇기에 위 방식은 테스트에 한계가 있다. 

하지만 EasyRandom을 통해서 위 문제를 해결할 수 있다. 

EasyRandom은 설정한 범위 내에서 랜덤한 값을 집어넣기 때문이다..! 그리고 EasyRandom.nextObject()라는 메서드를 통해 바로바로 객체를 생성하기에 대량의 객체들을 간단한 코드로 생성할 수 있다.

즉, 가독성 면에서도 우위를 가진다는 것이다. 

그렇다면 EasyRandom은 어떻게 사용하는 것일까? 바로 EasyRandom을 통해 객체를 생성해 보자!

 

 

1. EasyRandom을 통한 객체 생성

 

EasyRandom을 처음 접근할 때는 약간의 어려움이 있었다. 그 이유는 다른 사람들이 EasyRandom을 사용하는 방식이 다양했기 때문이었다. 어떤 것 하나를 제대로 보고 하려 해도 내가 원하는 부분이 나오지 않았기에 이것저것 찾아보았다. 그러던 중 공식 문서(깃허브)가 따로 있는 것을 알게 되었고 내가 원하는 부분만을 뽑아내어 easyRandom 객체를 생성할 수 있었다. 이 글에서는 EasyRandom 객체를 생성하는 여러 메서드 중 일부만을 사용하고 있다. 자세한 부분들은 아래의 깃허브 링크를 참고하길 바란다.

https://github.com/j-easy/easy-random

 

GitHub - j-easy/easy-random: The simple, stupid random Java beans/records generator

The simple, stupid random Java beans/records generator - GitHub - j-easy/easy-random: The simple, stupid random Java beans/records generator

github.com

 

 

우선 생성하려는 객체부터 확인해 보자. 

public class Article {
// 지도위에 남기는 게시글 객체
    private Long id;
    private String content;
    private Date create_date;
    private Double latitude;    // 위도
    private Double longitude;   // 경도
    private boolean private_map; // 
    private boolean public_map;
    private String title;
    private Long member_id;
    }
}

 

위의 객체는 필자가 진행하는 프로젝트 내의 객체 중 하나이다. 이제 테스트 객체를 생성하는 EasyRandom을 작성할텐데, 그전에 각 값들에 제한 사항 / 요구 사항을 확인하여 랜덤 값의 범위를 지정해 줘야 한다. 아래는 임의의 설정 정보이다.

  1. id는 Primary Key로  데이터베이스에 저장 시 auto increasement 방식으로 id 값이 채워질 것이다. 그렇기에 객체를 생성 후 db에 insert 할 때는 id값을 비워둔 상태로 넣고 있다.
  2. content와 title은 String 타입으로 길이가 4보다 크고 50보다 작은 문자열로 제한을 하고 있다.
  3. create_date는 2022년 5월에서 2023년 7월 사이로 하고 싶다. 
  4. 위도와 경도 값은 -175도에서 175도 사이로 하고 싶다.
  5. member_id는 article을 작성한 member의 id로 생성할 때 미리 고정된 값을 지정하고 싶다.

 

이제 위의 조건들을 만족하는 테스트 객체를 생성하는 EasyRandom 객체를 생성해보자. 

아래의 코드는 article 객체에 맞춘 EasyRandom 객체를 반환하는 메서드이다.

public class ArticleFeatureFactory {

  static public EasyRandom create(Long memberId) {
    Predicate<Field> articleIdPredicate = named("id").and(ofType(Long.class))
        .and(inClass(Article.class));
    Predicate<Field> memberIdPredicate = named("member_id").and(ofType(Long.class))
        .and(inClass(Article.class));
    EasyRandomParameters param = new EasyRandomParameters().excludeField(articleIdPredicate)
        .dateRange(LocalDate.of(2022, 5, 1), LocalDate.of(2023, 7, 22))
        .stringLengthRange(4, 50)
        .randomize(Double.class, new DoubleRangeRandomizer(-175.0, 175.0))
        .randomize(memberIdIdPredicate, () -> memberId);
    return new EasyRandom(param);
  }
}

 

중요한 부분은 EasyRandomParameters 타입의 param을 구성하는 과정이다. 

excludeField()는 내가 어떤 필드를 제외하여 객체를 생성하고 싶은지 지정할 수 있게 해 준다. 위의 코드에서는 id라는 이름의 필드 값을 제외토록 하였다. articleIdPredicate로 name, type, inClass를 설정해 주었는데 name만 설정하고 바로 대입을 해도 문제는 없다.

dateRange()는 date 타입의 값의 범위를 지정해줄 수 있게 해 준다. 위와 같이 LocalDate.of()를 통해 요구사하에 맞게 날짜를 지정해 줄 수 있다. 

stringLengthRange()는 이름에서 보듯이 문자열의 길이 범위를 원하는 만큼 지정을 해줄 수 잇다. 위의 코드에서는 4~50으로 설정하였다. 

Double 값인 latitude와 longitude 값도 stringLengthRange()처럼 doubleLengthRange()를 기대하였지만 아쉽게도 존재하지 않았다.

존재하는 Range()는 stringLengthRange(), timeRange(), collectionSizeRange(), dateRange() 이렇게 4가지였다. 

그렇다면 어떻게 Double 범위를 설정할 것인가? 

다행히 방법은 있었다. 위의 코드에서 보듯이 randomize() 를 사용하는 것이다. 원하는 타입과 타입에 맞춰 DoubleRangeRandomizer()를 통해 값을 조정을 해줄 수 있었다. 

다른 타입들도 위와 같이 되지 않을까? 하여 객체를 자세히 확인해 보니 AbstractRangeRandomizer라는 추상 클래스를 상속하고 있었다. 매개변수 타입은 <T>로 제네틱 타입으로 구현되어 있기에 다른 타입 또한 구현체로서 구현되어 있을 것이라는 생각을 하였다. 실제로 폴더 내부를 확인해 보니 아래와 같이 다양한 타입으로 존재하는 것을 확인할 수 있었다. 

 

 

마지막으로 randomize(memberIdPredicate, () -> memberId)와 같은 형식으로 미리 정의한 memberIdPredicate에 외부로부터 받은 memberId 값이 들어가도록 하여 EasyRandom 을 통해 생성한 테스트 객체는 모두 memberId가 고정되도록 할 수 있었다.

이후 EasyRandom 객체의 생성자를 호출하여 작성한 EasyRandomParameters 객체를 넣어줘서 반환하도록 하였다. 

이제 memberId 값을 입력하는 것만으로도 설정한 범위에 맞게 테스트 객체를 생성할 수 있다..!

 

//추가: 위의 코드에서 EasyRandomParameters 객체를 생성할 때, seed()라는 것도 추가 호출을 한다고 합니다..! seed()는 말 '시드'라고 하는데 테스트 객체를 생성할 때의 기준점 이라고 합니다. 동일한 시드를 가지고 테스트 객체를 생성하면 모두 동일한 패턴의 객체가 생성된다고 하네요... 저는 100만 건을 랜덤 하게 바로 생성해 가지고 없어도 큰 문제가 없었는데 항상 시드를 넣고 메서드를 구현하는 것 같네요...ㅎㅎ 저도 다음부턴 seed() 메서드도 추가해야겠습니다^^

 

이제 구현한 메서드를 가지고 테스트 객체를 생성해 보자!

먼저 메서드를 호출해 EasyRandom 객체를 꺼내야 한다.

EasyRandom articleEasyRandom = ArticleFeatureFactory.create(member.getId());

 

그다음 아래와 같이 nextObject() 메서드를 통해 객체를 바로 생성할 수 있다.

Article article = articleEasyRandom.nextObject(Article.class);

 

그렇다면 이러한 객체를 어떻게 대량으로 생성할까? 

필자는 처음에는 for문으로 List에 하나하나 추가해 나가려는 생각을 하였지만, 찾아보니 이보다 훨씬 깔끔한 방법이 있었다...

바로 Stream을 이용한 방식이다! 아래의 코드를 봐보자.

List<Article> articles = IntStream.range(0, 1000000)
                .parallel()
                .mapToObj(i -> articleEasyRandom.nextObject(Article.class))
                .collect(Collectors.toList());

 

너무 간결하지 않은가!

이렇게 for문을 사용하는 것보다 훨씬 간결한 방법으로 100만 건의 Article 객체를 생성해 리스트에 담을 수 있었다.

이제 이 객체 리스트를 데이터베이스에 삽입해야 한다.

하지만, 평소 하던 단일 insert를 100만 번 돌리면 아무리 빠른 컴퓨터라도 시간이 매우 많이 걸릴 것이다.

이럴 때 사용되는 것이 벌크 인서트(bulk insert)이다.

 

2. Mybatis: Bulk Insert를 통해 데이터베이스에 대량 데이터 삽입

 

이 글에서는 Mybatis를 통한 bulk Insert를 소개하겠다.

100만 개의 객체를 생성했으니 데이터베이스에 저장해야 한다. 하지만 매우 많은 데이터를 데이터베이스에 저장하는 것은 많은 부하가 생길 수밖에 없다. 우리는 이 과정에서도 부하를 줄일 수 있는 방법을 모색해야 한다. 그중 bulk Insert라는 방법이 있다. 아래는 일반적인 insert 문이다. 

 

//interface 내부의 메서드
int saveArticle(Article article);

// mapper.xml 파일 내부의 쿼리
<insert id="saveArticle" parameterType="Article" useGeneratedKeys="true" keyProperty="id">
    INSERT INTO article(
    content, latitude, longitude, public_map, private_map, title, create_date, member_id
    ) VALUES (#{content}, #{latitude}, #{longitude}, #{public_map}, #{private_map}, #{title},
    #{create_date}, #{member_id})
  </insert>

 

일반적으로 볼 수 있는 insert문이다. 이제 이 쿼리를 100만 번 실행하면 어떻게 될까? 어마어마한 시간이 걸릴 것이다. 필자는 시간이 너무 오래 걸려서 실행 중간에 그만두고 성공한 것을 보지 못하였다...

일반적으로 백엔드 서버 단에서 데이터베이스에 접근하는 비용이 크다고 한다. 그렇기에 코드 리펙토링을 할 때, 데이터 베이스의 접근을 최소화하는 방식을 고수하기도 한다. 

위에서는 데이터베이스에 100만 번을 접근하는 것이니 부하가 어마어마할 수밖에 없다. 

이럴 때 사용 되는 것이 bulkInsert이다. bulkInsert는 데이터베이스에 한번 접근하고 거기서 100만 번의 insert를 진행하는 방식이다. 

즉, 이전 단일 insert는 100만 번의 db 접근 + 100만 번의 insert이지만 bulk insert는 1번의 db 접근 + 100만 번의 insert 인 것이다. insert 하는 횟수는 변함이 없지만 db에 접근하여 처리하는 과정이 대폭 줄어들기에 성능향상이 있을 수 수밖에 없다. 

추가적으로 언급을 하자면 bulk insert를 할 때도 100만 건의 데이터를 한 번에 보내지 않고 일정 단위로 나눠서 보내는 것이 성능 향상을 이룬다고 한다. 예를 들어 1000개씩 나눠서 bulk insert를 실행하면 1000번의 db 접근(100만 건 = 1000개씩 1000번) + 100만 번의 insert 인데 대량의 데이터를 한번에 insert 하는 것이 생각보다 오래 걸린다고 한다. db 접근이 1000번 하는 시점에서부터 더 오래 걸릴 것 같은데 이는 아래의 테스트 진행 부분에서 확인해 보자.

그렇다면 bulk insert는 어떻게 작성할까? 

아래는 mybatis mapper 파일 내에 foreach를 통해 작성한 bulk insert 쿼리이다.

 

// interface 내부의 메서드
int saveArticleList(List<Article> articles);

// mapper.xml 내부의 쿼리
<insert id="saveArticleList" parameterType="java.util.List" useGeneratedKeys="true"
    keyProperty="id">
    INSERT INTO article(
    content, latitude, longitude, public_map, private_map, title, create_date, member_id)
    VALUES
    <foreach collection="list" index="index" item="article" separator=",">
      (#{article.content}, #{article.latitude}, #{article.longitude}, #{article.public_map},
      #{article.private_map}, #{article.title}, #{article.create_date}, #{article.member_id})
    </foreach>
  </insert>

 

단일 insert와 차이는 foreach 문을 사용하였고 메서드 매개변수로 대량의 데이터(리스트)를 담은 것이다. 큰 어려움 없이 bulk insert를 구현할 수가 있었다. 

 

이렇게 mapper 내부에 쿼리를 작성하였으니 테스트를 해보자.

아래는 테스트 코드이다.

 

public void create() {
        //given
        EasyRandom memberEasyRandom = MemberFeatureFactory.create(-1L);
        Member member = memberEasyRandom.nextObject(Member.class);
        memberRepository.saveMember(member);
        EasyRandom articleEasyRandom = ArticleFeatureFactory.create(member.getId());

        //when
        var stopWatch = new StopWatch();
        stopWatch.start();
        List<Article> articles = IntStream.range(0, 10000)
                .parallel()
                .mapToObj(i -> articleEasyRandom.nextObject(Article.class))
                .collect(Collectors.toList());
        stopWatch.stop();
        System.out.println("객체 생성시간: " + stopWatch.getTotalTimeSeconds());

        var queryStopWatch = new StopWatch();
        queryStopWatch.start();
        
// 단일 insert
//        for (int i = 0; i < articles.size(); i++) {
//            articleRepository.saveArticle(articles.get(i));
//        }

// 벌크 insert 1
//        for (int i = 0; i < articles.size(); i += 1000) {
//            List<Article> tempArticles = articles.subList(i, i + 1000);
//            articleRepository.saveArticleList(tempArticles);
//        }

// 벌크 insert 2
//            articleRepository.saveArticleList(articles);

        queryStopWatch.stop();
        System.out.println("DB 삽입시간: " + (queryStopWatch.getTotalTimeSeconds()));
    }

 

stopWatch를 통해 단일 insert와 벌크 insert 간의 시간을 측정하였다. 데이터는 1만으로 하였는데 단일 insert로는 100만 건을 실행을 짧은 시간에 끝내지 못하기에 비교를 확인하기 위해 1만 건으로 하였다.

주석 부분을 보면 단일 insert, 벌크 insert 1, 벌크 insert2로 구분되어 있는데 단일 insert는 insert 문을 1만 번 실행을, 벌크 insert 1은 1000개씩 나눠서 10번을, 벌크 insert 2는 바로 1만 건을 1번에 insert 하도록 구현하였다.

과연 결과는 어떻게 나올까?

 

1. 단일 insert 

 

 

2. 벌크 insert 1

 

 

3. 벌크 insert 2 

 

 

 

단일 insert와 벌크 insert 간에 확연한 차이가 보이는 것을 알 수 있다..!

 

그리고 1000 단위로 나눴던 벌크 insert 1이 벌크 insert 2보다 더 좋은 성능을 보인다. 이는 데이터가 많아질수록 확연한 차이를 보일 것이다. 데이터 베이스의 접근은 벌크 insert 1이 10번이었고 벌크 insert 2가 1번이었다. 하지만 10번 접근한 벌크 insert 1이 더 좋은 성능을 보인다...

이를 통해 데이터 베이스 접근 횟수를 감소하는 것뿐만이 아니라 데이터 베이스 내부에서 일어나는 일들도 고려해야 한 다는 것을 알 수 있다. 

이렇게 벌크 insert를 사용하여 수백만 건의 데이터를 더 빠르게 데이터베이스에 insert 할 수가 있다. 이제 본 목적인 인덱스를 적용해 보자!

 

 

 

* 그전에 필자가 삽질한 내용을 하나 언급하고자 한다. 위의 결과는 @SpringbootTest를 통해 실제 db에 데이터를 넣는 과정이다. 하지만 테스트 db 내에서 진행한다면 원하는 결과를 얻지 못할 것이다. 아래의 두 사진을 봐보자.

 

 

어느 것이 단일 insert고 벌크 insert인지 구분할 수 있는가? 놀랍게도 1.8초가 걸린 사진이 벌크 insert를 실행했을 때이다. 단일 insert일 때 보다 더 많은 시간이 걸렸다... 사실 두 사진 속 시간이 큰 차이가 난 것이 아니지만 실제 서버에서 돌렸을 때와는 많이 다르다는 것을 알 수 있다. 필자는 처음에 테스트 db로 실행하여 당연히 단일 insert가 훨씬 오래 걸리겠지 하는 기대와 전혀 다른 결과가 나와서 많이 헤맸던 적이 있다...

이 글을 읽는 분들은 이런 착오가 생기지 않길 바란다.

 

 

* 추가로 한 가지 더 언급을 하자면 Spring Data JPA에 saveAll이라는 메서드가 있는데 이는 bulk Insert가 아니라는 것을 인지하자... 아래의 saveAll이 어떻게 구현되어 있는지 봐보자.

 

// JPARepository의 구현체인 SimpleJpaRepository 내부 중
    @Transactional
	@Override
	public <S extends T> List<S> saveAll(Iterable<S> entities) {

		Assert.notNull(entities, "Entities must not be null!");

		List<S> result = new ArrayList<>();

		for (S entity : entities) {
			result.add(save(entity));
		}

		return result;
	}

 

위의 코드를 보면 for 문안에 save(entity)가 반복적으로 실행되는 것을 알 수 있다. 즉, save(insert 문)을 for문으로 돌리는 것이니 단일 insert를 for문으로 돌리는 것과 같다.

그러니 saveAll을 벌크 insert로 착각하지 않길 바란다.(필자도 계속 벌크 insert로 믿고 있었다ㅜ)

 

인덱스를 통해 조회 성능을 향상시키자!

 

드디어 인덱스를 적용할 차례이다. 우선 위의 벌크 insert를 통해 100만 건의 Article 데이터를 삽입하였다. 

그리고 아래는 적용할 쿼리이다. 

<select id="findPublicArticles" resultType="Article">
        select * from article where public_map = true and
        latitude between #{lowerLatitude} and #{upperLatitude} and
        longitude between #{lowerLongitude} and #{upperLongitude}
</select>

 

위 코드는 public_map 값이 true이고 latitude(위도)와 longitude(경도)가 주어진 범위에 있는 게시글을 조회하는 쿼리이다. 

필자가 진행하는 프로젝트에서는 지도를 기본으로 동작하는데 지도상 특정 위치에 표시된 Article 리스트를 가져오기 위해 위 쿼리를 사용한다.

만약 latitude와 longitude의 범위 각각 +5 ~ -5도 사이인 Article 리스트를 조회한다면 어떻게 될까? Easy Random 객체를 생성할 때 latitude와 longitude의 값은 +175 ~ -175도로 범위를 지정했었다. +5 ~ -5도는 전체 Article 중 일부분일 확률이 높다. 이러한 적은 범위의 Article 들을 조회하기 위해서 모든 Article 들을 하나하나 확인해야 할까? 만약 하나하나 확인하게 되면 데이터베이스 상에서 매우 많은 랜덤 액세스가 발생하게 될 것이고 이는 성능에 치명적일 것이다. 

 

이럴 때 인덱스를 사용하여 성능향상을 이룰 수 있다.

인덱스에 대해 궁금하다면 https://khdscor.tistory.com/50를 참고하길 바란다. 

 

인덱스 간단 정리(개념, 인덱스컬럼 결정)

프로젝트 개발을 하면서 데이터베이스를 다루는 작업을 많이 하게 된다. 무작정 쿼리를 막 날리면 성능상 좋지 않다는 것을 느끼게 될 것이다. 데이터베이스 성능을 향상하는 것이 매우 중요한

khdscor.tistory.com

 

 

인덱스의 컬럼으로 설정한 것은 public_map, latitude, longitude 이렇게 3가지이다. 이렇게 여러 컬럼이 포함된 인덱스를 결합 인덱스라고 한다.

이제부터 인덱스를 생성할 것인데 순서를 어떻게 지정해줘야 할까? 

public_map true, false 두 가지로 나뉘며 특정한 값을 찾아야 하고 latitude, longitude는 일정한 범위를 찾아야 한다. 일반적으로 결합 인덱스에서 순서를 결정할 때에는 범위를 스캔하는 것보다 값을 스캔하는 것이 우선권이 있다고 한다. 

결합 인덱스에 대한 부분은 https://khdscor.tistory.com/51를 참고하길 바란다.

 

DB - 결합인덱스 및 컬럼 순서 결정 방법

데이터 베이스를 다루면서 성능 향상을 위해 인덱스의 사용과 개념은 전 페이지에서 설명하였다. 특정 컬럼을 기준으로 정렬해 놓은 목차 같은 것이라고 할 수 있고 분류 대상과 분류 정보를 분

khdscor.tistory.com

 

 

이러한 우선권을 정하는 것이 정말 속도의 차이를 보이는지도 확인해 보자.

테스트 경우는 5가지이다.

  1. 인덱스가 없는 경우 
  2. public_map에 대한 인덱스만 가진 경우
  3. latitude, longitude에 대한 인덱스만 가진 경우
  4. public_map, latitude, longitude의 순서로 인덱스를 가진 경우
  5. latitude, longitude, public_map의 순서로 인덱스를 가진 경우

 

인덱스는 아래와 같이 생성할 수 있다.

create index public_map_index on article(public_map, latitude, longitude);

 

과연 결과는 어떻게 나왔을까?

아래의 결과를 보자.

 

 

1. 인덱스가 없는 경우 

 

 

2. public_map에 대한 인덱스만 가진 경우

 

 

3. latitude, longitude에 대한 인덱스만 가진 경우

 

 

4. public_map, latitude, longitude의 순서로 인덱스를 가진 경우

 

 

5. latitude, longitude, public_map의 순서로 인덱스를 가진 경우

 

 

1번 결과와 비교했을 때 2번 결과는 true, false 로만 나뉘는 public_map으로 인덱스를 타봤자 latitude, longitude를 확인하기 위해 랜덤 액세스를 일일이 해야하기 때문이 인덱스가 없는 실행과 큰 차이가 나지 않는 것 같다.

1번 결과와 비교했을 때 3번 결과는 확실히 범위 측면에서 크게 줄여주었기에 랜덤 엑세스 수가 적어져 속도가 더 빨라졌다. 이를 통해 알 수 있는 것은 public_map보다 latitude, longitude가 확인해야할 데이터를 더 줄여준다는 것을 알 수 있다.

1번 결과와 비교 했을 때 4번 결과는 예상대로 확실한 감소 수치를 보인다. 인덱스의 성능을 제대로 보여준 것이라고 생각한다. 

1번 결과와 비교 했을 때 5번 결과는 속도가 더 빨라졌지만 4번만큼의 속도는 나오지 않았다. 

이는 위에서 언급했던 데로 인덱스 순서를 정할 때 범위를 지정하는 컬럼보다는 값을 지정하는 컬럼을 앞에 순서에 놔야 더 빠르다는 말이 증명된 셈이다. 

 

이는 아래의 예시 사진을 통해서도 설명될 수 있다.

 

왼쪽이 4번일 경우의 인덱스이고 오른쪽이 5번일 경우의 인덱스를 간단하게 표현한 것이다.(latitude, longitude는 둘 다 범위를 나타내므로 설명을 위해 편의상 통합하여 표시하였다.)

범위는 90~100이며 public_map이 true인 값들을 찾고 싶다고 하자. 

4번일 경우 true를 우선적으로 찾고 90과 100 사이의 있는 데이터를 확인하면 된다.

하지만 5번일 경우 90과 100 사이의 범위를 지정한다고 해도 범위 사이에 있는 값 하나하나에 true인지 false인지가 구분되어 있기에 이를 하나하나 확인해 줘야 한다. 

즉, 결합 인덱스의 순서를 결정할 때는 범위를 표현하는 컬럼보다는 값을 나타내는 컬럼을 먼저 선별하는 것이 좋다는 것을 다시 한번 알 수 있었다. 또한 인덱스를 적용했을 때가 적용하지 않았을 때보다 속도 향상이 이뤄지는 것을 확인할 수 있었다.

 

 

* 참고로 인덱스는 테이블당 하나의 인덱스만 적용 가능하다고 한다. 예를 들어 join을 통해 두 테이블이 하나의 쿼리에 나오면 이 쿼리에는 테이블 당 하나씩 두 개의 인덱스가 적용될 수 있는 것이다. 

* 데이터가 많은 상태에서 인덱스를 생성하려니 제법 오랜 시간이 걸렸다... 삭제할 때도 너무 많은 시간이 걸려서 작업을 하기가 어려웠었다. 이는 인덱스가 이미 있는 상태에서 데이터를 추가하거나 삭제할 때에서 시간이 더 오래 걸릴 것이다. 즉, 읽기의 이점을 얻고 쓰기의 결점을 얻는다. 읽기와 쓰기의 트레이드 오프인 셈이다.

 

결론

이로서 EasyRandom으로 객체를 생성하고 생성한 객체를 bulk Insert를 통해 데이터베이스에 저장하고 대량의 데이터를 인덱스를 통해 조회하여 속도향상을 이뤄내는 과정을 마쳤다.

시작은 인덱스를 적용해 보자는 것이었지만 어느새 다양한 방법들을 터득해 나가는 자신을 볼 수가 있었다. 

그리고 똑같은 쿼리에 똑같은 인덱스여도 데이터가 어떻게 분포되어 있는지(어느 값을 가지는 레코드가 더 많고 적은 지)에 따라서도 속도는 확연히 달라질 수 있다고 한다. 위 글에서는 확인을 하지 않았지만 group by, order by에 따라서도 크게 달라질 것이다.

또한 조회하는 데이터가 많을 경우에는 테이블 스캔이 더 많은 것을 보면 인덱스가 만능이 아닌 것을 알 수 있다. 

 

결국, 인덱스에 대해서 실습한 내용은 세발의 피에 불과하며 아직 탐구해야 할 부분은 많이 남아있다. 

인덱스의 특성을 이해하고 잘 적용하는 것이 매우 중요하다고 한다. 

앞으로 프로젝트를 진행하면서 인덱스를 어떻게 적용할 수 있을지, 어떤 순서로 진행해야 할지, 정말 인덱스가 성능 향상을 이룰 수 있을지, 꼭 인덱스만으로 해결할 수 있는 것인지 등 계속해서 고민해 나가야겠다고 생각한다.



 

 

참고 자료

https://sweetquant.tistory.com/213

 

MySQL Workbench Lost Connection 에러 해결

MySQL Workbench CE에서 SQL을 실행하다 보면 아래와 같은 에러가 나오는 경우가 있습니다. - Lost connection to MySQL server during query 30.000 sec 이는 Workbench에서 오래 걸리는 SQL을 자동으로 끊기 때문입니다. W

sweetquant.tistory.com

https://velog.io/@bernard/%EB%8D%B0%EC%9D%B4%ED%84%B0%EA%B0%80-%ED%95%84%EC%9A%94%ED%95%B4...Object-Mother%EC%99%80-EasyRandom
https://berom.tistory.com/293

 

데이터가 필요해...!!(Object Mother와 EasyRandom)

예제 객체를 마구마구 생성해주는 EasyRanom 사용기입니다

velog.io

 

https://imksh.com/113

 

JPA saveAll이 Bulk INSERT 되지 않았던 이유

실습 환경 MySQL 5.7버전 사용 Windows 10 Entity ID 전략은 IDENTITY Java 11 서론 실무에서 MySQL 5.7 버전을 사용하고 있고, JPA Entity의 ID 전략은 IDENTITY를 사용하고 있는데, 이때 JPARepository를 이용한 saveAll과 sa

imksh.com


https://www.baeldung.com/java-easy-random

https://meansoup.github.io/docs/java/library/easyrandom

 

easyRandom - Java에서 테스트 객체 만들기

DEV & study log

meansoup.github.io

 

https://berom.tistory.com/293

 

Easy Random

Easy Random Spring에서 제공하는 Easy Random라는 라이브러리를 어떻게 사용하는지 알아보도록 하겠습니다. 이 라이브러리는 테스트 데이터를 생성하거나 랜덤한 값을 생성할 때 사용합니다 Easy Random

berom.tistory.com