관계형 데이터베이스 설계
JPA를 이용하여 연관관계를 해석할 때는 PK를 기준으로 잡고, 데이터베이스를 모델링하는 방식으로 구성한다.
위 사진처럼 각각 member,board,reply 테이블을 생성한다.
BaseEntity 클래스
Member 클래스
Board 클래스
Reply 클래스
각 테이블 생성후 각 엔티티에 맞는 Repository 인터페이스를 추가시킨다.
Member 객체를 100개 추가하고, Board 객체 또한 100개 생성하여 추가한다. 여기서 한 명의 사용자가 하나의 게시물을 등록하도록 작성한다. 이후 300개의 댓글을 1~100사이의 번호로 추가한다.(board_bno 칼럼 값이 임의의 번호이다.) 이렇게 1번부터 100번까지 게시물에 대해서 n개의 댓글이 추가된다.
MemberTest
여기서 Member객체부터 추가해주는 이유는 현재 3개의 테이블이 PK와 FK의 관계로 이루어져 있기 때문에 PK 쪽에서부터 시작하는것이다.
BoardTest
ReplyTest
각 테이블에 맞게 데이터가 생성된 것을 볼 수있다.
@ManyToOne
위에서 볼수있듯이 Board 엔티티 클래스는 @ManyToOne 어노테이션으로 writer변수를 지정한 것을 알 수있다.
이것은 다대일, N:1 관계를 말하며 DB설계 후(Board 테이블과 Member 테이블은 N:1 관계) 해당 사항을 반영한 것이다.
@ManyToOne : N:1 관계를 말한다. ( 반대는 @OneToMany )
Reply 엔티티 클래스 또한 마찬가지로 board 를 @ManyToOne 어노테이션으로 연관관계를 지정하였다.
Reply와 Board를 N:1 관계로 지정하였다.
Lazy loading(지연 로딩) / Eager loading(즉시 로딩)
fetch(패치) : " JPA에서 연관관계의 데이터를 어떻게 가져올 것인가 " 를 의미한다.
따라서 연관관계의 어노테이션 속성으로 아래와 같이 'fetch'모드를 지정한다.
- Eager loading: 특정한 엔티티를 조회할 때 연관관계를 가진 모든 엔티티를 같이 로딩
- Lazy loading: 참조 객체들의 데이터들은 무시하고 해당 엔티티의 데이터만을 가져온다.
위 두 가지 방식의 차이점을 예제로 알아본다.
테스트를 위해 rno가 1인 result를 get()으로 가져온다.
실행된 SQL을 보면 reply 테이블 , board 테이블, member 테이블까지 모두 조인으로 처리하여 가져오는 것을 볼 수있다.
Reply를 가져올 때 매번 Board와 Member를 조인해서 가져올 필요는 많지 않으므로 위와 같은 경우, Eagerloading은 효율적이지 못하다.
다음으로 아래의 Lazyloading 예제를 살펴본다.
위와 같이 Board 엔티티 클래스의 writer변수를 Lazyloding모드로 설정해준 후, BoardTest에 아래와 같이 작성한다.
위와 같이 select에 board만 출력되나, no session 오류가 뜨는 것을 볼 수있다. 이는 지연 로딩 방식으로 로딩하기 때문에 board 테이블만을 가져오는 것은 상관없으나, board.getWirter()에서 member 테이블을 로딩해야하기 때문에 오류가 뜨게 된다. 이미 데이터베이스와의 연결은 끝난 상태이기 때문에 member 테이블을 로딩하지 않고 위와 같은 결과를 출력하게 된다.
이럴때, 다시 한번 데이터베이스와 연결이 필요하기 때문에 @Transactional을 추가하여 해결할 수 있다.
상단에 @Transactional을 추가하면 해당 메서드를 하나의 '트랜잭션'으로 처리하라는 의미이기 때문에, 기본적으로 필요할 때 다시 데이터베이스와 연결이 생성된다. 위의 메서드를 다시 실행시키면
총 두번의 SQL문을 실행하게 된다. member 테이블이 필요하기 때문에 생성된 것으로, 지연 로딩을 사용하지 않았을 경우 자동으로 board 테이블과 member 테이블이 조인으로 처리된 것과는 차이가 있다.
여기서 지연 로딩은 조인을 하지 않기 때문에 단순하게 하나의 테이블을 이용하는 경우는 빠르지만, 연관관계가 복잡한 경우엔 여러 번의 쿼리가 실행된다는 단점이 있다.
따라서 보편 적인 코딩 가이드는 "Lazy loading을 기본으로 사용하고, 상황에 맞게 필요한 방법을 찾는다"이다.
(거의 모든 경우 실무에서도 Lazyloading을 사용한다.)
@ToString
@ToString()은 연관관계에서 주의해야할 사항이다.
@ToString() : 해당 클래스의 모든 멤버 변수를 출력한다.
위와 같이 User 클래스에 @ToString 어노테이션을 설정해준 후, 아래와 같이 필드 세팅을 해준다.
여기서 @ToString(exclude = "password")로 설정해준다.
위와 같이 패스워드를 제외하고 클래스명(필드1명=필드1값,필드2명=필드2값,...) 형식으로 출력되는 것을 볼 수있다.
위의 예제를 본 후, 다시 본래의 예제로 돌아오게 되면,
Board 클래스에 @ToString(exclude = "writer")를 설정해준 것을 볼 수있다.
만약 위의 exclude 가 없다면, writer 변수로 선언된 Member 객체 역시 출력해야 한다. 여기서 Member 객체를 출력하기 위해선 Member 객체의 toString() 또한 호출되어야 하고 이때 데이터베이스 연결이 필요하게 된다.
따라서 , 연관관계가 있는 엔티티 클래스의 경우 @ToString()을 할 때는 습관적으로 exclude 속성을 사용해야 한다.
JPQL 과 join
만약 목록 화면에서 게시글의 정보와 댓글의 수를 같이 가져오기 위해서는 단순히 하나의 엔티티 타입을 이용할 수 없다. 이에 대한 해결책으로 대부분 JPQL의 조인을 이용하여 처리한다.
스프링 부트 2버전 이후 JPA 는 엔티티 클래스 내에 전혀 연관관계가 없어도 조인을 이용할 수 있다.
엔티티 클래스 내부에 연관관계가 있을 경우와 없을 경우를 나누어 예제를 살펴본다.
Board 내에는 Member 를 참조하는 writer가 있으므로 엔티티 클래스 내부에 연관관계가 있다. 따라서 b.writer과 같은 형태로 사용한다. 내부에 있는 엔티티를 이용할 때는 'LEFT JOIN' 뒤에 'ON'을 이용하는 부분이 없다.
위의 테스트 코드로 작성한 쿼리문을 확인해보면
위와 같이 지연 로딩으로 처리 되었으나 조인 처리가 되어 한 번에 board 테이블과 member 테이블을 이용하는 것을 볼 수 있다.
그렇다면, 연관 관계가 없는 엔티티 조인 처리는 어떻게 되는지 살펴본다.
Board와 Member 사이에는 내부적 참조를 통한 연관관계가 있지만, Board와 Reply는 상황이 다르다. Reply 쪽이 @ManyToOne으로 참조하고 있으나 Board 입장에서는 Reply 객체들을 참조하고 있지 않다. 이런 경우에 직접 조인에 필요한 조건은 'on'을 이용한다.
'특정 게시물과 해당 게시물에 속한 댓글들을 조회' 하는 경우를 JPQL로 작성해본다.
중간에 'on'이 사용되면서 조인 조건을 직접 지정하는 부분이 추가되었다.
바로 테스트코드를 실행해보면
위와 같이 출력되는 것을 볼 수있다.
'코드로 배우는 스프링부트 웹 프로젝트' 카테고리의 다른 글
Springboot) JPA에서 M:N(다대다) 처리 (1) , N + 1 문제 (0) | 2022.01.03 |
---|---|
Springboot) @Controller와 @RestController의 차이 (0) | 2021.12.28 |
Springboot) 방명록의 조회,수정,삭제 처리 (0) | 2021.12.09 |
Springboot) GET & POST 방식의 차이점 / 등록 페이지와 등록 처리 (1) | 2021.12.09 |
Springboot) 목록 처리 (0) | 2021.12.06 |