Light Blue Pointer
본문 바로가기
Journal

Spatial Index 전환기

by 개발바닥곰발바닥!!! 2024. 8. 1.

프로젝트에서 거리순 정렬이 필요했는데 기존에는 그냥 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%