슬기로운 개발생활

[JPA] 연관관계 매핑 알아보기 (@ManyToOne, @OneToMany, @OneToOne, @ManyToMany)

by coco3o
반응형

연관관계 매핑

실제로 서비스되는 웹 어플리케이션에서 하나의 엔티티 타입만을 이용하는 경우는 많지 않습니다.

예를 들어 Member 엔티티와 Team 엔티티가 있을 때, 하나의 Team은 여러 Member를 갖는 관계를 가지고 있습니다.

이렇게 엔티티들이 서로 어떤 연관관계를 갖는지 파악하는 것은 매우 중요합니다.

 

연관관계 매핑이란 객체의 참조와 테이블의 외래 키를 매핑하는 것을 의미합니다.

JPA에서는 연관 관계에 있는 상대 테이블의 PK를 멤버 변수로 갖지 않고, 엔티티 객체 자체를 통째로 참조합니다.

물론 단순히 참조하는 것만으로는 연관관계를 맺을 수 없습니다. 매핑하는 방법은 뒤에서 알아보도록 하고,

그전에 연관관계 매핑을 이해하기 위한 3가지 키워드를 알아보겠습니다.

 

1. 방향

  • 단방향 관계 : 두 엔티티가 관계를 맺을 때, 한 쪽의 엔티티만 참조하고 있는 것을 의미
  • 양방향 관계 : 두 엔티티가 관계를 맺을 때, 양 쪽이 서로 참조하고 있는 것을 의미

데이터 모델링에서는 관계를 맺어주기만 하면 자동으로 양방향 관계가 되어 서로 참조하지만,

객체지향 모델링에서는 구현하고자 하는 서비스에 따라 단방향 관계인지, 양방향 관계인지 적절한 선택을 해야 합니다.

 

2. 다중성

관계에 있는 두 엔티티는 다음 중 하나의 관계를 갖습니다.

  • ManyToOne : 다대일 ( N : 1 )
  • OneToMany : 일대다 ( 1 : N )
  • OneToOne : 일대일 ( 1 : 1 )
  • ManyToMany : 다대다 ( N : N )

예를들어 하나의 Team은 여러 Member를 구성원으로 갖고 있으므로 Team 입장에서는 Member와 일대다 관계이며,

Member의 입장에서는 하나의 Team에 속하므로 다대일 관계입니다.

즉, 어떤 엔티티를 중심으로 상대 엔티티를 바라보느냐에 따라 다중성이 다르게 됩니다.

 

3. 연관관계의 주인 ( Owner )

객체를 양방향 연관관계로 만들면 연관관계의 주인을 정해야 합니다.

연관관계를 갖는 두 테이블에서 외래키를 갖게되는 테이블이 연관관계의 주인이 됩니다.

연관관계의 주인만이 외래 키를 관리(등록, 수정, 삭제) 할 수 있고, 주인이 아닌 엔티티는 읽기만 할 수 있습니다.


1. @ManyToOne - 단방향

@Entity
@Getter @Setter
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String username;

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

@Entity
@Getter @Setter
public class Team {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String name;
}

단방향은 한 쪽의 엔티티가 상대 엔티티를 참조하고 있는 상태입니다.

그렇기 때문에 위와같이 Member 엔티티에만 @ManyToOne 어노테이션이 있습니다.

 

@ManyToOne

Member 입장에선 Team과 다대일 관계이므로 @ManyToOne이 됩니다.

연관관계를 매핑할 때 이렇게 다중성을 나타내는 어노테이션은 필수로 사용해야 하며,

엔티티 자신을 기준으로 다중성을 생각해야 합니다.

 

@JoinColumn(name = "TEAM_ID")

@JoinColumn 어노테이션은 외래 키를 매핑할 때 사용합니다. name 속성에는 매핑 할 외래 키 이름을 지정합니다.

Member 엔티티의 경우 Team 엔티티의 id 필드를 외래 키로 가지므로, TEAM_ID를 작성하였습니다.

위와 같이 hibernate가 테이블을 정의하는 SQL문을 보면,

member 테이블에 외래키로 team_id를 갖는 것을 볼 수 있습니다.

 

이제 Member와 Team은 단방향 관계를 맺으므로 외래키가 생겼기 때문에 Member에서 Team의 정보들을 가져올 수 있습니다.

그런데 만약 Team에서 Member 정보들을 가져오고 싶다면 어떻게 해야 할까요?

 

데이터 모델링에서는 1:N 관계만 설정해주면 자동으로 양방향 관계가 되기 때문에

어느 테이블에서든 join만 해주면 원하는 컬럼들을 가져올 수 있지만, JPA에서는 양방향 관계를 맺음으로써 해결할 수 있습니다.


@OneToMany로 양방향 관계 맺기

@Entity
@Getter @Setter
public class Team {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String name;

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

Team은 Member를 List로 가지며, 연관관계의 주인을 정하기 위해 @OneToMany에 mappedBy 속성을 추가했습니다.

 

연관관계의 주인을 정하는 방법은 mappedBy 속성을 지정하는 것 입니다.

  • 주인은 mappedBy 속성을 사용하지 않고, @JoinColumn을 사용
  • 주인이 아닌 엔티티 클래스는 mappedBy 속성을 사용해 주인을 정할 수 있습니다.

주인은 mappedBy 속성을 사용할 수 없으므로 연관관계의 주인이 아닌 Team 엔티티에서 members 필드에

mappedBy의 속성으로 Member 테이블의 Team 필드 이름을 명시해줍니다.

(Team DB에 members FK 컬럼을 만들지 않게 하기 위해)


@OneToOne

한 유저는 한 개의 블로그만 만들 수 있는 블로그 서비스가 있다고 가정한다면,

유저와 블로그는 1:1 관계일 것입니다.

 

유저 엔티티 입장에서 블로그의 @Id를 외래 키로 가져서 블로그를 참조할 수 있고,

그 반대 경우인 블로그가 외래키를 가져서 유저를 참조하는 것도 가능합니다.

여기서 외래 키를 갖고 있는 테이블을 주 테이블이라고 합니다.

 

그렇다면 어떤 엔티티가 외래키를 갖고 있는 것이 맞을까요?

한 유저가 여러 개의 블로그를 가질 수 있도록 확장( OneToMany )될 수 있음을 고려 한다면, 

블로그에서 유저의 외래키를 갖는 것이 좋을 것이고,

유저를 조회 했을 때 자동으로 블로그 엔티티도 조회 되는 것이 좋겠다고 생각하면

유저에서 블로그의 외래키를 갖는 것이 좋을 것입니다.

 

예제에서는 후자의 방식인, 유저에 외래키를 두는 방식으로 구현해보겠습니다.

[ 단방향 ]

@Entity
@Getter @Setter
public class User {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long no;
    
    @Column(nullable = false)
    private String id;
    
    @OneToOne
    @JoinColumn(name = "blog_no")
    private Blog blog;
}

@Entity
@Getter @Setter
public class Blog {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long no;

    @Column(nullable = false)
    private String address;
}

[ 양방향 ]

@Entity
@Getter @Setter
public class Blog {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long no;

    @Column(nullable = false)
    private String address;
    
    @OneToOne(mappedBy = "blog")
    private User user;
}

양방향 관계이기 때문에 연관관계의 주인을 정해야 합니다. User 클래스 코드는 동일합니다.


@ManyToMany

실무 사용 금지 ×

중간 테이블이 숨겨져 있기 때문에 자기도 모르는 복잡한 조인의 쿼리(Query)가 발생하는 경우가 생길 수 있기 때문입니다.

다대다로 자동 생성된 중간 테이블은 두 객체의 테이블의 외래 키만 저장되기 때문에 문제가 될 확률이 높습니다.

중간 테이블에 외래 키 외에 다른 정보가 들어가는 경우가 많기 때문에,

다대다를 일대다, 다대일로 풀어서 만드는 것(중간에 매핑 테이블을 만드는 것)이 추후 변경에도 유연하게 대처할 수 있습니다.

이미지 출처 :&amp;nbsp;https://coding-start.tistory.com/72


연관관계에서는 ToString()을 주의

엔티티 간에 연관관계를 지정하는 경우엔 항상 @ToString()을 주의해야 합니다.

@ToString()은 해당 클래스의 모든 멤버 변수를 출력하는데,

연관관계 매핑이 되어있을 경우 그 객체 역시 출력해야 하기 때문에 이때 데이터베이스 연결이 필요하게 됩니다.

 

이런 문제로 인해 연관관계가 있는 엔티티 클래스의 경우 @ToString()할 땐 습관적으로 exclude 속성을 사용하는 것이 좋습니다.

exclude는 해당 속성값으로 지정된 필드는 toString()에서 제외해줍니다.


참고 : 

https://victorydntmd.tistory.com/208

https://jeong-pro.tistory.com/231

 

반응형

블로그의 정보

슬기로운 개발생활

coco3o

활동하기