기록 이유
Issue Tracker 프로젝트를 진행하면서 OAuth, 백엔드 자동 배포, 프론트 배포 등의 산을 넘어 드디어 API 를 개발하나 했는데, JPA 가
발목을 잡고 있다 🥲 열심히 복습하고 있지만 역시 실전은 넘나 다른 것... 헷갈리는 것 투성이지만 오늘 겪은 고민에 대해 기록해보려 한다.
다른 포스팅들과는 다르게 의식의 흐름대로 작성 ㅎ_ㅎ ~~
기록 시작 ~
프로젝트 내 Entity 종류
Issue 기준으로,
1. Issue (N) : Label (M)
- Issue 는 여러 Label을 가질 수 있고, Label 또한 여러 이슈를 가질 수 있다.
2. Issue (1) : Comment (N)
- Issue 는 여러 Comment 를 가질 수 있고, Comment 는 하나의 이슈에 속해져 있다.
3. Issue (N) : Assignee (M)
- Issue 는 어러 Assignee 를 가질 수 있고, Assignee 또한 여러 이슈를 가질 수 있다.
4. Issue (N) : Milestone (1)
- Issue 는 하나의 Milestone 만 가질 수 있고, Milestone 내에서는 여러 이슈를 가질 수 있다.
연관관계 매핑
처음 매핑에 관해 생각했을 때, Issue 와 Label 의 관계는 단순 일대다의 관계라고 생각했었다. 처음 그렇게 매핑을 진행했었는데
사실 영한님 강의에서도 나오는 내용이지만, @OneToMany 단방향 매핑은 권장하지않는 방법이란걸 알고 있었고 찝찝한 마음을
떨쳐낼 수 없었다 . @OneToMany 단방향을 사용하기보단 객체에서 손해를 보더라도 @ManyToOne 양방향 매핑을 권장한다는
사실도 알고는 있었지만 시도해본적이 없어서 제대로 이해하질 못했다.
그렇게 머리를 싸매고 있을 때.. 검봉이 이거 다대다 아닌가요 ?? 라고 하셔서 다시 보니 일대다가 아니라 다대다 관계더라 .. 🤣
그래서 연결 테이블을 직접 엔티티로 생성해서 [일대다 - 다대일 - 일대다] 로 풀어서 매핑을 시도해봤다. 코드로 확인하자.
꼭 저렇게 3개로 쪼갤 필요는 없는 것 같다. 역방향 조회가 필요없다면 [일대다 - 다대일] 매핑으로도 충분할 듯 ?
1. IssueLabel Entity (Issue, Label 연결 Entity)
다대다 관계는 JPA 에서 공식적으로 지원하는 스펙이지만, 크게 2가지 이유로 실무에서 절대 사용하면 안된다고 한다.
첫 번째) 연결 테이블을 자동으로 만들어주기 때문에 다른 데이터를 넣을 수가 없다. 즉, 연결만 하고 마는 테이블이 하나 생기는 것.
두 번째) 실무에서는 테이블 수십개가 돌아가는데, 개발자가 생각하지도 못한 쿼리가 나갈 수 있다.
따라서 IssueLabel 과 같은 Entity 로 연결 테이블을 승격시켜서 사용하면 된다.
아래 코드에서 보이듯, 이 Entity 는 개발자가 자유롭게 데이터를 추가, 삭제, 수정할 수 있으며 각 테이블 2개의 외래키를 관리한다.
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class IssueLabel {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "issue_id")
private Issue issue;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "label_id")
private Label label;
}
2. Issue, Label Entity
Issue 와 Label 의 외래키는 IssueLabel 에서 관리하기 때문에 두 객체는 연관관계의 주인이 아니다. 따라서 mappedBy 필요.
고민해볼 부분 ? 이라고 생각한 부분은 Label 객체의 @OneToMany 매핑 부분이다. Issue 에서 Label 을 조회하는 경우가 대부분이고
역방향 조회는 거의 없을 것이라고 생각해서 나중에 필요할 때 추가해주면 될 것 같다. (지금은 그냥 예제니까 써놓는 ㅎㅎ)
아무튼 앞으로 다대다는 연결 Entity 를 만들어서 외래키를 연결 Entity 에서 관리하고, 다대다를 일대다, 다대일로 풀어 사용하면 될 듯.
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class Issue {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
... 중략
// 외래키는 IssueLabel 에서 관리
@OneToMany(mappedBy = "issue")
private List<IssueLabel> issueLabels = new ArrayList<>();
... 중략
}
---
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class label {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
... 중략
// 외래키는 IssueLabel 에서 관리
@OneToMany(mappedBy = "label")
private List<IssueLabel> issueLabels = new ArrayList<>();
... 중략
}
3. 연관관계 편의 메서드 사용
양방향 매핑에서 중요한 부분 중 하나인, 두 개의 객체에 서로 값을 넣어줘야하는 점을 잊지 말자 ㅎ_ㅎ
현재 우리 프로젝트에는 Issue 객체에 양방향 연관관계 편의 메서드가 존재하고 Issue 객체는 아래와 같다.
(원래 @OneToMany 매핑되어있는 Comment, Assignee List 도 있지만 Label 로만 예시를 들기 위해 코드에 적지는 않았음)
새로운 Issue 를 생성하기 위한 정적 팩토리 메서드 of 도 추가해줬다.
public class Issue {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String author;
private String title;
private String content;
@Enumerated(value = EnumType.STRING)
private IssueStatus status = IssueStatus.OPEN;
private LocalDateTime createdAt;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "milestone_id")
private Milestone milestone;
@OneToMany(mappedBy = "issue")
private List<IssueLabel> issueLabels = new ArrayList<>();
private Issue(String author, String title, String content, LocalDateTime createdAt, Milestone milestone) {
this.author = author;
this.title = title;
this.content = content;
this.createdAt = createdAt;
this.milestone = milestone;
}
public static Issue of(String author, String title, String content, LocalDateTime createdAt, Milestone milestone) {
return new Issue(author, title, content, createdAt, milestone);
}
// 양방향 연관관계 편의 메서드
public void addLabels(List<Label> labels) {
for(Label label : labels) {
this.issueLabels.add(IssueLabel.of(this, label));
}
}
}
사용은 이런 식으로 @Transactional 안 쪽에서 해주면 됨.
@Transactional
public Long save(Dto dto) {
... 중략
List<Label> labels = labelRepository.findAllById(dto.getLabelIds());
Issue newIssue = Issue.of(~);
/*
양방향 연관관계 매핑 메서드를 사용해서 Issue 에도 Label 값을 넣어주고,
IssueLabel 의 Issue 와 Label 에도 값을 넣어준다.
*/
newIssue.addLabels(labels);
return issueRepository.save(newIssue).getId();
}
'프로젝트 기록 > Issue Tracker 삽질 기록' 카테고리의 다른 글
[Issue Tracker Project] Switch - Case Refactoring (0) | 2022.11.06 |
---|---|
[Issue Tracker Project] Issue 전체 조회 시 N + 1 발생과 개선 (0) | 2022.10.21 |
[Issue Tracker Project] 로그인 시 환경변수 이슈 발생 (0) | 2022.09.07 |
[Issue Tracker Project] Object Mapper 에서 @Getter 와 기본 생성자의 역할 ? (0) | 2022.08.01 |