Just Do IT!

양방향 매핑 제대로 하기 본문

개발 공부/Error

양방향 매핑 제대로 하기

MOON달 2025. 3. 25. 15:07
728x90
반응형

프로젝트를 하면서 하루 종일 한 오류만 붙잡고 있던 게 정말 오랜만이다.

간단히 코드 추가하는 걸로 끝났지만 간단하지 않았던 오류 해결과정이었다...

 

 

 

 

 

간단하게 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

 

728x90