-> 블로그 이전

[JPA] 연관관계

2022. 7. 2. 20:58Language`/JPA

 

객체는 "참조"를 사용해서 관계를 맺고, 테이블은 "외래키(FK")를 통해서 관계를 맺는다

 

따라서 우리는 "객체의 참조"와 "테이블의 FK"를 매핑시켜야 한다

JPA를 사용하려면 반드시 알아야하고 이해해야 하는 부분이고 가장 어려운 부분이다

 

방향

방향에는 [단방향 & 양방향]이 존재한다

Inflearn : 자바 ORM 표준 JPA 프로그래밍 - 기본편(김영한)

위의 예제는 "단방향"이고 이를 {객체 참조 - 테이블 FK}로 매핑하면 다음과 같다

@Entity
@Table(name = "member")
public class Member {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "member_id")
    private Long id;
    
    @Column(length = 10)
    private String userName;

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

------------------------------------------------------------------
@Entity
@Table(name = "team")
public class Team {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "team_id")
    private Long id;
    
    @Column(unique = true, length = 20)
    private String teamName;
}

위의 예제는 Member → Team으로 가는 방향만 존재하고, Team → Member로 가는 방향은 존재하지 않는다.

 

Inflearn : 자바 ORM 표준 JPA 프로그래밍 - 기본편(김영한)

위의 예제는 "양방향 매핑"이고 이를 객체입장에서 코딩하면 다음과 같아진다

@Entity
@Table(name = "member")
public class Member {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "member_id")
    private Long id;
    
    @Column(length = 10)
    private String userName;

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

------------------------------------------------------------------
@Entity
@Table(name = "team")
public class Team {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "team_id")
    private Long id;
    
    @Column(unique = true, length = 20)
    private String teamName;

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

 

사실 객체에는 "양방향"이라는 개념이 없다
"단방향 2개"가 존재하는 것이다

 

@JoinColumn

@JoinColumn은 FK를 매핑할 때 사용한다

속성 기능 default value
name 매핑할 외래키 이름 필드명 + "_" + 참조하는 테이블의 PK 컬럼명
referencedColumnName 외래키가 참조하는 대상 테이블의 컬럼명 참조하는 테이블의 PK컬럼명
foreignKey(DDL) 외래키 제약조건 직접 지정
-> 테이블 생성할 때만 사용
 
unique
nullable
insertable
updatable
columnDefinition
table
@Column에서 사용하는 것과 동일하게 사용하면 된다

referencedColumnName은 기본적으로 참조하는 FK가 원래 있던 테이블의 PK컬럼명을 택하기 때문에 굳이 설정해주지 않아도 된다.

그리고 정규화 관점에서도 설정하지 않는것이 좋다

 


다중성

다중성은 두 도메인간의 관계를 {일대일 / 일대다 / 다대일 / 다대다}중 하나로 결정할 수 있다

  • @OneToOne
  • @OneToMany
  • @ManyToOne
  • @ManyToMany

연관관계 주인

두 도메인에 대한 양방향 연관관계를 맺었다면 "연관관계의 주인"을 택해야 한다

<양방향 매핑 규칙>
1. 두 객체중 하나를 연관관계의 "주인"으로 지정
2. 연관관계의 "주인"만이 FK를 관리한다 (등록/수정)
-> "주인"이 아닌쪽은 "read" 권한만 존재한다
3. 주인은 mappedBy 속성을 사용하지 않고, 주인이 아니면 mappedBy로 주인을 지정해줘야 한다

 

사실 연관관계의 주인을 선택하는 것은 간단하다.

Table상에서 FK를 가진 객체를 주인으로 선택하면 된다

 

 

앙방향 매핑 주의점

1. 주인이 아닌 곳에만 값을 입력

@Entity
@Table(name = "member")
public class Member {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "member_id")
    private Long id;

    @Column(length = 10)
    private String userName;

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

@Entity
@Table(name = "team")
public class Team {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "team_id")
    private Long id;

    @Column(unique = true, length = 20)
    private String teamName;

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

현재 주인은 FK를 가지고 있는 Member이다

 

Team team = new Team();
team.setTeamName("teamA");
em.persist(team);

Member memberA = new Member();
memberA.setUserName("memberA");
em.persist(memberA);

Member memberB = new Member();
memberA.setUserName("memberA");
em.persist(memberB);

위의 코드처럼 실행을 하려고 했는데 여기서 문제점은 무엇일까

>> 연관관계 주인인 "Member"에 대해서 FK인 Team을 설정하지 않았다는 것이다

결과적으로 team은 정상적으로 들어갔지만 Member Table을 보면 team_id가 null인것을 확인할 수 있다

 

Team team = new Team();
team.setTeamName("teamA");
em.persist(team);

Member memberA = new Member();
memberA.setUserName("memberA");
memberA.setTeam(team);
em.persist(memberA);

Member memberB = new Member();
memberB.setUserName("memberB");
memberB.setTeam(team);
em.persist(memberB);

이렇게 setTeam을 해줘야 정상적으로 들어간다

 

그러면 이제 Team의 입장에서 List에 Member들이 잘 들어갔나 확인해보자

List<Member> memberList = em.find(Team.class, 1L).getMembers();
for (Member member : memberList) {
    System.out.println(member);
}
Hibernate: 
    select
        team0_.team_id as team_id1_1_0_,
        team0_.teamName as teamname2_1_0_ 
    from
        team team0_ 
    where
        team0_.team_id=?
Hibernate: 
    select
        members0_.team_id as team_id3_0_0_,
        members0_.member_id as member_i1_0_0_,
        members0_.member_id as member_i1_0_1_,
        members0_.team_id as team_id3_0_1_,
        members0_.userName as username2_0_1_ 
    from
        member members0_ 
    where
        members0_.team_id=?
        
Member{id=1, userName='memberA'}
Member{id=2, userName='memberB'}

 

정상적으로 들어가긴 하였다

>> 하지만 순수한 객체까지 고려한다면 양쪽 방향 모두에 값을 넣어주는 것이 안전하고 좋은 방법이다

양쪽 방향 모두 값을 입력하지 않으면 JPA를 사용하지 않는 "순수한 객체"에서는 심각한 데이터 정합성 문제가 발생하게 된다

 

※ 연관관계 편의 메소드

public class Member {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "member_id")
    private Long id;

    @Column(length = 10)
    private String userName;

    @ManyToOne
    @JoinColumn(name = "team_id")
    private Team team;
    
    // 연관관계 편의 메소드 (team설정할 때 team의 List<Member>에도 add해주기
    public void joinTeam(Team team){
        this.team = team;
        team.getMembers().add(this);
    }
    
    ...
    ...
}

이렇게 Member에 대해서 team을 설정해줄 때 "연관관계 편의 메소드"를 활용해서 양쪽 방향 모두 관리해주는게 안전하고 좋은 방식이다

연관관계 편의 메소드 이름은 "비즈니스적으로 의미있는 메소드"로 하는게 인식이 더 잘된다

 

Team team = new Team("teamA");
em.persist(team);

Member memberA = new Member();
memberA.setUserName("memberA");
memberA.joinTeam(team);
em.persist(memberA);

Member memberB = new Member();
memberB.setUserName("memberB");
memberB.joinTeam(team);
em.persist(memberB);

값이 정상적으로 잘 들어간 것을 확인할 수 있다

 

단 편의 메소드를 통해서 insert했을 경우 값에 대한 변경이 있을 경우 이전 값은 주인이 아닌쪽의 List에서 지워줘야 한다

Team teamB = new Team("teamB");
memberA.joinTeam(teamB);

memberA의 팀을 teamB로 바꿨다고 하자

그리고 teamA와 teamB의 List<Member>에 memberA가 있나 여부를 확인해보자

System.out.println("teamA에 여전히 memberA가 존재? = " + teamA.getMembers().contains(memberA));
System.out.println("teamB에도 memberA가 존재? = " + teamB.getMembers().contains(memberA));
-----------------------------------
teamA에 여전히 memberA가 존재? = true
teamB에도 memberA가 존재? = true

분명히 DB상에서는 memberA의 팀이 변경되었는데 여전히 teamA의 List<Member>에는 memberA가 남아있는 사실을 파악하였다

 

따라서 어떠한 값을 변경할 경우 이전에 양방향 연관관계 편의 메소드를 사용했다면 이러한 List에 들어있는 값을 잘 검증하고 있다면 지워주고 update해줘야 한다

// 연관관계 편의 메소드 (team설정할 때 team의 List<Member>에도 add해주기
public void joinTeam(Team team){
    if(this.team != null){
        this.team.getMembers().remove(this);
    }

    this.team = team;
    team.getMembers().add(this);
}

---------------------------------------
teamA에 여전히 memberA가 존재? = false
teamB에도 memberA가 존재? = true
  • 그냥 team.getMembers().remove(this)가 아니라 "this".team.getMembers().remove(this)로 해야 해당 멤버의 원래팀의 List에서 멤버를 지울 수 있다

 

2. 무한루프

toString, Lombok, JSON 생성 라이브러리, .... 등을 잘못 사용하면 양방향 관계에 의해서 무한루프가 발생할 수 있다