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

PURPLE·2022년 3월 28일
2

JPA

목록 보기
8/8


JPA를 사용하는 데 가장 중요한 일은 엔티티와 테이블을 정확히 매핑하는 것이다.
그러려면 JPA의 매핑 어노테이션을 잘 사용해야한다.
JPA가 지원하는 어노테이션은 크게 4가지로 분류할 수 있다.


데이터 중심 설계의 문제점

아래와 같은 객체와 테이블이 있다고 가정해보고,
테이블이 갖는 연관관계를 객체에 적용해보자.

//Member
@Entity
public class Member {
	@Id @GeneratedValue
    @Column(name = "member_id)
    private Long id;
    
    @Column(name = "username")
    private String name;
    
    @Column(name = "team_id")
    private Long temaId;		// 테이블의 외래키를 그대로 사용
}

// Team
@Entity
public class Team {
	@Id @GeneratedValue
    @Column(name = "team_id")
    private Long id;
    
    private String name;
}

위처럼 객체를 테이블에 맞추어 모델링 했을 경우, 저장과 조회는 다음과 같은 방식으로 하게된다.

// Team 저장
Team team = new Team();
team.setName = ("TeamA")
em.persist(team);

// Member 저장
Member member = new Member();
member.setName("member1");
member.setTeamId(team.getId()); // 외래키 식별자를 직접 다룸
em.persist(member);
//Member의 Team을 조회하고 싶은 경우
Member findMember = em.find(Member.class, member.getId());

Long temaId = findMember.getTeamId();
Team findTeam = em.find(Team.class, teamId);

위 코드처럼 데이터 중심으로 모델링 하게 되면, 연관관계 조회시 식별자로 재조회를 해야하는 번거로움이 발생한다. 관계형 데이터베이스는 연관된 객체를 찾을 때 외래키를 사용해서 찾으면 되지만, 객체에는 join 이라는 기능이 없다. 객체는 연관된 객체를 찾을 때 참조 를 사용해야한다.

단방향 연관관계

그렇다면 JPA로 객체 지향 모델링을 어떻게 구현할 수 있는지 알아보자.
위의 데이터 중심의 모델링을 객체 지향적으로 모델링을 하면 다음과 같은 모양을 갖는다.

//Member
@Entity
public class Member {

	@Id @GeneratedValue
    @Column(name = "member_id)
    private Long id;
    
    @Column(name = "username")
    private String name;
    
    @ManyToOne 						// N:1 연관관계
    @JoinColumn(name = "team_id")
    private Team team;
}

// Team
@Entity
public class Team {

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

위 방식으로 모델링을 하면 Team findTeam = member.getTeam(); 의 객체 그래프 탐색이 가능하다.
저장하는 코드도 아래와 같은 형식을 취하게 된다.

// Team 저장
Team team = new Team();
team.setName = ("TeamA")
em.persist(team);

// Member 저장
Member member = new Member();
member.setName("member1");
member.setTeam(team);
em.persist(member);

member.setTeam(team); 에서 JPA가 알아서 team 의 식별자 값을 member 의 외래키 값으로 사용한다.

@ManyToOne

객체지향방식으로 모델링한 Member 클래스에서 Team 이라는 객체를 참조해놓고 @ManyToOne 어노테이션을 달아주었다. @ManyToOne 은 해당 Entity가 참조하는 Entity와 N:1 관계임을 의미한다.
@ManyToOne 의 속성은 아래와 같다.

속성기능Default
optionalfalse로 설정하면 연관된 Entity가 항상 있어야 한다.true
fetch글로벌 fetch 전략을 설정한다.@ManyToOne=FethchType.EAGER
@OneToMany=FetchType.LAZY
cascade영속성 전이 기능을 사용한다.
targetEntity연관된 Entity의 타입 정보를 설정한다. 이 기능은 거의 사용하지 않는다.

@JoinCloumn

@ManyToOne 를 설정한 필드의 외래키를 매핑한다.

@ManyToOne
@JoinColumn(name = "team_id")
private Team team;

Member 에 매핑된 테이블이 team_id 를 외래키로 갖고 있다고 해석할 수 있다.
이 매핑 정보로 JPA는 team_idMemberTeam 을 join 하는 쿼리를 생성한다.

양방향 연관관계

@OneToMany

1:N 의 관계를 지닌 필드에 @OneToMany 어노테이션을 설정한다.
TeamMember1:N 의 관계이다. 때문에 Team 에서 Member 를 조회할 수 있도록 List<Member>members 를 참조했다. 이것을 JPA에게 알려주기 위해, @OneToMany 매핑 정보를 사용한다.

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

mappedBy

mappedBy 속성은 쉽게 말해, 해당 필드가 어디에 매핑되었는지를 알려주는 정보다. mappedBy 는 해당 필드를 @JoinColumn 으로 매핑한 Entity의 필드를 찾는다.

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

연관관계 주인 (Owner)

엄밀히 말하면 객체에는 양방향 연관관계라는 것이 없다.
객체는 단방향 연관관계를 2개 만들어 양방향 연관관계 처럼 보이게 한다.

이렇게 되면 고민거리가 하나 생길 수 있다.
Entity를 조작할 때, 어느 쪽의 Entity를 기준으로 조작할 것인가 하는 문제이다.
MemberTeam 을 추가할 지, TeammembersMemeber 를 추가할지, 마찬가지로 수정을 할 때도 같은 고민이 생기게 된다.

따라서 이 중에 하나를 정해서 테이블의 외래키를 관리해야 하는데 이것을 연관관계의 주인이라고 한다.

연관관계의 주인만이 데이터베이스 연관관계와 매핑되고, 외래키를 관리(등록, 수정, 삭제) 할 수 있다. 반면 주인이 아닌 쪽은 읽기만 할 수 있다.
연관관계의 주인은 mappedBy 를 갖지 않은 반대편의 Entity가 해당 연관관계의 주인이 된다.

그럼 mappedBy 를 이용해 주인을 정할 때 어떤 것을 주인으로 정해야할까.
연관관계의 주인을 정한다는 것은 외래키 관리자를 선택하는 것이라고 보면 된다.
때문에 외래키를 가진 Entity가 주인이 된다.

🚨 양방향 매핑시 주의점

양방향 연관관계 설정 후 가장 많이 하는 실수는 연관관계의 주인에 값을 입력하지 않는 것이다.
데이터베이스에 외래키 값이 정상적으로 저장되지 않을 경우 이 문제를 가장 먼저 의심해보면 된다.

Member member1 = new Member("kitty");
em.persist(member1);

Member member2 = new Member("doggy");
em.persist(member2);

Team team1 = new Team("team1");
team1.getMembers().add(member1);
team1.getMembers().add(member2);

em.persist(team1);

member1member2 를 저장하고, team1 의 컬렉션에 이들을 담은 후 team1 을 저장했다.
이 상태로 member 를 조회하면 아래와 같은 결과가 나온다.

member_idusernameteam_id
1kittynull
2doggynull

이유는 연관관계의 주인인 Member 가 아닌 Team.members 에만 값을 저장했기 때문이다.

✅ 중요한 것은 연관관계의 주인만이 외래키의 값을 변경할 수 있다 는 것이다.

그렇다면, 연관관계의 주인에만 값을 저장하면 상관 없느냐.
그렇지않다. 객체 관점에서 보면 양쪽 방향 모두에 값을 입력해주는 것이 안전하다.
왜냐하면 한 쪽에만 값을 입력하면 JPA를 사용하지 않는 순수한 객체 상태에서는 연관관계 주인에만 값을 저장하고, 반대 방향에서 연관관계를 조회할 시 (현재 예제로 따지면 team1.getMembers() 를 할 경우) 우리가 기대하는 결과가 나오지 않기 때문이다.

따라서 양방향은 양쪽 다 관계를 설정해야 한다.

Team team1 = new Tema("team1");
em.persist(team1);

// 양방향 연관관계 설정
Member member1 = new Memebr("kitty");
member1.setTeam(team1);					// 연관관계 설정 member1 -> team1		
team1.getMembers.add(member1);			// 연관관계 설정 team1 -> member1
em.persist(member1);

Member member2 = new Memebr("doggy");
member2.setTeam(team1);					// 연관관계 설정 member2 -> team1	
team1.getMembers.add(member2);			// 연관관계 설정 team1 -> member2
em.persist(member2);

연관관계 편의 메소드

양방향 연관관계는 결국 양쪽 모두를 신경써야한다.
하지만, 사람이 하는 일이다 보니 member.setTeam(team);team.getMembers().add(member1); 을 각각 호출하다 보면 둘 중 하나를 누락하는 실수가 날 수 있다.
이 두 코드를 하나인 것 처럼 사용하는 메소드를 만들어두면 실수를 줄일 수 있다.

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

setTeam() 메소드 하나로 양방향 관계를 모두 설정하도록 변경했다.

이제 위의 양방향 관계를 설정한 코드는 아래처럼 간단하게 수정이 가능하다.

Team team1 = new Tema("team1");
em.persist(team1);

Member member1 = new Memebr("kitty");
member1.setTeam(team1);					// 양방향 연관관계 설정
em.persist(member1);

Member member2 = new Memebr("doggy");
member2.setTeam(team1);					// 양방향 연관관계 설정			
em.persist(member2);

🚨 연관관계 편의 메소드 작성 시 주의사항

사실 setTeam() 에는 버그가 있다.
member1의 team을 team2로 변경한다고 가정해보자.

Team team2 = new Team("team2");
em.persist(team2);

member1.setTeam(team2);

이 경우, member1의 team1과의 관계는 제거되지 않은 상태이다. 따라서 연관관계를 변경할 때는 기존의 연관관계인 team1과 member1의 관계도 삭제하는 코드를 추가해야한다.

public class Member {
	
    private Team team;
    
    public void setTeam (Team team) {
    
    	if (this.team != null) {
        	this.team.getMembers().remove(this);
        }
        
    	this.team = team;
        team.getMembers().add(this);
    }
}

이처럼 단방향 연관관계를 2개 설정하여 양방향 연관관계 처럼 동작시키기 위해선 고민과 수고가 필요하다.
즉, 객체에서 양방향 연관관계를 사용하려면 로직을 견고하게 작성해야 한다.

정리

  • 객체의 연관관계 매핑은 단방향 연관관계 매핑만으로 이미 완료된 것이다.
  • 객체를 양방향 연관관계를 맺는다는 것은 객체 그래프 탐색 기능을 추가한 것이다.
  • 양방향 연관관계는 객체 양쪽 방향을 모두 관리해야 한다.
  • 연관관계의 주인은 외래키가 있는 쪽이여야 한다.

참고

강의_자바 ORM 표준 JPA 프로그래밍 - 기본편
교재_자바 ORM 표준 JPA 프로그래밍(김영한)

다정한 피드백 환영해요 🤗

profile
방향과 방법을 찾아가는 여정

1개의 댓글

comment-user-thumbnail
2023년 2월 21일

정리가 잘 되어있어서 이해하기 좋아요. 혹시 어떤 교재 혹은 강의로 공부하셨는지 여쭤봐도 될까요?

답글 달기