JPA Entity 연관 관계
연관 관계 정의 규칙
- 방향: 단방향, 양방향(객체 참조)
- 연관 관계의 주인: 양방향일 때, 연관 관계에서 관리 주체
- 다중성: 다대일(N:1), 일대다(1:N), 일대일(1:1), 다대다(N:M)
단방향, 양방향
데이터베이스는 외래 키 하나로 양 쪽 테이블 조인 가능(따라서 DB는 방향을 나눌 필요가 없음).
객체는 참조용 필드가 있는 객체만 다른 객체를 참조하는 것이 가능함.
두 객체 사이에 하나의 객체만 참조용 필드를 가지고 참조하면 단방향 관계, 두 객체 모두 각각 참조용 필드를 가지고 참조하면 양방향 관계.
- 엄밀히 따지면 양방향 관계는 없고 두 객체가 단방향 참조를 각각 가져 양방ㅇ향 관계처럼 사용하고 말하는 것
JPA를 사용해 DB와 패러다임을 맞추기 위해 객체는 연관 관계를 가질지 선택해야 함(사용 및 선택 여부는 비즈니스 로직에서 두 객체에 참조가 필요한지 판단)
- board.getPost()처럼 참조가 필요하면 Board → Post 단방향 참조
- 참조가 필요 없는 경우는 사용하지 않는다
- post.getBoard()처럼 참조가 필요하면 Post → Board 단방향 참조
- 참조가 필요 없는 경우는 사용하지 않는다
비즈니스 로직에 맞게 선택했는데 두 객체가 서로 단방향 참조를 했다면 양방향 연관 관계가 되는 것
단방향 연관 관계와 양방향 연관 관계를 구분하는 방법?
기본적으로 단방향 매핑으로 하고 나중에 역방향으로 객체 탐색이 꼭 필요하다고 느낄 때 추가
연관 관계의 주인
두 객체가 양방향 관계(단방향 관계 2개)를 맺을 때, 연관 관계의 주인을 지정해야 함
두 단방향 관계 중 제어의 권한(외래 키를 비롯한 테이블 레코드를 저장, 수정, 삭제 처리)을 갖는 실질적인 관계가 어떤 것인지 JPA에게 알려준다라고 보면 됨
연관 관계의 주인은 관계를 갖는 두 객체 사이에서 조회, 저장, 수정, 삭제가 가능하지만 주인이 아닌 경우 조회만 가능
연관관계의 주인은 주인이 아닌 객체에서 mappedBy 속성을 사용
// 연관관계 주인 필드
@ManyToOne
@JoinColumn(name = "user_id")
private User user;
// 연관관계 필드
@OneToMany(mappedBy = "user")
private List<Food> foodList = new ArrayList<>();
- 관계의 주인이 아닌 객체에서 외래키 주인의 필드명(JoinColumn으로 지정하고 있는 필드명)으로 지정
연관 관계 주인의 필요성
객체에서 양방향 연관 관계 관리 포인트가 두 개일 때 테이블과 매핑을 담당하는 JPA입장에서는 혼란을 주게 됨
- 객체 A에서 B를 수정할 때 FK를 수정할 지, 객체 B에서 A를 수정할 때 FK를 수정할지 결정하지 못함
그렇기 때문에 두 객체 사이의 연관 관계의 주인을 정해서 명확하게 객체 A에서 B를 수정할 때만 FK를 수정한다고 정하는 것임
DB에서 외래 키가 있는 테이블을 수정할 때는 연관 관계의 주인만 변경하면 되지만 객체의 입장에서는 둘 다 변경해주는 것이 좋음(두 참조를 사용하는 순수한 두 객체는 데이터 동기화를 해줘야 하기 때문)
다중성
데이터베이스를 기준으로 다중성을 결정.
연관 관계는 대칭성을 가진다
- 일대다 ↔ 다대일
- 일대일 ↔ 일대일
- 다대다 ↔ 다대다
다대일(N:1)
예) 게시판과 게시글의 관계
- 요구 사항
- 하나의 게시판(1)에는 여러 게시글(N)을 작성할 수 있음
- 하나의 게시글 하나의 게시판에만 작성할 수 있다
- 게시글 게시판은 다대일 관계를 가짐
DB를 기준으로 다중성(게시글N: 게시판1)을 결정
즉, 외래 키를 게시글(N)이 관리하는 일반적인 형태(DB는 무조건 다(N)쪽이 외래 키를 가짐)
→ 다대일(N:1) 단방향
@Entity
public class Post {
@Id @GeneratedValue
@Column(name = "POST_ID")
private Long id;
@Column(name = "TITLE")
private String title;
@ManyToOne
@JoinColumn(name = "BOARD_ID")
private Board board;
//... getter, setter
}
@Entity
public class Board {
@Id @GeneratedValue
private Long id;
private String title;
//... getter, setter
}
- 다대일 단방향에서는 다 쪽인 Post에서 @ManyToOne 만 추가해준 것을 확인할 수 있음
- 반대로 Board에서는 참조하지 않음(단방향 때문)
→다대일(N:1) 양방향
@Entity
public class Post {
@Id @GeneratedValue
@Column(name = "POST_ID")
private Long id;
@Column(name = "TITLE")
private String title;
@ManyToOne
@JoinColumn(name = "BOARD_ID")
private Board board;
//... getter, setter
}
@Entity
public class Board {
@Id @GeneratedValue
private Long id;
private String title;
@OneToMany(mappedBy = "board")
List<Post> posts = new ArrayList<>();
//... getter, setter
}
- 다대일 양방향으로 만드려면 일(1) 쪽에 @OneToMany 를 추가하고 양방향 매핑을 사용했으니 연관 관계의 주인을 mappedBy 로 지정해줌
- mappedBy로 지정할 때 값은 대상이 되는 변수명을 따라 지정하면 됩니다. 여기서는 Post 객체(대상)의 board라는 이름의 변수이기 때문에 board로 지정
일대다(1:N)
다대일의 기준은 연관관계의 주인 다(N)쪽에 둔 것이고 일대다의 기준은 연관관계의 주인을 일(1)쪽에 둔 것입니다.
※ 참고로 실무에서는 일대다(1:N) 단방향은 거의 쓰지 않도록 합니다.
→ 일대다(1:N) 단방향
데이터베이스 입장에서는 무조건 다(N)쪽에서 외래키를 관리
일(1)쪽 객체에서 다(N) 쪽 객체를 조작(생성,수정,삭제)하는 방법
@Entity
public class Post {
@Id @GeneratedValue
@Column(name = "POST_ID")
private Long id;
@Column(name = "TITLE")
private String title;
//... getter, setter
}
@Entity
public class Board {
@Id @GeneratedValue
private Long id;
private String title;
@OneToMany
@JoinColumn(name = "POST_ID") //일대다 단방향을 @JoinColumn필수
List<Post> posts = new ArrayList<>();
//... getter, setter
}
- @OneToMany에 mappedBy가 없어짐, 양방향이 아니기 때문
- @JoinColumn을 이용해서 조인
실제 사용
//...
Post post = new Post();
post.setTitle("가입인사");
entityManager.persist(post); // post 저장
Board board = new Board();
board.setTitle("자유게시판");
board.getPosts().add(post);
entityManager.persist(board); // board 저장
//...
- 동작을 살펴보면, post를 저장할 때는 멀쩡하게 insert 쿼리가 나감.
- board를 저장할 때는 Board를 insert하는 쿼리가 나간 후에 post를 update하는 쿼리가 나감.
- board.getPosts().add(post); 부분 때문에 문제 발생
- Board 엔티티는 Board 테이블에 매핑되기 때문에 Board 테이블에 직접 지정할 수 있으나, Post 테이블의 FK(BOARD_ID)를 저장할 방법이 없기 때문에 조인 및 업데이트 쿼리를 날려야 하는 문제가 있음
치명적인 단점
- 일만 수정한 것 같은데 다른 수정이 생겨 쿼리가 발생하는 것
- Board를 저장했는데 왜 Post가 수정이 되지? 이런 생각을 하게 만듦.
- 업데이트 쿼리 때문에 성능상 이슈는 그렇게 크지는 않음.
일대다(1:N) 단방향 연관 관계 매핑이 필요한 경우는 그냥 다대일(N:1) 양방향 연관 관계를 매핑해버리는게 추후에 유지보수에 훨씬 수월
→ 일대다(1:N) 양방향 (실무 사용 금지 ❌)
키워드는 @JoinColumn(updatable = false, insertable = false) 이지만, 일대다 양방향을 사용해야할 때는 다대일 양방향 사용하도록 하는게 더 좋음
일대일(1:1)
주 테이블에 외래키를 넣을 수도 있고, 대상 테이블에 외래키를 넣을 수도 있음
- 일대일(1:1)이기 때문에 테이블 A, B가 있을 때, A가 주 테이블이면 B가 대상 테이블이고, B가 주 테이블이면 A가 대상 테이블임
→ 일대일(1:1) 단방향
게시글(Post)에 첨부파일(Attach)을 반드시 1개만 첨부할 수 있다고 가정
@Entity
public class Post {
@Id @GeneratedValue
@Column(name = "POST_ID")
private Long id;
@Column(name = "TITLE")
private String title;
@OneToOne
@JoinColumn(name = "ATTACH_ID")
private Attach attach;
//... getter,setter
}
@Entity
public class Attach {
@Id @GeneratedValue
@Column(name = "ATTACH_ID")
private Long id;
private String name;
//... getter, setter
}
→ 일대일(1:1) 양방향
단순하게 똑같이 @OneToOne 설정하고 mappedBy 설정만 해서 읽기 전용으로 만들어주면 양방향도 간단하게 됨
@Entity
public class Attach {
@Id @GeneratedValue
@Column(name = "ATTACH_ID")
private Long id;
private String name;
@OneToOne(mappedBy = "attach")
private Post post;
//... getter, setter
}
Post테이블(주 테이블)이 아닌 Attach테이블(대상 테이블)에 외래 키(FK)를 갖고 있을 때
→ 일대일(1:1) 단방향 지원 안함 ❌
다대다(N:N)
- 실무 사용 금지 ❌
- 중간 테이블이 숨겨져 있기 때문에 자기도 모르는 복잡한 조인의 쿼리(Query)가 발생하는 경우가 생길 수 있기 때문
- 다대다로 자동생성된 중간테이블은 두 객체의 테이블의 외래 키만 저장되기 때문에 문제가 될 확률이 높음
- JPA를 해보면 중간 테이블에 외래 키 외에 다른 정보가 들어가는 경우가 많기 때문에 다대다를 일대다, 다대일로 풀어서 만드는 것(중간 테이블을 Entity로 만드는 것)이 추후 변경에도 유연하게 대처할 수 있음
추가
@Transactional을 사용하는 이유
영속성 컨텍스트의 기능
- 1차 캐시
- 쓰기 지연 저장소
- 변경 감지
- 지연로딩
변경 감지를 하기 위해서는 영속성 컨텍스트 상태여야 하는데 트랜잭션 생명주기 = 영속성 컨텍스트 생명주기이기 때문에 @Transactional 필요, save일 때도 트랜잭션을 걸어주는 이유는 여러 개를 저장하는 상황에서 1개라도 오류가 발생 시 전체적으로 rollback이 필요한 경우 트랜잭션을 걸고, 오류 발생 전의 정상적인 것은 저장해야 할 경우 트랜잭션 미 사용
지연로딩된 Entity 조회를 할 경우 트랜잭션은 필수
'항해 99 > Spring' 카테고리의 다른 글
TDD(Test-Driven Development), JUnit (0) | 2024.03.03 |
---|---|
Swagger 사용하기 (0) | 2024.03.01 |
Spring - Bean, 로그인/회원가입, Security, Validation (1) | 2024.02.26 |
RESTful API, 관심사 분리, @Setter 지양 (0) | 2024.02.25 |
프로젝트 세팅 - UCD, API 명세서, ERD, Git 연동 (0) | 2024.02.22 |