[JPA] 연관관계 매핑

2024. 4. 5. 15:14카테고리 없음

N:1 관계

가장 많이 사용하는 연관관계이다.

N:1 자체로도 많이 사용하지만, M:N 관계를 매핑할 때 사용하기도 한다.

public class Member {
    @Id @GeneratedValue
    private Long id;
    private String name;

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

 

  • @ManyToOne: 말 그대로 N:1을 나타내는 어노테이션이다.
  • @JoinColumn: Team을 참조할 Foreign Key의 이름을 정해준다. 
  • FetchType.LAZY: DB에서 데이터를 조회할 때 Team 객체까지 한번에 가져올지, 빈 Proxy만 가져왔다가 필요할 때 조회할지 선택할 수 있다. FetchType.EAGER는 한번에, FetchType.LAZY Proxy를 가져온다.

 

N:1 양방향 매핑

아래와 같이 Team에서 Member 리스트를 갖고있을 수 있다.

Member에서 Team을 @ManyToOne으로 매핑해야 사용할 수 있다.

이 경우, Team은 Member 리스트를 읽는 것만 허용되고, 등록이나 수정은 할 수 없다. (cascade 속성을 설정해주면 가능하다.)

public class Team {
    @Id @GeneratedValue
    private Long id;
    private String name;

    @OneToMany(mappedBy = "team")
    private List<Member> member;
}

 

  • @OneToMany: 1:N 관계를 나타내는 어노테이션
  • mappedBy: Member 테이블의 Team 변수명. private Team team; 이라고 선언했기 때문에 "team"이 된다.

1:N 관계

한 객체가 반대편 객체를 List 형태로 갖는 구조이다.

1:N 특성 상 외래 키가 N 쪽에 있어야 하기 때문에,

연관관계의 주인이 반대편 테이블의 Foreign Key를 관리하는 특이한 구조가 된다.

public class Team {
    @Id @GeneratedValue
    private Long id;
    private String name;

    @OneToMany(fetch = FetchType.LAZY)
    @JoinColumn(name = "team_id")
    private List<Member> member;
}

 

  • @OneToMany: 1:N 매핑
  • @JoinColumn: Member 테이블에 생성될 Foreign Key의 이름을 명시한다. 만약 @JoinColumn이 없을 경우 중간에 테이블이 하나 추가되기 때문에 꼭 사용해야 한다.

이 관계는 구조도 비정상적이고, 연관관계 관리를 위해 UPDATE 쿼리까지 날려야 해서 비효율적이다.

그러니 꼭 사용해야 하는게 아니라면 N:1 양방향 매핑을 사용하는 것이 좋다.

 

1:N 양방향 매핑

이런 매핑은 없다.

읽기 전용 필드를 사용하는 꼼수가 있지만, 그럴바엔 N:1 양방향을 사용하자.

public class Member {
    @Id @GeneratedValue
    private Long id;
    private String name;

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

1:1 매핑

1:1은 뒤집어도 1:1이기 때문에 Foreign Key를 어디에 두어도 상관없다. 상황에 맞게 선택하자.

매핑 방식은 N:1과 유사하다. @ManyToOne / @OneToMany @OneToOne으로 바꿔주기만 하면 된다.

public class Member {
    @Id @GeneratedValue
    private Long id;
    private String name;

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

 

1:1 양방향 매핑

public class Team {
    @Id @GeneratedValue
    private Long id;
    private String name;
    
    @OneToOne(mappedBy = "team")
    private Member member;
}

M:N 관계

N:1과 마찬가지로 많이 사용되는 관계이다.

매핑하는 방법이 @ManyToMany를 이용하는 방법, @ManyToOne을 이용하는 방법 총 2가지가 있다.

우선 @ManyToMany를 사용하는 방법부터 알아보자.

 

DB는 테이블 2개만으로 M:N 관계를 만들 수 없다.

때문에 @ManyToMany 어노테이션을 사용하면 연결 테이블을 자동으로 생성해준다.

public class Member {
    @Id @GeneratedValue
    private Long id;
    private String name;

    @ManyToMany
    @JoinTable(name = "member_team")
    private List<Team> teamList;
}

 

  • @ManyToMany: M:N 매핑
  • @JoinTable: 연결 테이블의 이름

 

@ManyToMany 양방향 매핑

public class Team {
    @Id @GeneratedValue
    private Long id;
    private String name;

    @ManyToMany(mappedBy = "teamList")
    private List<Member> memberList;
}

 

  • @ManyToMany: M:N 매핑
  • mappedBy: N:1 관계에서 봤듯이 Member 테이블의 Team 리스트 변수를 넣으면 된다.

@ManyToOne을 사용한 M:N 관계 매핑

@ManyToMany를 사용할 경우 어노테이션만 넣으면 JPA에서 자동으로 연결 테이블을 생성해주기에 편리하지만, 다음과 같은 단점들이 존재한다.

 

  • 추가적인 정보를 넣을 수 없다. 예를 들어, member_team 테이블에 Member가 Team에 가입한 날짜를 저장하고 싶어도, JPA가 자동으로 생성하는 테이블이기 때문에 넣을 수 없다.
  • 예상할 수 없는 쿼리가 실행된다. JPA가 연결 테이블을 자동 생성함으로써 그 테이블은 JPA에서 자동으로 관리해주기 때문에, CRUD를 할 때 중간 테이블을 거치는 쿼리 또한 자동으로 실행시켜버린다.

간단한 프로젝트에선 쓸만 하지만, Entity간의 관계가 복잡한 실무에서는 쓰지 않는것이 좋다.

@ManyToMany 대신 연결 테이블을 Entity로 승격하여 직접 생성하고, @ManyToOne 양방향 관계로 직접 매핑해주자.

 

우선 연결 테이블을 선언해준다.

Entity로 승격되었기 때문에 가입일자 같은 추가적인 데이터가 들어갈 수 있다.

(member_id, team_id) Primary Key를 선언할 수도 있지만, 제약조건이 많아질 경우 독립적인 ID가 있는 것이 유연하게 대처하기 좋기 때문에 별도의 ID를 만드는 것이 좋다.

@Entity
public class MemberTeam {
    @Id @GeneratedValue
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "member_id")
    private Member member;

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

 

그 다음 Member, Team의 @ManyToMany 연결 테이블에 대한 @OneToMany로 변경해준다.

public class Member {
    @Id @GeneratedValue
    private Long id;
    private String name;

    @OneToMany(mappedBy = "member")
    private List<MemberTeam> teamList;
}
public class Team {
    @Id @GeneratedValue
    private Long id;
    private String name;

    @OneToMany(mappedBy = "team")
    private List<MemberTeam> memberList;
}

연관관계 편의 메소드

연관관계가 매핑 되었더라도 객체 상태일 때를 고려하여 양쪽 모두에 값을 설정해야 한다.

예를 들어 Member와 Team을 조회했는데, Team에 Member를 추가하고 Member에 아무런 설정도 해주지 않으면 Team에는 Member가 들어가있지만, Member 입장에서는 Team이 NULL인 상황이 된다.

 

이런 현상을 방지하기 위해 편의 메소드를 작성하면 좋다.

코드가 복잡하지 않기 때문에 보기만 해도 이해가 될 것이다.

 

public class Member {
    @Id @GeneratedValue
    private Long id;
    private String name;

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

    public void changeTeam(Team team) {
        if(team != null) {
            team.getMemberList().add(this);
            this.team = team;
        }
    }
}
public class Team {
    @Id @GeneratedValue
    private Long id;
    private String name;

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

    public void addMember(Member member) {
        memberList.add(member);
        member.changeTeam(this);
    }
}

 

코드를 보면 changeTeam / addMember 함수를 호출할 때 자신의 값만 설정하는 것이 아니라 상대편의 값도 같이 설정하는 것을 볼 수 있다.

이렇게 연관관계 편의 메소드를 정의해놓고 사용하면 데이터에 모순이 생기는 것을 방지할 수 있다.