본문 바로가기

정보/Database

연관 관계 Mapping (1)

0. Entity 설정과 DB 중심적 Entity의 문제점

  • 메타데이터는 Entity Class 내에 적어 두는 것을 선호
  • Index도 마찬가지, @Table()에 적어두기
  • Table 이름과 Column이름 조직 컨벤션에 맞게 바꾸는 것 중요.
    • spring boot에서는 camel -> snake로 자동으로 바꿔주는 기능도 있으니.

 

객체 지향적인 Entity?

public class Order {
    @Id    @GeneratedValue    @Column(name = "ORDER_ID")
    Long id;

    @Column(name = "MEMBER_ID")
    Long memberId; // 이 부분이 객체지향스럽지 않음
    // 이렇게 될 경우 em.find(Member.class, order.getMemberId())를 통해 member를 찾아야하기 때문에
}

 

-  아래 처럼 되어야 객체 지향적인 Entity가 된다.

public class Order {
    @Id    @GeneratedValue    @Column(name = "ORDER_ID")
    Long id;

    Member member; // 이것처럼 객체를 가져야함!
	// 이러면 order.getMember를 통해 가져올수있다.
}

 

이를 위해서 연관관계 매핑이 필요하다.

➡️ 그리고 이를 알기 위해 객체와 테이블 연관관계의 차이를 이해해야함

  • 객체의 참조 vs 테이블의 외래 키 매핑
  • 방향 (단방향, 양방향) , 다중성, 연관 관계의 주인

 

 

1. 연관 관계가 필요한 이유

 - 연관 관계가 있는 테이블 사이의 생성 시

Team team = new Team();
team.setName("TEAM_A");
em.persist(team);    // Team을 만들고, Member에 직접연결해줘야하고..

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

 

 - 조회 시

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

Long findTeamId = findMember.getTeamId();
Team findTeam = em.find(Team.class, findTeamId);

 

➡️ 객체를 테이블에 맞추어 데이터 중심 모델링은 협력 관계를 만들 수 없다.

테이블: 외래 키로 JOIN 사용해서 연관 테이블 찾음 vs 객체: 참조

 

 

 

2. 단방향 연관 관계

객체에는 참조값을 그대로! TeamId가 있는 게 아님.

@ManyToOne // Member가 N이고 Team이 1이니까
@JoinColumn(name = "TEAM_ID") //Join mapping
Team team;
// 어노테이션 없이 단순히 이렇게 참조만 사용하면 JPA에서 어노테이션 매핑을 요구

 

사용할 때 다음처럼 객체지향적으로 바꿀 수 있다.

// 저장
Team team = new Team();
team.setName("Team_C");
em.persist(team);

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

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

 

 

 

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

사실은 테이블의 연관관계에서는 양방향 이런게 없다. TEAM의 PK에서 Member의 FK에 JOIN하면 된다.

문제는 객체임. 객체는 List Members를 넣어줘야 한다.

@OneToMany(mappedBy = "team")
List<Member> members = new ArrayList<>(); // 관례로 NPE방지를 위해 넣는다.
// Member의 team 필드로 매핑되었다.

 

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

Member findMember = em.find(Member.class, 1L);
Team team1 = findMember.getTeam();
member.setTeam(team1);

em.flush();

List<Member> members = findMember.getTeam().getMembers();
for (Member m : members) {
    System.out.println(m.getUsername());
}

양방향 연관 관계를 사용한 DB 조회

 

 

간단 Trouble Shooting 1 : ToString 순환 참조로 인한 StackOverFlow발생

@ToString에 Member가 Team을, Team이 Member를 참조하기 때문에 발생함

=> ToString(exclude = {"team"}) 또는 해당 필드에 @ToString.Exclude 옵션을 붙여줄 것.

 

참고, JSON 생성 라이브러리 같은 것도 무한루프에 걸린다.

Entity를 직접 Controller에서 보내버리면 Entity가 가진 연관관계가 양방향이면, 무한루프에 걸릴 것이다.

(Entity는 절대 Controller에서 반환 하지 마라. DTO쓴다면 상관없을 거긴 하다.)

 

 

 

4. mappedBy 속성의 이해

  • Table은 FK존재만으로도 양방향 관계가 형성이 되어있다. (양쪽으로 JOIN이 가능하다)
  • 객체는 회원과 팀간의 단방향 연관관계, 팀과 회원간의 단방향 연관관계로
    참조값 두 개를 집어넣어서 만들어줘야 한다.
    • 서로 다른 단방향 연관관계 2개를 집어넣어야 양방향 관계임.
  • Dilemma : 그러면 FK를 뭐로 설정해야 하는가?
    • member의 team을 바꿔야 하는가?
    • Team의 members를 바꿔야 하는가?
    • 둘 중 하나로 주인을 결정해야 한다 (연관관계의 주인)

 

5. 연관 관계의 주인

객체 두 관계 중 하나를 연관 관계의 주인으로 지정

연관관계의 주인만이 외래키를 관리(등록, 수정) ➡️ 주인이 아닌 쪽은 읽기만 가능.

주인은 mappedBy 속성 사용X, 아니면 mappedBy 속성으로 주인 지정

그러면 주인을 지정하는 기준은? DB 테이블 상 1:N에서 N쪽으로 외래 키 지정. 다시 말해서

"외래 키가 있는 곳을 주인으로 정해라."
Member member = new Member();
member.setUsername("member123");
em.persist(member);

Team team = new Team();
team.setName("TeamA");
team.getMembers().add(member);
// insert쿼리가 member, team들어가도 teamId 매핑안됨
// 당연하지. 연관관계의 주인이 member인데 team에 집어넣으니까 안되는 것
em.persist(team);

 

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

Member member = new Member();
member.setUsername("member123");
member.setTeam(team);
em.persist(member); // 주인인 member에 team 을 set해야 적용된다.

 

 

간단한 Trouble Shooting 2 : 양방향 연관 관계시 1차 캐시를 주의해야 함

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

            Member member = new Member();
            member.setUsername("member13413");
            member.setTeam(team);
            em.persist(member);
            //em.flush();
            //em.clear();
            Team findTeam = em.find(Team.class, team.getId());
            List<Member> members = findTeam.getMembers();

            for (Member m : members) {
                System.out.println("m = " + m.getUsername());
            }

위 처럼 flush를 안하고 EntityManager의 find를 통해 찾을 경우, 1차 캐시를 사용하기 때문에

아직 findTeam의 members는 비어 있다. => SELECT query가 나가지 않음.

 

➡️ 객체 지향적으로 생각해 봤을 때, 양쪽에 다 값을 집어 넣어주는게 맞다.

team.getMembers().add(member); 를 해줘야한다는 것.

 - 테스트 코드 작성시에도 이게 중요하다.

양방향 연관 관계시, 양쪽에 다 값을 세팅해줘야 한다.
양방향 연관 관계를 위한 method를 만들어서 Human Error를 방지할 것.
setter를 바꾸지 말고, 새로 메서드 만들 것.
public void changeTeam(Team team) {
    this.team = team;
    team.getMembers().add(this);
} // Member에 Team을 넣는 경우

public void addMember(Member member) {
    member.setTeam(this);
    members.add(member);
} // Team에 Member를 넣는 경우

 

단, 둘 중 하나만을 선택해서 사용할 것! (상황을 보고 할 것.)

 

 

6. 정리

  • 단방향 매핑만으로도 연관관계 매핑은 완료
  • 양방향은 반대 방향으로 조회를 위해서의 기능 추가만 된 것임.
  • 설계 입장에서 본다면, 단방향 매핑만으로 최대한 진행해보고, 필요할 때 양방향 추가
    • JPQL에서 역방향으로 탐색할 일이 많긴 함

'정보 > Database' 카테고리의 다른 글

연관 관계 Mapping (3)  (0) 2024.11.17
연관 관계 Mapping (2)  (1) 2024.11.15
Entity Mapping  (3) 2024.11.15
JPA Persistence Context 는 Git과 흡사하다  (0) 2024.11.13
JPA : Java ORM 표준  (2) 2024.11.13