5 분 소요

자바 ORM 표준 JPA 프로그래밍에서 읽은 내용을 정리하는 글입니다😄

객체는 참조를 통해 관계를 맺고, 테이블은 외래 키를 통해 관계를 맺는다.
5장에서는 객체의 참조테이블의 외래 키매핑하는 것이 목표다.

5.1 단방향 연관관계

회원과 팀이 있고 회원이 팀에 속하는 상황을 생각해보자. 정확히는 여러 회원이 팀에 속하는 상황이다.
이 때 회원과 팀은 다대일(N:1) 단방향 관계이다. 객체외 테이블 각각의 연관관계를 살펴보면

  • 객체 연관관계
    • 회원 객체는 Member.team 필드로 팀 객체와 관계를 맺는다.
    • 회원 객체와 팀 객체는 단방향 관계다. (회원→팀(O), 팀→회원(X))
  • 테이블 연관관계
    • 회원 테이블은 TEAM_ID 외래 키로 팀 테이블과 관계를 맺는다.
    • 회원 테이블과 팀 테이블은 양방향 관계다.

테이블 연관관계에서는 어떻게 양방향 관계를 가질까?
회원과 팀을 조인하는 다음 SQL를 보면 같은 결과를 얻는다는걸 알 수 있다.

//회원과 팀을 조인
SELECT *
FROM MEMBER M
JOIN TEAM T ON M.TEAM_ID = T.ID

//팀과 회원을 조인
SELECT *
FROM TEAM T
JOIN MEMBER M ON T.TEAM_ID = M.TEAM_ID

객체에서의 참조는 반드시 단방향으로 이루어지기 때문에 양방향으로 만들려면 서로 참조할 수 있는 필드가
있어야 한다. 즉, 양방향 관계가 사실은 서로 다른 단방향 관계 2개가 된다.
그러나 테이블은 외래 키 하나로 양방향으로 조인할 수 있다. 이게 객체, 테이블 연관관계 간의 큰 차이점이다.

객체가 참조를 사용해 연관관계를 탐색하는 것을 객체 그래프 탐색이라 한다.

Member member1 = new Member("member1","회원1");
Team team1 = new Team("team1","팀1");

member1.setTeam(team1);

Team findTeam = member1.getTeam()l //객체 그래프 탐색

데이터베이스 테이블의 회원과 팀의 관계를 살펴보자.
다음 코드는 테이블 DDL이며 멤버, 팀에 대한 테이블 정의와 회원 테이블 외래키 제약조건으로 구성된다.
위에서 언급한 것처럼 데이터베이스는 외래 키를 사용해서 연관관계를 탐색한다. 이를 조인이라 한다.

CREATE TABLE MEMBER (
  MEMBER_ID VARCHAR(255) NOT NULL,
  TEAM_ID VARCHAR(255),
  USERNAME VARCHAR(255),
  PRIMARY KEY (MEMBER_ID)
)

CREATE TABLE TEAM (
  TEAM_ID VARCHAR(255) NOT NULL,
  NAME VARCHAR(255),
  PRIMARY KEY (TEAM_ID)
)

ALTER TABLE MEMBER ADD CONSTRAINT FK_MEMBER_TEAM
  FOREIGN KEY (TEAM_ID)
  REFERENCES TEAM

JPA를 사용해서 객체 연관관계와 테이블 연관관계를 매핑해보자.
연관관계 매핑이라 써진 부분에 있는 어노테이션을 보면 @ManyToOne, @JoinColumn이 있다.
각각 다대일 관계와 매핑할 외래 키 컬럼을 의미한다.

//매핑한 회원 엔티티
@Entity
public class Member {

  @Id
  @Column(name = "MEMBER_ID")
  private String id;

  private String username;

  //연관관계 매핑
  @ManyToOne
  @JoinColumn(name="TEAM_ID")
  private Team team;

  //Getter, Setter ...
}
//매핑한 팀 엔티티
@Entity
public class Team {

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

  private String name;

  //Getter, Setter ...
}

@JoinColumn 속성은 다음과 같다.

  • name : 매핑할 외래 키 이름 (기본값: 필드명 + _ + 참조할 테이블 기본 키 컬럼명)
  • referenceColumnName : 외래 키가 참조하는 대상 테이블의 컬럼명
  • foreignKey(DDL) : 외래 키 제약조건 직접 지정 (테이블 생성 시 사용)
  • unique, nullable, insertable, updatable, columnDefinition, table

@ManyToOne 속성은 다음과 같다.

  • optional : false로 설정 시 연관된 엔티티가 반드시 있어야 함 (기본값: true)
  • fetch : 글로벌 페치 전략
  • cascade : 영속성 전이 기능
  • targetEntity : 연관된 엔티티 타입 정보 (거의 사용X)

5.2 연관관계 사용

저장

연관관계를 매핑한 엔티티를 저장하는 예제는 다음과 같다.

//팀1 저장
Team team = new Team("team1","팀1");
em.persist(team1);

//회원1 저장
Member member1 = new Member("member1","회원1");
member1.setTEam(team1);
em.persist(member1);

회원 엔티티는 팀 엔티티를 참조하고 저장한다. 그러면 JPA는 참조한 팀의 식별자(Team.id)를 외래 키로
사용해 적절한 등록 쿼리를 생성한다.

조회

연관관계가 있는 엔티티 조회 방법은 크게 2가지다.
객체 연관관계는 객체 그래프 탐색을, 테이블은 객체지향 쿼리(JPQL)를 사용한다.
전자의 예시는 member.getTeam()가 있고, 후자 예시는 다음과 같다.

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();

//resultList의 멤버 출력 코드

jpql에서 from Member m join m.team t를 보면 회원이 팀 필드를 통해 Member와 Team을 조인한다.
그리고 where절에선 조인한 t.name을 갖고 팀1에 속한 회원만 검색했다.
(:teamName처럼 :로 시작하는 것은 파라미터를 바인딩받는 문법이다)

수정

팀1 소속인 회원1을 팀2로 수정해보자.

Team team2 = new Team("team2","팀2");
em.persist(team2);

//회원1에 새로운 팀2 설정
Member member = em.find(Member.class, "member1");
member.setTeam(team2);

실행되는 SQL은 다음과 같다.

UPDATE MEMBER
SET TEAM_ID='team2', ...
WHERE ID='member1'

엔티티의 값을 수정하면 트랜잭션을 커밋할 때 플러시가 일어나 변경 감지 기능이 작동한다.
따라서 변경사항이 DB에 자동으로 반영된다. 연관관계를 수정할 때도 마찬가지이다.

//연관관계 제거
Member member = em.find(Member.class, "member1");
member1.setTeam(null);

실행되는 SQL은 다음과 같다.

UPDATE MEMBER
SET TEAM_ID=null, ...
WHERE ID='member1'

연관된 엔티티를 삭제할 때는 외래 키 제약조건 때문에 기존에 있던 연관관계를 먼저 제거하고
삭제해야 한다. 다음은 팀1 삭제에 대한 예시이다.

member1.setTeam(null) //회원1 연관관계 제거
member2.setTeam(null) //회원2 연관관계 제거
em.remove(team); //팀 삭제

5.3 양방향 상관관계

객체에서는 참조하려면 member.getTeam()를 사용해야 한다. 즉, 필드에 팀 정보가 있어야 한다.
역으로 팀 객체에서 회원 객체를 참조하려면 회원을 참조할 수 있는 필드가 반드시 있어야 한다.
팀 객체 입장에서는 일대다 관계이므로, 여러 회원과 연관관계를 맺을 수 있도록 컬렉션을 사용해야
한다. 때문에 Team.members를 List 컬렉션으로 추가하면 해야 한다.

테이블은 외래 키 하나로 양방향을 조회할 수 있으므로 별도로 추가할 내용은 없다.

양방향 연관관계 매핑

회원 엔티티에서는 변경할 내용이 없고, 팀 엔티티에서는 다음의 내용이 추가돼야 한다.

@Entity
public class Team {

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

  private String name;

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

  //Getter, Setter ...
}

일대다 관계이므로 @OneToMany를 추가했고 mappedBy는 반대쪽 매핑 필드 이름을 값으로 주면 된다.
mappedBy 속성은 양방향 매핑일 때 사용한다. 다음에 나오는 연관관계 주인과 관련이 있다.

일대다 컬렉션 조회는 team.getMembers()를 통해서 출력을 통해 확인할 수 있다.

5.4 연관관계의 주인

테이블은 외래 키 하나로 두 테이블의 연관관계를 관리한다. 객체는 단방향으로 매핑 시 참조를 하나만
사용하므로 이 참조로 외래 키를 관리하면 되는데, 양방향이면 두 곳에서 서로를 참조한다.
이렇게 되면 객체의 참조는 둘인데 외래 키는 하나이다. 때문에 두 객체 연관관계 중 하나를 정해서
테이블의 외래키를 관리해야 한다. 이를 연관관게의 주인이라 한다.

연관관계 주인만이 DB 연관관계와 매핑되고 외래 키를 관리할 수 있게된다.
주인이 아니라면 읽기만 가능하다. 연관관계 주인은 mappedBy 속성으로 설정하면 된다.
연간관계 주인은 외래 키 관리자와 같으므로 외래 키가 있는 곳에 설정해야 한다.
앞에서 다룬 회원-팀 예시에선 회원 테이블이 외래 키를 갖고 있으므로 Member.team이 주인이 된다.
주인이 아닌 Team.membersmappedBy="team"속성을 사용해 주인이 아님을 설정해주면 된다.
주인인 Member.team에는 mappedBy 속성을 사용하지 않는다.

class Team {

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

연관관계 주인이 아닌 곳에 입력된 값은 외래 키에 영향을 주지 않는다.
DB에 외래 키 값이 저장되지 않으면 이 경우를 의심해보자.

//연관관계 주인X : 무시됨
team.getMembers().add(member);

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

연관관계 주인이 아닌 경우 무시되는 케이스는 JPA를 사용하는 상황일 때 적용된다.
그렇지 않으면 순수한 객체 상태에서 양방향 연관관계로서의 의미를 갖지 못하는 경우가 발생할 수 있다.
때문에 객체의 양방향 연관관계는 양쪽 모두 관계를 맺어주는게 안전하다.

Team team1 = new Team("team1", "팀1");
em.persist(team1);

Member member1 = new Member("member1", "회원1");

member1.setTeam(team1);           //member1→team1
team1.getMembers().add(member1);  //team1→member1
em.persist(member1);

회원에서 팀으로, 팀에서 회원으로의 관계를 설정하는 코드가 각각 나뉘어 있는데 실수로 하나만 호출하게
되면 양방향이 깨지게 되는 문제가 발생할 수 있다. 때문에 Member.setTeam()을 리팩토링 하면 다음과 같고
이 메소드를 연관관계 편의 메소드라 한다.

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

member1의 팀을 처음엔 team1로 설정했다가 team2로 바꾸는 상황을 생각해보자.
member1.setTeam(team1) 뒤에 member1.setTeam(team2)가 이어지면 team1에서 member1로의 연관관계가
끊어지지 않는 문제가 발생한다. 때문에 다음과 같은 코드를 추가해야 한다.

//기존 팀과의 관계 제거
if(this.team != null) {
  this.team.getMembers().remove(this);
}

카테고리:

업데이트:

댓글남기기