@GetMapping("/api/likebeers/{userId}")
요청에 대한 것으로
userId에 해당하는 사용자가 좋아요한 맥주 리스트를 보여주는 API 구현과정에서 발생한 N+1문제를 BE팀원분과 함께 해결했던 과정을 기록했다.
“지연로딩(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;
}
}
@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
}
User findUser = userService.findOne(userid)
에서 userid에 해당하는 user 조회 시 select 쿼리 발생
findUser.getLikeBeers()
수행 시 likeBeer 조회 쿼리 발생
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();
}
}
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
}
JPQL 문법에 맞게 쿼리 작성함
likebeer와 beer가 join된 것을 볼 수 있다!
문제1
)을 거쳤는데 쿼리문을 직접 작성하여 where 조건으로 유저와 매핑시켰다.(불필요했던 user 조회 쿼리(문제1
) 생략)