본문 바로가기
JAVA/JPA

[Spring + JPA] Entity 연관 관계 매핑 - 관계 유형

by 개미가되고싶은사람 2024. 11. 8.

목차

    연관관계 매핑 시 고려사항

    관계 유형

    관계 유형은 데이터베이스 입장에서 필요한 개념으로 적절한 매핑으로 데이터 무결성과 일관성을 유지할 수 있습니다. 관계 유형에 따라 일대일, 일대다, 다대다와 같은 다양한 방식으로 객체와 테이블 간의 연관성을 정의할 수 있으며, 이는 쿼리 성능, 데이터 접근 방식, 객체 간의 상호작용에 영향을 미칩니다. 그리고 관계 유형에 따라 외래키 관리와 연관관계 주인의 설정이 달라지므로, 잘못 설정할 경우 JPA가 비효율적이거나 작성할 수 있습니다.

     

    단방향/양방향

    테이블은 외래키 하나로 양쪽에서 조회가 가능하므로 단방향과 양방향으로 구분할 필요가 없습니다. 반면 객체는 참조용 필드가 있는 쪽으로만 조회가 가능해서 한쪽 엔티티에서만 다른 엔티티를 참조하면 단방향, 두 엔티티가 서로를 참조하면 양방향으로 구분됩니다. 이 경우 단방향이 두 개가 양방향처럼 보일 수 있습니다. 그럼 테이블에서는 어떤 필드가 외래키를 관리하는지를 명확히 정해야 하며, 이를 위해 연관관계 주인이 필요합니다.

     

    연관관계의 주인

    JPA에서 연관관계 주인은 외래키를 관리하는 필드입니다. 테이블은 외래키 하나로 연관관계를 맺고, 객체는 양방향 조회를 위해 각자 참조 필드를 가져야 합니다. 즉 연관관계 주인으로 지정한 필드가 외래키를 관리하며, 연관관계 주인이 아닌 필드는 데이터베이스에 영향을 주지 않고 단순히 객체 간의 조회 기능만 제공합니다.

     

    관계유형 매핑 종류

    Many-to-One (N:1 관계) - 단방향

    @Entity
    public class Member {
        @Id
        @GeneratedValue
        @Column(name = "MEMBER_ID")
        private Long id;
        
        private String username;
        
        @ManyToOne
        @JoinColumn(name="TEAM_ID")
        private Team team; // 연관관계 주인
    }
    
    @Entity
    public class Team {
        @Id
        @GeneratedValue
        @Column(name = "TEAM_ID")
        private Long id;
        
        private String name;
    }
    • @ManyToOne : N:1 관계를 표현하는 애노테이션, @ManyToOne이 붙은 엔티티가 N이고 반대 엔티티가 1인 경우 
    • @JoinColumn(name="TEAM_ID") : 외래키를 지정하는 애노테이션

    위에 사진은 테이블을 객체로 표현한 사진으로 Member 객체에 Team 필드를 외래키인 TEAM_ID으로 매핑한 단방향 매핑입니다.  

    이 경우, N쪽(Member 객체)에서 외래키를 관리하게 됩니다. 단방향 매핑이란 한 객체가 다른 객체를 참조하지만, 반대 방향으로는 참조하지 않는 관계를 의미합니다. 따라서 Member 객체는 TEAM_ID를 통해 Team 객체와 연결되지만, Team 객체는 Member 객체를 직접 참조하지 않습니다.

     

     

    One-to-Many (1:N 관계) - 단방

    One-to-many 관계에서는 1쪽에서 연관 관계 주인이 되어 외래키를 관리합니다. 하지만 관계형 데이터베이스의 설계상 외래키는 반드시 N측에 포함될 수밖에 없습니다. 이 말은 즉 연관관계 주인이 외래키를 관리하지 않고 반대편 엔티티에서 외래키를 관리하기 때문에 관리가 복잡해지고 성능 이슈가 발생할 수 있기 때문입니다. 

     

    예를 들어, Member 객체를 저장할 때 Member 엔티티에는 Team 엔티티에 대한 정보가 없으므로 INSERT 구문에서 외래키가 저장되지 않고 데이터베이스에 들어갑니다. 이후 Team 객체가 저장될 때 Team 객체의 연관 관계 정보를 기반으로 Member 객체에 UPDATE 구문이 호출되어 외래키가 저장됩니다. 이로 인해 한 번의 저장 작업에 UPDATE가 항상 함께 호출되어 성능 저하가 발생할 수 있습니다. 따라서 단방향 관계를 설정할 때는 1:N보다는 N:1 단방향 관계를 사용하는 것이 좋습니다. 

    @Entity
    public class Member {
        @Id
        @GeneratedValue
        @Column(name = "MEMBER_ID")
        private Long id;
        
        private String username;
    }
    
    @Entity
    public class Team {
        @Id
        @GeneratedValue
        @Column(name = "TEAM_ID")
        private Long id;
        
        private String name;
        
        @OneToMany
        @JoinColumn(name = "TEAM_ID") // 연관관계 주인
        private List<Member> members = new ArrayList<>();
    }

    테이블을 객체로 표현한 사진

     

    결론

    • 단방향 관계를 설정할 때는 1:N < N:1이 성능이 좋습니다.
    • 연관관계 주인이 외래키를 관리하지 않아 관리가 복잡해지고 성능 이슈가 발생
    • 자식 테이블 INSERT 시 추가적으로 UPDATE 쿼리문이 실행됩니다.

     

    One-to-One (1:1 관계)

    각 엔티티가 서로 단 하나의 관계만 가지는 관계입니다. 외래키는 어느 쪽 엔티티에 위치해도 큰 문제가 없지만, 주로 관계의 소유자가 외래 키를 갖도록 설정합니다.

    일대일 관계에서 관계주인 필드가 외래키를 관리하지 않으면 지원하지 않습니다. 즉 관계주인 필드는 외래키를 관리해야 하는 필드여야 합니다.

    @Entity
    public class Member {
    
        @Id
        @GeneratedValue
        @Column(name = "MEMBER_ID")
        private Long id;
    
        @OneToOne
        @JoinColumn(name = "LOCKER_ID")
        private Locker locker; // 연관관계 주인
    }
    
    @Entity
    public class Locker {
    
        @Id
        @GeneratedValue
        @Column(name = "LOCKER_ID")
        private Long id;
        private String name;
    
    }

    연관관계 주인을 변경하고 싶다면 @joinColumn 위치를 서로 바꿔주면 됩니다.

     

    양방향 매핑인 경우

    더보기

    양방향 매핑이면 mappedBy 속성도 사용

     

     

    Many-to-Many (N:N 관계)

    관계형 데이터베이스에서 다대다(N:N) 관계는 두 개의 정규화된 테이블로 직접 표현할 수 없습니다. 이를 해결하기 위해 중간에 테이블을 생성하여 양방향 1:N + N:1 관계로 풀어내야 합니다. 예를 들어, 학생과 수업 간의 관계를 표현할 때, 학생 테이블과 수업 테이블 사이에 새로운 테이블을 생성해 각 학생이 여러 수업을 수강하고, 각 수업에 여러 학생이 등록될 수 있도록 합니다. 반면, 객체는 List<>와 같은 컬렉션을 사용하여 두 개의 객체 간에 다대다 관계를 직접 표현할 수 있습니다.

    @Entity
    public class Member {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        @Column(name = "MEMBER_ID")
        private Long id;
        private String name;
    
        @ManyToMany
        @JoinTable(name = "MEMBER_PRODUCT",
        joinColumns = @JoinColumn(name = "MEMBER_ID"),
        inverseJoinColumns = @JoinColumn(name = "PRODUCT_ID"))
        private List<Product> product = new ArrayList<>();
    }
    
    @Entity
    public class Product {
        @Id
        @GeneratedValue
        @Column(name = "PRODUCT_ID")
        private Long id;
        private String productName;
    }

    JPA와 같은 ORM에서는 다대다 관계를 사용하는 것이 권장되지 않습니다.

     

    공식문서

    https://docs.jboss.org/hibernate/orm/current/userguide/html_single/Hibernate_User_Guide.html#associations-many-to-many:~:text=For%20%40ManyToMany%20associations,FKM7J0BNABH2YR0PE99IL1D066U%20table%3A%20PERSON_ADDRESS

     

     

     

     

    참조

    https://cjw-awdsd.tistory.com/47

    https://dev-coco.tistory.com/106

    https://liltdevs.tistory.com/150

    https://velog.io/@blackbean99/1.-SpringBoot-JPA%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0-JPA-%EC%97%94%ED%8B%B0%ED%8B%B0-%EC%97%B0%EA%B4%80%EA%B4%80%EA%B3%84-%EB%A7%A4%ED%95%91-%EA%B0%9C%EB%85%90-%EC%84%A4%EB%AA%85-.-%EC%98%88%EC%A0%9C