기록

[자바 ORM 표준 JPA 프로그래밍] 다양한 연관관계 매핑: 일대다, 다대일, 다대다 (6장) 본문

교육/책

[자바 ORM 표준 JPA 프로그래밍] 다양한 연관관계 매핑: 일대다, 다대일, 다대다 (6장)

youngyin 2024. 11. 27. 23:59

시작하면서

Java ORM 표준 JPA에서 다양한 연관관계 매핑을 다루는 6장을 읽고 중요한 내용을 정리해 보았습니다. 이 글에서는 객체의 관계를 어떻게 매핑하고, 각각의 경우에 장단점은 무엇인지 살펴보겠습니다. 특히 연관관계의 종류와 주의할 점들을 중심으로 설명하겠습니다.

1. 다대일 관계 (N:1)

한쪽 객체가 여러 객체를 참조하는 관계로, 예를 들어 회원(Member)이 팀(Team)에 속해 있을 때, 여러 회원들이 하나의 팀에 속할 수 있으므로 다대일 관계가 성립됩니다.

다대일 단방향 매핑 (N:1)

다대일 단방향 관계에서는 한쪽에서만 참조가 가능합니다. 예를 들어 회원이 팀을 참조하지만, 팀은 회원을 참조하지 않는 경우입니다. 이런 경우에는 다음과 같이 매핑할 수 있습니다.

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

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

    // Getter, Setter
}

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

    // Getter, Setter
}

@ManyToOne 어노테이션을 사용해 다대일 관계를 정의하고, @JoinColumn을 통해 외래 키 컬럼을 명시합니다.

다대일 양방향 매핑 (N:1, 1:N)

팀과 회원 간의 연관관계를 양방향으로 설정하려면 양쪽 엔티티가 서로를 참조해야 합니다. 이때 연관관계의 주인은 외래 키를 가진 쪽, 즉 "다"쪽인 Member 엔티티입니다.

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

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

    // Getter, Setter
}

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

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

    // Getter, Setter
}

양방향 매핑에서는 mappedBy 속성을 사용하여 연관관계의 주인이 아님을 명시해야 합니다. 이를 통해 연관관계를 설정하고 관리할 수 있습니다.

2. 일대다 관계 (1:N)

하나의 엔티티가 여러 다른 엔티티를 참조하는 관계로, 예를 들어 하나의 팀이 여러 회원을 가질 수 있는 상황입니다. 이때는 보통 자바의 Collection 타입(List, Set, Map)을 사용하여 매핑합니다. 가장 많이 사용되는 것은 List이며, 데이터의 순서를 보장해야 하거나 중복을 허용할 때 유용합니다.

일대다 단방향 매핑의 단점

일대다 단방향 매핑은 외래 키가 다른 테이블에 존재하기 때문에 저장할 때 추가적인 UPDATE 쿼리가 발생합니다. 이는 성능상의 단점으로 이어질 수 있습니다. 예를 들어, 회원을 생성하고 나서 팀과의 연관관계를 설정하려면 INSERT 쿼리 이후에 추가로 UPDATE가 발생하게 됩니다. 다음과 같은 예시를 통해 확인해보겠습니다.

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

    @OneToMany
    @JoinColumn(name = "team_id") // MEMBER 테이블에 외래 키가 생성됨
    private List<Member> members = new ArrayList<>();

    // Getter, Setter
}

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

    // Getter, Setter
}

위와 같은 구조에서 Member 엔티티가 생성되고 나서 Team과 연관관계를 설정한다는 것은 Member 객체가 어느 팀에 속하는지 지정하는 것을 의미합니다. 예를 들어, Member 객체의 team 필드에 특정 Team 객체를 할당하는 것입니다. 다음과 같은 자바 코드를 통해 연관관계를 설정할 수 있습니다:

Member member = new Member();
Team team = new Team();
team.setName("Team A");

// 팀을 먼저 저장
entityManager.persist(team);

// 회원에 팀을 설정
member.setTeam(team);
entityManager.persist(member);

위 코드에서는 member.setTeam(team)을 통해 회원 객체에 팀을 지정하여 연관관계를 설정합니다. 이를 통해 JPA가 외래 키 값을 적절히 설정할 수 있게 됩니다. 다음과 같은 쿼리가 발생합니다:

  1. INSERT INTO MEMBER (username) VALUES ('John Doe');
  2. UPDATE MEMBER SET team_id = 1 WHERE id = 1;

이처럼 두 번의 쿼리가 발생하는 것은 성능에 부담을 줄 수 있습니다.

따라서 이런 경우에는 다대일 양방향 매핑을 사용하는 것이 더 좋습니다. 다대일 매핑을 사용하면 외래 키가 "다"쪽에 위치하기 때문에 연관관계를 설정하면서 한 번의 INSERT로 저장할 수 있어 효율적입니다. 다음은 다대일 양방향 매핑의 예시입니다:

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

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

    // Getter, Setter
}

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

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

    // Getter, Setter
}

회원 객체를 생성한 후 이를 특정 팀과 연결할 수 있습니다.

Team team = new Team();
team.setName("Team A");
entityManager.persist(team);

Member member = new Member();
member.setUsername("John Doe");
member.setTeam(team); // 연관관계를 설정하는 부분
entityManager.persist(member);

위 코드에서는 member.setTeam(team)을 호출함으로써 MemberTeam 간의 관계를 설정합니다. 이를 통해 JPA는 외래 키 값을 자동으로 지정하게 됩니다. 이렇게 설정하여 한 번의 INSERT로 처리할 수 있습니다:

INSERT INTO MEMBER (username, team_id) VALUES ('John Doe', 1);

이렇게 하면 추가적인 UPDATE 쿼리가 발생하지 않아 성능적으로 더 효율적입니다.

3. 일대일 관계 (1:1)

일대일 관계에서는 양쪽 엔티티가 서로 하나의 관계만을 가집니다. 예를 들어, 회원은 하나의 사물함을 사용하고, 사물함도 하나의 회원에게만 할당되는 경우를 생각할 수 있습니다.

일대일 관계에서 외래 키를 주 테이블에 가질지, 대상 테이블에 가질지 선택할 수 있습니다.

    • 주 테이블(회원)에 외래 키: 회원이 사물함을 참조(회원.사물함)하는 방식으로, 주로 사용되는 방식입니다.
    • 대상 테이블(사물함)에 외래 키: 사물함이 회원을 참조(사물함.회원)하는 방식입니다. 이 경우 지연 로딩을 사용할 때 프록시로 관리하는 데 한계가 있습니다.

[참고] 일대일 관계에서는 외래 키가 대상 테이블에 있는 경우, 해당 테이블에 접근할 때마다 즉시 로딩을 해야 올바르게 참조할 수 있는 문제가 발생할 수 있습니다. 이를 해결하려면 프록시 대신에 바이트코드 조작(bytecode instrumentation)을 사용하거나, 주 테이블에 외래 키를 두는 설계가 더 유리할 수 있습니다.

  •  
@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;

    @OneToOne
    @JoinColumn(name = "locker_id")
    private Locker locker;

    // Getter, Setter
}

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

    @OneToOne(mappedBy = "locker")
    private Member member;

    // Getter, Setter
}

4. 다대다 관계 (N:N)

JPA에서 다대다 관계는 중간 테이블을 사용하여 풀어내야 합니다. 예를 들어 회원이 여러 상품을 주문하고, 하나의 상품이 여러 회원에 의해 주문될 수 있다면 다대다 관계가 됩니다. 이를 표현하기 위해 중간 테이블(Member_Product)을 추가합니다.

다대다 매핑의 한계와 극복

@ManyToMany 어노테이션을 사용하면 JPA가 중간 테이블을 자동으로 관리해 주기 때문에 매우 간편하지만, 실무에서는 중간 테이블에 추가적인 컬럼(예: 주문 날짜, 수량 등)이 필요할 때가 많습니다. 이 경우에는 @ManyToMany를 사용할 수 없으며, 중간 엔티티를 직접 생성하여 매핑해야 합니다.

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

    @OneToMany(mappedBy = "member")
    private List<MemberProduct> memberProducts = new ArrayList<>();

    // Getter, Setter
}

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

    @OneToMany(mappedBy = "product")
    private List<MemberProduct> memberProducts = new ArrayList<>();

    // Getter, Setter
}

// 중간 테이블
@Entity
public class MemberProduct {
    @Id @GeneratedValue
    private Long id;

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

    @ManyToOne
    @JoinColumn(name = "product_id")
    private Product product;

    private int orderQuantity;
    private LocalDate orderDate;

    // Getter, Setter
}

이렇게 다대다 관계를 풀어서 매핑하면 중간 엔티티에 추가적인 정보를 담을 수 있어 실무에서 유용하게 사용할 수 있습니다.

마무리하며

JPA에서 연관관계를 매핑하는 다양한 방법들을 살펴보았습니다. 다대일, 일대다, 일대일, 다대다 관계를 각각 상황에 맞게 적절히 사용하는 것이 중요합니다. 특히 양방향 연관관계에서는 연관관계의 주인을 명확히 정하고, 효율적인 쿼리를 생성할 수 있도록 설계하는 것이 핵심입니다.

Comments