자바 ORM 표준 JPA 프로그래밍 - 기본편 챕터 5 정리

정종일·2023년 5월 22일
0

Spring

목록 보기
10/18

연관관계 매핑 기초

1. 단방향 연관관계


객체 연관관계

  • 필드로 팀 객체와 연관관계를 맺음
  • 회원과 팀 객체는 단방향 관계
  • Member.team으로 팀을 알 수 있지만 반대로 팀은 회원을 알 수 없음

테이블 연관관계

  • 외래 키로 팀 테이블과 연관관계를 맺음
  • 회원 테이블과 팀 테이블은 양방향 관계
  • 회원 테이블의 TEAM_ID 외래 키를 통해서 회원과 팀을 Join할 수 있고 반대로 팀과 회원도 조인 가능

객체 연관관계와 테이블 연관관계의 차이

  • 참조를 통한 연관관계는 언제나 단방향
  • 객체는 양방향 관계가 아니라 서로 다른 단방향 관계 2개
  • 테이블은 외래키 하나로 양방향으로 조인할 수 있다

순수한 객체 연관관계

순수한 객체 단방향, 다대일(N:1) 클래스

순수한 객체 단방향, 다대일(N:1) 인스턴스

객체는 참조를 사용해서 연관관계를 탐색할 수 있는데 이것을 객체 그래프 탐색이라 한다

테이블 연관관계

데이터베이스는 외래 키를 사용해서 연관관계를 탐색할 수 있는데 이것을 조인이라 한다

객체 관계 매핑

@Entity
public class Member {
		@Id
		@Column(name ="MEMBER_ID")
		private Long id;

		private String username;

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

}

@Entity
public class Team {
		@Id
		@Column(name = "TEAM_ID")
		private Long id;

		private String name;
}
  • 객체 연관관계 : 회원 객체의 Member.team 필드 사용
  • 테이블 연관관계 : 회원 테이블의 MEMBER.TEAM_ID 외래 키 컬럼을 사용

@ManyToOne: N:1 연관관계
@OneToMany: 1:N 연관관계
@JoinColumn: 조인 컬럼은 외래키를 매핑할 때 사용, 생략가능
@ManyToMany: N:N 연관관계
대부분 릴레이션 테이블을 이용하여 N:1 , 1:N 관계로 이용
@OneToOne: 1:1 연관관계

@JoinColumn

외래키를 매핑할 때 사용

name: 매핑할 외래키 이름
referencedColumnName: 외래키가 참조하는 대상 테이블의 컬럼 명
foreignKey(DDL): 외래 키 제약조건을 직접 지정 가능
테이블을 생성할 때만 새용
unique,nullable,insertable,updatable,columnDefinition,table: @Column의 속성과 같다

@ManyToOne

다대일 관계에서 사용

optional: false로 설정하면 연관된 엔티티가 항상 있어야 한다
fetch: 글로벌 패치 전략 설정 (EAGER, LAZY)
cascade: 영속성 전이 기능 설정
targetEntity: 연관된 타입 정보 설정

2. 연관관계 사용


저장

JPA에서 엔티티를 저장할 때 연관된 모든 엔티티는 영속 상태이어야 한다

조회

연관관계가 있는 엔티티 조회 방법은 2가지이다

  1. 객체 그래프 탐색 (객체 연관관계를 사용한 조회)

    Member member = em.find(Member.class, "member");
    Team team = member.getTeam(); // 객체 그래프 탐색
  2. 객체지향 쿼리 사용 (JPQL)

    private static void query(EntityManager em) {
    		String jpql = "select m from Member m join m.team t where t.name = :teamName";
    		List<Member> resultList = em.createQuery(jpql, Member.class)
    				.setParameter("teamName", "팀1")
    				.getResultList();
    • SQL과 JPQL을 비교하면 JPQL은 객체를 대상으로 하며 SQL보다 간결하다
    • :teamName과 같이 :로 시작하는 것은 파라미터를 바인딩 받는 문법

수정

수정은 em.update() 같은 메소드가 없다. 단순히 불러온 엔티티의 값만 변경해두면 트랜잭션을 커밋할 때 플러시가 일어난다.

연관관계 제거

member.setTeam(null); // 연관관계 제거

연관된 엔티티 삭제

엔티티를 삭제하려면 기존에 있던 연관관계를 먼저 제거하고 삭제해야 한다. 그렇지 않으면 외래 키 제약조건으로 인해 데이터베이스에서 오류가 발생

team = {member1, member2}
member1.setTeam(null);
em.remove(team); // member2 가 남아있어 에러 발생

member1.setTeam(null);
member2.setTeam(null);
em.remove(team); // 정상작동

3. 양방향 연관관계


  • 회원 → 팀, 팀 → 회원

데이터베이스 테이블은 외래 키 하나로 양방향으로 조회할 수 있다.

양방향 연관관계 매핑

@Entity
public class Member {
		@Id
		@Column(name ="MEMBER_ID")
		private Long id;

		private String username;

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

}

@Entity
public class Team {
		@Id
		@Column(name = "TEAM_ID")
		private Long id;

		private String name;

		@OneToMany(mappedBy = "team")
		private List<Member> members = new ArrayList<Member>();
}

mappedBy 속성은 양방향 매핑일 때 사용하는데 반대쪽 매핑의 필드 이름을 값으로 주면 된다.

4. 연관관계의 주인


객체의 양방향 연관관계는 단방향이 2개라고 생각하면 된다. 그래서 ManyToOne에 외래키를 지정해주었음에도 OneToMany에 mappedBy 속성이 필요하다 !

엔티티를 양방향 연관관계로 설정하면 객체의 참조는 둘인데 외래키는 하나다. 따라서 둘 사이에 차이가 발생한다. 이러한 차이로 인해 JPA에서는 두 객체 연관관계중 하나를 정해서 테이블의 외래키를 관리해야하는데 이것을 연관관계 주인(Owner) 라고 한다.

양방향 매핑의 규칙

연관관계의 주인만이 데이터베이스 연관관계와 매핑되고 외래키를 관리할 수 있다. 반면에 주인이 아닌 쪽은 읽기만 할 수 있다.

mappedBy를 사용하여 연관관계의 주인을 지정할 수 있다.

연관관계의 주인은 외래키가 있는 곳

연관관계의 주인은 테이블에 외래키가 있는 곳으로 정해야 한다.

다대일, 일대다 관계에서는 항상 다 쪽이 외래 키를 가진다. 다 쪽인 @ManyToOne은 항상 연관관계의 주인이 되므로 mappedBy 속성이 없다.

5. 양방향 연관관계 저장


team.getMembers().add(member); // 무시 (연관관계의 주인이 아님)

주인이 아닌 곳에 입력된 값은 외래키에 영향을 주지 않는다. 코드는 데이터베이스에 저장할 때 무시된다.

member.setTeam(team); // 연관관계 설정 (연관관계 주인)

Member.team은 연관관계의 주인이다. 엔티티 매니저는 이곳에 입력된 값을 사용하여 외래 키를 관리한다.

6. 양방향 연관관계 주의점


흔히 하는 실수는 연관관계의 주인에는 값을 입력하지 않고, 주인이 아닌 곳에만 값을 입력하는 것이다.

team.getMembers().add(member); // member의 team에 null 값이 입력됨

순수한 객체까지 고려한 양방향 연관관계

객체 관점에서 양쪽 방향에 모두 값을 입력해주는 것이 가장 안전하다.

member.setTeam(team);
List<Member> members = team.getMembers();
// result = empty

member.setTeam(team);
team.getMembers().add(member);
List<Member> members = team.getMembers();
// result = member 1명

결론 : 객체의 양방향 연관관계는 양쪽 모두 관계를 맺어주자 !!

연관관계 편의 메소드

양방향 관계에서 두 코드는 하나인 것처럼 사용하는 것이 안전하다

public class Member {
		private Team team;

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

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

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

setTeam() 메소드에는 버그가 있다

member.setTeam(team1);
member.setTeam(team2);
Member findMember = team1.getMember(); // team2로 바뀌었으나 여전히 조회가 됨

삭제되지 않은 관계

team1 → team2로 변경할 때 team1의 관계를 제거하지 않았다.

즉, 기존팀과 연관된 관계를 삭제하는 코드를 추가해주어야 한다.

public void setTeam(Team team) {
		if(!ObjectUtils.isEmpty(this.team)){
				this.team.getMembers().remove(this):
		}
		this.team = team;
		team.getMembers().add(this);
}

👍 참고사항

삭제되지 않아도 데이터베이스 외래 키를 변경하는 데에는 문제가 없다.

하지만 영속성 컨텍스트가 아직 살아있는 상태에서 호출하면 문제가 발생하는 것!

정리

  • 단방향 매핑만으로 테이블과 객체의 연관관계 매핑은 이미 완료
  • 단방향을 양방향으로 만들면 반대방향으로 객체 그래프 탐색 기능이 추가됨
  • 양방향 연관관계를 매핑하려면 객체에서 양쪽방향을 모두 관리해야함
  • 연관관계의 주인은 외래키의 위치와 관련해서 정해야지 비지니스 중요도로 접근하면 안된다.

⚠️ 주의사항

양방향 매핑시에는 무한루프에 빠지지않게 주의 !

이런 문제는 엔티티를 JSON으로 변환할 때 자주 발생. JSON 라이브러리들은 무한루프에 빠지지 않도록 하는 어노테이션이나 기능을 제공.

혹은 Rs안에 Entity가 아닌 Dto를 넣어 해결하도록 하자

👍 참고사항

일대다를 연관관계의 주인으로 선택하는 것은 불가능하지는 않다. 하지만 성능관리 측면에서 권장하지 않는다. 될 수 있으면 외래키가 있는 곳을 연관관계의 주인으로 선택하자.

profile
제어할 수 없는 것에 의지하지 말자

0개의 댓글