enginner_s2eojeong

JPA에서 Index 설정하는 방법 (feat. 쿼리 속도 향상 시키기) - 1편 본문

Backend/Database

JPA에서 Index 설정하는 방법 (feat. 쿼리 속도 향상 시키기) - 1편

_danchu 2025. 2. 13. 01:41

ReciGuard는 알레르기를 가진 사용자에게 안전한 레시피를 추천하는 서비스다.

 

때문에 사용자별 알레르기 정보를 반영하기 위해 정규화된 데이터베이스 구조를 설계하였으나, 레시피와 사용자의 알레르기 정보를 매칭하고 필터링하는 과정에서 Recipe, RecipeIngredient, Ingredient, UserIngredient, User 테이블 간에 빈번한 JOIN 연산이 발생하는 문제가 발생했다. 이러한 성능 저하로 인해 데이터 로딩 시간도 길어졌고 이는 곧 사용자 경험 저하로 이어질 우려가 있다.

 

이를 위해 해결 방법을 강구하던 중, 작년 데이터베이스 수업 시간에 배웠던 Index가 떠올랐다.


Index란?

: 특정 테이블에서 데이터를 더 빠르게 검색할 수 있도록 도와주는 보조 구조

  • 인덱스는 데이터를 복사하여 별도의 자료구조(B-Tree 등)로 정리한 것으로 원하는 데이터를 빠르게 찾을 수 있도록 함.
  • 일반적으로 WHERE 조건, JOIN 연산, GROUP BY, ORDER BY 등의 연산 속도를 높이는 역할을 함.
  • 하지만 데이터가 많아지면 인덱스 유지 비용이 증가하고 쓰기 연산이 많을수록 인덱스 갱신 오버헤드가 커짐.

비유하자면,

  1. 책의 목차와 비슷함.
  2. 단점) 목차가 있으면 특정 내용을 빠르게 찾을 수 있지만 내용이 바뀌면 목차도 업데이트해야 함.

예를 들어 Recipe 테이블에서 특정 recipe_id에 해당하는 Recipe를 찾는다고 할 때,

SELECT * FROM Recipe WHERE recipe_id = "2025";

 

이런 식으로 recipe_id를 목차로 하는 Index를 별도로 만들어둔다면 검색 속도가 훨씬 빨라진다.

CREATE INDEX recipe_id_index ON Recipe(recipe_id);

 


JPA에서 Index 설정하기

recipe_ingredient 테이블에서 recipe_id, ingredient_id에 대해서 각각 Index를 만들어주는 코드는 다음과 같다.

@Index를 선언하고, name에는 Index 이름, columnList에는 목차로 뽑을 속성명을 명시해주면 된다.

@Entity
@Getter @Setter
@NoArgsConstructor
@Table(name = "recipe_ingredient",
        indexes = {
                @Index(name = "idx_recipe_ingredient_recipe_id", columnList = "recipe_id"),
                @Index(name = "idx_recipe_ingredient_ingredient_id", columnList = "ingredient_id")
        }
)
public class RecipeIngredient {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "recipe_ingredient_id")
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "recipe_id")
    @JsonIgnore
    private Recipe recipe;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "ingredient_id")
    private Ingredient ingredient;

    private String quantity;
}

 

터미널에서 생성된 Index를 확인해보면 다음과 같이 잘 만들어진 것을 확인할 수 있다.

 

 

이렇게 따로 각각의 Index 를 생성할 수도 있고, 2개의 column으로 하나의 Index를 만드는 것도 가능하다.

나는 사용자(user_id)에게 알레르기를 유발할 수 있는 재료(ingredient_id)레시피(recipe_id)재료(ingredient_id)들을 비교하는 필터링을 진행해야하기 때문에,

 

  • RecipeIngredient (recipe_id, ingredient_id) → 레시피별 재료 조회 시 빠르게 검색 가능
  • UserIngredient (user_id, ingredient_id) → 사용자가 가진 재료 조회 시 빠르게 검색 가능

이런 방식으로 2개의 column으로 이루어진 Index를 만들 예정이다.

 

RecipeIngredient

@Entity
@Getter @Setter
@NoArgsConstructor
@Table(name = "recipe_ingredient", 
	indexes = @Index(name = "idx_recipe_ingredient_recipe_id", 
    			 columnList = "recipe_id, ingredient_id"))
public class RecipeIngredient {
    .
    .
    .
}

 

UserIngredient

@Entity
@Getter @Setter
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "user_ingredient", 
	indexes = @Index(name = "idx_user_ingredient_recipe_id", 
    			 columnList = "user_id, ingredient_id"))
public class UserIngredient {
    .
    .
    .
}

 

Recipe

@Entity
@Getter @Setter
@NoArgsConstructor
@Table(name = "recipe", indexes = @Index(name = "idx_recipe_cuisine", columnList = "cuisine"))
public class Recipe {
    .
    .
    .
}

 

결과)

인덱스들이 다 잘 만들어진 것을 확인할 수 있다 !

 

지금까지 만들어진 인덱스들을 사용했을 때 성능 향상이 되었는지 다음 편에서 로그로 확인해보도록 하겠다.