BACKEND/SPRING

연관관계 매핑 - 연관관계 매핑 종류와 방향

우진하다 2023. 8. 20. 20:52

 

"연관관계 매핑"은 객체 지향 프로그래밍과 관계형 데이터베이스 사이에서 관계를 매핑할 때 사용되는 개념입니다. 
주로 ORM (Object-Relational Mapping) 프레임워크, Java의 JPA (Java Persistence API) 또는
Python의 SQLAlchemy에서 자주 사용됩니다.

연관관계 매핑 종류와 방향

연관관계를 맺는 두 엔티티 간에 생성할 수 있는 연관관계의 종류

일대일 (OneToOne): 한 엔티티가 다른 엔티티를 하나만 참조할 수 있습니다.
일대다 (OneToMany): 한 엔티티가 여러 개의 다른 엔티티를 참조할 수 있습니다.
다대일 (ManyToOne): 여러 엔티티가 한 개의 다른 엔티티를 참조할 수 있습니다.
다대다 (ManyToMany): 여러 엔티티가 여러 다른 엔티티를 참조할 수 있습니다. 
다대다는 실제 데이터베이스 테이블에서는 잘 표현되지 않으므로, 
중간 테이블을 사용하여 두 개의 일대다 관계로 분해하여 사용하는 것이 일반적입니다.

연관관계의 방향
단방향: 한 쪽만 다른 쪽을 알고 있는 관계입니다.
양방향: 양쪽 모두 서로를 알고 있는 관계입니다.
양방향 연관관계에서는 주로 "주인"과 "비주인"으로 관계의 주도권을 구분하여, 
연관관계의 주인만이 데이터베이스 연관관계와 매핑되는 외래 키를 관리합니다.

 

일대일 매핑

일대일 단방향 매핑
일대일 단방향 매핑에서는 한 엔티티가 다른 엔티티를 참조하게 되지만, 반대 방향으로는 참조하지 않습니다.

회원(Member)과 회원의 프로필(Profile) 사이에 일대일 관계를 가정해보겠습니다. 
각 회원은 하나의 프로필만 가질 수 있습니다.

@Entity
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @OneToOne
    @JoinColumn(name = "profile_id")
    private Profile profile;

    // ...
}

@Entity
public class Profile {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String nickname;

    // ...
}

일대일 양방향 매핑
일대일 양방향 매핑에서는 한 엔티티가 다른 엔티티를 참조하며, 그 반대 방향 엔티티도 첫 번째 엔티티를 참조합니다. 
이때 주의할 점은 연관관계의 주인을 명확히 해야 합니다. 
연관관계의 주인만이 데이터베이스 연관관계와 매핑되는 외래 키를 관리하게 됩니다.

Member와 Profile 사이의 일대일 관계에서 양방향으로 연결하겠습니다. 여기서 Member가 연관관계의 주인입니다.

@Entity
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @OneToOne
    @JoinColumn(name = "profile_id")
    private Profile profile;

    // ...
}

@Entity
public class Profile {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String bio;

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

    // ...
}

 

다대일, 일대다 매핑

다대일 단방향 매핑
다대일 단방향 매핑에서는 여러 엔티티가 한 개의 엔티티를 참조합니다. 참조하는 쪽에만 외래 키가 존재하게 됩니다.

회원(Member)이 여러 개 있을 때, 각 회원은 하나의 팀(Team)에만 속하게 되는 경우를 가정합니다.

@Entity
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

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

    // ...
}

@Entity
public class Team {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    // ...
}

 

다대일 양방향 매핑
다대일 양방향 매핑에서는 여러 엔티티가 한 개의 엔티티를 참조하며, 참조되는 엔티티도 여러 엔티티를 인식합니다.

위의 예시에서 Team 엔티티가 여러 Member 엔티티들을 알 수 있도록 매핑합니다.

@Entity
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

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

    // ...
}

@Entity
public class Team {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

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

    // ...
}

 

일대다 단방향 매핑
일대다 단방향 매핑에서는 한 엔티티가 여러 엔티티를 참조합니다. 하지만 참조된 엔티티 쪽에서는 참조하는 엔티티를 알지 못합니다.

하나의 팀(Team)이 여러 회원(Member)들을 참조하나, 회원은 어느 팀에 속하는지 모르는 상황을 가정합니다.

@Entity
public class Team {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @OneToMany
    @JoinColumn(name = "team_id")
    private List<Member> members = new ArrayList<>();

    // ...
}

@Entity
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    // ... 다른 필드들 ...
}

이 구성에서 Team 엔티티는 여러 Member 엔티티들을 참조합니다. 
@JoinColumn을 사용하여 Member 테이블에 team_id 외래 키를 생성하며, 이를 통해 Team과 Member를 연결합니다.

단, 이런 방식의 일대다 단방향 매핑은 권장되지 않는 경우도 있습니다.
JPA의 내부 동작 방식에 따라 테이블에 의도치 않은 UPDATE SQL이 발생할 수 있기 때문입니다.
일대다 양방향 매핑이나 다대일 양방향 매핑을 사용하는 것이 더 효율적일 때가 많습니다

 

다대다 매핑

다대다 단방향 매핑
다대다 단방향 매핑에서는 한 엔티티가 여러 엔티티를 참조하며, 반대 쪽 엔티티는 참조하는 엔티티를 알지 못합니다. 
실제 관계형 데이터베이스에서는 다대다 관계를 직접 표현할 수 없어서, 중간에 연결 테이블(매핑 테이블)이 필요합니다.

학생(Student)과 과목(Course)의 관계를 생각해보겠습니다. 학생은 여러 과목을 수강할 수 있습니다.

@Entity
public class Student {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToMany
    @JoinTable(name = "student_course",
               joinColumns = @JoinColumn(name = "student_id"),
               inverseJoinColumns = @JoinColumn(name = "course_id"))
    private List<Course> courses = new ArrayList<>();

    // ...
}

@Entity
public class Course {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    // ... 다른 필드들 ...
}

 

다대다 양방향 매핑
다대다 양방향 매핑에서는 한 엔티티가 여러 엔티티를 참조하고, 반대 쪽 엔티티도 참조하는 엔티티를 인식합니다. 
마찬가지로 중간에 연결 테이블이 필요합니다. 양방향 매핑에서는 연관관계의 주인을 정해야 합니다.

학생(Student)과 과목(Course)의 관계에서, 과목 쪽에서도 해당 과목을 수강하는 학생들을 알 수 있도록 합니다.

@Entity
public class Student {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToMany
    @JoinTable(name = "student_course",
               joinColumns = @JoinColumn(name = "student_id"),
               inverseJoinColumns = @JoinColumn(name = "course_id"))
    private List<Course> courses = new ArrayList<>();

    //  ...
}

@Entity
public class Course {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @ManyToMany(mappedBy = "courses")
    private List<Student> students = new ArrayList<>();

    // ...
}

mappedBy 속성을 사용하여 연관관계의 주인이 Student 엔티티의 courses 필드임을 나타냅니다.

다대다 관계는 복잡하며, 실무에서는 연결 테이블을 엔티티로 변환하여 일대다, 다대일 관계로 분리하는 것이 관리하기 좋습니다.

 

영속성 전이

영속성 전이는 한 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만드는 것을 의미합니다. 
CascadeType의 여러 값 중에서 특정 작업을 선택하여 연관된 엔티티에 전이시킬 수 있습니다.

영속성 전이를 사용하는 주요 상황은 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장하는 경우입니다.

주요 Cascade 타입
PERSIST: 부모 엔티티가 저장될 때 자식 엔티티도 함께 저장됩니다.
REMOVE: 부모 엔티티가 삭제될 때 자식 엔티티도 함께 삭제됩니다.
MERGE: 부모 엔티티가 병합될 때 자식 엔티티도 함께 병합됩니다.
REFRESH: 부모 엔티티가 새로고침될 때 자식 엔티티도 함께 새로고침됩니다.
ALL: 모든 변경사항이 자식 엔티티에도 전이됩니다.

예제
부모 엔티티인 Post와 자식 엔티티인 Comment 사이의 관계에서 영속성 전이를 적용하겠습니다. 
Post를 저장하거나 삭제할 때 연관된 Comment도 함께 저장하거나 삭제됩니다.

@Entity
public class Post {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String title;
    private String content;

    @OneToMany(mappedBy = "post", cascade = {CascadeType.PERSIST, CascadeType.REMOVE})
    private List<Comment> comments = new ArrayList<>();

    // ...
}

@Entity
public class Comment {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String text;

    @ManyToOne
    private Post post;

    // ...
}