[2022 하계 모각코] 프로젝트하면서 배운 것(API 성능 최적화)

Kyunghwan Ko·2022년 8월 29일
0

22년도 하계 모각코

목록 보기
13/13

@GetMapping("/api/likebeers/{userId}") 요청에 대한 것으로
userId에 해당하는 사용자가 좋아요한 맥주 리스트를 보여주는 API 구현과정에서 발생한 N+1문제를 BE팀원분과 함께 해결했던 과정을 기록했다.

API 성능 최적화

  • JPA에서 x-To-One 조회 시 N+1 문제가 발생할 수 있으므로 이를 막기위해 fetch join을 사용한다.
  • JPA에서 컬렉션(One-To-Many)을 조회할 경우 관계 일 경우 fetch join 사용 시 오류가 발생할 수 있다.

“지연로딩(LazyLoading) + Batch 전략”을 사용했음

default_batch_fetch_size: 100을 application.yml에 추가

좋아요한 맥주(LikeBeer)와 맥주(Beer) 엔티티 간의 관계는 다대일(ManyToOne)이기 때문에 아래와 같이 코드를 작성했다.(N+1 문제를 방지하기 위해 fetch = LAZY 옵션 사용)

// LikeBeer 엔티티(좋아요한 맥주)
@Entity
@Table(name = "like_beer")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter @Setter
public class LikeBeer {

    @Id @GeneratedValue()
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    private User user;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "beer_id")
    private Beer beer;

    public LikeBeer(Beer beer) {
        this.beer = beer;
    }
}

1. 성능 최적화 전

@GetMapping("/api/likebeers/{id}")
    public WrapperClass showLikeBeers(@PathVariable("id") Long userid){
        User findUser = userService.findOne(userid); // 문제1. user 조회
        List<LikeBeer> likeBeers = findUser.getLikeBeers(); // 문제2. likebeer 조회
        List<Beer> beers = new ArrayList<>();
        for (LikeBeer likeBeer : likeBeers) {
            beers.add(likeBeer.getBeer());
            System.out.println("likeBeer.getBeer() = " + likeBeer.getBeer());
        }
		// 문제3. likebeer 수만큼 beer 조회
        List<BeerDto> beerDtos = beers.stream().map(b -> new BeerDto(b)).collect(Collectors.toList());
        return new WrapperClass(beerDtos); //api의 확장이 가능하도록 wrapper 클래스로 감싸서 리스트를 return
    }

문제1. User 조회시 select 쿼리 발생

User findUser = userService.findOne(userid) 에서 userid에 해당하는 user 조회 시 select 쿼리 발생

문제2. findUser에서 .getLikeBeers() 수행 시 select 쿼리 발생

findUser.getLikeBeers() 수행 시 likeBeer 조회 쿼리 발생

문제3. map(b -> new BeerDto(b) DTO로 변환 시 Beer 엔티티 필드에 접근(터치)하므로 beer를 조회하는 쿼리가 likeBeer 수 만큼 발생(N+1문제!)

// BeerDto
@Data
public class BeerDto {
    private Long id;
    private String imageUrl;
    private String beerName;
    private double totalPoint;
    private String information;

    public BeerDto(Beer beer) {
        this.id = beer.getId();
        this.imageUrl = beer.getImageUrl();
        this.beerName = beer.getBeerName();
        this.totalPoint = beer.getTotalPoint();
        this.information = beer.getInformation();
    }
}


2. 성능 최적화 후(fetch join을 사용)

LikeBeer와 Beer는 ManyToOne 관계이기에 fetch는 LAZY이지만 함께 조회하는 일이 있기 때문에 Eager처럼 동작하도록 하기 위해 fetch join을 적용했다.

@GetMapping("/api/likebeers/{id}")
    public WrapperClass showLikeBeers(@PathVariable("id") Long userid){
        //fetch join 사용
        List<LikeBeer> likeBeers = likeBeerService.findAllWithBeer(userid);
        List<Beer> beers = new ArrayList<>();
        for (LikeBeer likeBeer : likeBeers) {
            beers.add(likeBeer.getBeer());
            System.out.println("likeBeer.getBeer() = " + likeBeer.getBeer());
        }
        List<BeerDto> beerDtos = beers.stream().map(b -> new BeerDto(b)).collect(Collectors.toList());
        return new WrapperClass(beerDtos); //api의 확장이 가능하도록 wrapper 클래스로 감싸서 리스트를 return
    }

likeBeer Repository 내 fetch join을 사용하여 직접 sql을 날리는 모습

JPQL 문법에 맞게 쿼리 작성함

쿼리 결과

likebeer와 beer가 join된 것을 볼 수 있다!

  • Fetch Join을 사용하여 likeBeer를 조회할 때 Many-To-One 관계를 갖는 Beer 까지 한 번에 조회
    • N+1 문제 해결(beer의 개수만큼 반복되는 beer 조회 쿼리를 하나로 줄일 수 있었다)
  • 기존 userid를 받아 User를 다시 조회 하는 과정(문제1)을 거쳤는데 쿼리문을 직접 작성하여 where 조건으로 유저와 매핑시켰다.(불필요했던 user 조회 쿼리(문제1) 생략)

결론적으로 sql문을 대폭 줄여 쿼리 성능을 최적화할 수 있었다!

profile
부족한 부분을 인지하는 것부터가 배움의 시작이다.

0개의 댓글