[Spring] @ManyToOne 어노테이션 2 default_batch_fetch_size 활용한 데이터 출력

류재성's avatar
Mar 18, 2024
[Spring] @ManyToOne 어노테이션 2 default_batch_fetch_size 활용한 데이터 출력
 
💡
default_batch_fetch_size는 Hibernate ORM에서 사용하는 설정 값 중 하나로, 한 번의 쿼리로 가져올 엔티티의 최대 수를 지정할 수 있다. 이 설정은 성능 최적화에 주로 사용되며, 특히 1+N 쿼리 문제를 해결하는 데 유용하다.
 

1. fetch 속성 Lazy, Eager

 
public List<Board> findAll(){ Query query = em.createQuery("select b from Board b order by b.id desc",Board.class); return query.getResultList(); }
 
 
notion image
 
@Test public void findAll_test(){ boardReposiroty.findAll(); }
notion image
 
💡
Lazy 일 때 쿼리 한 번 발동
 
 
 
 
notion image
 
 
 
notion image
 
💡
사진에선 잘렸지만 user 관련 쿼리가 3번 발동된다.
 
insert into user_tb(username,password,email,created_at) values ('ssar','1234','ssar@nate.com',now()); insert into user_tb(username,password,email,created_at) values ('cos','1234','cos@nate.com',now()); insert into user_tb(username,password,email,created_at) values ('love','1234','love@nate.com',now()); insert into board_tb(title,content,user_id,created_at) values ('제목1','내용1',1,now()); insert into board_tb(title,content,user_id,created_at) values ('제목2','내용2',1,now()); insert into board_tb(title,content,user_id,created_at) values ('제목3','내용3',2,now()); insert into board_tb(title,content,user_id,created_at) values ('제목4','내용4',3,now());
 
💡
쿼리가 3번 발동하는 이유는 게시글을 작성한 유저가 총 3명이기 때문이다. 모든 더미의 USER ID 를 1로 변경하면 쿼리가 한 번만 발동한다.
 
notion image
 
💡
Eager 속성일 때, findById 는 조인을 해서 데이터를 가져왔는데, findAll 일 때는 Board 만 조회한 뒤에 User가 게시글을 쓴 수만큼 select 절이 더 발생한다. 컬렉션 조회시 Eager 는 쓰면 안된다.
 
 

2. Lazy Loading 상태일 때

 
💡
만약 Lazy 속성 상태일 때 Board 객체에 User 를 조회하게 된다면 어떻게 될까?
//board의 객체만 조회 후 board 객체에 있는 user의 username을 조회했을 때 @Test public void findAll_lazyLoading_test(){ List<Board> boardList = boardReposiroty.findAll(); boardList.forEach(board -> { System.out.println(board.getUser().getUsername()); }); }
 
notion image
 
💡
Eager 속성과 마찬가지로 Board 를 조회 후 여러번 쿼리를 날려 user 데이터를 조회한다.
 
 

3. default_batch_fetch_size 사용했을 때

 
hibernate: ddl-auto: create show-sql: true properties: hibernate: format_sql: true default_batch_fetch_size: 10 //Lazy 속성일 때 인쿼리를 만들어 defer-datasource-initialization: true
 
💡
default_batch_fetch_size 의 값 만큼 in 쿼리의 사이즈가 정해진다.
 
notion image
 
💡
default_batch_fetch_size Board 엔티티가 먼저 조회되고, User 엔티티가 조회될 때 다시 쿼리를 한 번 더 날려서 데이터를 조회한다. 이때 In 쿼리가 발동되며 In쿼리로 여러 데이터를 간편하게 조회할 수 있다.
 

4. In 연산자

 
💡
IN 연산자는 SQL에서 사용되는 조건 연산자 중 하나로, 주어진 값의 목록 중에서 하나라도 일치하는 값이 있는지를 검사할 때 사용된다. 이 연산자는 특히 여러 개의 가능한 값 중에서 선택해야 할 때 유용하며, WHERE 절에서 사용할 수 있다.
예를 들어, 특정 사용자들의 ID로 사용자 정보를 조회하고 싶을 때, 각 ID를 개별적으로 비교하기보다는 IN 연산자를 사용하여 쿼리를 더 간단하고 읽기 쉽게 만들 수 있다.
 
User 테이블의 id가 1,2,3,4 중 하나와 일치하는 테이블을 조회
SELECT u FROM User WHERE u.id in (1, 2, 3, 4);
 
 
 

5. In연산자 동적쿼리로 만들기

 
💡
default_batch_fetch_size 를 쿼리로 직접 구현해보자.
 
public List<Board> findAllV2(){ // Board 테이블 조회 String q1 = "select b from Board b order by b.id desc"; List<Board> boardList = em.createQuery(q1,Board.class).getResultList(); //Board 테이블의 user_id 뽑아오기 int[] userId = boardList.stream().mapToInt(board -> board.getUser().getId()).distinct().toArray(); //만들어야 할 인쿼리 select u from User u where u.id in(":id1",":id2",":id3") //동적쿼리 만들기 String q2 = "select u from User u where u.id in("; for(int i=0;i<userId.length;i++){ if(i==userId.length-1){ q2=q2+":id"+i+")"; }else{ q2=q2+":id"+i+","; } } Query query =em.createQuery(q2,User.class); for(int i =0;i<userId.length;i++){ query.setParameter("id"+i,userId[i]); } List<User> userList = query.getResultList(); // 찾은 user_id를 이용해 Board 에 User 객체 넣기 boardList.stream().forEach(b -> { User user = userList.stream().filter(u -> u.getId() == b.getUser().getId()).findFirst().get(); b.setUser(user); }); //이렇게도 가능 // for (Board board : boardList){ // for (int i = 0; i < userList.size(); i++) { // User user = userList.get(i); // if (user.getId() == board.getUser().getId()){ // board.setUser(user); // } // } // } return boardList; }
 
notion image
 
테스트를 했을 때 Board 조회 이후에 user 를 조회할 때 1번의 쿼리로 User 객체를 모두 조회했다.
 
💡
Lazy 상태에서 조인 쿼리를 쓰는 것 보다 두 번의 조회가 속도면에서 훨씬 유리하다. 그러므로 default_batch_fetch_size 를 사용하자.
 

6. default_batch_fetch_size 를 사용해 데이터 출력하기

BoardRepository
public List<Board> findAll(){ Query query = em.createQuery("select b from Board b order by b.id desc",Board.class); return query.getResultList(); }
 
BoardController
@GetMapping({ "/"}) public String index(HttpServletRequest request) { List<Board> boardList = boardReposiroty.findAll(); request.setAttribute("boardList",boardList); return "index"; // 리퀘스트디스패쳐 방식으로 가방을 내부적으로 전달. }
 
index.mustache
{{#boardList}} <div class="card mb-3"> <div class="card-body"> <h4 class="card-title">{{title}}</h4> <a href="/board/{{id}}" class="btn btn-primary">상세보기</a> </div> </div> {{/boardList}}
 
notion image
 
notion image
 
💡
Board 데이터만 필요한 페이지에서는 Board 쿼리만 전달된다.
 
notion image
 
BoardController
@GetMapping("/board/{id}") public String detail(@PathVariable Integer id,HttpServletRequest request) { // int 를 쓰면 값이 없으면 0, Integer 를 넣으면 값이 없을 때 null 값이 들어옴. Board board = boardReposiroty.findById(id); request.setAttribute("board",board); return "board/detail"; }
 
BoardRepository
public Board findById(int id){ Board board = em.find(Board.class,id); return board; }
 
notion image
notion image
 
💡
만약 조인이 필요한 페이지라면 Board 데이터가 조회되고, User 데이터 두번 조회된다. 조인 쿼리보다 속도 면에서 훨씬 좋다.
 
Share article
RSSPowered by inblog