연관관계 매핑 기초

twocowsong·2023년 4월 27일
0

김영한_jpa

목록 보기
6/13
  • 객체와 테이블 연관관계 차이를 이해
  • 객체의 참조와 테이블의 외래키를 매핑

용어 이해

• 방향(Direction): 단방향, 양방향
• 다중성(Multiplicity): 다대일(N:1), 일대다(1:N), 일대일(1:1), 다대다(N:M) 이해
• 연관관계의 주인(Owner): 객체 양방향 연관관계는 관리 주인 이 필요

단방향 연관관계

1개의 팀은 여러명에 회원정보를 가질수있고, 여러명에 회원은 팀 1개에 포함되어있어야 하는 연관관계가 존재할경우 아래와같이 소스코드를 만들수 있습니다.

@Getter
@Setter
@Entity
public class Member {
	@Id @GeneratedValue(strategy = GenerationType.AUTO)
	@Column(name = "MEMBER_ID")
	private Long id;

	@Column(name = "USERNAME")
	private String username;

	@Column(name = "TEAM_ID")
	private Long teamId;

	public Member() {
	}
}

@Getter
@Setter
@Entity
public class Team {
	@Id @GeneratedValue(strategy = GenerationType.AUTO)
	@Column(name = "TEAM_ID")
	private Long id;
	private String name;

}

저장로직은 아래와 같을것입니다.

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

	Member member = new Member();
	member.setUsername("member1");
	member.setTeamId(team.getId());
	em.persist(member);
    ...

허나 실상 저장로직을 자세히보면, 이상한부분이있습니다.
저렇게 Team을 INSERT 후 Member를 INSERT하는 식이된다면
조회할 때는 아래와같은 로직을 지나게됩니다.

Member findMember = em.find(Member.class, member.getId());
Long teamId = findMember.getTeamId();
Team findTeam = em.find(Team.class, findMember.getTeamId());

매번 팀을 조회하기위해 2번에 find를 수행하게되는거죠, 연관관계가 없기때문에 객체지향답지 않습니다. 연관관계가 매핑이된다면 많은 프로세스가 줄어들게됩니다.

단방향 연관관계

@Getter
@Setter
@Entity
public class Member {
	@Id @GeneratedValue(strategy = GenerationType.AUTO)
	@Column(name = "MEMBER_ID")
	private Long id;

	@Column(name = "USERNAME")
	private String username;

// @Column(name = "TEAM_ID")
// private Long teamId;
	@ManyToOne // 회원은 N이고 팀은 1이기 때문
	@JoinColumn(name = "TEAM_ID") // 연결할 컬럼명
	private Team team;

	public Member() {
	}
}

@ManyToOne 어노테이션을 이용하여 Team객체와 연관관계를 추가해주었습니다.
이렇게되면 저장로직은 아래와 같이 변하게됩니다.

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

	Member member = new Member();
	member.setUsername("member1");
	member.setTeam(team);
	em.persist(member);

member.setTeam(team); 한줄로 JPA가 알아서 Team에 PK값을 Member FK값에 INSERT 하게 해주게됩니다.

	Member findMember = em.find(Member.class, member.getId());
	Team findTeam = findMember.getTeam();

조회 로직도 이렇게 바로 getTeam으로 조회할수있습니다.
즉, 객체의 참조와 DB에 외래키를 매핑해서 연관관계를 표현할수 있게됩니다.

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

JPA에 꽃이라고 할수있는 양방향 연관관계입니다.

현재 Member에서 Team조회가 가능했지만, Team에서 Member는 불가능했습니다.
그 기능을 추가하기위해 Team객체에 List members참조를 추가하게됩니다.
이때 신기하게도 Team에 테이블은 변하는게 없습니다.
잘 생각해보면 테이블에 연관관계는 방향이없습니다. FK한개로 양쪽에서 모두 조회가 가능합니다.
문제는 객체입니다. 객체에서는 참조가 방향이있기때문에 참조를 추가해줘야하는 상황입니다.

	...
    @OneToMany(mappedBy = "team") // 1:N 매핑에서, Member입장에서 매핑된 변수명을 적어주시면됩니다.
	private List<Member> members = new ArrayList<>();
    ...

Team객체에 위와같이 추가 후 @OneToMany를 추가하였습니다.
아까 Member객체에 Team에 객체를 매핑시킬때와 반대인 어노테이션이 추가된것입니다.

	Member findMember = em.find(Member.class, member.getId());
	List<Member> members = findMember.getTeam().getMembers();
	for (Member target : members) {
		System.out.println("member ID : " + target.getUsername());
	}

그러면, 이렇게 Member.getTeam().getMembers() 호출하여 MemberList를 조회할 수 있게됩니다.

여기서 Team객체에서 양방향 설정을 하기위해 mappedBy의 설정을 하였습니다.

mappedBy

양방향 매핑 규칙시 설정되며, 객체의 두 관계중 하나를 연관관계의 주인으로 지정합니다.
현재는 Member객체에 Team이 주인이될지, Team객체에 members가 주인이될지, 연관관계의 주인만이 외래 키를 관리(등록, 수정)하며 주인이 아닌쪽은 읽기만 가능합니다.
주인은 mappedBy속성을 사용하면안되며, 주인이 아니면 mappedBy 속성으로 주인 지정해야합니다.

누구를 주인으로?

외래 키가 있는 있는 곳을 주인으로 정해라!

Member테이블과 Team테이블이 있는경우, Member테이블에 FK가 있고 Team테이블에는 없으니 이럴때는 Member테이블에 Team을 주인으로 추천합니다.
만약 Team에 members를 주인으로 설정하게될 경우 Team에 members를 수정하게될때 다른테이블(MEMBER)테이블에 업데이트 쿼리가 실행됨으로 논리적으로 이상한 상태가됩니다.

양방향 매핑시 주의할점

Member member = new Member();
member.setUsername("member1");
em.persist(member);

Team team = new Team();
team.setName("Team A");
team.getMembers().add(member);
em.persist(team);

연관관계의 주인이 Member객체였지만 위처럼 저장로직을 만들게되면 아래와같이 데이터가 null이 저장되게 됩니다.

연관관계의 주인보다 먼저 의존하고있는 데이터가 INSERT로직이 먼저 실행됐기 때문이죠.

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

Member member = new Member();
member.setUsername("member1");
member.setTeam(team);
em.persist(member);

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

위처럼 연관관계에서 참조하고있는 Team객체가 먼저 INSERT 된 후 Member가 INSERT가 되야합니다.

정상적으로 저장된걸 확인할수 있습니다.

단, 여기서 로직을 하나더 추가해줘야합니다.

team.getMembers().add(member);

연관관계에서 참조하고있는 Team객체에도 Member를 추가해줘야합니다.
em.flush(); em.clear();를 사용하면 DB에 바로 업데이트가 되지만 사용하지않는다면, 추후 로직에서 문제가 발생하기 때문입니다.

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

Member member = new Member();
member.setUsername("member1");
member.setTeam(team);
em.persist(member);

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

System.out.println("=======================");
for (Member mem : members) {
	System.out.println("Member ID : " + mem.getId());
}
System.out.println("=======================");

기존 로직에 flush와 clear하지않을경우 Member ID는 아무것도 찍히지 않게됩니다.
em.find(Team.class, team.getId()); 로직으로 1차캐시에서 가져온 데이터는 없기때문에 결과론적으론 members값에는 아무런 데이터가 없게됩니다.
이를 해결하기위해 양쪽(Team, Member)객체에 세팅을 해줘야합니다.

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

Member member = new Member();
member.setUsername("member1");
member.setTeam(team); // 1. member 객체에도 설정
em.persist(member);

team.getMembers().add(member); // 2. team객체에도 추가한 member객체 add

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

하지만 사람이기때문에, 2번을 까먹는경우가 있어 메소드를 추가하면 편해지게됩니다.

public void chageTeam(Team team) {
	this.team = team;
	team.getMembers().add(this);
}

그러면 1번 이였던 member.setTeam(team); -> member.chageTeam(team); 로 수정시 2번은 입력하지않아도 문제가 발생하지 않게됩니다.

profile
생각하는 개발자

0개의 댓글