도입
이번 시간에는 JPA에서 엔티티를 객체적으로 묶어보는 시간을 가져보려고 한다. DB처럼 구현을 하게 될 경우 연관이 되어있는 클래스에는 다른 엔티티의 key값을 넣어주게 되는데, 그러면 다시 그 키값으로 객체를 찾아주어야하는 번거로움이 생긴다. 따라서 이번시간에는 객체 자체를 받을 수 있는 연관관계 매핑을 알아보도록 하겠다.
*지난 시간에는 기본키를 매핑하는 방법과 키값을 자동으로 주입시키는 전략을 알아보았다.
2022.01.12 - [Tech-Stack & Language/JPA] - JPA 기본 키 매핑하기
연관관계의 매핑
연관관계의 매핑이 필요한 이유는 객체지향적으로 코드를 구현하기 위해서이다. DB의 테이블에서는 연관관계를 구사하기 위하여 외래키를 사용한다. 테이블과 동일하게 객체에 외래키를 넣어준다면 객체와 연관되어있는 객체를 찾기 위하여 그 키 값으로 객체를 또 찾아주어야하는 번거로움이 있다. 하지만 객체적인 구현이라면 다른 객체를 참조하는 값을 필드로 가지고 있으면 된다. 이렇게 구현을 하기 위해서 연관관계 매핑을 해주는 것이다.
단방향 연관관계
단방향 연관관계를 연관관계를 단방향으로 수행하는 것을 말한다. 축구선수와 축구팀이 있다고 생각해보자. 축구팀 1개에는 축구선수 여러명이 포함될 수 있다. 그렇다면 축구선수와 축구팀의 관계는 1:n이 된다.
팀과 축구선수를 테이블로 저장을 한다면 위와 같은 관계로 들어갈 것이다. 이 때 축구선수에 외래키로 팀의 키가 들어가게 된다.
//Team Class
@Entity
public class Team {
@Id @GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
}
//Player Class
@Entity
public class Player {
@Id @GeneratedValue
@Column(name ="PLAYER_ID")
private Long id;
@ManyToOne
@JoinColumn(name = "TEME_ID")
private Team team;
}
annotation을 사용하여 연관관계를 매핑하게 된다. 단방향으로 매핑할 경우 한 곳에서만 연관관계를 설정하여주면 된다. Player에서 Team으로 단방향 연관관계를 지정하면 @ManyToOne을 참조하는 객체에게 해주면 된다. 여기서 ManyToOne인 이유는 Player가 n이고 Team이 1의 관계이기 때문이다. 그리고 join해줄때 테이블에서 외래키로 사용되는 PK를 입력해주면 된다.
단방향에서 양방향으로
위처럼 실행시키는 것 만으로 충분히 객체와 연관관계를 구성해줄 수 있다. 초기의 설계는 모두 단방향으로 표현하여 연관관계를 매핑하는 것이 바람직하다. 단방향 연관관계에서 양방향으로 설계는 추후 개발을 하다가 연관관계가 없는 반대편 객체에서 계속 여기 객체를 조회해야하는 요구가 생겨 구현을 해주어도 상관이 없다. 왜냐하면 단방향에서 양방향으로 변경되었다고 하여서 테이블의 변화가 전혀 없기 때문이다.
양방향 연관관계
양방향 연관관계를 구현할 때는 RDB와 객체의 구현의 차이점으로인해 발생하는 문제를 정리할 필요가 있다. 양방향으로 매핑을 해주면 RDB의 테이블은 달라질까? 아니다. RDB의 경우 그냥 조인해서 사용해주면 되기 때문에 외래키는 한개로 그대로 수행될 것이다. 그렇다면, 양방향으로 연관을 지어준다는 의미는 객체끼리 서로 단방향으로 쏴주는 모습이 된다는 의미이다.
이렇게 될 경우 두개의 객체가 서로의 값을 바꿔준다고 생각해보자. A팀의 A_a 선수가 있다. A팀 객체에서 선수목록 중에 A_a선수를 B팀으로 변경시켰다. A_a선수 객체는 팀을 C팀으로 변경하였다. 그렇게 되면 트렌젝션이 종료되었을 때, A_a선수의 팀은 어디로 설정되어 있는게 맞을까? 굉장히 애매한 상황이 발생한다.
이렇게 애매한 상황을 해결하기 위하여 연관관계의 주인을 설정하여준다.
연관관계의 주인
연관관계의 주인은 양방향으로 연관되어있는 두 객체 중에서 값을 변경할 수 있는 객체를 의미한다. 양방향 연관관계의 주인을 찾는 방법은 양방향 연관관계를 구현하면서 mappedBy 값을 지정해줌으로써 설정해줄 수 있다.
//Team Class
@Entity
public class Team {
@Id @GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
@OneToMany(mappedBy = "team")
List<Player> players;
}
//Player Class
@Entity
public class Player {
@Id @GeneratedValue
@Column(name ="PLAYER_ID")
private Long id;
@ManyToOne
@JoinColumn(name = "TEME_ID")
private Team team;
}
Team이 1 그리고 Player가 n 이기 때문에 annotation은 @OneToMany가 된다. 그리고 Player를 연관관계의 주인으로 잡아주기 위하여 Team에 mappedBy값을 넣어주어, Player의 team객체를 지정해서 연관관계를 설정해주었다.
객체 두개 중 연관관계의 주인을 누구로 설정해주어야 좋을까? 비즈니스로직을 고려하여 연관관계의 주인을 잡아주는 것도 되지만, 되도록이면 테이블에서 외래키를 가지고 있는 쪽으로 잡아주는 것이 바람직하다. ManyToOne에서는 당연히 Many의 부분의 값이 외래키를 가지고 있으니, Many를 연관관계의 주인으로 잡아주면 된다. 이유는 타 개발자들이 이해를 할 수 있기 위해서 + 성능 때문이다. 내가 변경을 요청한 테이블에 쿼리가 날라가는게 아닌, 다른 테이블로 쿼리가 날라가는 모습을 보면 왜 그런지 이해를 못할 수도 있다.
정리해보면 연관관계의 주인은 항상 외래키를 가지고 있는 쪽에서 수행해 주는 것이 좋다. 하지만 그렇다고 해서 다른 쪽에서 외래키를 갖지 못하는 것은 아니다. 각각이 외래키를 갖는 것을 알아보겠다.
@ManyToOne
ManyToOne은 1:n에서 n에 위치한 객체이고, 외래키를 보유한 객체이므로 연관관계의 주인이 될 경우 크게 문제는 없다.
@OneToMany
JPA 스펙으로 OneToMany가 연관관계의 주인이 될 수 있다. 그러나 OneToMany가 주인이 될 경우 변경이 될 경우 다른 테이블로 업데이트 쿼리를 추가로 날려주어야 해서, 다른 사람이 봤을때 복잡할 수 있고 성능적으로도 조금 문제가 있다. 그리고 양방향으로 연관관계를 맺어주려고 ManyToOne에 mappedBy를 사용해 매핑할 수 없다.(mappedBy를 지원하지 않는다.)
그리고 Many쪽에서 One으로의 참조가 전혀 없어서 OneToMany를 써야할 것 같으면 그냥 ManyToOne을 사용해서 양방향, 연관관계편의 메소드를 사용해서 해결할 수 있다.(물론 n쪽이 의미없는 참조가 생길 수 있지만..)
@OneToOne
OneToOne의 경우 두개의 테이블 중 어느 곳에서든 외래키를 보유할 수 있다. ManyToOne과 다른점은 외래키에 유니크 제약조건이 걸려있다는 것 뿐이다. 그런데 문제는 외래키를 어느 곳으로 설정하냐에 따라 연관관계의 주인이 변경된다. 연관관계의 주인은 항상 자신의 테이블에 외래키가 있는 경우에만 할 수 있다.(타 테이블에 외래키가 있을 경우 연관관계의 주인이 될 수 없음)
외래키를 어느 테이블에 맵핑을 해야할지 판단을 해야하는데 그 부분은 상황을 잘 판단해서 DBA분들과 함께 조율해야한다.
@ManyToMany
ManyToMany는 실무에서는 사용할 수 없다. ManyToMany는 테이블적으로 매핑이 전혀 불가능하기 때문에 DB에서도 중간테이블을 두어 양 옆 테이블의 PK를 FK로 받아서 @ManyToOne, @OneToMany로 풀어서 사용한다. 굉장히 편리해보이는데, 이것을 사용하지 못하는 이유는 중간 테이블을 건드릴 수 없기 때문이다. 중간 테이블에는 단순하게 FK만 저장되면 안되고 매우 여러가지 추가적인 정보가 들어가 주어야한다. 따라서 중간에서 매개역할을 할 수 있는 Entity를 하나 생성하여 @ManyToOne, @OneToMany로 해결하면 된다. (물론 중간 테이블이 둘 다 @ManyToOne이 될 것이다.
이 때 구현을 하며 고민을 해봐야하는 부분은 FK 두개를 묶어서 PK로 사용할 것인지 혹은 PK를 따로 생성할 것인지의 여부이다.
양방향 연관관계에서 자주 하는 실수
양방향 연관관계로 매핑을하게 될 경우 자주하는 실수 중 하나는 연관관계의 주인이 아닌 값으로 변경을 요청하는 경우다. 연관관계의 주인이 아닌 값으로 변경을 요청하게되면 변경이 이루어지지 않아 생각했던 로직대로 결과가 나오지 않을 수 있다.
그리고 하나의 트랜젝션 안에서 연관관계의 주인에게 입력을 하였다고 하더라도, 트랜젝션이 커밋되지 않은 상황에는 Team에서 Player를 조회해도 조회된 값이 없을 것이다. 따라서 양방향으로 구현을 해주고 값을 넣어줄 때는 양쪽 모두 값을 세팅해주는 것이 바람직하다. 매번 양쪽에 값을 세팅해주는 것을 잊어버릴 수 있으므로 양방향으로 값을 변경해주는 연관관계 편의 메소드를 구현하여 사용할 수 도 있다.
연관관계 편의 메소드
연관관계 편의 메소드를 구현하는 방법은 setter처럼 구현을 해주면 되는데, 양 방향으로 연결만 해주면 된다.
//Team Class
@Entity
public class Team {
@Id @GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
@OneToMany(mappedBy = "team")
private List<Player> players;
public List<Player> getPlayers(){
return players;
}
}
//Player Class
@Entity
public class Player {
@Id @GeneratedValue
@Column(name ="PLAYER_ID")
private Long id;
@ManyToOne
@JoinColumn(name = "TEME_ID")
private Team team;
public void changeTeam(Team team){
team.getPlayers().add(this);
this.team = team;
}
}
위 changeTeam이 양방향 연관관계 메소드이다. Player에서 team을 변경해줄 때 해당 Team에서의 값을 변경해준다.
연관관계편의메소드를 양방향에서 모두 구현하지 않아도 되고, 사용에 용이해 보이는 곳인 한곳에만 구현해서 사용해주면 된다. 만약 두 객체 모두에 구현을 하게 되고 잘못 사용하게 된다면 무한루프에 빠져 서버가 죽게되는 문제가 발생할 수 도 있다.
이 글은 김영한님의 강의 "자바 ORM 표준 JPA 프로그래밍"를 공부한 후 작성한 글 입니다.
'Back-end > JPA' 카테고리의 다른 글
JPA 프록시 객체와 지연로딩 (0) | 2022.02.02 |
---|---|
JPA 상속관계 매핑하기(+ @MappedSuperclass) (0) | 2022.01.16 |
JPA 기본 키 매핑하기 (0) | 2022.01.12 |
JPA로 필드와 칼럼 매핑하는 annotation 알아보기 (0) | 2022.01.10 |
JPA로 객체와 테이블 매핑하기 (0) | 2022.01.07 |