본문 바로가기

개발 일지

[TIL]이노베이션 캠프 81일차

1. 개발 진행 상황 & 개발 중 발생한 이슈와 해결

기본 기능 구현 -> 추가 기능 구현 -> 부가 기능 구현 이런 식으로 진행하는 중이라, 기존에 있던 코드도 필요에 의해 바꾸기도하고 새로운 것과의 에러 때문에 코드를 바꾸기도 하고있다.

 

이번 프로젝트에서 제일 변경이 많았던 부분은 코스와 장소 부분이고 둘의 하나이지만 둘인 그런느낌의 관계도 그렇고, 프론트엔드에서 구현하면서 수정해야할 부분이 많았다.

 

내가 맡은 기능에선 찜하기 기능이 변경이 많았다.

처음에 기능 구현하면서도 이 데이터를 저장해야되나? 고민했던것이 기억난다.

해당 게시글에 총 좋아요 수만 저장되면 되겠다 싶어서 그렇게 구현을 했었는데 마이페이지 기능이 구현되면서 데이터 저장이 필요하다는 것을 알게되었다. 그리고 프론트엔드에서도 현재 이 사용자가 찜하기를 했는 지 안했는 지 여부가 필요하다고했다.

 

그래서 기존에 단순 카운트가 업/다운 되는 방식이 아닌 사용자가 어떤 게시글을 찜하기를 했는지 데이터를 저장하는 코드로 변경을 했다.

(67일차 개발일지)

https://k-sky.tistory.com/135

 

[TIL]이노베이션 캠프 67일차

1. 개발 진행 상황 post 찜하기 기능 코드 리팩토링 기존 코드 // Post @ColumnDefault("0") @Min(0) private int heart; // 코스(게시글) 찜하기 public void addHeart() { this.heart += 1; } // 코스(게시글)..

k-sky.tistory.com

 

그리고 추가로 장소에 대한 찜하기 기능도 따로 구현했다.

그 외에도 내가 구현한 부분아닌 다른 기능에서도 코드는 계속 추가되고 있었다.

각각 기능 구현할 때는 기능은 되니까 문제가 없다고 생각했는데, 이게 거의 다 구현되고 보니 문제가 발생했다.

 

회원(1):게시글(n), 게시글(1):장소(n), 게시글(1): 찜하기(n), 장소(1):찜하기(n) 등 이런식으로 다 관계가 있었고 대부분이 joincoloum으로 조인되어있어서 게시글 CRUD에 에러가 발생하기 시작했다.

 

n:1관계(default: EAGER)냐, 1:n관계냐에 따라 fetch타입 기본형이 다르고, EAGER타입은 조인된 Entity까지 조회가 즉시 되어서 편리했지만 찾아보니 LAZY타입을 권장한다. Post c(1)를 가져오고 연관된 place를 가져오기 위해 place의 개수(N)만큼 SQL을 실행하게 되는 N+1문제를 야기하기 때문인데 이를 해결하기 위한 방법으로 FetchType은 LAZY로 하고, fetchJoin을 적용한다.

 

현재 코드에는 post와 Place는 fetchJoin이 되어있는 상태였지만, LAZY를 적용하면 게시글 전체조회에서 에러가 발생했다.

 

코드를 살펴보니 게시글 전체 조회와 단건 조회가 완전 다른 로직으로 조회되고 있었다.

전체적으로 코드에 수정할 부분이 많았다.

문제점 1. LAZY로 전체 조회 시 에러 해결 하기 위해 fetchJoin을 전체 조회와 단건 조회 둘 다 적용 필요

문제점 2. 전체 조회에 불필요한 코드 삭제

문제점 3. 전체적인 코드 형식 맞추기

 

기존 코드

 

// Post Domain

    @OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
    @JoinColumn(name = "Course_Id")
    private List<Place> place;
    
    
// Controller

    // 코스(게시글) 전체 조회
    @GetMapping("/api/course")
    public List<PostResponseDto> getAllPosts(Model model,
                                  @PageableDefault(size=15, sort="id",
            direction = Sort.Direction.DESC)Pageable pageable) {
        return postService.getAllPost(model,pageable);
    }
    
    // 코스(게시글) 상세 조회
    @GetMapping( "/api/course/{courseId}")
    public Post getPost(@PathVariable Long courseId) {
        return postService.getPost(courseId);
    }   
    
// Repository Interface

    // 코스(게시글) 전체 조회
    List<PostResponseDto> findAllByOrderByModifiedAtDesc(Pageable pageable);
    
    // 코스(게시글) 상세 조회
    @Query(value = "SELECT c" +
            " FROM Post c" +
            " LEFT JOIN FETCH c.place p" +
            " WHERE  c.id = :CourseId")
    Optional<Post> findByJoinPlace(@Param("CourseId") Long CourseId);

// Service

    // 코스(게시글) 전체 조회
    @Transactional(readOnly = true)
    public List<PostResponseDto> getAllPost(Model model, Pageable pageable) {
        List<PostResponseDto> ResponsePage = postRepository.findAllByOrderByModifiedAtDesc(pageable)
                .stream()
                .map(PostResponseDto::new)
                .collect(Collectors.toList());
        model.addAttribute("postPage", ResponsePage);

        return ResponsePage;
    }
    
    // 코스(게시글) 단건 조회
    @Transactional
    public PostResponseDto getPost(Long courseId) {
        Post post = postRepository.findByJoinPlace(courseId).orElseThrow(
                () -> new BusinessException("존재하지 않는 게시글 id 입니다.", ErrorCode.POST_NOT_EXIST)
        );
        return new PostResponseDto(post);
    }

 

수정 코드

// Post Domain

    @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    @JoinColumn(name = "Course_Id")
    private List<Place> place;
    
    
// Controller

    // 코스(게시글) 전체 조회
    @GetMapping("/api/course")
    public Slice<PostResponseDto> getAllPosts(@PageableDefault(size=15, sort="id", direction = Sort.Direction.DESC)Pageable pageable) {
        return postService.getAllPost(pageable);
    }
    
    // 코스(게시글) 단건 조회
    @GetMapping( "/api/course/{courseId}")
    public Slice<PostResponseDto> getPost(@PathVariable Long courseId, @PageableDefault(size=15, sort="id", direction = Sort.Direction.DESC)Pageable pageable) {
        return postService.getPost(courseId, pageable);
    }
    
// Repository Interface

    // 코스(게시글) 전체 조회 + 단건 조회
 	@Query(value = "SELECT c" +
            " FROM Post c" +
            " LEFT JOIN FETCH c.place p")
    Slice<PostResponseDto> findByJoinPlace(Pageable pageable);
    
// Service

    // 코스(게시글) 전체 조회
    @Transactional(readOnly = true)
    public Slice<PostResponseDto> getAllPost(Pageable pageable) {
        return  postRepository.findByJoinPlace(pageable);
    }
    
    // 코스(게시글) 단건 조회
    @Transactional
    public Slice<PostResponseDto> getPost(Long courseId, Pageable pageable) {
        postRepository.findById(courseId).orElseThrow(
                () -> new BusinessException("존재하지 않는 게시글 id 입니다.", ErrorCode.POST_NOT_EXIST)
        );
        return postRepository.findByJoinPlace(pageable);
    }

해결 1. LAZY로 fetch타입을 적용해도 에러가 발생하지 않음, fetchJoin을 전체 조회와 단건 조회 둘 다 적용함

 

해결 2. 전체 조회에 불필요한 코드 삭제

카테고리 조회 구현 하다가 코드 복붙해서 썼던 것이 계속 유지되고 있었음, 간단한 조회만으로 되는 코드라 불필요한 코드를 삭제함

 

해결 3. 전체적인 코드 형식 맞추기

페이징 처리를 slice형식으로 맞춤, 단건 조회는 페이징 처리가 굳이 필요한가 싶기도한데..쿼리가 짧은 느낌은 아니라 그냥 페이징 처리되게 함

 

 

 

2. 참고 레퍼런스

https://jojoldu.tistory.com/165

 

JPA N+1 문제 및 해결방안

안녕하세요? 이번 시간엔 JPA의 N+1 문제에 대해 이야기 해보려고 합니다. 모든 코드는 Github에 있기 때문에 함께 보시면 더 이해하기 쉬우실 것 같습니다. (공부한 내용을 정리하는 Github와 세미

jojoldu.tistory.com

 

3. 오늘 한 일 / 회고

오늘도 코드 수정을 했다.

얕은 이해로 코드 복붙하면서 기능 구현하고 지금은 그 얕은 이해로 짠 코드를 이해하려고 더 공부하면서 아는게 조금 씩 확장되다보니까 

수정할게 많다. 내일도 계속 공부하면서 코드 수정해야지