Skip to content

Trouble shooting

Heejun Yang edited this page Nov 7, 2021 · 3 revisions

코드를 짜며 고민했던 것의 기록들입니다.

희준

dto에서 List<>초기화

아래에서, dto를 활용하게 되면 널포인트 익셉션에러가 뜬다.

@Getter
@AllArgsConstructor
public class RecipeListResponseDto {
    private Long recipeId;
    private String nickname;
    private String title;
    private String content;
    private List<String> image ;
    private LocalDateTime regdate;
    private int commentCount;
    private int likeCount;
    private boolean likeStatus;

    public RecipeListResponseDto(Recipe recipe, UserDetailsImpl userDetails, RecipeLikesRepository recipeLikesRepository){
        this.recipeId = recipe.getId();
        this.nickname = recipe.getUser().getNickname();
        this.title = recipe.getTitle();
        this.content = recipe.getContent();
        this.regdate = recipe.getRegDate();
        this.commentCount = recipe.getRecipeCommentList().size();
        recipe.getRecipeImagesList().forEach((RecipeImage)->this.image.add(RecipeImage.getImage()));
        this.likeCount = recipe.getRecipeLikesList().size();

        Optional<RecipeLikes> foundRecipeLike = recipeLikesRepository.findByRecipeIdAndUserId(recipe.getId(),userDetails.getUser().getId());
        this.likeStatus = foundRecipeLike.isPresent();
    }
}

해결: 멤버변수 선언시 초기화를 해주니까 해결이 된다. 원인은 images.add를 하는데 객체가 생성이 되어 있지 않았기 때문이라 추측된다.

@Getter
@AllArgsConstructor
public class RecipeListResponseDto {
    private Long recipeId;
    private String nickname;
    private String title;
    private String content;
    private List<String> images = new ArrayList<>();
    private LocalDateTime regdate;
    private int commentCount;
    private int likeCount;
    private boolean likeStatus;

cascade 옵션 시 delete쿼리 문제

  • 업로드한 이미지들을 수정하기 위해, 먼저 기존 RECIPE_IMAGE테이블에서 특정 Recipe에 해당하는 이미지들을 모두 삭제를 하고 새로운 사진을 INSERT하는 방법을 선택하였다.
  • 그러나 다음 쿼리가 날라가지 않음.
recipeImageRepository.deleteAllByRecipe(recipe);
  • 구체적인 문제상황:

1번 레시피글에 사진이 5장 등록되어있음. 새로운 2장으로 사진을 수정하고자 함. 그러나, 기존 사진이 삭제가 되지 않았음.

  • 디버깅 시 쿼리가 delete쿼리 날라가는가? 아니? 트랜잭션이 끝날 때 아무 쿼리도 날라가지 않음.

  • 원인 추측:

  1. jpa쿼리를 잘못 썼나? 아무리 봐도 아니다.
  2. 혹시 영속성 관련 문제인가? 정답
  • Recipe엔티티:

아래처럼 케스케이드 옵션을 가지고 있다. 부모역할을 한다.

		@OneToMany(mappedBy = "recipe", cascade = CascadeType.ALL)
    @JsonBackReference
    private List<RecipeImage> recipeImagesList;
  • RecipeImage엔티티:

단순히 이미지와 recipe를 연결해주고있는 엔티티이다.

@Getter
@NoArgsConstructor
@Entity
public class RecipeImage extends BaseEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "image_id")
    private Long id;

    private String image;

    @ManyToOne(fetch = FetchType.LAZY)
    @JsonManagedReference
    @JoinColumn(name = "recipe_id")
    private Recipe recipe;
  • cascade = CascadeType.ALL 을 사용한 이유

부모(Recipe)를 삭제할 때 자식(recipeImageList)도 삭제하여참조무결성 오류를 방지하기 위해.

해결책:

  • Recipe엔티티:

케스케이드옵션을 REMOVE로 하거나, orphanRemoval = true를 설정한다.

		@OneToMany(mappedBy = "recipe", cascade = CascadeType.REMOVE)
    @JsonBackReference
    private List<RecipeImage> recipeImagesList;
		@OneToMany(mappedBy = "recipe", orphanRemoval = true)
    @JsonBackReference
    private List<RecipeImage> recipeImagesList;

특정 기간동안의 데이터 검색하는 쿼리 시 서버시간, db시간 조정

특정기간 동안에 발생한 row만 뽑아내는 쿼리를 날리려 하는데, 서버시간과 DB시간이 맞지 않았다.

해결방법:

  1. 스프링부트의 시간을 서울시간으로 설정.
 @PostConstruct
    void started() {
        TimeZone.setDefault(TimeZone.getTimeZone("Asia/Seoul"));
    }

2.디비(mysql)의 시간도 서울로 설정한다.

select now() //현재시간 확인
set time_zone='Asia/Seoul';

이제 쿼리날릴 때 아래처럼 LocalDateTime으로 보낼 때, 현재시간으로 보내면 스프링부트와 디비시간 모두 한국시간으로 잘 나온다.

 @Query("select r.id as recipeId, r.title as title, r.content as content , r.price as price " +
            "from Recipe r join r.recipeLikesList l " +
            "where l.regDate between :startDate and :endDate " +
            "group by r.id order by count(l.recipe) desc ")
    List<PopularRecipeInterface> findPopularRecipe(LocalDateTime startDate, LocalDateTime endDate);
  • 주의사항: 쿼리를 날릴때 p6spy로 쿼리내용을 보면 2021-10-31T01:45:20.253+0900 처럼 찍힘을 알 수 있다. 처음에 +0900을 보고, 현재시간에 9시간을 더해준다고 오해를 해서, 현재도 한국시간인데 9시간을 또 더하다니?라고 생각을 해서 조금 해맸다. 여기서 +0900은 utc에 비해 9시간을 더해줘서 현재시간이 됐다는 "결과"를 설명해주는 것이다.

JpaRepository에서 sql, jpql로 원하는 칼럼만 조회하기

다음처럼 인터페이스를 하나 만들어야 한다. alias로 설정한 값들에 대한 getter함수를 설정하는 것이다.

@Query(value = "select r.id as recipeId, r.title as title, r.content as content , r.price as price " +
            "from Recipe r join r.recipeLikesList l " +
            "where l.regDate between :startDate and :endDate " +
            "group by r.id order by count(l.recipe) desc ")
    List<PopularRecipeInterface> findPopularRecipe(LocalDateTime startDate, LocalDateTime endDate);
public interface PopularRecipeInterface {
    Long getRecipeId();
    String getTitle();
    String getContent();
    int getPrice();
}

특정기간(일,주,월)동안 좋아요 가장 많이 받은 top3의 레시피 1,2,3등 순서대로 출력하는 로직 sql 고민들.

  • 시도 1 다른 테이블을 join을 하고, join한 테이블의 칼럼을 group by를 하여야 한다. 따라서 데이터 jpa로는 한계가 있다고 판단하여 jpql쿼리를 짰다.
@Query("select r.id " +
        "from Recipe r join r.recipeLikesList l " +
        "where l.regDate between :startDate and :endDate " +
        "group by r.id order by count(l.recipe) ")

List<Long> findPopularRecipeId(LocalDateTime startDate, LocalDateTime endDate);
  • 시도 2 그러나 top3를 하려면 limit키워드를 쓰면 된다 생각했는데, jpql은 limit를 지원하지 않았다...충격... 따라서 native sql로 쿼리를 작성하였다.
@Query(value="SELECT r.recipe_id " +
        "FROM recipe r JOIN recipe_likes l ON r.recipe_id = l.recipe_id " +
        "WHERE l.regdate BETWEEN :startDate AND :endDate " +
        "GROUP BY r.recipe_id order by count(l.recipe_id) desc limit 3",
nativeQuery = true)

List<Long> findPopularRecipeId2(LocalDateTime startDate, LocalDateTime endDate);
  • 시도 3 비즈니스 로직을 짜다보니, top3의 레시피의 id뿐만 아니라, 그 레시피가 가지고 있는 다른 정보들이 필요했다. 가져온 id로 또 레시피정보를 가져오는 쿼리를 보내는 것은 비효율적이라 판단하여, 위 시도2번에서 id를 가져오는 것만이 아니라 레시피의 모든 정보를 다 가져오도록 시도햐였다.
@Query(value = "select re from Recipe re where re.id in (" +
        "select r.id as recipeId " +
        "from Recipe r join r.recipeLikesList l " +
        "where l.regDate between :startDate and :endDate " +
        "group by r.id order by count(l.recipe) desc) " +
        "order by field(re.id, " +
        "select r.id as recipeId " +
        "from Recipe r join r.recipeLikesList l " +
        "where l.regDate between :startDate and :endDate " +
        "group by r.id order by count(l.recipe) desc) ")
List<Recipe> findPopularRecipe2(LocalDateTime startDate, LocalDateTime endDate);

그러나, 문제가 발생..Recipe 전체를 가져오기는 하나, top3의 순서가 지켜지지 않았다...order by field라는 게 있다길래 시도했으나 jpql은 order by field를 지원하지 않았다. 따라서 sql에서 시도를 하고자 하였다.

  • 시도 4 위 시도 3에서 top3의 순서를 지키지 않으며 Recipe를 반환한 것을 보완하고자, order by field를 적용하였다.
  @Query(value = "SELECT * from recipe r2 " +
        "where r2.recipe_id " +
        "in " +
        "(SELECT r.recipe_id " +
        "FROM recipe r JOIN recipe_likes l ON r.recipe_id = l.recipe_id " +
        "WHERE l.regdate BETWEEN '2021-11-01' AND '2021-11-08' " +
        "GROUP BY r.recipe_id order by count(l.recipe_id) desc) " +
        "ORDER BY FIELD(r2.recipe_id,(SELECT r.recipe_id " +
        "                             FROM recipe r JOIN recipe_likes l ON r.recipe_id = l.recipe_id " +
        "                             WHERE l.regdate BETWEEN :startDate AND :endDate " +
        "                             GROUP BY r.recipe_id order by count(l.recipe_id) desc))"
,nativeQuery = true)
List<Recipe> findPopularRecipe3(LocalDateTime startDate, LocalDateTime endDdate);

그러나, 이역시 문제가 발생....order by field는 다음처럼 활용해야 했다. order by field(id, 1,3,4,5) 즉, select문을 넣으니까 오류가 발생한 것이다. 분명히 select문을 넣는 경우가 있을텐데, 방법을 못 찾았을 거라 생각하기에 다음 리팩토링때 보완하고자 한다.

결론: 효율을 위해 많은 시도를 하였지만, 일단 구현을 먼저 하기위해 시도 2번 방법을 선택하였음.