[JPA] 연관관계 매핑(1)

imcool2551·2022년 4월 5일
0

JPA

목록 보기
4/12
post-thumbnail
post-custom-banner

본 글은 인프런 김영한님의 JPA 로드맵을 기반으로 정리했습니다.

1. 연관관계 매핑


엔티티와 테이블은 모두 서로 연관관계를 맺는다. 앤티티는 참조로 관계를 맺고 테이블은 외래키로 관계를 맺는다. 연관관계를 이해하기 위해 몇가지 용어를 짚고 넘어가자.

  • 방향(Direction): 단방향, 양방향

  • 다중성(Multiplicity): 다대일(N:1), 일대다(1:N), 일대일(1:1), 다대다(N:M)

  • 연관관계의 주인(Owner): 엔티티 양방향 연관관계는 관리 주인이 필요

다중성은 다음글에서 자세히 알아보고 이번 글에서는 단방향 연관관계와 양방향 연관관계의 차이, 그리고 연관관계의 주인에대해서 살펴보자.

테이블의 경우 외래키로 관계를 맺기 때문에 기본적으로 양방향 연관관계이다. 그 말은 외래키를 갖고 있는 쪽에서 기본키를 가진 테이블로 찾아갈 수 있고, 반대로 기본키를 가진 테이블에서 외래키를 가진 테이블로 찾아갈 수 있다는 것이다.

그러나 엔티티의 경우 얘기가 다르다. 엔티티는 단방향, 양방향 연관관계 중 하나를 택할 수 있다. 양방향 연관관계의 경우 연관관계의 주인을 정해줘야한다.

2. 테이블 종속적인 연관관계


간단한 예제를 통해 연관관계의 개념을 이해해보자. 요구사항은 다음과 같다.

  • 팀과 선수 엔티티가 있다.

  • 회원은 하나의 팀에만 소속될 수 있다.

  • 회원과 팀은 N:1 관계이다.

@Entity
@Getter @Setter
public class Player {

    @Id @GeneratedValue
    @Column(name = "PLAYER_ID")
    private Long id;

    private String username;

    @Column(name = "TEAM_ID")
    private Long teamId;
}
@Entity
@Getter @Setter
public class Team {

    @Id @GeneratedValue
    @Column(name = "TEAM_ID")
    private Long id;

    private String name;
}

위의 코드는 엔티티를 테이블에 맞추어 설계한 방식이다. 이는 객체지향적인 설계가 아니다. 테이블은 외래키를 통해 연관관계를 맺지만 엔티티(객체)는 참조를 통해 연관관계를 맺는다.

3. 객체지향적인 연관관계


객체지향적으로 참조를 통해 연관관계를 맺도록 엔티티 설계를 변경해보자. 참조를 통한 연관관계는 단방향, 양방향 중 한 가지를 선택해야한다.

3.1 단방향 연관관계

단방향 연관관계는 한 엔티티에서 다른 엔티티로 참조를 가지지만, 반대쪽의 엔티티는 참조를 가지지 않는것이다. 코드로 살펴보자.

@Entity
@Getter @Setter
public class Player {

    @Id @GeneratedValue
    @Column(name = "PLAYER_ID")
    private Long id;

    private String username;

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

}
@Entity
@Getter @Setter
public class Team {

    @Id @GeneratedValue
    @Column(name = "TEAM_ID")
    private Long id;

    private String name;
}

단방향 연관관계는 간단하다. 테이블 기준으로 외래키를 들고 있는 Player 엔티티가 Team 엔티티에대한 참조를 가지고 있으면 된다. 외래키가 아닌 엔티티를 참조하는 것에 주목하자.

참조하는 엔티티 필드에는 @ManyToOne과 같은 애노테이션을 통해 다중성을 명시해야한다. 또한, @JoinColumn을 통해 테이블상의 외래키 컬럼과 매핑했다. @JoinColumn 애노테이션은 Player가 Player와 Team사이에서 연관관계의 주인임을 뜻한다. 연관관계의 주인이 필요한 이유는 양방향 연관관계에서 더욱 명확히 드러난다.

3.2 양방향 연관관계

양방향 연관관계를 맺으면 단방향 연관관계와 달리 엔티티간에 서로 참조할 수 있다. 이는 사실 단방향 연관관계 두 개를 합친것에 불과하지만 개념적으로 이해하기 쉽도록 양방향 연관관계라고 부르곤한다. 코드로 살펴보자.

@Entity
@Getter @Setter
public class Player {

    @Id @GeneratedValue
    @Column(name = "PLAYER_ID")
    private Long id;

    private String username;

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

}
@Entity
@Getter @Setter
public class Team {

    @Id @GeneratedValue
    @Column(name = "TEAM_ID")
    private Long id;

    private String name;

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

Player 엔티티는 변화가 없다.

Team 엔티티에는 List<Player> 타입의 필드가 추가되었다. 이제 Team 엔티티에서도 연관된 Player 엔티티를 탐색할 수 있다. 그리고 참조하는 엔티티의 다중성을 명시하는 @OneToMany 애노테이션을 달아줬다.

@OneToMany의 속성으로 주어진 mappedBy는 해당 엔티티가 연관관계의 주인이 아님을 뜻한다. 연관관계의 주인이 무엇인지 무엇인지 살펴보자.

연관관계의 주인은 테이블의 외래키를 관리하는 엔티티를 말한다. 연관된 테이블을 연관된 엔티티로 매핑하고 양방향 관계를 맺을 때 외래키를 관리하는 엔티티를 설정해줘야한다. 특별한 이유가 없다면 외래키가 있는쪽을 연관관계의 주인으로 설정하는 것이 좋다.

예제를 분석해보자. DB 테이블에서 1:N 관계의 경우 항상 N쪽이 외래키를 가진다. 즉, Player 테이블에 외래키가 있다는 것이다. Player 엔티티를 연관관계의 주인으로 설정해야 Player 테이블의 외래키 등록, 수정 등의 작업을 Player 엔티티를 통해 할 수 있다.

테이블 상에서 외래키가 없는 Team 엔티티가 연관관계의 주인이 되면 Team 엔티티를 통해 Player 테이블의 외래키를 관리해줘야한다. 이는 직관적이지도 않고 실수할 위험이 높으니 피하는 것이 좋다. 연관관계의 주인이 아닌 엔티티는 연관된 엔티티를 조회만 할 수 있다. 다중성을 명시해주는 애노테이션의 속성 mappedBy를 통해 연관관계의 주인이 아님을 명시하면 된다.

4. 양방향 연관관계 주의점


양방향 연관관계는 외래키를 관리하는 엔티티와 주인과 단순히 조회만 할 수 있는 엔티티로 구별이 된다. 외래키 관리와 별도로 양쪽 객체의 값을 설정하는 문제가 있다. 다른말로 외래키를 관리하는 것과 객체의 상태를 관리하는 것은 독립된 문제라는 뜻이다. 이를 해결하기 위해 연관관계 편의 메서드를 만드는 것을 추천한다.

@Entity
@Getter @Setter
public class Player {

    @Id @GeneratedValue
    @Column(name = "PLAYER_ID")
    private Long id;

    private String username;

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

    //==연관 관계 편의 메서드==//
    public void changeTeam(Team team) {
        this.team = team;
        team.getPlayers().add(this);
    }
}

changeTeam처럼 하나의 메서드를 통해 양쪽 엔티티에 모두 상태를 설정해주는 메서드를 흔히 연관관계 편의 메서드라고 부른다. 연관관계 편의 메서드는 양방향 연관관계에서 필요하며, 비즈니스마다 더 적절해 보이는 곳에 한 개만 생성하면 된다. 비즈니스가 복잡한 경우 null체크나 중복 체크 등 검증 로직을 추가할 수 있다.

여기서 한 가지 짚고 넘어가야할 중요한 점이 있다. 단방향 매핑만으로도 이미 연관관계 매핑은 완료된다. 양방향 매핑은 반대 방향으로 객체 그래프를 탐색할 수 있는 기능을 추가했을 뿐이다. 단방향 매핑만으로 테이블의 연관관계를 표현할 수 있다. 양방향 매핑은 테이블에 영향을 주지않고 단순히 편의기능을 추가한것이다. 살펴본 것처럼, 양방향 매핑은 편의메서드 등으로 양쪽 객체의 상태를 관리해줘야 하는 번거로움이 발생한다. 그러니 양방향 매핑은 꼭 필요할 때 추가하도록 하자. 그래야 관리의 복잡성이 늘어나지 않는다.

5. 실전 예제


이전 글에서 외래키를 직접 매핑한 엔티티를 다음 그림처럼 참조를 사용하도록 고쳐보자.

OrderItem <-> Item 만 단방향 연관관계를 맺고 그 이외에는 양방향 연관관계를 맺었다. OrderOrderItem과 양방향 연관관계를 맺는것은 비즈니스상 합리적이다. 그러나 MemberOrder를 참조하는 것은 의심해볼 필요가 있다. 주문이 필요한 경우 Order를 통해 직접 조회하면 되지, 굳이 Member를 통해서 가져오는 것이 부자연스럽다고 느낄 수 있다. 이 예제에서는 단지 연관관계 매핑을 연습하기위해 추가했을뿐이다.

@Entity
@Getter @Setter
public class Member {

    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

    private String name;

    private String city;
    private String street;
    private String zipcode;

    // 양방향 관계는 의심해보자. 멤버가 주문을 가지는 것은 좋은 설계가 아닐 수 있다.
    @OneToMany(mappedBy = "member")
    private List<Order> orders = new ArrayList<>();
}
@Entity
@Getter @Setter
@Table(name = "ORDERS")
public class Order {

    @Id @GeneratedValue
    @Column(name = "ORDER_ID")
    private Long id;

    @ManyToOne
    @JoinColumn(name = "MEMBER_ID")
    private Member member;

    private LocalDateTime orderDate;

    @Enumerated(EnumType.STRING)
    private OrderStatus orderStatus;

    @OneToMany(mappedBy = "order")
    private List<OrderItem> orderItems;

    //==연관관계 편의 메서드==//
    public void addOrderItem(OrderItem orderItem) {
        orderItems.add(orderItem);
        orderItem.setOrder(this);
    }
}
@Entity
@Getter @Setter
public class OrderItem {

    @Id @GeneratedValue
    @Column(name = "ORDER_ITEM_ID")
    private Long id;

    @ManyToOne
    @JoinColumn(name = "ORDER_ID")
    private Order order;

    @ManyToOne
    @JoinColumn(name = "ITEM_ID")
    private Item item;

    private int orderPrice;

    private int count;
}
@Entity
@Getter @Setter
public class Item {

    @Id @GeneratedValue
    @Column(name = "ITEM_ID")
    private Long id;

    private String name;
    private int price;
    private int stockQuantity;
}
profile
아임쿨
post-custom-banner

0개의 댓글