본문 바로가기
개인 공부/JPA

05 연관관계 매핑 기초

by hanyugyeong 2023. 10. 4.
반응형
SMALL

객체 관계 매핑(ORM)에서 가장 어려운 부분이 바로 객체 연관관계와 테이블 연관관계를 매핑하는 일이다. 

객체의 참조와 테이블의 외래 키를 매핑하는 것이 이 장의 목표다. 

-> 방향, 다중성, 연관관계의 주인 

 

5.1 단방향 연관관계 

* 회원과 팀이 있다. 

* 회원은 하나의 팀에만 소속될 수 있다.

* 회원과 팀의 다대일 관계다. 

 

  • 객체 연관관계
    • 회원 객체는 Member.team 필드(멤버변수)로 팀 객체와 연관관계를 맺는다.
    • 회원과 팀 객체는 단방향 관계다. 회원은 Member.team 필드를 통해서 팀을 알 수 있지만 반대로 팀은 회원을 알 수 없다.

테이블 연관관계

  • 회원 테이블은 TEAM_ID 외래 키로 팀 테이블과 연관관계를 맺는다.
  • 회원 테이블과 팀 테이블은 양방향 관계다. 회원 테이블의 TEAM_ID 외래 키를 통해서 회원과 팀을 조인할 수 있고 반대로 팀과 회원도 조인할 수 있다.
SELECT *
FROM MEMBER M
JOIN TEAM T ON M.TEAM_ID = T.ID;
 

객체 연관관계와 테이블 연관관계의 가장 큰 차이

  • 참조를 통한 연관간계는 언제나 단방향. 객체간에 연관관계를 양방향으로 만들고 싶으면 반대쪽에도 필드를 추가해서 참조를 보관해야 한다. 결국 연관관계를 하나 더 만들어야 한다.
  • 이렇게 양쪽에서 서로 참조하는 것을 양방향 연관관계라 한다. 정확히 이야기하면 이것은 양방향 관계가 아니라 서로 다른 단방향 관계 2개다.
  • 반면에 테이블은 외래 키 하나로 양방향으로 조인할 수 있다.

단방향 연관관계다. 

class A {
  B b;
}
class B { }

양방향 연관관계다.

class A {
  B b;
}
class B {
  A a;
}

5.1.1 순수한 객체 연관관계

JPA를 사용하지 않은 순수한 회원과 팀 클래스의 코드다. 

public class Member {
    private String id;
    private String username;

    @Getter
    @Setter
    private Team team; //팀의 참조를 보관
}
@Getter
@Setter
public class Team {
    private String id;
    private String name;
}

회원1과 회원 2를 팀 1에 소속시키자. 

Team team1 = new Team("team1", "팀1");

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

member1.setTeam(team);
member2.setTeam(team);

Team findTeam = member1.getTeam();

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

5.1.2 테이블 연관관계 

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;

5.1.3 객체 관계 매핑 

지금까지 객체만 사용한 연관관계와 테이블만 사용한 연관관계를 각각 알아보았다. 

@Entity
@Getter
@Setter
pubilc class Member {
    @Id
    @Column(name = "MEMBER_ID")
    private String id;

    private String username;

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

@Entity
@Getter
@Setter
pubilc class Team {
    @Id
    @Column(name = "TEAM_ID")
    private String id;

    private String name;
}

* 객체 연관관계 : 회원 객체의 Member.team 필드 사용 

* 테이블 연관관계 : 회원 테이블의 MEMBER.TEAM_ID 외래 키 칼럼을 사용 

 

Member.team 과 MEMBER.TEAM_ID를 매핑하는 것이 연관관계 매핑이다. 연관관계 매핑 코드를 분석해보자. 

5.1.4 @JoinColumn

@JoinColumn은 외래 키를 매핑할 때 사용한다. 

속성 기능  기본값
name 매핑할 외리 키 이름 필드명 + _ + 참조하는 테이블의 기본 키 컬럼명
referencedColumnName 외래 키가 참조하는 대상 테이블의 컬럼명 참조하는 테이블의 기본 키 컬럼명
foreignKey(DDL) 외래 키 제약조건을 직접 지정. 테이블을 생성할 때만 사용 -
unique
nullable
insertable
updatable
columnDefinition
table
@Column 속성과 같음 -

5.1.5 @ManyToOne

@ManyToOne 어노테이션은 다대일 관계에서 사용한다. 

 

속성 기능 기본값
optional false로 설정하면 연관된 엔티티가 항상 있어야 함 true
fetch 글로벌 페치 전략을 설정 @MayToOne=EAGER, @OneToMany=LAZY
cascade 영속성 전이 -
targetEntity 연관된 엔티티의 타입 정보를 설정(제너릭) -

다음 코드는 targetEntity 속성의 사용 예다. 

 

@OneToMany

private List<Member> members; // 제네릭으로 타입 정보를 알 수 있다. 

 

@OneToMany (targetEntity=Member.class)

private List members; // 제네릭이 없으면 타입 정보를 알 수 없다. 

5.2 연관관계 사용

5.2.1 저장

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

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

5.2.2 조회

Member member = em.find(Member.class, "member1");
Team team = member.getTeam(); //객체 그래프 탐색

5.2.3 수정

//새로운 팀2
Team team2 = new Team("team2", "팀2");
em.persist(team2);

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

변경사항을 데이터베이스에 자동으로 반영한다. 이것은 연관관계를 수정할 때도 같은데, 참조하는 대상만 변경하면 나머지는 JPA가 자동으로 처리한다. 

  • 단순히 불러온 엔티티의 값만 변경해두면 트랜잭션을 커밋할 때 플러시가 일어나면서 변경 감지 기능이 작동.
  • 변경 사항을 DB에 자동으로 반영

5.2.4 연관관계 제거 

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

5.2.5 연관된 엔티티 삭제 

연관된 엔티티를 삭제하려면 기존에 있던 연관관계를 먼저 제거하고 삭제해야 한다. 

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

5.3 양방향 연관관계 

  • 양방향 매핑의 장점
    • 단방향 매핑으로 이미 연관관계 매핑은 완료
    • 양방향은 반대 방향으로 객체 그래프 탐색 기능이 추가된 것
    • 단방향 매핑을 잘하고 양방향 매핑은 필요할 때 추가해도 됨

5.3.1 양방향 연관관계 매핑

이제 양방향 관계를 매핑하자. 

@Entity
public class Member {
    // 동일
}

@Entity
public class Team {
  //추가되는 부분
  @OneToMany(mappedBy = "team")
  private List<Membmer> members = new ArrayList<>();
}
  • mappedBy
    • 양방향 매핑일 때 사용
    • 반대쪽 매핑의 필드 이름을 값으로 사용
    • 반대쪽 매핑이 Member.team 이므로 "team"

5.3.2 일대다 컬렉션 조회

팀에서 회원 컬렉션으로 객체 그래프 탐색을 사용해서 조회한 회원들을 출력한다. 

Team team = em.find(Team.class, "team1");
List<Member> members = team.getMembers(); //팀 -> 회원, 객체 그래프 탐색
for (Member member : members) {
    //...
}

5.4 연관관계의 주인

  • 객체에는 양방향 연관관계는 없음.
    • 서로 다른 단방향 연관관계 2개를 어플리케이션 로직으로 양방향인 것처럼 보이게 할 뿐
  • 데이터베이스 테이블은 외래 키 하나로 양쪽이 서로 조인 가능
    • 테이블은 외래 키 하나만으로 양방향 연관관계 가능
  • 엔티티를 양방향 연관관계로 설정하면 객체의 참조는 둘인데 외래키는 하나
    • 따라서 둘 사이에 차이가 발생
    • 두 객체 연관관계 중 하나를 정해서 테이블의 외래 키를 관리해야 하는데 이것을 연관관계의 주인(Owner)이라 함

5.4.1 양방향 매핑의 규칙:연관관계의 주인

  • 두 연관관계 중 하나를 연관관계의 주인으로 설정
  • 연관관계의 주인만이 데이터베이스 연관관계와 매핑되고 외래 키를 관리(등록, 수정, 삭제)할 수 있음
  • 반면에 주인이 아닌 쪽은 읽기만 할 수 있음
  • 주인은 mappedBy 속성을 사용하지 않음
  • 연관관계의 주인을 정한다는 것은 사실 외래 키 관리자를 선택하는 것

다음 두 코드를 보자. 

* 회원 -> 팀 (Member.team) 방향 

class Member {
	@ManyToOne
    @JoinColumn(name="TEAM_ID")
    private Team team;
}

 

* 팀->회원(Team.members) 방향 

class Team {
	@OneToMany
    private List<Member> members = new ArrayList<Member>();
	
}

연관관계의 주인을 정한다는 것은 사실 외래 키 관리자를 선택하는 것이다. 

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

  • 회원 테이블이 외래 키를 가지고 있으므로 Member.team이 주인
  • 주인이 아닌 Team.members에는 mappedBy="team" 속성을 사용해서 주인이 아님을 설정
    • 여기서 mappedBy 값으로 사용된 team은 연관관계의 주인인 Member 엔티티의 team 필드를 말함
데이터베이스 테이블의 다대일, 일대다 관계에서는 항상 다 쪽이 외래키를 가짐.
다 쪽인 @ManyToOne은 항상 연관관계의 주인이 되므로 mappedBy를 설정할 수 없다.

5.5 양방향 연관관계 저장

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

5.6 양방향 연관관계의 주의점

양방향 연관관계를 설정하고 가장 흔히 하는 실수는 연관관계의 주인에는 값을 입력하지 않고, 주인이 아닌 곳에만 값을 입력하는 것이다. 

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

Team team = new Team("team1", "팀1");
//주인이 아닌 곳만 연관관계 설정
team.getMembers().add(member1);
em.persist(team1);

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

  • 객체 관점에서 양쪽 방향에 모두 값을 입력해주는 것이 가장 안전
    • 양쪽 방향에 모두 값을 입력하지 않으면 순수한 객체 상태에서 심각한 문제가 발생할 수 있음
//팀1
Team team1 = new Team("team1", "팀1");
Member member1 = new Member("member1", "회원1");
member1.setTeam(team); //연관관계 설정 member1 -> team1

List<Member> members = team1.getMembers();
//members.size()는?
  • ORM은 객체와 데이터베이스 둘 다 고려
//팀1
Team team1 = new Team("team1", "팀1");
em.persist(team1);

Member member1 = new Member("member1", "회원1");
//양방향 연관관계 설정
member1.setTeam(team);           //연관관계 설정 member1 -> team1
team1.getMembers().add(member1); //연관관계 설정 team1 -> member1, 저장시 사용하지는 않음
em.persist(member1);

5.6.2 연관관계 편의 메소드

양방향 연관관계는 결국 양쪽 다 신경 써야 한다. 다음처럼 member.setTeam(team)과 team.getMembers().add(member)를 각각 호출하다 보면 실수로 둘 중 하나만 호출해서 양방향이 깨질 수 있다. 

public class Member {
    private Team team;

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

//연관관계 설정
member1.setTeam(team1);

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

member1.setTeam(teamA);
member1.setTeam(teamB);
Member findMember = teamA.getMember(); //member1이 여전히 조회
  • teamB로 변경할 때 teamA -> member1 관계를 제거하지 않음
public class Member {
    public void setTeam(Team team) {
        //기존 팀과 관계를 제거
        if (this.team != null) {
            this.team.getMembers().remove(this);
        }
        this.team = team;
        team.getMembers().add(this);
    }
}

5.7 정리 

단방향 매핑과 비교해서 양방향 매핑은 복잡하다. 단방향과 비교해서 양방향의 장점은 반대방향으로 객체 그래프 탐색 기능이 추가된 것뿐이다. 

 

내용을 정리하면 다음과 같다. 

* 단방향 매핑만으로 테이블과 객체의 연관관계 매핑은 이미 완료되었다. 

* 단방향을 양방향으로 만들면 반대방향으로 객체 그래프 탐색 기능이 추가된다. 

* 양방향 연관관계를 매핑하려면 객체에서 양쪽 방향을 모두 관리해야 한다. 

반응형
LIST

'개인 공부 > JPA' 카테고리의 다른 글

JPA N+1 문제  (0) 2024.04.12
05 연관관계 매핑 시작 실전예제  (0) 2023.10.04
04 엔티티 매핑  (0) 2023.09.20
03 영속성 관리  (0) 2023.09.19
02 JPA 시작  (0) 2023.09.19