본문 바로가기
JPA

[JPA] JPA 연관관계 매핑 - 단방향, 양방향, 주인 설정 및 mappedBy 속성 활용하기

by 곰민 2023. 4. 3.
728x90

JPA에서 연관관계 매핑의 중요 개념들, 단방향 및 양방향 연관관계, 양방향 연관관계에서의 주인 설정, mappedBy 속성, 객체 지향과 관계형 디비 두 가지 다른 패러다임 간에 오는 차이점등을 살펴봅니다.
그리고 그를 통해서 JPA를 사용한 객체 관계 매핑에 대해 효율적으로 접근해보도록 하겠습니다.

아.. 오노...

 

 

연관관계가 필요한 이유


‘객체지향 설계의 목표는 자율적인 객체들의 협력 공동체를 만드는 것이다.’

조영호(객체지향의 사실과 오해)

 

 

 

teamId → 외래키를 그대로 가져와버림

 

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

Member member = new Member();
member.setUsername("memeber1");
member.setTeamId(team.getId()); //외래키 식별자를 직접 다루게됨..
em.persist(member);

Member findMember = em.find(Member.class, member.getId())
Long findTeamId = findMember.getTeamId(); //팀아이디 가져와서
Team findTeam = em.find(Team.class, findTeamId); // 또거기서 팀가져오고 해야됨

 

객체를 테이블에 맞추어 데이터 중심으로 모델링하면, 협력 관계를 만들 수 없다.

테이블은 외래 키로 조인을 사용해서 연관된 테이블을 찾고

객체는 참조를 사용해서 연관된 객체를 찾는다.

테이블과 객체 사이에는 이러한 간격이 존재한다.

 

단방향 연관관계


@Entity
public class Member {

	@Id @GeneratedValue
	privatee Long id;

	@Column(name = "USERNAME")
	private String name;
	private int age;

	@ManyToOne // 디비 관점에서 관계가 어떻게 되냐?
	@JoinColumn(name="TEAM_ID") //조인해야 되는 컬럼이 무엇이냐?
	private Team team;
	....
}

member.setTeam(team); //바로 팀 넣어주고
em.persist(member);

Member findMember = em.find(Member.class, member.getId())
Team findTeam = findMember.getTeam(); //바로 끄집어 낼 수 있음

//깨알 다른팁
//지금의 경우 persist 이후 find 에서는 1차캐시에서 가져오기 때문에 쿼리가 안나감
// 디비 쿼리 확인하고 싶으면
// persist 이후 flush()호출해서 싱크 맞춘후 clear()호출하고 이후 코드 수행하면됨

---------------------------------------------
//팀을 바꾸고 싶다면?
Team teamB = new Team();
teamB.setName("TeamB");
em.persist(teamB);

//회원 1에 새로운 팀B 설정
member.setTeam(teamB);

 

양방향 연관관계와 연관관계의 주인


JPA에 포인터와 같은 존재다..

 

 

테이블은 외래키로 조인하면 되기 때문에 사실 테이블은 바뀔게 없다.

즉 디비 테이블의 연관관계는 방향이라는 개념 자체가 없고 그저 외래키 하나로 서로의 연관을 다 알 수 있다.

 

문제는 객체의 경우인데.

Team 에다가 List members 라고 넣어줘야 양쪽으로 갈 수가 있게 된다.

객체는 참조가 필요하다는 것이 외래키의 가장 큰 차이점

 

Team
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
//new ArrayList<>();로 초기화를 해줘야함. 그래야 nullpointexception이 나질 않음.
=================================================
Member
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
Member findMember = em.find(Member.class, member.getId());
List<Member> members = findMember.getTeam().getMembers();
//member에서 team으로 team에서 member로

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

 

 

객체간의 연관 관계는 단방향으로 최대한 설계하는 것이 좋은데 그 이유는

 

간결한 코드와 가독성.

단방향 연관관계는 양방향에 비해 코드가 더 간결하고 이해하기 쉽다.

양방향 연관관계에서는 연관된 양쪽 클래스 모두 관리해야 하기 때문에 코드 복잡성이 증가한다.

 

메모리 사용 최적화

양방향 연관관계에서는 서로 참조하고 있는 객체를 모두 유지해야 하므로 메모리 사용량이 증가한다.

반면 단방향 연관관계에서는 한 방향으로만 참조를 유지하므로 메모리 사용량이 상대적으로 줄 수 있다.

 

성능

단방향 연관관계에서는 필요한 객체만 로드하므로 성능이 향상됩니다.

양방향 연관관계에서는 종종 불필요한 객체까지 로드해야 하는 상황이 생길 수 있다.

 

 

연관관계의 주인과 mappedBy


 

 

 

 

테이블은 외래키 하나면 양쪽으로 그냥 다가능하기 때문에
위에서 계속 말하듯 연관관계가 1개 회원 ↔ 팀의 연관관계 1개(양방향) 이거나 또는 사실상 방향이 없다고 봐야한다. 

 

member에있는 team_id외래키 값 외래키로 조인하면 알 수가 있다 (멤버 입장에서도 팀입장에서도)
외래키 하나만으로 양쪽에 연관관계가 외래키 값 하나로 사실상 끝이 난다. 

 

반면에 객체는 객체 연관관계가 두 가지라고 볼 수 있다.

1. 회원 → 팀 연관관계 1개(단방향)

2. 팀 → 회원 연관관계 1개(단방향)

위 와 같이 단방향 연관관계가 두가지 존재한다고 볼 수 있다.

 

객체의 양방향 관계


객체의 양방향 관계는 사실 양방향 관계가 아니라 서로 다른 단방향 관계 2개라고 볼 수 있다.

객체를 양방향으로 참조하려면 단방향 연관관계를 2개 만들어야 한다.

 

A → B(a.getB())

class A{ B b; }

 

B → A(b.getA())

class B { A a; }

 

위 코드와 같이 참조를 각각 만들어 놔야 한다.

 

테이블의 양방향 연관관계


테이블은 외래키 하나로 두 테이블 연관관계 관리

MEMBER.TEAM_ID 외래키하나로 양쪽으로 조인 가능

SELECT *
FROM MEMBER M
JOIN TEAM T ON M.TEAM_ID = T>TEAM_ID

SELECT *
FROM TEAM T
JOIN MEMBER M ON T.TEAM_ID = M.TEAM_ID

 

딜레마


 

도대체 뭘로 매핑해야 할까?

Member에 team 값을 바꿧을때 외래키 값이 업데이트되어야 하는가?

Team에 member를 바꿨을 때 외래키 값이 업데이트되어야 하는가?

 

이런 딜레마가 존재 한다.

둘 중에 어떤 것을 바라봐야 하는지?

 

Member에 팀을 바꿀 때 도대체 Member를 바꿔야 하는지?

Team의 member를 바꿔야 하는지?

 

데이터베이스는 단지 외래키 값만 업데이트되면 된다..

 

양방향이 되면서 팀에 있는 members를 신경 써야 되니까 문제가 발생하며 둘 중 하나로 외래키를 관리해야 한다.

 

연관관계의 주인(Owner)


양방향 매핑 규칙은 그래서 연관관계의 주인을 지정하는 것이다.

객체의 두 관계 중 하나를 연관관계의 주인으로 지정

연관관계의 주인만이 외래 키를 관리(등록, 수정)

주인이 아닌 쪽은 읽기만 가능

주인은 mappedBy 속성 사용 x

주인이 아니면 mappedBy 속성으로 주인 지정

 

누구를 도대체 그럼 주인으로 해야 되지?


 

public class Member {
	...
	@ManyToOne
	@JoinColumn(name = "TEAM_ID") // 매핑을 했음 연관관계의 주인.
	private Team team;
}

public class Team{
	...
	@OneToMany(mappedBy = "team") //team에 의해서 관리가되 mappedBy가 적힌곳은 조회만가능
	// 값을 아무리 넣어도 반영이 안됨. 읽기만 가능.
	private List<Member> members = new ArrayList<>();
}

그래서 누구를 주인으로 해야 할까?

 

외래 키가 있는 곳을 주인으로 정해야 한다.

 

여기서는 Member.team이 연관관계의 주인

그렇게 해야 헷갈리지 않습니다.

Team 이 연관관계 주인이면

Team에 members의 값을 바꿨는데

MEMBER update 쿼리가 나감.

이과정 자체가 헷갈림..

이후 다룰 성능 이슈도 존재 외래키가 있는 곳을 주인으로 딱 정하세요

 

1 대 다 관계에서는 보통 다(N) 쪽이 외래키를 가지므로 다(N) 쪽이 관계의 주인이 됩니다.

디비입장에서 보면 외래키가있는 곳이 거의 다(N) 외래키가 없는 곳이 1입니다.

디비 테이블로 따져서 다 쪽이 주인이 되면 됩니다.

 

 

양방향 매핑 시 가장 많이 하는 실수


연관관계의 주인에 값을 입력하지 않음


Team team = newTeam();
team.setName("TeamA");
em.persist(team);

Member member = new Member();
member.setName("member1");

team.getMembers().add(member); //team에 mebers에 member를 넣음
em.persist(member);

 

 

id username team_id
1 member1 null

 

Team에 있는 members는 mappedBy 된 읽기 전용이다.

연관관계 주인인 Member에 있는 team에 값을 넣어줘야 한다.

 

member.setTeam(team); //연관관계 주인에 값을 넣고
//team.getMembers().add(member); 주인이 아닌 부분은 주석처리하면?

 

id username team_id
2 member1 1
TEAM_ID NAME
1 TeamA

 

team_id에 값이 들어가 있고

Team 테이블에 id에 team_id 값이 정상적으로 들어가 있음

 

member.setTeam(team); //연관관계 주인에 값을 넣고
team.getMembers().add(member); //주석처리 안해도 가능 어차피 업데이트할때 이 값을 안 넣음

 

🔥 양방향 매핑 시에 연관관계에 주인에 값을 안 넣어서 외래키 값이 null 인 경우를 주의해야 한다.

 

양방향 매핑 시에는 양쪽에다가 값을 넣어주는 게 맞다..


Team team = newTeam();
team.setName("TeamA");
em.persist(team);

Member member = new Member();
member.setName("member1");
member.setTeam(team);
em.persis(member);

em.flush();
em.clear();

Team findTeam = em.find(Team.class, team.getId());
ListM<Member> members = findTeam.getmembers();
for (Member m : members){
	System.out.println("m = " + m.getUsername());
}

 

객체지향적으로 생각해본다면 양쪽에 값을 넣어주기는 해아 한다.

반복문 내부에 list에 세팅한 값이 없어도 jpa에서 지연로딩이라고 하는데 Team find 해서 Team 데이터를 디비에서 가져와서 세팅을 하고 getMembers를 할 때 쿼리를 한방 날린다.

 

Hibernate:
	select
		members0_.TEAM_ID as TEAM_ID3_0_0_,
		...
	from
		Member members0_
	where
		members0_.TEAM_ID=?

 

외래키가 다 세팅이 되어 있기 때문에 외래키를 활용해서 나와 연관된 값을 가져온다.

그렇다면 값을 양쪽으로 넣을 필요가 없을까?

 

값을 둘 다 넣지 않다면 두 가지 문제가 존재한다.

 

Team team = newTeam();
team.setName("TeamA");
em.persist(team);

Member member = new Member();
member.setName("member1");
member.setTeam(team);
em.persis(member);

//em.flush();
//em.clear();

Team findTeam = em.find(Team.class, team.getId());
ListM<Member> members = findTeam.getmembers();
for (Member m : members){
	System.out.println("m = " + m.getUsername());
}

 

첫 번째 문제

flush와 clear를 주석 처리한다면

위에서 값 설정 해놓은 것들이 1차 캐시에 올라가 있는데 아무것도 없는 순수 객체상태 이기 때문에 값을 가져온다면 아무것도 안 나온다.

 

 

두 번째 문제

테스트 케이스 만들 때에도 jpa 없이도 순수하게 자바 코드상태로 테스트 케이스 작성하는데 member.getTeam 은 값이 나오는데 반대는 값이 안 나오는 케이스가 존재하게 된다.

 

순수 객체 상태를 고려해서 항상 양쪽에 값을 설정하자.
양방향 연관관계에 있어서는 양쪽에다가 값을 세팅하는 것이 좋다.
연관관계 편의 메서드를 생성하자.
양방향 매핑 시에 무한 루프를 조심하자 예 : toString(), lombok, JSON 생성라이브러리

 

Team team = newTeam();
team.setName("TeamA");
em.persist(team);

Member member = new Member();
member.setName("member1");
member.setTeam(team); //**
em.persis(member);

team.getMembers().add(member); //**

 

연관관계 편의 메서드를 활용하자


값을 양쪽 객체 둘 다 추가하기 힘드니까 연관관계 편의 메서드라고 하나 만드는 것을 추천한다.

 

class Memeber(){

	...
	public void changeTeam(Team team){
		this.team = team;
		team.getMembers().add(this); //나자신 인스턴스를 넣어줌
	}
}

 

Team team = newTeam();
team.setName("TeamA");
em.persist(team);

Member member = new Member();
member.setName("member1");
member.changeTeam(team); //
em.persis(member);

//team.getMembers().add(member); 

 

양쪽으로 이러면 세팅이 들어가게 되며 이렇게 되면 휴먼에러를 줄일 수 있다.

하나만 호출해도 양쪽으로 값이 걸리기 때문.

 

연관관계 편의 변경하는 거나 jpa 상태 변경하는 것은 set을 잘 안 쓴다.
java getter setter 관례 때문에 로직이 없을 때만 set 넣고 로직이 들어가면 이름을 바꿔둠

 

사실 연관관계 편의메서드 할 때는 Null 값 체크등의 추가 로직을 넣을 수도 있다.

 

team.addMember() //팀쪽에도 추가를 할 수 있음

public class Team(){
	...
		
	public void addMember(Member member){
		member.setTeam(this);
		members.add(member);
	}
}
// 양 쪽에 있으면 문제가 생길 수 도 있기 때문에 한쪽은 지우는 것을 권장.

 

양방향 매핑 시에서의 무한루프?


public class Member(){
	
	@Override
	public String toString(){
		return "Member{"+
						"id=" + id +
						"j, username='" + username + '\\'' +
						", team=" + team + //team.toString() 을 또 호출한다는 의미
						'}';
	}
}

 

team에 왔는데

 

public class Team(){
	
	@Override
	public String toString(){
		return "Member{"+
						"id=" + id +
						"j, username='" + username + '\\'' +
						", members=" + members + //컬렉션 하나하나에 있는 members에 toString()을 다가져옴
						'}';
	}
}

stackoverflow발생..
양쪽에서 계속 호출하기 때문에...

 

개판이다 이거에요그냥

 

양방향이 걸려있으면 엔티티를 json으로 바꿔버리는 순간 문제가 생긴다.

json 생성라이브러리가 문제가 자주 발생하는 포인트는

Controller에서 엔티티를 바로 반환하면 엔티티를 json으로 바꿀 때 문제가 생긴다.

 

lombok에서 toString 만드는 것은 웬만하면 사용하지 않는 것을 권장한다.

JSON 생성 라이브러리는 컨트롤러에는 엔티티를 절대 반환하지 말 것.
컨트롤러에서 엔티티를 반환하면 JSON로 뽑는데 이런 무한 루프가 생길 수도 있다.

엔티티는 변경될 수가 있으며 엔티티를 변경하는 순간 api 스펙이 바뀌어버리기 때문에 엔티티는 웬만하면 단순히 값만 있는 dto로 변환해서 반환하는 것을 추천한다.

 

양방향 매핑 정리


JPA 설계를 할 때 단방향 매핑으로 설계를 끝내는 것이 좋다.
테이블 설계를 하면서 객체 설계를 다 하게 되고 그때부터 외래키들이 어느 정도 나오게 된다.
결국 다(N) 쪽에서 단방향 매핑을 걸어서 다 들어가야 되는데 이때 양방향 매핑을 하면 안 됨.

단방향 매핑으로 어떻게든 설계를 끝내는 것이 좋다.

 

단방향 매핑만으로도 연관관계 매핑은 완료할 수 있다.
양방향 매핑은 반대방향으로 조회(객체 그래프 탐색) 기능이 추가된 것뿐
JPQL에서 역방향으로 탐색할 일이 많다
단방향 매핑을 잘하고 양방향은 필요할 때 추가해도 된다.(테이블에 영향을 주지 않음.) Team → List members 추가 한 예시

 

연관관계의 주인을 정하는 기준


비즈니스 로직을 기준으로 연관관계 주인을 정하면 안 되며 연관관계의 주인은 외래 키의 위치를 기준으로 정해야 한다.

디비에서 어 외래키가 들어가네? 그럼 주인으로 정해보자.

 

 

참조


자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런 | 강의 (inflearn.com)

 

더욱 자세하고 세세한 내용은 김영한님 JPA책이나 강의를 들으시는 것을 추천합니다. 그저 갓..

728x90

댓글