본문 바로가기
Framework and Tool/JPA

JPA - 다양한 연관관계 - N : 1 과 1 : N

by ocwokocw 2021. 7. 3.

- 참조: 자바 ORM 표준 JPA 프로그래밍

- 다양한 연관관계

앞에서 몇 가지 예제를 통해 연관관계를 작성해보았다. JPA 에 익숙하지 않다면 JPA를 사용하여 연관관계 코드를 작성하는게 헷갈릴 수 있다. 이때에는 연관관계 맵핑시 고려할 사항을 순차적으로 정해놓고 천천히 생각해보면 좀 더 수월하다.

  • 다중성: N : 1 인지 1 : N 인지에 따라 @ManyToOne, @OneToMany 어떤 다중성 어노테이션을 사용할 지 정한다.
  • 단방향, 양방향: 하나의 엔티티가 다른 엔티티를 참조하는지 서로 참조하는지를 정한다.
  • 연관관계의 주인, 연관관계 편의메소드: 양방향이라면 두 엔티티중 외래키를 관리할 연관관계의 주인과 연관관계 편의메소드 작성을 고려한다.

- N : 1 단방향

회원 엔티티와 팀 엔티티가 있다고 가정하자. 회원은 1 개의 팀에만 속하며, 회원에서는 팀을 참조할 수 있지만 팀에서는 회원을 참조할 수 없다. 이때 회원 대 팀은 N : 1 이며, 회원에서 팀을 참조하는 단방향 연관관계이다.

위의 UML 에서 User 가 일방적으로 Team 을 참조하고 있기 때문에 단방향 연관관계이다.

 

N : 1 에서 1 에 해당하는 Team 엔티티부터 작성해보자. Team 엔티티는 객체 참조가 없기 때문에 연관관계 관련해서 특별하게 작성할 코드는 없다.

 

@Entity
public class Team {

	@Id @GeneratedValue
	@Column(name = "TEAM_ID")
	private Long id;
	
	private String name;

 

여태까지 예제를 따라하면서 생각해보았다면 User 도 어렵지 않게 작성할수가 있을 것이다.

 

@Entity
public class User {

	@Id @GeneratedValue
	@Column(name = "USER_ID")
	private Long id;
	
	private String userName;
	
	@ManyToOne
	@JoinColumn(name = "TEAM_ID")
	private Team team;

 

일단 User : Team = N : 1 이므로 다중성 어노테이션은 @ManyToOne 을 설정한다. 그리고 @JoinColumn 으로 Team 의 PK 를 참조할 외래키를 설정한다.


- N : 1 양방향

만약 팀에서도 회원목록을 참조할 수 있다고 가정하면 다중성은 그대로 유지되지만 회원과 팀은 단방향에서 양방향으로 바뀐다.

UML 다이어그램의 연관이 Directed Association 이 아닌 Association 이 되었다. 그리고 Team의 field 에 users 가 추가되었다. users 필드의 자료형은 User 이며 다중성은 [] 안에 표시된 *(Multi) 이다. User 와 Team 은 전체(whole) 과 부분(part) 의 관계는 아니라고 생각했기 때문에 Aggregation 은 표시하지 않았다.

 

Team 에서 User 를 참조함에 따라 변하는 Team 의 엔티티 코드를 변경해보자.

 

@Entity
public class Team {

	@Id @GeneratedValue
	@Column(name = "TEAM_ID")
	private Long id;
	
	private String name;

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

 

다중성은 Team 에서 여러 명의 User 를 참조하므로 1 : N 즉 @OneToMany 어노테이션을 이용해야 한다. 그리고 양방향이므로 연관관계의 주인을 생각해야 하는데, User 테이블에서 TEAM_ID 를 컬럼으로 외래키를 관리할것이므로 mappedBy 속성값은 User 에서 Team 을 참조하는 필드명인 team 을 할당한다.

 

양방향이므로 연관관계 편의메소드를 작성해야 한다. 연관관계 편의메소드는 User 에서 setTeam 메소드에 작성한다. 책에서는 양쪽에 편의 메소드를 작성하고 무한 루프를 막는 조치를 취했는데, 개인적으로 한곳에서 편의메소드를 작성하는게 더 낫다고 생각한다.

 

public void setTeam(Team team) {
	
	if(this.team != null) {
		this.team.getUsers().remove(this);
	}
	
	this.team = team;
	
	if(team != null) {
		team.getUsers().add(this);
	}
}

- 1 : N 단방향

1 : N 은 N : 1 의 반대 방향이다. 사실 생각해보면 1 : N 을 따로 다루어야 하는지 의문이 들것이다. 왜냐하면 회원 : Team 의 관계에서 N : 1 양방향을 살펴보았다는것은 이미 Team 에서 User 를 참조할 때 1 : N 을 다룬것이나 마찬가지라고 생각할 수 있기 때문이다.

 

N : 1 단방향과 N : 1 양방향에서는 USER 테이블의 TEAM_ID 외래키를 User 엔티티에서 관리했었다. 그런데 1 : N 단방향이되면 USER 의 TEAM_ID 외래키를 Team 엔티티에서 관리하게 된다. 즉 Team 에서 User 를 참조하는데 외래키는 상대 엔티티에 있고 연관관계의 주인은 없는 상황이다. @OneToMany 를 사용하는데 연관관계의 주인이 없어서 mappedBy 속성으로 맵핑을 하지 않는다는 얘기가 된다.

 

User 엔티티는 아래와 같다. 특별한 연관관계가 없이 평범하다.

 

@Entity
public class User {

	@Id @GeneratedValue
	@Column(name = "USER_ID")
	private Long id;
	
	private String userName;

 

문제는 Team 엔티티이다. 다중성은 Team 이 여러 User 를 참조하므로 @OneToMany 이다. 그런데 연관관계 주인이 없으므로 mappedBy 속성을 사용하지 않는다. 대신 연관관계를 맵핑하려면 @JoinColumn 을 사용해주어야 한다. 

 

이때 헷갈릴수가 있는데 TEAM 테이블에서 USER_ID 컬럼을 통해서 USER 를 참조하니까 USER_ID 가 아닌가? 라고 생각할 수 있다. DB 의 테이블 설계는 이미 USER 테이블의 TEAM_ID 로 TEAM 테이블을 참조하게 설계한것이다. 만약 TEAM 테이블에서 USER_ID 컬럼으로 1 : N 을 관리하겠다고 생각하면 회원은 여러명이므로 TEAM 의 중복 레코드가 회원수만큼 늘어난다. 그러므로@JoinColum 에는 User 테이블의 TEAM_ID 를 이용해서 Join 을 해야하므로 "TEAM_ID" 가 된다.

 

@Entity
public class Team {

	@Id @GeneratedValue
	@Column(name = "TEAM_ID")
	private Long id;
	
	private String name;

	@OneToMany
	@JoinColumn(name = "TEAM_ID")
	private List<User> users = new ArrayList<>();

 

위와 같이 설정하고 H2 에서 조회해보면 테이블이 아래와 같이 만들어진다.

만약 @JoinColumn 을 명시하지 않으면 JPA 는 기본적으로 조인 테이블전략을 구사한다. 아래는 @JoinColumn 을 주석처리하고 실행하고 H2 로 테이블을 조회한 모습이다. TEAM_USER 라는 조인 테이블이 생성되었다. 조인 테이블은 DB 에서 M : N 관계를 1 : M, N : 1 풀기위해서 사용하는데 굳이 1 : N 관계를 위해서 조인테이블을 사용할 필요는 없기 때문에 @JoinColumn 을 사용해주도록 하자.

1 : N 단방향은 Insert query 1 번으로 처리가능한 과정을 update query 까지 수행해야 한다는점도 있다. 만약 Team1 에 User1, User2 가 연관된다고 할 때, 1 : N 단방향을 실행하면 수행결과에서 마지막에 해당 User의 TEAM_ID 를 갱신해주어야 하는데, N : 1 단방향을 실행하면 Insert SQL 한번으로 연관관계를 설정할 수 있다.

 

========= 1 : N 실행결과
Hibernate: 
    /* create one-to-many row com.example.demo.test.Team.users */ update
        user 
    set
        team_id=? 
    where
        user_id=?
Hibernate: 
    /* create one-to-many row com.example.demo.test.Team.users */ update
        user 
    set
        team_id=? 
    where
        user_id=?


========= N : 1 실행결과
Hibernate: 
    /* insert com.example.demo.test.Team
        */ insert 
        into
            team
            (name, team_id) 
        values
            (?, ?)
Hibernate: 
    /* insert com.example.demo.test.User
        */ insert 
        into
            user
            (team_id, user_name, user_id) 
        values
            (?, ?, ?)
Hibernate: 
    /* insert com.example.demo.test.User
        */ insert 
        into
            user
            (team_id, user_name, user_id) 
        values
            (?, ?, ?)	

 

절대적이라는것은 없다지만 1 : N 단방향보다는 N : 1 양방향을 사용하는것이 좋다. 성능은 둘째치고 외래키관리를 다른 엔티티에서 해야한다는 점도 부담스럽다.


- 1 : N 양방향

1 : N 양방향이라는 말은 N : 1 양방향이라는말과 같다. 그래서 N : 1 양방향 맵핑을 사용하면 된다. 코드로는 변환할 수 있지만 굳이 그렇게 할 필요가 없다.

 

만약 억지로 코드로 변환한다면 User 엔티티에서 @ManyToOne, @JoinColumn(name="TEAM_ID", insertable=false, updatable=false) 와 같이 설정할 수는 있지만 1 : N 의 단방향 단점을 그대로 갖는다. 따라서 N : 1 양방향을 사용하도록 하자.

댓글