[JPA] 다대일 관계 양방향 매핑 과 연관 관계 주인, 연관관계 편의 메소드

GilLog·2021년 7월 18일
7

JPA

목록 보기
6/8

🙆‍♂️ import 🙇‍♂️

자바 ORM 표준 JPA 프로그래밍


양방향 매핑

아래와 같은 관계를 가지는 두 TABLE이 있다고 하자.

USERGROUP은 다대일 관계이고,

GROUP에서 USER는 일대다 관계이다.

위 두 TABLE을 JPA에서 Entity로 Mapping할때

일대다 관계의 경우 여러 건과 연관관계를 맺을 수 있으므로, Collection을 사용해야한다.
JPA는 List 포함 Collection, Set, Map 등을 지원

위 관계에서 USER, GROUP Entity 예시는 아래와 같다.

@Entity
public class User {
    
    @Id
    @Column(name = "U_ID")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;
    
    private String name;
    
    @ManyToOne
    @JoinColumn(name = "G_ID")
    private Group group;
    
    public void setGroup(Group group) {
    	if(this.group != null) {
            this.group.getUser().remove(this)
        }
        this.group = group;
        group.getUsers().add(this);
    }
}

@Entity
public class Group {

    @Id
    @Column(name = "G_ID")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;
    
    private String name;
    
    @OneToMany(mappedBy = "group")
    private List<User> users = new ArrayList<User>();
    
    public void addUser(User user) {
    	this.users.add(user);
        if (user.getGroup != this) {
            user.setGroup(this);
        }
    }
}

다대일 관계USER 측에는 @ManyToOne Annotation을,

일대다 관계GROUP측에는 @OneToMany Annotation을 사용해주면 되는데,

이렇게 양방향 관계를 맺게되면 연관관계의 주인을 지정해주어야 한다.

일반적으로 외래키가 저장되는 다 측을(USER) 연관관계의 주인으로 지정하는데,

@JoinColumn Annotation으로 해당 Table에서 외래키인 G_IDname 속성으로 지정해주면 된다.

    @ManyToOne
    @JoinColumn(name = "G_ID")
    private Group group;

한편 연관관계 주인의 반대 Entity(GROUP) 측에는 mappedBy를 지정해주어야 한다.

연관관계 주인에서 설정한 Field 명groupmappedBy로 설정한다.

    @OneToMany(mappedBy = "group")
    private List<User> users = new ArrayList<User>();

@ManyToOne은 mappedBy 속성을 설정할 수도 없음, 항상 외래키가 저장되어 연관관계 주인이 되기 때문

양방향 매핑 정리

단순하게 정리하면 양방향 매핑에서 아래를 생각하면서 매핑하면된다.

  • 두 객체 연관관계 중 테이블 외래키를 관리하는 쪽을 연관관계의 주인이라 함.
    보통 외래키가 저장되는 테이블을 연관관계 주인으로 설정

  • 연관관계 주인만이 DB 연관관계와 매핑되고, 외래키를 관리(등록, 수정, 삭제)할 수 있음.

  • 연관관계 주인은 mappedBy 속성을 사용하지 않음.

  • 연관관계 주인이 아니면 mappedBy 속성으로 연관관계의 주인을 지정해주어야 함.

  • @ManyToOne은 항상 연관관계의 주인이되고, mappedBy 속성을 설정 조차 할 수 없음.
    DB 다대일 관계에서 다측에 외래키가 저장되기 때문

연관관계 편의 메소드

양방향 연관관계는 양 쪽 객체를 모두 신경써야 하는데,

하나의 메소드에서 양측에 관계를 설정하게 해주는 것이 안전하다.

이렇게 한번에 양방향 관계를 설정하는 메소드를 연관관계 편의 메소드 라 부른다.

아까전 UserGroup Entity에서 아래 메소드연관관계 편의 메소드이다.

public class User {

    ...
    
    public void setGroup(Group group) {
    	if(this.group != null) {
            this.group.getUser().remove(this)
        }
        this.group = group;
        group.getUsers().add(this);
    }
}


@Entity
public class Group {

    ...
    
    public void addUser(User user) {
    	this.users.add(user);
        if (user.getGroup != this) {
            user.setGroup(this);
        }
    }
}

연관관계 편의 메소드를 작성할 때 주의사항

  1. 다대일측(User)에서 연관관계를 지정할 때 기존 연관관계는 끊어주어야 한다.
public class User {

    ...
    
    public void setGroup(Group group) {
        // 이 부분에서 기존에 Group과 연관관계가 있다면
        // Group에서 해당 User를 먼저 제거한다.
    	if(this.group != null) {
            this.group.getUser().remove(this)
        }
        
        
        this.group = group;
        group.getUsers().add(this);
    }
}

  1. 연관관계를 저장할 때 무한 루프를 주의해야 한다.
public class User {

    ...
    
    public void setGroup(Group group) {
    	if(this.group != null) {
            this.group.getUser().remove(this)
        }
        this.group = group;
        // 이부분이 없으면 무한 루프에 걸린다.
        if(!group.getUsers().contains(this)) {
            group.addUser(this);
        }
    }
}


@Entity
public class Group {

    ...
    
    public void addUser(User user) {
    	this.users.add(user);
        // 이부분이 없으면 무한 루프에 걸린다.
        if (user.getGroup != this) {
            user.setGroup(this);
        }
    }
}

만약 addUsersetGroupmethod에서 if 부분 로직을 제거 하면 아래와 같다.

public class User {

    ...
    
    public void setGroup(Group group) {
    	if(this.group != null) {
            this.group.getUser().remove(this)
        }
        this.group = group;
        group.addUser(this);
    }
}


@Entity
public class Group {

    ...
    
    public void addUser(User user) {
    	this.users.add(user);
        
        user.setGroup(this);
    }
}

만약 아래를 호출해보면 아래 과정으로 무한루프에 빠진다.

User user = new User();
Group group = new Group();
user.setGroup(group);
  1. setGroup의 if(this.group != null)은 빠져나간다.
  2. user의 group은 this.group = group으로 group 객체의 주소값을 참조한다.
  3. group의 addUser(user)를 호출한다.
  4. group의 List<User> users에 user를 add한다.
  5. user의 setUser(group)을 호출한다.
  6. setGroup의 if(this.group != null)은 group을 참조하므로 group에서 user를 제거한다.
  7. user의 group은 this.group = group으로 group 객체의 주소값을 참조한다.
  8. group의 addUser(user)를 호출한다.
    ....

따라서 setGroup의 if(!group.getUsers().contains(this)),

addUser의 if (user.getGroup != this) 로직과 같이

무한루프에 빠지지않게 체크해주는 로직을 반드시 추가해야 한다.


  1. toString()에서도 무한 루프에 걸릴 수 있다.

lombok을 사용하다 가볍게 @ToString을 적었다가 낭패를 볼 수 있다.

만약 User에도 toString이 있고, Group에도 toString이 있다면 마찬가지로 무한 루프에 걸릴 수 있으므로 주의해야한다.

User user = new User();
Group group = new Group();
user.setGroup(group);

user.toString();

/////// toString 예시
#####user#####
id=1
userId=gillog
group=
	#####group####
    	id=1
        name=java
        users={####user####
        	....
profile
🚀 기록보단 길록을 20.10 ~ 22.02 ⭐ Move To : https://gil-log.github.io/

5개의 댓글

comment-user-thumbnail
2022년 1월 26일

어려워 jpa 쑤ㅠㅃ!ㅏ

답글 달기
comment-user-thumbnail
2022년 5월 13일

감사합니다 !

답글 달기
comment-user-thumbnail
2023년 2월 28일

정말 좋은글 감사합니다! 덕분에 많은 도움이 됐어요!!

답글 달기
comment-user-thumbnail
2023년 3월 17일

안녕하세요 글 잘봤습니다
"하나의 메소드에서 양측에 관계를 설정하게 해주는 것이 안전하다."라고 하셨는데
예시에는 이해를 위해 User와 Group에 다 연관관계 편의 메서드를 작성하신건가요?

1개의 답글