[JPA] 연관관계 매핑 - 양방향 연관관계

leehyunjon·2022년 7월 18일
0

JPA

목록 보기
7/10

양방향 연관관계

양방향 연관관계 매핑

단방향 연관관계에서는 Member가 Team을 참조로 조회할 수 있었지만, Team에서는 Member를 조회할 수 있는 필드가 없었다.

하지만 양방향 연관관계에서는 Member가 Team을 참조로 조회하고 Team에서도 Member를 참조로 조회할 수 있다.

비록 Member에서 Team을 참조, Team에서 Member를 참조하는 단방향 두개로 이루어져있는것이지만.

@Entity
public class Member{
	@Id
    Long id;
    
    @ManyToOne
    Team team;
    
    String username;
}

@Entity
public class Team{
	@Id
    Long id;
    
    @OneToMany(mappedBy = "team")
    List<Member> members = new ArrayList<>();
    
    String name;
}

Member와 Team은 다대일(N:1)의 관계였지만 반대로 Team과 Member는 일대다(1:N)의 관계를 가진다. 그렇기 때문에 Member는 Team과의 참조에 @ManyToOne으로 다대일 관계를, Team은 Member의 참조에 @OneToMany으로 일대다 관계를 만들어준다.

이렇게 양방향 관계를 맺어주게되면 양방향에서 객체 그래프 탐색이 가능해진다.

Team team = em.find(Team.class, 1);
List<Member> members = team.getMembers();

for(Member m : members){
	System.out.println("username : "+m.getUsername();
}

//결과
username : 회원1
username : 회원2

@OneToMany

@OneToMany 어노테이션으로 일대다 관계를 맺어주려면 List< Member > members를 추가해 일대다 관계를 맺어줄 수 있다.

@OneToMany의 옵션인 mappedBy 속성은 일대다 반대쪽 매핑 필드명을 속성 값으로 설정한다.

또한 반대쪽 엔티티와 양방향 연관관계가 있음을 알려준다.


연관관계 주인

연관관계 주인 : 두 연관관계 중 하나를 정해서 테이블의 외래키를 관리하는 것.

객체 연관관계는 두개의 참조를 이용해 연관관계를 형성하고 테이블 연관관계는 하나의 외리캐를 이용해 연관관계를 형성한다.

양방향 연관관계를 맺음으로써 객체는 두개의 참조를, 테이블은 하나의 외래키를 사용하기 때문에 두개의 차이가 발생하게 된다.

그렇기 때문에 하나의 엔티티에서 외래키를 관리해주어야한다. 이 역할을 하는 것이 바로 연관관계의 주인이다.

연관관계 매핑의 규칙

  • 연관관계 주인만이 데이터베이스 연관관계와 매핑된다.

  • 연관관계 주인만이 외래키를 관리(등록, 수정, 삭제)할 수 있다.

  • 연관관계 주인이 아니라면 읽기만 할 수 있다.

mappedBy

참조 : https://velog.io/@conatuseus/%EC%97%B0%EA%B4%80%EA%B4%80%EA%B3%84-%EB%A7%A4%ED%95%91-%EA%B8%B0%EC%B4%88-2-%EC%96%91%EB%B0%A9%ED%96%A5-%EC%97%B0%EA%B4%80%EA%B4%80%EA%B3%84%EC%99%80-%EC%97%B0%EA%B4%80%EA%B4%80%EA%B3%84%EC%9D%98-%EC%A3%BC%EC%9D%B8

연관관계 주인을 설정하기 위해서 mappedBy 속성을 사용한다.

  • 주인이 아니라면 mappedBy 속성을 이용해서 속성값으로 연관관계 주인을 지정한다.

  • 주인은 mappedBy 속성을 사용하지 않는다.

그렇기 때문에 양방향 관계에서 @OneToMany의 mappedBy를 사용하는 Team은 주인이 아니다.

그 반대편의 Member가 연관관계의 주인으로써 외래키를 관리하게 된다.

즉, '다(N)'쪽이 연관관계의 주인이다. @ManyToOne은 항상 연관관계의 주인이된다.


양방향 연관관계 주의할점

연관관계 주인이 외래키를 관리하기 때문에 주인이 아닌쪽에서 외래키(연관관계)를 다루게 되면 무시된다.

Team team = em.find(Team.class, 1);

//주인이 아닌 쪽에서 연관관계 설정
team.getMembers().add(member1); //무시
team.getMembers().add(member2); //무시

em.persist(team);

//member1.getTeam() : null
//member2.getTeam() : null

연관관계 주인만이 외래 키의 값을 관리(등록, 수정, 삭제)할 수 있다.


순수한 객체까지 고려한 양방향 연관관계

연관관계의 주인에서만 연관관계 관리가 가능한 것을 확인했다. 하지만 그렇다고 주인에서만 연관관계를 관리해야하는 걸까?

JPA를 사용하지않고 순수 객체만으로 테스트코드를 짠다고 가정했을 때

public void test(){
	Team team1 = new Team("team1","팀1");
    
    Member member1 = new Member("member1","회원1");
    Member member2 = new Member("member2","회원2");
    
    member1.setTeam(team1);
    member2.setTeam(team2);
    
    List<Member> members = team1.getMembers();
    System.out.println(members.size()); //0
}

위의 코드에서 연관관계 주인인 member1과 member2에 team을 등록했다.
JPA상으로는 주인에서 연관관계를 등록한 것은 맞다. 하지만 순수한 객체 입장에서는 team1에는 아무런 참조가 등록되지 않았다.

ORM은 데이터베이스도 중요하지만 객체도 중요하기때문에 데이터베이스 뿐만아니라 객체도 함께 고려해야한다.

public void test(){
	Team team1 = new Team("team1","팀1");
    Member member1 = new Member("member1","멤버1");
    Member member2 = new Member("member2","멤버2");
    
    member1.setTeam(team1);
    team1.getMembers().add(member1);
    
    member2.setTeam(team1);
    team1.getMembers().add(member2);
    
    List<Member> members = team1.getMembers();
    System.out.println(members.size()); //2
}

이렇게 연관관계 뿐만 아니라 순수 객체를 고려하면서 연관관계를 맺어주니 기대했던 결과가 반환되었다. 이렇게 하므로써 순수한 객체상태에서도 동작하고 테이블의 외래키도 정상 동작하게된다.

편의 메서드

양방향 연관관계는 결국 둘다 신경써야한다. 하지만 등록할때마다 양쪽을 서로 등록하기에는 복잡하고 휴먼에러로 둘 중 하나만 호출하여 양방향이 깨질 가능성이 있다.

이러한 문제를 해결하기 위해 하나의 메소드에서 양방향 연관관계를 등록하도록 한다.

public class Member{
	@ManyToOne
    Team team;
    
    public void setTeam(Team team){
    	this.team = team;
        team.getMembers().add(this);
    }
}

편의 메서드 주의사항

양방향 연관관계 등록은 완벽하다. 하지만 연관관계 삭제시 순수 객체에서 완전히 삭제되지 않는다.

member1.setTeam(team1);
member1.setTeam(team2); //member1의 참조를 team2로 변경

List<Members> members = team1.getMembers();
for(Member member : members){
	System.out.println("username : "+member.username);	//member1는 team2를 참조하고 있지만 team1에서는 여전히 member1을 참조하고 있다.
}

그렇기 때문에 편의 메서드에서 참조 삭제도 고려하여 코드를 리팩토링 해야한다.

public class Member{
	@ManyToOne
    Team team;
    
    public void setTeam(Team team){
    	if(this.team != null){
        	//기존 team이 있다면 참조하던 team이 있다는 의미.
            //새로운 team으로 수정하기 위해서 기존 참조하던 team에서 해당 member를 제거한다.
        	this.team.getMembers().remove(this);	
        }
        this.team = team;
        team.getMembers().add(this);
    }
}

참조하고 있던 객체(team1)를 수정하기 위해서는 기존 참조객체(team1)에서 해당 객체(member)를 삭제해주고 새로운 참조객체(team2)에 해당 객체(member)를 추가해주어야 한다.


Reference

https://ultrakain.gitbooks.io/jpa/content/chapter8/chapter8.1.html

https://ultrakain.gitbooks.io/jpa/content/chapter8/chapter8.1.html

https://velog.io/@conatuseus/%EC%97%B0%EA%B4%80%EA%B4%80%EA%B3%84-%EB%A7%A4%ED%95%91-%EA%B8%B0%EC%B4%88-2-%EC%96%91%EB%B0%A9%ED%96%A5-%EC%97%B0%EA%B4%80%EA%B4%80%EA%B3%84%EC%99%80-%EC%97%B0%EA%B4%80%EA%B4%80%EA%B3%84%EC%9D%98-%EC%A3%BC%EC%9D%B8

https://github.com/hyunjong-96/SpringBoot_Study/blob/main/JPA/4.%EC%97%B0%EA%B4%80%EA%B4%80%EA%B3%84.md

profile
내 꿈은 좋은 개발자

0개의 댓글