이전글 - [Spring Boot] JPA (1) - 환경셋팅 / Entity 클래스 만들기
이전글 - [Spring Boot] JPA (2) - CRUD 기능을 위한 JPA Repository 만들기
앞서 올린 게시글로 엔티티 구성과 레파지토리를 통한 DB 접근을 간단하게 해보았다.
이번에는 엔티티에 대한 관계를 SpringBoot에서는 어떻게 나타낼 수 있는지 한번 알아보자.
@OneToMany
- 1:N관계
@ManyToOne
- N:1관계
@ManyToMany
- N:N관계
@OneToOne
- 1:1관계
@JoinColumn
- 외래키 설정
@Column
과 거의 동일하다고 생각하면된다.
일반적으로는 참조x, JPA가 알아서 찾아줌
@JoinTable
- 관계(연결) 테이블을 만들어준다.(N:M에서 주로 쓰임)
User과 Post로 관계 구성을 해보겠다.
@Entity @Getter @Setter @NoArgsConstructor @Table(name="USERS")
public class User {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id") Integer id;
@Column(name = "u_id",length = 255) String userId;
@Column(name="u_pw", length = 255) String userPassword;
@Column(name="u_name", length = 255) String userName;
@OneToMany(mappedBy = "author", cascade = CascadeType.ALL) private List<Post> posts =new ArrayList<>();
public User(String userId, String userPassword, String userName) {
this.userId = userId;
this.userPassword = userPassword;
this.userName = userName;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return Objects.equals(id, user.id);
}
@Override
public int hashCode() {return Objects.hash(id);}
}
@Table(name="POSTS") @Entity @Getter @Setter @NoArgsConstructor
public class Post {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "p_id") Long id;
@Column(name="p_title") String title;
@Column(name="p_contents") String contents;
@ManyToOne @JoinColumn(name="u_id",referencedColumnName = "u_id") User author;
public Post(String title, String contents, User author) {
this.title = title;
this.contents = contents;
this.author = author;
author.getPosts().add(this);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Post post = (Post) o;
return Objects.equals(id, post.id);
}
@Override
public int hashCode() {return Objects.hash(id);}
}
위처럼 구성하면 된다. 제대로 완성했는지 궁금하다면, 아래의 조건에 따라 테스트를 진행해보면 된다.
이 부분에서 문제 발생 시
[Trouble Shooting] JPA에서 부모 엔티티에 값이 미등록될 때의 원인과 해결 전략 을 참조하면 된다.
문자열을
@Id
로 지정하면 User에 추가가 안되는 오류를 만났었는데, 그 부분은 확인 후 Trouble Shooting에 추가해서 올릴 예정이다.
User 관점으로 Post를 보면 1:N 관계이다.
그리고, Post 관점으로 User를 보면 N:1 관계이다.
그렇기 때문에, 위의 예제를 통해서 1:N과 N:1에 대해서 표기가 가능하다.
User와 Profile로 구성해보겠다.
@Entity @Getter @Setter @NoArgsConstructor @Table(name="USERS")
public class User {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id") Integer id;
@Column(name = "u_id",length = 255) String userId;
@Column(name="u_pw", length = 255) String userPassword;
@Column(name="u_name", length = 255) String userName;
@OneToOne(mappedBy = "user", cascade = CascadeType.ALL) Profile profile;
public User(String userId, String userPassword, String userName) {
this.userId = userId;
this.userPassword = userPassword;
this.userName = userName;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return Objects.equals(id, user.id);
}
@Override
public int hashCode() {return Objects.hash(id);}
}
@Entity
@Getter
@Setter
@Table(name="PROFILES")
@NoArgsConstructor
public class Profile {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name="p_id") Integer id;
String introduction;
@OneToOne @JoinColumn(name="u_id") User user;
public Profile(String introduction, User user) {
this.introduction = introduction;
this.user = user;
user.setProfile(this);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Profile profile = (Profile) o;
return Objects.equals(id, profile.id);
}
@Override
public int hashCode() {return Objects.hash(id);}
}
위처럼 구성하면 된다. 제대로 완성했는지 궁금하다면, 아래의 조건에 따라 테스트를 진행해보면 된다.
여기서 중요한 포인트가 있다.
먼저 양쪽 전부 @OneToOne
을 사용하고 있다. 그러는 중에도 mappedBy를 사용하여 Owner 객체를 가르킨다.
즉, 소유자와 피소유자가 나누어지는 구조를 볼 수 있다.
student와 course로 구성해보겠다.
N:M 관계는 다른 관계들과 다르게 표기하는 방식이 2가지가 있다.
다른 관계들과 차이가 있는 이유는 실제 관계형 데이터베이스에는 N:M 이라는 개념이 존재하지않는다.
이는 전체 코드 보다는 관계가 일어나는 대상 위주로 코드를 작성하겠다.
@ManyToMany
어노테이션 사용@Entity
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToMany
@JoinTable(
name = "student_course",
joinColumns = @JoinColumn(name = "student_id"),
inverseJoinColumns = @JoinColumn(name = "course_id")
)
private List<Course> courses;
// constructors, getters, setters
}
@Entity
public class Course {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToMany(mappedBy = "courses")
private List<Student> students;
// constructors, getters, setters
}
일단 @ManyToMany
를 사용해서 간결하게 n:m관계를 나타냈다.
여기서 @JoinTable
이라는 어노테이션이 등장한다. 이는 말그대로 조인 테이블을 만드는 것이다.
조금 더 쉽게 설명하자면 아래에서 만드는 관계(연결) 테이블을 자동으로 만들어주는 것이다.
@JoinTable( name = "student_course", joinColumns = @JoinColumn(name = "student_id"), inverseJoinColumns = @JoinColumn(name = "course_id") )
" student_course라는 이름의 연결 테이블을 만들 건데, 현재 student_id 랑 연결이 되고, 역방향으로는 course_id 와 연결된다. "
라는 의미이다.
@OneToMany
와@ManyToOne
으로 재구성@Entity
public class Student {
// ...
@OneToMany(mappedBy = "student")
private List<StudentCourse> studentCourses;
// ...
}
@Entity
public class Course {
// ...
@OneToMany(mappedBy = "course")
private List<StudentCourse> studentCourses;
// ...
}
@Entity
public class StudentCourse {
// ...
@ManyToOne
@JoinColumn(name = "student_id")
private Student student;
@ManyToOne
@JoinColumn(name = "course_id")
private Course course;
// ...
}
@ManyToMany
사용을 일부러 피하기 위해, 관계 테이블을 두어서 @OneToMany
와 @ManyToOne
를 사용할 수 있게 구성했다.
내가 이 말을 듣고 알 수 없는 호기심이 들었고, 무조건 나쁘다면 이런 어노테이션은 왜 존재할까? 라는 생각으로 공부를 했다.
그 결과, 나는 무조건 나쁘지는 않다 라는 결론이 나왔다.
@ManyToMany
장점
- 간편한 N:M 표현을 할 수 있다.
- 코드의 가독성 자체가 증가한다. (별도의 클래스가 필요없기 때문이다)
@ManyToMany
단점
- 조인 테이블의 컬럼에 추가 정보를 저장하기 어렵다.
- JPA 구현체마다 생성되는 조인 테이블이 다를 수도 있다.
1:N/N:1로 재구성시 장점
- 추가 정보 저장에 용이하다. (연결 엔티티를 사용해서 훨씬 간편한 접근이 가능하다.)
- 조인 테이블의 엔티티를 통해 조인 테이블에 대한 명시적 제어가 가능하다.
1:N/N:1로 재구성시 단점
- 복잡성이 증가한다. (엔티티 구조가 복잡해짐)
- 추가 엔티티를 필요로 하기 때문에 가독성이 떨어진다.
그럼 언제 무엇을 선택하는게 좋을지 생각할 것이다.
내가 내린 결론은 간단한 프로젝트에서는 오히려 @ManyToMany
를 사용하는 것이 훨씬 더 직관적이고, 여러 동작들에 유연함을 목표로 한다면 @ManyToOne
과 @OneToMany
를 사용하는 것이 더 좋다고 생각했다.
공부를 하면서 느낀건, 수동과 자동이라는 차이점이 있는 것이고, 이는 당연하게도 자동이면 유연성에 한계점이 존재할 수밖에 없고, 수동으로 하면 프로젝트에 맞게 유연한 설계가 가능하고 저장또한 수월한게 당연한 이야기이다.
단방향과 양방향을 간단하게 알고 가면 편할거 같아 설명하겠다.
지금까지 적어온 코드 전부 양방향 맵핑이다. 부모 엔티티와 자식 엔티티 모두 맵핑이 된 경우이다.
User와 Post가 있고, 이를 클래스로 관계를 구성했다.
User객체에서 Post에 참조를 가지지만, Post객체에서 User객체에 참조를 가지지않는다면..?
이는 양방향으로 맵핑 되어야할까? 단방향 맵핑이 되어야할까?
이는 간단하게 한쪽으로의 참조만 일어난다면, 당연하게도 양방향보다는 단방향이 효율적이다. ( 코스트가 낮아지기 때문에)
양방향은 지금까지 해왔으니 간단한 개념적인 포인트만 알아보겠다.
JPA에서 양방향 매핑 시, 실제로 데이터베이스에 영향을 주는 주체 엔터티를 가리키며, 외래 키를 가지고 있고 변경을 관리하는 역할을 한다.
mappedBy
라고 하면 아마 기억날 것이다.
mappedBy
를 사용하지않는 곳이 연관관계의 주인이다.
사실 이렇게 말하면 엄청 복잡하고 어려운 개념 같은데, DB를 배운 사람이라면 아래의 특성이 기억 날것이다.
외래키는 항상 primary key를 참조해야한다. 이 때, primary key가 연관관계의 주인이다. 말 그대로 관계에서의 Owner라는 것이다.
단방향 맵핑을 어디에 지정해야할지 고민인가?
간단하게 생각해야한다. 말 그대로 참조가 일어나는 곳에만 맵핑을 하면 된다.
User와 Post 관계에서 위에 설명한 상황이 일어난다면, User 쪽에만 단방향 맵핑을 해주면 된다. 그러나, User Post 관계에서도 다같이 사용하는 공용 게시판인 경우에는 양방향 맵핑이 더 좋을 수 도 있다는 소리이다.
한 관계에서 단방향과 양방향에 대한 정답은 없다. 이 것은 프로젝트의 규모, 확장성 등을 고려하여 선택되어야할 요소이다.