Back-end/JPA

자바 ORM 표준 JPA 연관관계

이안_ian 2023. 5. 27. 00:11
반응형

단방향 연관관계

객체지향 모델링

출처에서 사용된 예시

하나의 팀이 여러 개의 멤버를 가질 때 보통 Member에서 team_id를 갖고 있는 설계를 많이 하게된다.

하지만 객체지향에서는 외래키 대신 객체 자체를 들고 있는 형태가 되어야 한다.

그래서 위와 같은 내용으로 Member쪽에 이렇게 설정을 해야한다.

@Entity
public class Member {
  중략..
  
  @ManyToOne(fetch = FetchType.LAZY) //Member, Team 관계를 나타내는 다:1(N:1)
  @JoinColumn(name = "TEAM_ID")
  private Team team;
}

이런식으로 단방향 매핑을 한 뒤에 member를 find 할 경우 매핑된 Team의 정보까지 같이 join되어 불러온다.

Member findMember = em.find(Member.class, 1L);
Team findTeam = findMember.getTeam();
String temaName = findMember.getTeam().getName();

만약에 현재 위에서 불러온 findMember의 team_id가 1일 경우 100인 team으로 바꾸고 싶을 경우 find로 객체를 불러와서 set만 해주게되면 DB에서는 team_id가 교체가된다.

Member findMember = em.find(Member.class, 1L);
Team newTeam = em.find(Team.class, 100L);

//DB에서는 Member 테이블에 있는 team_id가 1 -> 100으로 변경됨
findMember.setTeam(newTeam);

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

지금 상태에서는 Team에서 Member가 조회가 안된다.

근데 테이블 관계에서 둘은 외래키로 왔다갔다할 수 있다.

객체에서는 양방향으로 설정해줘야 하는데 Team에 List로 members를 넣어줘야한다.

이 부분이 객체와 테이블 연관관계의 가장 큰 차이점이다.

@Entity
public class Team {
  중략..
  
  @OneToMany(mappedBy = "team") //Member에서 사용하는 Team 객체 변수명
  private List<Member> members = new ArrayList<>(); //new ArrayList는 nullPoint도 막아주고, 관례적으로 사용
}

이렇게하면 이제 Team이 갖고 있는 멤버들이 조회가 된다.

Member findMember = em.find(Member.class, member.getId());
List<Member> members = findMember.getTeam().getMembers();

for(Member m : members){
  sout("m = " + m.getUsername());
}

테이블의 양방향 관계는 외래 키 하나로 두 테이블의 연관관계를 관리한다.

select *

from member m

join team t on m.team_id = t.team_id

 

select *

from team t

join member m on m.team_id = t.team_id

 

그러나 객체의 양방향 관계는 사실 양방향이 아니라 서로 다른 단방향 관계 2개다. 그래서 객체를 양방향으로 참조하려면 단방향 연관관계를 2개 만들어야 한다.

A -> B (a.getB())

B -> A (b.getA())

 

 

단방향일 때는 큰 문제가 없는데 양방향일 때는 둘다 참조가 되기 때문에 Member가 Team을 바꾸고 싶을 때 Member에서  외래키를 바꿔야할지? Team에서 외래키를 바꿔야할지? 고민을 하게된다.

그렇기때문에 둘 중 하나로 외래 키를 관리해야 한다.

연관관계의 주인(Owner)

양방향 매핑 규칙

  • 객체의 두 관계중 하나를 연관관계의 주인으로 지정
  • 연관관계의 주인만이 외래 키를 관리(등록, 수정)
  • 주인이 아닌쪽은 읽기만 가능
  • 주인은 mappedBy 속성 사용X
  • 주인이 아니면 mappedBy 속성으로 주인 지정

그래서 Team에 mappedBy가 지정된 members에서는 조회만 가능할 뿐 어떠한 수정이 되지않는다.

누구를 주인으로?

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

그리고 DB관점에서 외래 키가 있는 곳이 N이다. 없는 곳이 1이고(N:1), @ManyToOne 설정하는 곳

여기서는 Member.team이 연관관계 주인이며 진짜 매핑이고, Team.members는 가짜 매핑(조회용)

 

아래와 같은 실수를 하지 말아야한다.

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

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

team.getMembers().add(member);

//이럴경우 member에 team_id는 null이 된다. 연관관계 주인으로 값을 설정해야함
em.persist(member);

 

그리고 순수 객체 상태를 고려해서 항상 양쪽에 값을 설정하자.

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

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

em.persist(member);

//이때 위에서 추가된 memeber는 출력되지 않는다, 왜냐면 1차 캐시 데이터를 불러오기 때문에
//em.flush()나 em.clear()를 하고 find하기전까진 미노출이다.
Team findTeam = em.find(Team.class, team.getId());

//그렇기에 양쪽 다 데이터를 넣어주는게 좋다.
team.getMembers().add(member);

 

연관관계 편의 메소드를 생성하자.

@Entity
public class Member {
  중략..
  
  public void setTeam(Team team){  //chageTeam 같은 메소드로 변경하자 setter는 지양
    this.team = team;
    
    team.getMembers().add(this);
  }
}

 

양방향 매핑시에 무한 루프를 조심하자.

예) toString(), lombok, JSON 생성 라이브러리

@Entity
pulbic class Member {
  중략..
  
  @ManyToOne
  @JoinColumn(name = "team_id")
  private Team team;
  
  @Override
  public String toString(){
    //간소화 리턴 값
    return "team=" + team;
  }
}

@Entity
pulbic class Team {
  중략..
  
  @OneToMany(mappedBy = "team")
  private List<Member> members = new ArrayList<>();
  
  @Override
  public String toString(){
    //간소화 리턴 값
    return "members=" + members;
  }
}

이런식으로 서로가 서로를 계속 호출하게되어 무한루프에 빠져 StackOverflowError가 발생한다.

 

출처 : https://www.inflearn.com/course/ORM-JPA-Basic

 

 

반응형