-> 블로그 이전

[JPA] 1. 다대일 연관관계 (@ManyToOne)

2022. 7. 3. 14:20Language`/JPA

당연하지만 다대일 관계의 반대는 항상 일대다 관계이고 일대다 관계의 반대는 항상 다대일이다

DB Table에서 {일(1) & 다(N)} 관계에서 FK는 항상 다(N)쪽에 존재한다. 따라서 객체 양방향 관계에서 연관관계의 주인은 항상 다(N)쪽이다

 

1. 다대일 단방향

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

다대일 단방향은 가장 많이 사용하는 연관관계이고 가장 대표적인 연관관계이다

당연히 다대일 연관관계의 반대는 일대다이다

 

Member(N)

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

    private String username;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "team_id")
    private Team team;
}

Team(1)

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

    private String name;
}

 

위와 같이 엔티티상에서 설계해주면 된다

  • @~~~ToOne은 나중에 설명하겠지만 기본 fetchType이 EAGER이므로 JPA의 "N+1"문제가 발생할 수 있으므로 가급적이면 모든 @~~~ToOne관계의 fetchType은 LAZY로 설정해줘야 한다

 

위의 다대일 단방향 연관관계에서는 객체입장에서 Member → Team으로 가는 방향은 존재하지만 Team → Member로 가는 방향은 존재하지 않는다

  • 기본적으로 모든 매핑은 "단방향"으로 설계하고 필요한 경우에만 양방향 관계를 추가해주면 좋다. 추가적으로 JPQL을 활용하게되면 양방향 매핑을 활용할 일이 많고, 양방향 매핑을 활용하면 데이터 정합성 문제가 발생하지 않도록 잘 설계해야 한다

2. 다대일 양방향

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

단방향에서 양방향 관계를 추가해보자

 

Member(N)

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

    private String username;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "team_id")
    private Team team;

    public void joinTeam(Team team){
        if(this.team != null){
            this.team.getMembers().remove(this);
        }

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

Team(1)

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

    private String name;

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

달라진 점은 Team에 "List<Member>"가 들어오고 @OneToMany를 통해서 mappedBy를 지정했다는 점이다

Team의 입장에서는 Member가 N이므로 List를 통해서 여러 Member들을 담을 수 있도록 설계해야 한다

 

※ 연관관계 편의 메소드

// Member
public void joinTeam(Team team){
    if(this.team != null){
        this.team.getMembers().remove(this);
    }

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

// team 
public void addMember(Member member){
    if(member.getTeam() != null){
        member.getTeam().getMembers().remove(member);
    }
    
    this.getMembers().add(member);
    member.setTeam(this);
}

테스트를 위해서 양쪽에 다 편의 메소드를 생성해주었고 실제로 사용할 때는 둘 중 하나에만 작성해야 무한루프에 빠지는 위험에서 벗어날 수 있다

 

현재 위의 편의 메소드에는 컬렉션 중복을 방지해기 위해서 "중복 방지 로직"을 작성해보았다

하지만 이 중복 방지 로직이 없다면 어떻게 될까?

 

1. Member의 joinTeam에 중복 방지 로직이 없다면...

public void joinTeam(Team team){
    this.team = team;
    team.getMembers().add(this);
}
Member member = new Member("member1");
member.joinTeam(teamA);
member.joinTeam(teamB);

현재 member는 teamA에 join을 하였다가 나중에 teamB로 join을 했다고 가정하자

그러면 상식적으로 teamA의 List<Member> 컬렉션에는 member가 없는것이 정상적이다

 

이제 결과를 살펴보면

teamA에 member 존재? = true
Member{id=1, username='member1', team=Team{id=1, name='teamA'}}
====================================================================
teamB에 member 존재? = true
Member{id=1, username='member1', team=Team{id=2, name='teamB'}}

이렇게 중복 방지 로직이 없으면 member라는 하나의 사람이 team 2개에 전부 존재한다는 말이 안되는 상황이 벌어진다

 

따라서 중복 방지 로직이 필요하고 이를 적용한 결과를 살펴보자

public void joinTeam(Team team){
    if(this.team != null){
        this.team.getMembers().remove(this);
    }

    this.team = team;
    team.getMembers().add(this);
}
Member member = new Member("member1");
member.joinTeam(teamA);
member.joinTeam(teamB);
<중복 방지 로직>
만약 member가 이미 team에 있다면 해당 team의 List<Member>에도 member가 있을 것이다.
따라서 해당 List에서 member를 remove시켜주고, 새로운 team에 적용하는 로직이다
teamA에 member 존재? = false
================================================================
teamB에 member 존재? = true
Member{id=1, username='member1', team=Team{id=2, name='teamB'}}

이제 원하는대로 로직이 실행된 것을 확인할 수 있다

 

양쪽 엔티티 어느곳이든 연관관계 편의 메소드를 작성할 수 있다
하지만 어차피 값을 설정해줘야 하는 곳은 "주인"쪽이고 따라서 "주인"쪽에 연관관계 편의 메소드를 작성하는 것이 더 편리하고 효율적이다