Journal

Spatial Index 전환기

개발바닥곰발바닥!!! 2024. 8. 1. 18:25

프로젝트에서 거리순 정렬이 필요했는데 기존에는 그냥 QueryDSL로 Order By 유클리드 거리를 하고 있었다.

너무 비효율적이라는 생각이 들어서 더 좋은 방법이 없나 찾아보다가 공간 인덱스를 발견했다.

https://www.baeldung.com/hibernate-spatial

 

결론부터 말하자면 적용 후 성능이 151ms 에서 63ms 으로 58.28% 개선되었다

Hibernate-Spatial 개념 정리 → 링크

 

나에게 필요한 것은 특정한 좌표들을 비교하는 일이니까 Point를 선택해서 썼다

우선 관련된 설정을 해준다

 

latitude와 longitude를 Point로 바꿔줬다

⚠️주의⚠️

경도 longitude → x

위도 latitude → y

 

 

 

 		@Column
    private Double latitude;

    @Column
    private Double longitude;

 		import org.locationtech.jts.geom.Point;
 		
 		@Column
    private Point location;

 

 

 

.longitude(post.getLongitude())
            .latitude(post.getLatitude())

.longitude(post.getLocation().getX())
            .latitude(post.getLocation().getY())

 

 

Repository 단은 기존에 이렇게 생겼었다

✨개선 1. Spatial Index 사용

@Override
public List<Post> getPostsByDistance(int page, User user) {

less
Copy code
    QPost post = QPost.post;
    int offset = page * pagesize;

    QueryResults<Post> results = jpaQueryFactory.selectFrom(post)
        .orderBy(((post.latitude.subtract(user.getLatitude()))
            .multiply((post.latitude.subtract(user.getLatitude())))
            .add((post.longitude.subtract(user.getLongitude()))
                .multiply((post.longitude.subtract(user.getLongitude())))))
            .asc())
        .offset(offset)
        .limit(pagesize)
        .fetchResults();

    List<Post> closestPosts = results.getResults();
    return closestPosts;
} 

String jpql = "SELECT p FROM Post p " +
                "WHERE p.location IS NOT NULL " +
                "ORDER BY distance(p.location, :userLocation) ASC";
TypedQuery<Post> query = entityManager.createQuery(jpql.toString(), Post.class);
query.setFirstResult(offset);
query.setMaxResults(pagesize);

return query.getResultList();

 

 

✨개선 2. 과도하게 오버로딩된 메서드들을 하나로 통합

@RequiredArgsConstructor
@Repository
public class PostCustomRepositoryImpl implements PostCustomRepository {

    private final JPAQueryFactory jpaQueryFactory;

    @PersistenceContext
    private EntityManager entityManager;
    private final int pagesize = 5;

    @Override
    public List<Post> getPostsByDistance(int page, User user) {

        QPost post = QPost.post;
        int offset = page * pagesize;

        String jpql = "SELECT p FROM Post p " +
                "WHERE p.location IS NOT NULL " +
                "ORDER BY distance(p.location, :userLocation) ASC";

        TypedQuery<Post> query = entityManager.createQuery(jpql, Post.class);
        query.setParameter("userLocation", user.getLocation());
        query.setFirstResult(offset);
        query.setMaxResults(pagesize);

        return query.getResultList();
    }

    @Override
    public List<Post> getPostsByStatus(int page, PostStatusEnum status, User user) {
        if (user.getLongitude() == null || user.getLatitude() == null) {
            return getPostsByStatusWithoutAddress(page, status);
        } else {
            return getPostsByStatusWithAddress(page, status, user);
        }
    }

    @Override
    public List<Post> getPostsByCategory(int page, User user, CategoryEnum category) {

        List<Post> posts;
        if (user.getLongitude() == null || user.getLatitude() == null) {
            posts = getPostsByCategoryWithoutAddress(page, category);
        } else {
            posts = getPostsByCategoryWithAddress(page, category, user);
        }
        return posts;
    }

    @Override
    public List<Post> getPostsByKeyword(int page, User user, String keyword) {

        List<Post> posts;
        if (user.getLongitude() == null || user.getLatitude() == null) {
            posts = getPostsByKeywordWithoutAddress(page, keyword);
        } else {
            posts = getPostsByKeywordWithAddress(page, keyword, user);
        }
        return posts;
    }

    @Override
    public List<Post> getPostsByStatusAndCategory(int page, PostStatusEnum status,
        CategoryEnum category, User user) {

        if (user.getLongitude() == null || user.getLatitude() == null) {
            return getPostsByStatusAndCategoryWithoutAddress(page, status, category);
        } else {
            return getPostsByStatusAndCategoryWithAddress(page, status, category, user);
        }
    }

    @Override
    public List<Post> getPostsByStatusAndKeyword(int page, PostStatusEnum statusEnum,
        String keyword, User user) {
        if (user.getLatitude() == null || user.getLongitude() == null) {
            return getPostsByStatusAndKeywordWithoutAddress(page, statusEnum, keyword);
        } else {
            return getPostsByStatusAndKeywordWithAddress(page, statusEnum, keyword, user);
        }
    }

    @Override
    public List<Post> getPostsByCuisine(int page, User user, String cuisine) {
        if (user.getLongitude() == null || user.getLatitude() == null) {
            return getPostsByCuisineWithoutAddress(page, cuisine);
        } else {
            return getPostsByCuisineWithAddress(page, cuisine, user);
        }
    }

    @Override
    public List<Post> getPostsByStatusAndCuisine(int page, PostStatusEnum statusEnum,
        String cuisine, User user) {
        if (user.getLongitude() == null || user.getLatitude() == null) {
            return getPostsByStatusAndCuisineWithoutAddress(page, statusEnum, cuisine);
        } else {
            return getPostsByStatusAndCuisineWithAddress(page, statusEnum, cuisine, user);
        }
    }

    private List<Post> getPostsByStatusWithoutAddress(int page, PostStatusEnum status) {
        QPost post = QPost.post;
        int offset = page * pagesize;

        QueryResults<Post> results = jpaQueryFactory
            .selectFrom(post)
            .where(post.postStatus.eq(status))
            .orderBy(post.deadline.desc())
            .offset(offset)
            .limit(pagesize)
            .fetchResults();

        return results.getResults();
    }

    private List<Post> getPostsByStatusWithAddress(int page, PostStatusEnum status, User user) {
        QPost post = QPost.post;
        int offset = page * pagesize;

        QueryResults<Post> results = jpaQueryFactory
            .selectFrom(post)
            .where(post.postStatus.eq(status))
            .orderBy(((post.latitude.subtract(user.getLatitude()))
                .multiply((post.latitude.subtract(user.getLatitude())))
                .add((post.longitude.subtract(user.getLongitude()))
                    .multiply((post.longitude.subtract(user.getLongitude())))))
                .asc())
            .offset(offset)
            .limit(pagesize)
            .fetchResults();

        return results.getResults();
    }

    private List<Post> getPostsByStatusAndCategoryWithoutAddress(int page, PostStatusEnum status,
        CategoryEnum category) {
        QPost post = QPost.post;
        int offset = page * pagesize;

        QueryResults<Post> results = jpaQueryFactory
            .selectFrom(post)
            .where(post.postStatus.eq(status)
                .and(post.category.eq(category)))
            .orderBy(post.deadline.desc())
            .offset(offset)
            .limit(pagesize)
            .fetchResults();

        return results.getResults();
    }

    private List<Post> getPostsByStatusAndCategoryWithAddress(int page, PostStatusEnum status,
        CategoryEnum category, User user) {
        QPost post = QPost.post;
        int offset = page * pagesize;

        QueryResults<Post> results = jpaQueryFactory
            .selectFrom(post)
            .where(
                post.postStatus.eq(status)
                    .and(post.category.eq(category)))
            .orderBy(((post.latitude.subtract(user.getLatitude()))
                .multiply((post.latitude.subtract(user.getLatitude())))
                .add((post.longitude.subtract(user.getLongitude()))
                    .multiply((post.longitude.subtract(user.getLongitude())))))
                .asc())
            .offset(offset)
            .limit(pagesize)
            .fetchResults();

        return results.getResults();
    }

    private List<Post> getPostsByCategoryWithoutAddress(int page, CategoryEnum category) {
        QPost post = QPost.post;
        int offset = page * pagesize;

        QueryResults<Post> results = jpaQueryFactory.selectFrom(post)
            .where(post.category.eq(category))
            .orderBy(post.deadline.desc())
            .offset(offset)
            .limit(pagesize)
            .fetchResults();

        List<Post> closestPosts = results.getResults();
        return closestPosts;
    }

    private List<Post> getPostsByCategoryWithAddress(int page, CategoryEnum category, User user) {
        QPost post = QPost.post;
        int offset = page * pagesize;

        QueryResults<Post> results = jpaQueryFactory.selectFrom(post)
            .where(post.category.eq(category))
            .orderBy(
                ((post.latitude.subtract(user.getLatitude()))
                    .multiply((post.latitude.subtract(user.getLatitude())))
                    .add((post.longitude.subtract(user.getLongitude()))
                        .multiply((post.longitude.subtract(user.getLongitude())))))
                    .asc())
            .offset(offset)
            .limit(pagesize)
            .fetchResults();

        List<Post> closestPosts = results.getResults();
        return closestPosts;
    }

    private List<Post> getPostsByStatusAndKeywordWithoutAddress(int page, PostStatusEnum statusEnum,
        String keyword) {
        QPost post = QPost.post;
        int offset = page * pagesize;

        QueryResults<Post> results = jpaQueryFactory
            .selectFrom(post)
            .where(post.store.contains(keyword).and(post.postStatus.eq(statusEnum)))
            .orderBy(post.deadline.desc())
            .offset(offset)
            .limit(pagesize)
            .fetchResults();

        List<Post> closestPosts = results.getResults();
        return closestPosts;
    }

    private List<Post> getPostsByStatusAndKeywordWithAddress(int page, PostStatusEnum statusEnum,
        String keyword, User user) {
        QPost post = QPost.post;
        int offset = page * pagesize;

        QueryResults<Post> results = jpaQueryFactory
            .selectFrom(post)
            .where(post.store.contains(keyword).and(post.postStatus.eq(statusEnum)))
            .orderBy(((post.latitude.subtract(user.getLatitude()))
                .multiply((post.latitude.subtract(user.getLatitude())))
                .add((post.longitude.subtract(user.getLongitude()))
                    .multiply((post.longitude.subtract(user.getLongitude())))))
                .asc())
            .offset(offset)
            .limit(pagesize)
            .fetchResults();

        List<Post> closestPosts = results.getResults();
        return closestPosts;
    }

    private List<Post> getPostsByCuisineWithoutAddress(int page, String cuisine) {
        QPost post = QPost.post;
        int offset = page * pagesize;

        QueryResults<Post> results = jpaQueryFactory.selectFrom(post)
            .where(post.cuisine.eq(cuisine))
            .orderBy(post.deadline.desc())
            .offset(offset)
            .limit(pagesize)
            .fetchResults();

        List<Post> closestPosts = results.getResults();
        return closestPosts;
    }

    private List<Post> getPostsByCuisineWithAddress(int page, String cuisine, User user) {
        QPost post = QPost.post;
        int offset = page * pagesize;

        QueryResults<Post> results = jpaQueryFactory.selectFrom(post)
            .where(post.cuisine.eq(cuisine))
            .orderBy(
                ((post.latitude.subtract(user.getLatitude()))
                    .multiply((post.latitude.subtract(user.getLatitude())))
                    .add((post.longitude.subtract(user.getLongitude()))
                        .multiply((post.longitude.subtract(user.getLongitude())))))
                    .asc())
            .offset(offset)
            .limit(pagesize)
            .fetchResults();

        return results.getResults();
    }

    private List<Post> getPostsByStatusAndCuisineWithoutAddress(int page, PostStatusEnum status,
        String cuisine) {
        QPost post = QPost.post;
        int offset = page * pagesize;

        QueryResults<Post> results = jpaQueryFactory
            .selectFrom(post)
            .where(post.postStatus.eq(status)
                .and(post.cuisine.eq(cuisine)))
            .orderBy(post.deadline.desc())
            .offset(offset)
            .limit(pagesize)
            .fetchResults();

        return results.getResults();
    }

    private List<Post> getPostsByStatusAndCuisineWithAddress(int page, PostStatusEnum status,
        String cuisine, User user) {
        QPost post = QPost.post;
        int offset = page * pagesize;

        QueryResults<Post> results = jpaQueryFactory
            .selectFrom(post)
            .where(post.postStatus.eq(status)
                .and(post.cuisine.eq(cuisine)))
            .orderBy(((post.latitude.subtract(user.getLatitude()))
                .multiply((post.latitude.subtract(user.getLatitude())))
                .add((post.longitude.subtract(user.getLongitude()))
                    .multiply((post.longitude.subtract(user.getLongitude())))))
                .asc())
            .offset(offset)
            .limit(pagesize)
            .fetchResults();

        return results.getResults();
    }

    private List<Post> getPostsByKeywordWithoutAddress(int page, String keyword) {
        QPost post = QPost.post;
        int offset = page * pagesize;

        QueryResults<Post> results = jpaQueryFactory
            .selectFrom(post)
            .where(post.store.contains(keyword))
            .orderBy(post.deadline.desc())
            .offset(offset)
            .limit(pagesize)
            .fetchResults();

        return results.getResults();
    }

    private List<Post> getPostsByKeywordWithAddress(int page, String keyword, User user) {

        QPost post = QPost.post;
        int offset = page * pagesize;

        QueryResults<Post> results = jpaQueryFactory
            .selectFrom(post)
            .where(post.store.contains(keyword))
            .orderBy(((post.latitude.subtract(user.getLatitude()))
                .multiply((post.latitude.subtract(user.getLatitude())))
                .add((post.longitude.subtract(user.getLongitude()))
                    .multiply((post.longitude.subtract(user.getLongitude())))))
                .asc())
            .offset(offset)
            .limit(pagesize)
            .fetchResults();

        return results.getResults();
    }

}

package com.moayo.moayoeats.backend.domain.post.repository;

import com.moayo.moayoeats.backend.domain.post.entity.CategoryEnum;
import com.moayo.moayoeats.backend.domain.post.entity.Post;
import com.moayo.moayoeats.backend.domain.post.entity.PostStatusEnum;
import com.moayo.moayoeats.backend.domain.user.entity.User;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.persistence.TypedQuery;
import lombok.RequiredArgsConstructor;
import org.locationtech.jts.geom.Point;
import org.springframework.stereotype.Repository;

import java.util.ArrayList;
import java.util.List;

@RequiredArgsConstructor
@Repository
public class PostCustomRepositoryImpl implements PostCustomRepository {

    @PersistenceContext
    private final EntityManager entityManager;
    private final int pagesize = 5;

    @Override
    public List<Post> getPosts(int page, User user, PostStatusEnum status, CategoryEnum category, String keyword, String cuisine) {
        int offset = page * pagesize;
        Point userLocation = user != null ? user.getLocation() : null;

        StringBuilder jpql = new StringBuilder("SELECT p FROM Post p WHERE 1=1");

        if (status != null) {
            jpql.append(" AND p.postStatus = :status");
        }
        if (category != null) {
            jpql.append(" AND p.category = :category");
        }
        if (keyword != null && !keyword.trim().isEmpty()) {
            jpql.append(" AND p.store LIKE :keyword");
        }
        if (cuisine != null && !cuisine.trim().isEmpty()) {
            jpql.append(" AND p.cuisine = :cuisine");
        }
        if (userLocation != null) {
            jpql.append(" AND p.location IS NOT NULL ORDER BY distance(p.location, :userLocation) ASC, p.deadline DESC");
        } else {
            jpql.append(" ORDER BY p.deadline DESC");
        }

        TypedQuery<Post> query = entityManager.createQuery(jpql.toString(), Post.class);

        if (status != null) {
            query.setParameter("status", status);
        }
        if (category != null) {
            query.setParameter("category", category);
        }
        if (keyword != null && !keyword.trim().isEmpty()) {
            query.setParameter("keyword", "%" + keyword + "%");
        }
        if (cuisine != null && !cuisine.trim().isEmpty()) {
            query.setParameter("cuisine", cuisine);
        }
        if (userLocation != null) {
            query.setParameter("userLocation", userLocation);
        }

        query.setFirstResult(offset);
        query.setMaxResults(pagesize);

        return query.getResultList();
    }
}

 

✨개선 4. Builder Pattern 적용

이렇게 하고 나서 뭔가 Query를 만들때 Builder 패턴을 적용하고 싶어서 이렇게 해봤다

public class PostQueryBuilder {

    private StringBuilder jpql;
    @PersistenceContext
    private EntityManager entityManager;
    private TypedQuery<Post> query;
    private PostStatusEnum status;
    private CategoryEnum category;
    private String keyword;
    private String cuisine;
    private Point userLocation;

    public PostQueryBuilder(EntityManager entityManager) {
        this.entityManager = entityManager;
        this.jpql = new StringBuilder("SELECT p FROM Post p WHERE 1=1");
    }

    public PostQueryBuilder withStatus(PostStatusEnum status) {
        this.status = status;
        if (status != null) {
            jpql.append(" AND p.postStatus = :status");
        }
        return this;
    }

    public PostQueryBuilder withCategory(CategoryEnum category) {
        this.category = category;
        if (category != null) {
            jpql.append(" AND p.category = :category");
        }
        return this;
    }

    public PostQueryBuilder withKeyword(String keyword) {
        this.keyword = keyword;
        if (keyword != null && !keyword.trim().isEmpty()) {
            jpql.append(" AND p.store LIKE :keyword");
        }
        return this;
    }

    public PostQueryBuilder withCuisine(String cuisine) {
        this.cuisine = cuisine;
        if (cuisine != null && !cuisine.trim().isEmpty()) {
            jpql.append(" AND p.cuisine = :cuisine");
        }
        return this;
    }

    public PostQueryBuilder orderByDistance(User user) {
        this.userLocation = user.getLocation();
        if (userLocation != null) {
            jpql.append(" AND p.location IS NOT NULL ORDER BY distance(p.location, :userLocation) ASC");
            return AndOrderByDeadline();
        }
        return orderByDeadline();
    }

    public PostQueryBuilder AndOrderByDeadline() {
        jpql.append(", p.deadline DESC");
        return this;
    }

    public PostQueryBuilder orderByDeadline() {
        jpql.append(" ORDER BY p.deadline DESC");
        return this;
    }

    public TypedQuery<Post> build() {
        query = entityManager.createQuery(jpql.toString(), Post.class);

        if (status != null) {
            query.setParameter("status", status);
        }
        if (category != null) {
            query.setParameter("category", category);
        }
        if (keyword != null && !keyword.trim().isEmpty()) {
            query.setParameter("keyword", "%" + keyword + "%");
        }
        if (cuisine != null && !cuisine.trim().isEmpty()) {
            query.setParameter("cuisine", cuisine);
        }
        if (userLocation != null) {
            query.setParameter("userLocation", userLocation);
        }

        return query;
    }
}

 

 

✨개선 4. 공통 Super class 를 사용해서 다른 자료형들을 하나의 자료구조에 가변 용량으로 저장

Object 를 Map에다 넣으면 하드코딩을 줄일 수 있을 것 같아서 적용해 보았다.

하드코딩도 줄어들고 필요한 parameter만 Map에 들어가기 때문에 더 효율적이 된다

package com.moayo.moayoeats.backend.domain.post.repository.builder;

import com.moayo.moayoeats.backend.domain.post.entity.CategoryEnum;
import com.moayo.moayoeats.backend.domain.post.entity.Post;
import com.moayo.moayoeats.backend.domain.post.entity.PostStatusEnum;
import com.moayo.moayoeats.backend.domain.user.entity.User;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.persistence.TypedQuery;
import org.locationtech.jts.geom.Point;

import java.util.HashMap;
import java.util.Map;

public class PostQueryBuilder {

    private final StringBuilder jpql;
    @PersistenceContext
    private final EntityManager entityManager;
    private final Map<String, Object> parameters = new HashMap<>();

    public PostQueryBuilder(EntityManager entityManager) {
        this.entityManager = entityManager;
        this.jpql = new StringBuilder("SELECT p FROM Post p WHERE 1=1");
    }

    public PostQueryBuilder withStatus(PostStatusEnum status) {
        if (status != null) {
            jpql.append(" AND p.postStatus = :status");
            parameters.put("status", status);
        }
        return this;
    }

    public PostQueryBuilder withCategory(CategoryEnum category) {
        if (category != null) {
            jpql.append(" AND p.category = :category");
            parameters.put("category", category);
        }
        return this;
    }

    public PostQueryBuilder withKeyword(String keyword) {
        if (keyword != null && !keyword.trim().isEmpty()) {
            jpql.append(" AND p.store LIKE :keyword");
            parameters.put("keyword", "%" + keyword + "%");
        }
        return this;
    }

    public PostQueryBuilder withCuisine(String cuisine) {
        if (cuisine != null && !cuisine.trim().isEmpty()) {
            jpql.append(" AND p.cuisine = :cuisine");
            parameters.put("cuisine", cuisine);
        }
        return this;
    }

    public PostQueryBuilder orderByDistance(User user) {
        Point userLocation = user.getLocation();
        if (userLocation != null) {
            jpql.append(" AND p.location IS NOT NULL ORDER BY distance(p.location, :userLocation) ASC");
            parameters.put("userLocation", userLocation);
            return andOrderByDeadline();
        }
        return orderByDeadline();
    }

    public PostQueryBuilder andOrderByDeadline() {
        jpql.append(", p.deadline DESC");
        return this;
    }

    public PostQueryBuilder orderByDeadline() {
        jpql.append(" ORDER BY p.deadline DESC");
        return this;
    }

    public TypedQuery<Post> build() {
        TypedQuery<Post> query = entityManager.createQuery(jpql.toString(), Post.class);
        parameters.forEach(query::setParameter);
        return query;
    }
}

사실 아래처럼 QueryDsl 쓰고싶었는데

특정 범위 내인지 판단하는 메서드 등은 있지만

두 지점간의 거리를 구하고 싶을때는 아직 JPQL을 사용해야 하는 것 같다

 

성능 개선 결과

Original

package com.moayo.moayoeats.backend.domain.post;

import com.moayo.moayoeats.backend.domain.post.dto.request.PostRequest;
import com.moayo.moayoeats.backend.domain.post.service.impl.PostCreateServiceImpl;
import com.moayo.moayoeats.backend.domain.user.entity.User;
import com.moayo.moayoeats.backend.domain.user.repository.UserRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.stream.IntStream;

@SpringBootTest
public class PostServiceTest {

    private static final String[] CITY_NAMES = {"Seoul", "Busan", "Incheon", "Daegu", "Daejeon"};
    private static final String[] CITY_COORDINATES = {"(lat:37.5665,lng:126.9780)", // Seoul
            "(lat:37.4563,lng:126.7052)", // Incheon
            "(lat:35.8722,lng:128.6011)", // Daegu
            "(lat:36.3504,lng:127.3845)",  // Daejeon
            "(lat:35.1796,lng:129.0756)" // Busan
    };
    @Autowired
    private PostCreateServiceImpl postCreateService;
    @Autowired
    private UserRepository userRepository;
    private User user;

    @BeforeEach
    public void setUp() {
        user = userRepository.findById(1L).orElseThrow();
    }

    public void createMultiplePosts(int numberOfRequests, User user) {
        IntStream.range(0, numberOfRequests).forEach(i -> {
            PostRequest postRequest = new PostRequest(
                    CITY_COORDINATES[i % CITY_COORDINATES.length],
                    CITY_NAMES[i % CITY_NAMES.length] + " Store",
                    "10000",
                    "3000",
                    "30",
                    "12",
                    "KOREAN");
            postCreateService.createPost(postRequest, user);
        });
    }

    @Test
    public void testCreateMultiplePosts() {
        int numberOfRequests = 500;

        createMultiplePosts(numberOfRequests, user);
    }
}

151ms

 

Spatial Index

package com.moayo.moayoeats.backend.domain.post.service;

import com.moayo.moayoeats.backend.domain.post.dto.request.PostRequest;
import com.moayo.moayoeats.backend.domain.post.service.impl.PostCreateServiceImpl;
import com.moayo.moayoeats.backend.domain.user.entity.User;
import com.moayo.moayoeats.backend.domain.user.repository.UserRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.stream.IntStream;

@SpringBootTest
public class PostServiceTest {

    private static final String[] CITY_NAMES = {"Seoul", "Busan", "Incheon", "Daegu", "Daejeon"};
    private static final String[] CITY_COORDINATES = {"(lat:37.5665,lng:126.9780)", // Seoul
            "(lat:37.4563,lng:126.7052)", // Incheon
            "(lat:35.8722,lng:128.6011)", // Daegu
            "(lat:36.3504,lng:127.3845)",  // Daejeon
            "(lat:35.1796,lng:129.0756)" // Busan
    };
    @Autowired
    private PostCreateServiceImpl postCreateService;
    @Autowired
    private UserRepository userRepository;
    private User user;

    @BeforeEach
    public void setUp() {
        user = userRepository.findById(1L).orElseThrow();
    }

    public void createMultiplePosts(int numberOfRequests, User user) {
        IntStream.range(0, numberOfRequests).forEach(i -> {
            PostRequest postRequest = new PostRequest(CITY_COORDINATES[i % CITY_COORDINATES.length], CITY_NAMES[i % CITY_NAMES.length] + " Store", "10000", "3000", "30", "12", "KOREAN");
            postCreateService.createPost(postRequest, user);
        });
    }

    @Test
    public void testCreateMultiplePosts() {
        int numberOfRequests = 500;

        createMultiplePosts(numberOfRequests, user);
    }
}

63ms

 

  1. 성능 개선된 차이: 151ms - 63ms = 88ms
  2. 성능 개선율: (151ms88ms)×100≈58.28%