김찬진의 개발 블로그

23/12/25 [다양한 연관관계 매핑 - FK 관리 주체, 양방향 연관관계 여부에 따라] 본문

1일1배움/JPA (김영한 님)

23/12/25 [다양한 연관관계 매핑 - FK 관리 주체, 양방향 연관관계 여부에 따라]

kim chan jin 2023. 12. 25. 15:05

테이블과 객체간 큰 개념 차이가 있다.

 

테이블은 두 테이블간 어떤 한 테이블만이 FK를 가지더라도 두 테이블들은 서로 연관관계를 맺을 수 있다.

예를 들어 아래와 같이 말이다.

CREATE TABLE Author
(
AuthorID INT PRIMARY KEY,
AuthorName VARCHAR(255) NOT NULL
);

CREATE TABLE Book
(
BookID INT PRIMARY KEY,
Title VARCHAR(255) NOT NULL,
AuthorID INT,
FOREIGN KEY (AuthorID) REFERENCES Author(AuthorID)
);

INSERT INTO Author (AuthorID, AuthorName) VALUES
(1, '작가1'),
(2, '작가2');

INSERT INTO Book (BookID, Title, AuthorID) VALUES
(101, '책1', 1),
(102, '책2', 1),
(103, '책3', 2);

 

 

Author
AuthorID (PK) AuthorName
1 작가1
2 작가2

 

Book
BookID (PK) Title AuthorID (FK)
101 책1 1
102 책2 1
103 책3 2

 

 

SELECT * FROM Author a
JOIN Book b ON a.authorID = b.authorID;

SELECT * FROM Book b
JOIN Author a ON b.authorID = a.authorID;

 

AUTHORID AUTHORNAME BOOKID TITLE AUTHORID
1 작가1 101 책1 1
1 작가1 102 책2 1
2 작가2 103 책3 2

 

BOOKID TITLE AUTHORID AUTHORID AUTHORNAME
101 책1 1 1 작가1
102 책2 1 1 작가1
103 책3 2 2 작가2

 

 

 

반면, 두 객체간 어떤 한 객체가 참조변수를 가지더라도 두 객체들은 서로 연관관계를 맺을 수 없다. 

참조변수를 가지지 못한 객체는 참조할 수 없기 때문이다.

예를 들어 아래와 같이 말이다.

@Entity
class Author {

    @Id @GeneratedValue(strategy = GenerationType.AUTO)
    private Long authorID;

    priavte String authorName;

    // 현재 Author 는 Book 을 참조할 방법이 없다

}
@Entity
class Book {

    @Id @GeneratedValue(strategy = GenerationType.AUTO)
    private Long bookID;

    priavte String title;
    
    @ManyToOne
    @JoinColumn(name = 'authorID')
    private Author author; // Book 는 Author 를 참조할 수 있다
}

 

 

Author 가 본인에게 달린 Book 객체들을 조회하고 싶다면 다음과 같이 Author 클래스를 변경해야 한다.

추가적으로 연관관계 편의 메서드도 달아준다. (Author 것을 택할지 Book 것을 택할지 선택해야 한다.)

@Entity
class Author {

    @Id @GeneratedValue(strategy = GenerationType.AUTO)
    private Long authorID;

    priavte String authorName;
    
    @OneToMany(mappedBy = "author") // 주인 명시
    private List<Book> books = new ArrayList<>(); // 대상
    
    public void addBook(Book book) { // 연관관계 편의 메서드1
        book.setAuthor(this);
        books.add(book)
    }
}
@Entity
class Book {

    @Id @GeneratedValue(strategy = GenerationType.AUTO)
    private Long bookID;

    priavte String title;
    
    @ManyToOne
    @JoinColumn(name = 'authorID')
    private Author author;

    public void changeAuthor(Author author) { // 연관관계 편의 메서드2
        this.author = author;
        author.getBooks().add(this);
    }
}

 

이것이 테이블과 객체간의 차이이다.

단방향일 때에는 객체간 서로 연관관계를 맺을 필요가 없기 때문에 그 차이를 느끼지 못하지만

양방향일 때에는 객체간 서로 연관관계를 맺어야 하므로 객체에 참조변수를 만들어주는 식으로 그 차이를 없앨 수 있다.

 

 

그럼 연관관계 매핑을 할 때 고려할 것은 2가지이다.

1. 누가 FK를 관리할 것인지 (누가 주인, 대상이 될 것인지)

    - @JoinColumn(name = "칼럼이름")

2. 단방향에서 그칠건지 더 나아가 양방향으로 만들 것인지 

    - @ManyToOne 주인 <-> @OneToMany 대상   

    - @OneToMany 주인 <-> @ManyToOne 대상

    

 

 

 

 

결론을 정리하면 다음과 같다.

표를 이해하기 위해 표 아래 질문과 정리한 내용을 공부해보자

제일 어려웠던 고민

Q.

다대일 양방향에서 

@OneToMany(mappedBy = "team")

private List<Member> members

은 가능하지만

 

일대다 양방향에서

@ManyToOne(mappedBy = "members")

private Team team

불가능하고 (표에 존재하지 않음)

 

@ManyToOne
@JoinColumn(name = "TEAM_ID", insertable = false, updatable = false)
private Team team

만 가능한 이유?

A.

일대다 양방향의 경우, 객체는 Many (Member)가 FK(TEAM_ID)를 관리할 수 없다. (대상)

테이블은 Many (MEMBER)가 FK(TEAM_ID)를 무조건 관리한다 (주인)

 

그래서 @ManyToOne @JoinColumn(name = "TEAM_ID") 로 객체(Member)가 FK를 가져오는 것이다.

단, 일대다 양방향의 경우 Member는 대상이므로 읽기 전용으로 제한해야 한다

@ManyToOne @JoinColumn(name = "TEAM_ID", insertable = false, updatable = false)

 

 

 

1-1. 다대일 단방향

멤버는 팀을 알고 싶지만, 팀은 멤버들을 알고 싶지 않다.

@Entity
public class Member {

    @Id @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "MEMBER_ID")
    private Long id;

    @Column(name = "USERNAME")
    private String username;

    @ManyToOne // 다대일
    @JoinColumn(name = "TEAM_ID") // 주인
    private Team team; 
    
    // getter, setter 생략
}
@Entity
public class Team {

    @Id @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "TEAM_ID")
    private Long id;

    private String name;
    
    // getter, setter 생략
}

 

1-2. 다대일 양방향

멤버는 팀을 알고 싶고, 팀도 멤버들을 알고 싶다. 

 

@Entity
public class Member {

    @Id @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "MEMBER_ID")
    private Long id;

    @Column(name = "USERNAME")
    private String username;

    @ManyToOne // 다대일
    @JoinColumn(name = "TEAM_ID") // 주인
    private Team team; 
    
    public void changeTeam(Team team) { // 양방향 연관관계 편의 메서드1
        this.team = team;
        team.getMembers().add(this);
    }
    
    // getter, setter 생략
}
@Entity
public class Team {

    @Id @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "TEAM_ID")
    private Long id;

    private String name;

    @OneToMany(mappedBy = "team") // 일대다, 대상
    private List<Member> members = new ArrayList(); 
    
    public void addMember(Member member) { // 양방향 연관관계 편의 메서드2
        member.setTeam(this);
        members.add(member);    
    }

    // getter, setter 생략
}

 

2-1. 일대다 단방향    (권장X )

팀은 멤버들을 알고 싶고, 멤버는 팀을 알고 싶지 않다.

@Entity
public class Team {

    @Id @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "TEAM_ID")
    private Long id;

    private String name;

    @OneToMany // 일대다
    @JoinColumn(name = "TEAM_ID") // 주인
    private List<Member> members = new ArrayList<>(); 
    
    // getter, setter 생략
}
@Entity
public class Member {

    @Id @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "MEMBER_ID")
    private Long id;

    @Column(name = "USERNAME")
    private String username;
        
    // getter, setter 생략
}

 

2-2. 일대다 양방향    (권장X)

팀은 멤버들을 알고 싶고, 멤버도 팀을 알고 싶다.

@Entity
public class Team {

    @Id @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "TEAM_ID")
    private Long id;

    private String name;

    @OneToMany // 일대다
    @JoinColumn(name = "TEAM_ID") // 주인
    private List<Member> members = new ArrayList<>();
    
    // getter, setter 생략
}
@Entity
public class Member {

    @Id @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "MEMBER_ID")
    private Long id;

    @Column(name = "USERNAME")
    private String username;
    
    @ManyToOne // 다대일
    @JoinColumn(name = "TEAM_ID", insertable = false, updatable = false) // 읽기 전용 주인 (대상)
    private Team team; 
    
    // getter, setter 생략
}

 

3-1. 일대일 단방향 - MEMBER에 FK(LOCKER_ID)가 있는 경우

멤버는 사물함을 알고 싶고, 사물함은 멤버를 알고 싶지 않다.

@Entity
public class Member {

    @Id @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "MEMBER_ID")
    private Long id;

    @Column(name = "USERNAME")
    private String username;

    @OneToOne
    @JoinColumn(name = "LOCKER_ID")
    private Locker locker;
}
@Entity
public class Locker {

    @Id @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "LOCKER_ID")
    private Long id;

    private String name;
}

 

3-2. 일대일 양방향 - MEMBER에 FK(LOCKER_ID)가 있는 경우

멤버는 사물함을 알고 싶고, 사물함도 멤버를 알고 싶다.

@Entity
public class Member {

    @Id @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "MEMBER_ID")
    private Long id;

    @Column(name = "USERNAME")
    private String username;

    @OneToOne
    @JoinColumn(name = "LOCKER_ID")
    private Locker locker;
}
@Entity
public class Locker {

    @Id @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "LOCKER_ID")
    private Long id;

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

 

3-3. 일대일 단방향 - LOCKER에 FK(MEMBER_ID)가 있는 경우

불가능

 

3-4. 일대일 양방향 - LOCKER에 FK(MEMBER_ID)가 있는 경우

멤버는 사물함을 알고 싶고, 사물함도 멤버를 알고 싶다.

@Entity
public class Member {

    @Id @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "MEMBER_ID")
    private Long id;

    @Column(name = "USERNAME")
    private String username;

    @OneToOne(mappedBy = "member")
    private Locker locker;
}
@Entity
public class Locker {

    @Id @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "LOCKER_ID")
    private Long id;

    private String name;
    
    @OneToOne
    @JoinColumn(name = "MEMBER_ID")
    private Member member;
}

 

Comments