Just Do IT!
양방향 매핑 제대로 하기 본문
프로젝트를 하면서 하루 종일 한 오류만 붙잡고 있던 게 정말 오랜만이다.
간단히 코드 추가하는 걸로 끝났지만 간단하지 않았던 오류 해결과정이었다...
간단하게 Article을 생성하는 API를 개발하고 있었다.
생성할 때 연관된 article_link, article_file 도 함께 저장되는데 그 부분에서 계속 오류가 생겼다.
DB에는 잘 저장되고, 각각 조회도 잘 되는데 이상하게 create한 후 response에서는 나오지 않는 것이다.
우선, Article entity는 아래와 같다.
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Entity
public class Article extends BaseEntity {
private String title;
private String content;
@Enumerated(EnumType.STRING)
private PriorityType priority;
private LocalDateTime deadline;
@Enumerated(EnumType.STRING)
private ArticleStatus status;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id", nullable = false)
private Member member;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "stage_id", nullable = false)
private Stage stage;
@OneToMany(mappedBy = "article", cascade = CascadeType.ALL)
private List<Comment> commentList = new ArrayList<>();
@OneToMany(mappedBy = "article", cascade = CascadeType.ALL)
private List<ArticleFile> articleFileList = new ArrayList<>();
@OneToMany(mappedBy = "article", cascade = CascadeType.ALL)
private List<ArticleLink> articleLinkList = new ArrayList<>();
// 부모 게시글을 위한 필드 (답글이 부모 게시글을 참조)
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "parent_article_id") // 부모 댓글을 참조하는 외래키
private Article parentArticle;
// 자식 게시글 리스트 (양방향 관계에서 부모 게시글이 자식 게시글을 가질 수 있게 설정)
@OneToMany(mappedBy = "parentArticle", cascade = CascadeType.ALL)
private List<Article> childArticles = new ArrayList<>();
// 부모 게시글이 없으면 일반 게시글, 있으면 답글
public boolean isChildComment() {
return parentArticle != null;
}
@Builder
public Article(String title, String content, PriorityType priority, LocalDateTime deadline, Member member, Stage stage, ArticleStatus status,
List<ArticleFile> articleFileList, List<ArticleLink> articleLinkList) {
this.title = title;
this.content = content;
this.priority = priority;
this.deadline = deadline;
this.member = member;
this.stage = stage;
this.status = status;
this.articleFileList = articleFileList != null ? articleFileList : new ArrayList<>();
this.articleLinkList = articleLinkList != null ? articleLinkList : new ArrayList<>();
}
}
여기 보면 ArticleFileList, ArticleLinkList가 연관관계로 매핑되어 있는 걸 확인할 수 있다.
그리고 이전에 내가 작성했던 서비스 코드는
@Transactional
public ArticleModifyResponse createArticle(ArticleModifyRequest request, UserDetailsImpl userDetails) {
... 생략 ...
Article article = Article.builder()
.title(request.getTitle())
.content(request.getContent())
.priority(request.getPriority())
.deadline(request.getDeadLine())
.member(member)
.stage(stage)
.status(ArticleStatus.PENDING) // 기본 상태는 PENDING
.build();
article = articleRepository.save(article);
// fileList와 articleLinkList 처리 (최대 10개 제한)
if (request.getFileList() != null) {
for (ArticleFileDTO fileDTO : request.getFileList()) {
ArticleFile file = ArticleFile.builder()
.name(fileDTO.getName())
.url(fileDTO.getUrl())
.article(article)
.build();
articleFileRepository.save(file);
}
}
if (request.getLinkList() != null) {
for (ArticleLinkDTO linkDTO : request.getLinkList()) {
ArticleLink link = ArticleLink.builder()
.urlAddress(linkDTO.getUrlAddress())
.urlDescription(linkDTO.getUrlDescription())
.article(article)
.build();
articleLinkRepository.save(link);
}
}
... 생략 ...
}
코드가 길어서 문제가 된 일부 코드만 가져왔다.
여기 보면, Article이 생성되고 저장하고, 그 뒤에 만약 file과 link가 존재한다면 저장하는 로직이었다.
그런데 이렇게 하고 포스트맨으로 돌려보면,
{
"title": "Test21",
"content": "This is a test article content.",
"priority": "HIGH",
"deadLine": "2025-06-01T12:00:00",
"memberId": 5,
"stageId": 1,
"linkList": [
{
"urlAddress": "http://example.com/link1",
"urlDescription": "Link 1 Description"
},
{
"urlAddress": "http://example.com/link2",
"urlDescription": "Link 2 Description"
}
]
}
이렇게 request를 보내도
{
"status": "success",
"code": "200",
"message": "게시글 생성 성공",
"data": {
"title": "Test21",
"content": "This is a test article content.",
"priority": "HIGH",
"deadLine": "2025-06-01T12:00:00",
"memberId": 5,
"stageId": 1,
"fileList": [],
"linkList": []
}
}
이렇게 linkList가 [], null 로 나오는 것이었다.
DB를 확인해보면 제대로 잘 저장되는 걸 확인할 수 있었다.
도저히 이 흐름이 이해가 안 갔는데, 가만히 보면 내가 빠뜨린 부분이 있다.
각각 저장을 한 뒤에, Article에도 filelist / linklist를 추가해줘야 하는데 그걸 하지 않아서 계속 null로 나오는 것이었다.
하나의 Article이 여러 개의 ArticleLink를 가질 수 있고, 각 ArticleLink는 하나의 Article에 속해야 하고,
JPA에서 관리하려면 두 엔티티의 관계를 양방향으로 설정해야 하는 것이다.
그래서 fileList, LinkList가 존재하는 경우
존재하는 리스트를 Article 객체에 같이 저장해주어야 나중에 response에서도 확인할 수가 있다.
if (request.getLinkList() != null) {
for (ArticleLinkDTO linkDTO : request.getLinkList()) {
ArticleLink link = ArticleLink.builder()
.urlAddress(linkDTO.getUrlAddress())
.urlDescription(linkDTO.getUrlDescription())
.article(article)
.build();
articleLinkRepository.save(link);
article.getArticleLinkList().add(link);
}
}
그래서 이런 식으로
article.getArticleLinkList().add(link);
이 코드를 추가해주었다.
이걸 추가함으로써, Article 객체의 articleLinkList에 link를 추가하는 작업이 이루어지는 것이다.
file 역시 마찬가지이다.
혼자서는 해결하지 못했고 좋은 팀원들을 만난 덕분에 오랜 시간에 걸쳐(...이건 내 탓) 해결할 수 있었다.
이제는 정상적으로 나온다.
양뱡향 매핑에 대해 공부를 더 해봐야겠다.
공부할 영상 미리 저장해놓는다.
https://www.youtube.com/watch?v=brE0tYOV9jQ