티스토리 뷰

안녕하세요 강정호입니다. 오늘을 스프링 부트에서 엔티티를 맵핑하는 방법과 테스트 코드 작성에 대해 알아볼게요.


이렇게 Category와 Board는 다음과 같이 1:N 관계를 맺고 있습니다.

1개의 카테고리는 여러개의 게시물을 갖는 것이지요.



그렇다면 엔티티는 다음과 같이 생성됩니다.


Board 엔티티


@Entity
@Table(name = "board")
@Getter
@Setter
public class Board {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String title;
private String content;
private int readCount;
private LocalDateTime createDate;

@ManyToOne
@JoinColumn(name = "category_id") //실제 FK의 컬럼명이 된다.
private Category category; // 이 보드가 어떤 카테고리에 속해 있는지를 식별해주는 FK

1) @ManyToOne : Board 측면에서 Category와의 관계는 Many - One 관계이기 때문에 @ManyToOne 어노테이션을 사용하게 됩니다.


2) @JoinColumn : FK를 가지는 엔티티가 @JoinColumn 어노테이션을 사용합니다. 논리적으로 Board가 어떤 카테고리에 속하는지를 식별해야하기 때문에 Board가 FK를 가집니다.


3)name="category_id" : 이것이 실제 Board 테이블에 있는 Category테이블의 FK 컬럼명이 된다.



Category 엔티티


@Entity //엔티티이기 때문에 어노테이션 붙인다
@Table(name="category") //엔티티와 관련을 맺고 있는 테이블이 category 테이블이라고 명시
@Getter
@Setter
public class Category {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;

@OneToMany(mappedBy="category")
private List<Board> boards;

1) @OneToMany : Category의 입장에서 Board를 여러개 가지기 때문에 One - Many 관계이다.

 

2) mappedBy : Category가 Board와 어떤 관계를 가지고 있는지를 표시하는 속성이다. Board는 @JoinColumn을 사용해서 Category와의 관계를 표시하지만, Category는 mappedBy를 이용해서 Board와의 관계를 표시한다. mappedBy에 들어가는 것은 Board에서의 Category 객체 변수이다.


** FK를 가지면? @JoinColumn 사용. // FK를 안가지면? mappedBy 사용.





BoardRepository 생성


package examples.boot.myshop.repository;

import examples.boot.myshop.entity.Board;
import org.springframework.data.jpa.repository.JpaRepository;

public interface BoardRepository extends JpaRepository<Board, Long> {
/*
* 이렇게만 인터페이스를 선언해도
* 기본적으로 입력, 수정, 삭제, 조회가 가능하다.
* 이것은 인터페이스이지만 이것을 구현하는 클래스를 만들지 않아도 된다.
* Spring Data JPA가 자동으로 이것을 구현하는 클래스를 프록시 객체로 만들어준다.
* */



CategoryRepository 생성




import examples.boot.myshop.entity.Category;
import org.springframework.data.jpa.repository.JpaRepository;

public interface CategoryRepository extends JpaRepository<Category, Long> {

/*
* 이렇게만 인터페이스를 선언해도
* 기본적으로 입력, 수정, 삭제, 조회가 가능하다.
* 이것은 인터페이스이지만 이것을 구현하는 클래스를 만들지 않아도 된다.
* Spring Data JPA가 자동으로 이것을 구현하는 클래스를 프록시 객체로 만들어준다.
* */

}




테스트 코드1 : 같은 트랜잭션 내에서의 1차 캐시


@RunWith(SpringRunner.class)
@SpringBootTest
@Transactional // test에서 @Transactional을 사용하면 자동 롤백된다.
public class MyshopApplicationTests {

@Autowired //테스트할 클래스를 오토와이어로 주입
CategoryRepository categoryRespository;

@Autowired
BoardRepository boardRepository;

@Test
public void contextLoads() {
}

@Test
public void test1(){
Category category=categoryRespository.getOne(1L);
System.out.println(category.getId());
System.out.println(category.getName());
Category category2 = categoryRespository.getOne(1L);
if(category==category2){
System.out.println("category==category2");
}


위의 test1()의 결과는 다음과 같다.


이상하지 않나요?? 분명히 getOne() 이라는 메서드를 2번 썼는데, 실제로 쿼리는 1번만 작성되었어요.

그 이유는 같은 트랜잭션 내부에서는 1차 캐시가 적용되기 때문입니다. category 인스턴스가 persistence context의 1차 캐시에 저장되어 있어서 2번째 getOne()을 할 때는 캐시에 있던 인스턴스가 반환된 것입니다.


1차 캐시란?

엔티티 매니저가 persist() 또는 find()를 하면 그 엔티티는 managed 상태가 되면서 persistence context의 1차 캐시에 저장이 됩니다. 1차 캐시는 일종의 map으로 생각하면 되는데 key는 @Id이고, value는 엔티티 인스턴스가 된다.



테스트 코드2 : 프록시 객체와 쿼리 실행 시점


@Test
public void test2(){
System.out.println("------------------------------");
System.out.println(categoryRespository.getClass().getName()); //프록시 객체 출력
Category category = categoryRespository.getOne(1L); //Category 쿼리문 실행
List<Board> boards=category.getBoards(); //Board 쿼리가 실행되지 않았다.
System.out.println("------------------------------");
System.out.println(boards.getClass().getName()); //PersistenceBag
System.out.println("------------------------------");

for(Board board : boards){ //실제로 Board 쿼리가 실행된 시점
System.out.println(board.getTitle());
}
System.out.println("------------------------------");


위의 테스트 코드 결과는 다음과 같다


쿼리가 실행되지 않고, getTitle 할 때 쿼리가 실행된 이유?? 이건 선생님께 다시 질문






테스트 코드 3 : 쿼리 메서드를 이용해서 값 가져오기



@Test
public void test3(){
List<Board> list=boardRepository.findAllByName("kim");
//System.out.println(list);
for(Board board : list){
System.out.println(board.getTitle());
}
}

실제로 findAllByName 이라는 메서드는 없다. 하지만 BoardRepository에서 해당 메서드를 만들 때 메서드의 이름이 자동완성 되는 것을 알 수 있다. 그 이유는 Spring Data JPA가 메서드 이름으로 쿼리문을 만들기 때문이다. 그것이 바로 "Query Method"이다.


public interface BoardRepository extends JpaRepository<Board, Long> {
public List<Board> findAllByName(String name);

}


결과는 다음과 같다.





테스트 코드 4 : 1+N문제 코드와 JPQL



@Test
public void test4(){
List<Board> list1=boardRepository.findAll(); //이것은 1+N문제를 발생시킨다.
List<Board> list2=boardRepository.getBoards();//쿼리가 1번만 실행된다. 조인한 결과를 가져올 수 있다.

for(Board board : list2){
System.out.println(board.getTitle());
System.out.println(board.getCategory().getName());
}


}


쿼리 결과문


노란색 사각형이 1+N 쿼리를 실행하는 부분이고

연두색 사각형이 JPQL을 사용하여 쿼리를 1번만 실행시키는 부분이다.


밑에 출력문은 JPQL을 이용하여 출력한 결과이다.


그럼 1+N 문제는 무엇일까?  1+N 문제   <--- 여기 링크에서 확인하라




댓글