@OneToMany의 로딩 바식은 기본적으로 지연(Lazy) 로딩
게시물을 조회하는 경우 Board 객체와 BoardImage 객체들을 생성해야 하므로 2번의 select가 필요
testReadWithImage

testReadWithImages()를 실행하면 우선 board 테이블에 대한 select가 일어난 후에 에러가 발생하게 됨
이미지
실행 결과를 보면 log.info()로 Board의 출력까지 끝난 후에 다시 select를 실행하려고하는데 데이터베이스와 연결이 끝난 상태이므로 'no session'이라는 메시지가 뜨는 것을 볼 수 있다
해결 방안
@Transactional을 적용하면 필요할 때마다 메소드 내에 추가적인 쿼리를 여러번 실행하는 것이 가능해짐
@EntityGraph란?
연관관계가 있는 엔티티를 조회할 경우 지연 로딩으로 설정되어 있으면 연관관계에서 종속된 엔티티는 쿼리 실행 시 select 되지 않고 proxy 객체를 만들어 엔티티가 적용시킨다. 그 후 해당 프록시 객체를 호출할 때마다 그때그때 select 쿼리가 실행된다.
프록시 객체는 실제 엔티티의 대리자로서 엔티티의 상태가 실제로 필요할 때까지 데이터베이스에서 로딩되지 않음
결론
한 번에 조인 처리해서 select가 이루어지도록 하는 방법
BoardRepository

EntityGraph에는 attributePaths라는 속성을 이용해서 같이 로딩해야하는 속성을 명시할 수 있다
결과

실행결과를 보면 board 테이블과 board_image 테이블의 조인 처리가 된 상태로 select가 실행되면서 BoardImage를 한 번에 처리할 수 있게 된 것을 확인할 수 있음
게시물과 첨부파일의 수정은 다른 엔티티들 간의 관계와는 조금 다른 점이 있음
실제 처리 과정에서 첨부파일은 그 자체가 변경되는 것이 아니라 기존의 모든 첨부파일이 삭제되고 새로운 첨부파일들로 추가되기 때문
EntityGraph에는 attributePaths라는 속성을 이용해서 같이 로딩해야하는 속성을 명시할 수 있습니다.
테스트 코드에 findByIdWithImages()를 이용하도록 수정
BoardRepositoryTest

테스트 결과로 새로운 BoardImage의 insert와 기존 BoardImage의 update가 이루어짐
현재 cascade 속성이 All로 지정되었기 때문에 상위 엔티티(Board)의 상태 변화가 하위 엔티티(BoardImage)까지 영향을 주긴 했지만 삭제되지는 않았기 때문에, 만일 하위 엔티티의 참조가 더 이상 없는 상태가 되면 @OneToMany에 orphanRemoval 속성값을 true로 지정해주어야만 실제 삭제가 이루어짐
Board


게시물 삭제에는 게시물을 사용하는 댓글들을 먼저 삭제해야함
(다만 이 경우 다른 사용자가 만든 데이터를 삭제하는 것은 문제가 될 수 있으므로 주의)
ReplyRepository
특정한 게시물에 해당하는 데이터를 삭제하는 쿼리 메소드 추가

BoardRepositoryTests


결과
Board,BoardImage,Reply는 존재하는 경우 댓글 삭제
상위 엔티티에서 @OneToMany과 같은 연관 관계를 유지하는 경우 한번에 게시물과 첨부파일을 같이 처리할 수 있다는 장점도 있는 반면 목록 처리 시 예상치 못한 문제를 만들기 때문에 주의
정확한 테스트를 위해 기존의 Board,BoardImage,Reply를 모두 삭제
drop table board_image;
drop table reply;
drop table board;
BoardRepositoryTests

testInsertAll()은 번호가 5의 배수인 경우 첨부파일이 없는 게시물이 작성되고 나머지는 3개의 첨부파일이 있는 상태가 되도록 구성
목록 데이터를 처리하기 위해서 Querydsl을 이용하는 BoardSearch 인터페이스에 메소드 추가
Board

BoardSearchImpl

BoardSearchImpl의 searchWithAll() 내용은 Board와 Reply를 left join 처리하고 쿼리를 실행해서 내용을 확인하는 것
BoardRepositoryTests

실행되는 쿼리들을 살펴보면 다음 그림과 같이 목록을 가져오는 쿼리 한 번과 하나의 게시물마다 board_image에 대한 쿼리가 실행되는 상황을 볼 수 있는데, 이것을 'N+1' 문제라고 한다(N은 게시물마다 각각 실행되는 쿼리, 1은 목록을 가져오는 쿼리)
1) Board에 대한 페이징 처리가 실행되면서 limit로 처리
2) System.out.println()을 통해 Board의 bno 값을 출력
3) Board 객체의 imageSet을 가져오기 위해서 board_image 테이블을 조회하는 쿼리 실행
4) 2,3의 과정이 반복적으로 실행
N+1으로 실행되는 쿼리는 데이터베이스를 엄청나게 많이 사용하기 때문에 문제가 된다. 이를 해결하기 위해 @BatchSize를 이용하는 것. @BatchSize에는 size라는 속성을 지정해서 N번에 해당하는 쿼리를 모아서 한 번에 실행할 수 있다.
Board

@BatchSize의 size의 지정된 수만큼 BoardImage를 조회할 때 한 번에 in 조건으로 사용된다.
in 조건은 조건의 범위를 지정하는 데 사용되며, 지정된 값 중에서 하나 이상과 일치하면 조건에 맞는 것으로 처리됨.
@BatchSize를 사용하는 이유 : 모델이 한 번에 몇 개의 데이터를 처리하느냐를 결정하는 값
배치 크기는 모델이 데이터를 한 번에 처리하는 양을 나타내며, 이렇게 여러 이미지를 동시에 처리함으로써 모델의 가중치를 효율적으로 업데이트할 수 있습니다. 작은 배치 크기는 더 자주 업데이트되지만 노이즈가 많을 수 있고, 큰 배치 크기는 더 안정적인 그레이디언트(gradient)를 제공할 수 있지만 메모리 요구가 높을 수 있습니다.
목록과 관련된 처리는 반드시 limit와 같은 페이징 처리가 실행되는지 체크해야한다. limit가 없다면 테이블의 모든 데이터에 대한 처리가 이루어진다는 것을 의미하기 때문에 성능에 영향을 주게 된다. @EntityGraph를 이용해서 목록을 처리하지 않는 이유이기도 함
추가로 한 번 더 쿼리가 실행되기는 하지만 Board와 BoardImage들을 한 번에 처리할 수 있다는 점은 분명히 장점이 될 수 있으므로 해당 결과에 댓글 개수를 처리하도록 수정해서 최종적으로 DTO를 구성하도록 합니다.
엔티티 객체를 DTO로 변환하는 방식은 ModelMapper를 이용하거나, Projections를 이용했지만 Board 객체 안에 Set과 같이 중첩된 구조를 처리할 경우에는 직접 튜플(Tuple)을 이용해서 DTO로 변환하는 방식을 사용하는 것이 편리
BoardListAllDTO
dto 패키지에 Board와 BoardImage, 댓글 개수를 모두 반영할 수 있는 BoardListAllDTO 클래스와 BoardImage 엔티티를 처리하기 위한 BoardImageDTO 클래스를 추가

BoardService
listWithAll()추가

BoardServiceImpl
메소드 틀만 작성
@Override
public PageResponseDTO<BoardListAllDTO> listWithAll(PageRequestDTO pageRequestDTO){
return null;
}
Querydsl을 이용해서 동적 쿼리를 처리하는 BoardSearch와 BoardSearchImpl 클래스의 리턴 타입은 BoardListAllDTO 타입으로 수정
BoardSearchImpl
다양한 필터 옵션을 사용하는 게시물에 대한 검색 기능을 구현하고 있으며, 페이지네이션 및 댓글 수의 집계를 지원. 또한 연관된 BoardImage 엔터티를 DTO로 매핑하는 작업을 처리


List<Tuple>을 이용하는 방식은 Projections를 이용하는 방식보다 번거롭기는 하지만, 코드를 통해서 마음대로 커스터마이징 할 수 있다는 장점도 있다. 앞의 코드에서는 List<Tuple>의 결과를 List<BoardListAllDTO>로 변경하고 있다.(아직 BoardImage에 대한 처리는 하지 않은 상태)
BoardServiceImpl
Board 객체 내 BoardImage 들을 추출해서 BoardImageDTO로 변환하는 코드를 추가
BoardSearchImpl내 searchWithAll() 내 List<Tuple>을 처리하는 부분을 아래와 같이 작성

최종적으로 Querydsl을 이용해서 페이징 처리하기 전에 검색 조건과 키워드를 사용하는 부분의 코드를 추가해서 searchWithAll()을 완성
