[Spring] Author API & Post API 실습 : 연관관계 설정

이지연·2026년 1월 22일

개요

프로그램 관점에서 1:N 관계를 잡는 이유(needs)는 크게 “무결성”과 “개발 편의성”으로 정리할 수 있다.

  • 무결성(FK 설정의 필요성)
    FK는 “존재하지 않는 회원을 참조하는 게시글” 같은 데이터를 구조적으로 막아준다. 물론 프로그램 로직으로도(작성자 조회 후 없으면 예외) 제약을 걸 수는 있지만, FK를 두면 DB가 마지막 방어선 역할을 해서 실수나 예외 케이스에도 데이터 정합성이 깨질 가능성이 줄어든다.

  • 개발 편의성/확장성(조회·DTO 조립 부담 감소)
    연관관계가 없으면 “회원이 쓴 글 수”, “게시글 작성자 이름” 같은 요구사항이 나올 때마다 Author/Post를 각각 조회해서 조립해야 한다. 그러다 보면 서비스에서 Repository를 여러 개 주입받아 여기저기 조회하고, 케이스별로 DTO도 계속 늘어나면서 코드가 퍼지고 복잡해지기 쉽다.
    반대로 1:N 연관관계를 제대로 잡으면 객체 그래프 탐색이나 조인 기반 조회가 자연스러워져서, 요구사항 추가에 대응하는 비용이 줄어든다.


Post 엔티티 작성자 정보 설계 진화 : email → id → 객체참조(@ManyToOne)

Post에 작성자 정보를 “문자열(email)”로 저장하는 방식은 초기에 빠르지만, 참조 무결성을 DB가 보장할 수 없고(email이 없는 작성자도 들어갈 수 있음), Author 이메일이 변경될 때 과거 Post 정합성이 깨지기 쉽다.

그래서 보통은 authorId(Long)로 바꾸며(변경에 강한 PK 사용), 최종적으로는 지금처럼 JPA 연관관계로 Post -> Author를 객체로 연결하는 형태가 가장 ORM스럽다.
이 단계로 오면 post.getAuthor()처럼 객체 그래프 탐색이 가능해지고, 서비스/DTO 조립을 위해 Author를 매번 재조회하는 코드가 줄어든다.

초기(authorEmail 저장)

  • 장점: 단순하고 빠름(조인 없이 이메일 즉시 확인 가능)
  • 단점: 무결성 보장이 약하고, 이메일 변경 시 과거 데이터 정합성이 깨지기 쉬움

변경1(authorId 저장)

  • 장점: PK 기반이라 변경에 강함, join/인덱스도 정석
  • 단점: JPA 관점에선 “관계”가 아니라 숫자 필드라서 매번 서비스에서 Author를 다시 조회/조립하게 됨

변경2(@ManyToOne Author author)

  • 장점: 객체 그래프 탐색이 자연스럽고(post.getAuthor()), DB 관계(FK)와 코드 모델이 일치
  • 결과적으로 “서비스에서 DI로 양쪽 레포지토리 들고 다니면서 DTO 조립”하는 코드가 줄어드는 방향
    참조키가 설정된 모습 확인 가능

Post 엔티티 참조관계 설정

Post는 여러 개가 하나의 Author를 참조한다 (Post 기준 N:1 관계)

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "author_id", foreignKey = @ForeignKey(ConstraintMode.CONSTRAINT), nullable = false)
private Author author;
  • @ManyToOne
    Post 여러 개가 Author 하나를 참조한다는 뜻이고, FK는 Post 테이블에 생긴다(= 연관관계 주인은 Post).

    • fetch = LAZY
      ManyToOne은 기본 로딩 전략이 즉시 로딩(EAGER)이라 불필요하게 같이 당겨오는 문제가 생기기 쉬워서, 실무에서는 LAZY 명시가 거의 표준에 가깝다. 이렇게 해두면 Post를 조회할 때 Author를 “필요할 때만” 가져오게 되어 불필요한 로딩을 줄일 수 있다.
  • @JoinColumn

    • nullable = false
      “Post는 반드시 Author가 있어야 한다”는 정책을 DB 레벨까지 같이 강제한다. 이게 있어야 프로그램에서 검증을 빼먹어도 DB에서 마지막으로 막아준다.
    • ConstraintMode.CONSTRAINT
      FK 제약조건을 실제로 생성하겠다는 의미다. (반대로 FK를 아예 안 만들고 싶으면 NO_CONSTRAINT가 선택지이고, 이건 “무결성을 DB가 아니라 프로그램이 전부 책임진다”는 정책일 때만 고려하는 편이다.)

추가로 Post@ToString이 붙어 있는데, 연관 필드(author)가 포함되면 LAZY 로딩이 의도치 않게 발생할 수 있다. 실무에서는 보통 author 필드를 @ToString.Exclude로 제외하는 선택을 많이 한다(양방향이면 순환 참조 위험도 있음).


Author 엔티티 참조관계 설정(양방향 OneToMany의 의미)

Author 하나는 여러 개의 Post를 참조한다(1:N 관계)

@OneToMany(mappedBy = "author", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
@Builder.Default
private List<Post> postList = new ArrayList<>();
  • @OneToMany
    Author 한 명이 Post 여러 개를 참조한다는 뜻이다.

  • mappedBy = "author"

    • 연관관계의 주인은 Post.author 이고, Author.postList는 반대편(읽기 전용 성격) 컬렉션이다.
    • 즉, mappedBy는 “조회 컬럼 지정”이 아니라 “이 관계는 Post 쪽 필드(author)에 의해 매핑된다”를 선언하는 옵션이다.
    • @OneToMany는 선택, @ManyToOne은 거의 필수

      보통 N(Post) 입장에서는 1(Author)이 필요해서 ManyToOne이 필수가 되고, 1(Author) 입장에서 N(Post)을 항상 들고 있어야 하는 건 아니라 OneToMany는 필요할 때만 여는 편이 많다.
      다만 지금은 “회원 상세에서 글 개수(postCount) 반환” 같은 요구가 있어 postList가 실제로 쓰이는 상황이다.

  • fetch = LAZY
    OneToMany의 기본값이 LAZY라, 명시적으로 작성해두면 팀원 간 혼동이 줄어든다.

  • cascade = CascadeType.ALL / PERSIST / REMOVE
    여기 문단은 용어가 조금 섞여 있어서 아래처럼만 고치면 정확해진다.

    • PERSIST: 부모 저장 시 자식 저장이 전파(저장 코드 간결화 목적)
    • REMOVE: 부모 삭제 시 자식 삭제가 전파(hard delete 정책일 때)
    • ALL: PERSIST, MERGE, REMOVE 등 주요 전파가 전부 포함(= REMOVE도 포함)
  • orphanRemoval = true

    • “부모 컬렉션에서 자식을 제거하면(orphan) DB에서도 자식을 삭제”하는 옵션이다.
    • “자식의 자식까지”는 orphanRemoval 하나로 자동 해결되는 게 아니라, 각 관계(부모-자식) 단위로 필요하면 별도로 설정해야 한다는 식으로 이해하면 된다.

soft delete(delYn)와 Cascade REMOVE의 충돌 가능성

Post는 delYn으로 soft delete를 하고 있다.

public void deletePost() { this.delYn = "YES"; }

그런데 Author 쪽은 cascade = ALL이라 REMOVE가 포함되어 있다. 즉 정책이 섞일 수 있다.

  • 정책 A: “글은 soft delete로 숨긴다”
  • 정책 B: “작성자 삭제 시 글은 hard delete로 같이 지운다(REMOVE 전파)”

이 둘이 동시에 존재하면 운영/요구사항에 따라 혼선이 생길 수 있다. 그래서 보통은 아래 중 하나로 정리한다.

  • (선택 1) 전부 soft delete 정책
    Author 탈퇴 시 Post도 전부 delYn만 업데이트하고, 실제 delete는 하지 않음
    → 이 경우 cascade REMOVE(즉 ALL 포함)를 빼는 게 더 일관됨

  • (선택 2) 탈퇴는 hard delete 정책
    Author 삭제 시 Post도 실제로 삭제되어야 함
    → 이 경우 cascade REMOVE(또는 ALL)가 정책과 일치

즉, 현재 설정은 “정말 Author 삭제 시 Post를 물리적으로 지워도 된다”는 정책이 확실할 때 가장 자연스럽다.


정리

  • Post는 ManyToOne + FK(author_id) + LAZY로 “작성자 필수”를 강하게 보장하고,
  • Author는 OneToMany(mappedBy) + cascade ALL + orphanRemoval로 “Post 생명주기가 Author에 종속”되는 강한 정책을 선택한 상태다.
  • Post가 soft delete를 쓰고 있으니, Author 삭제를 hard delete로 할지(REMOVE 유지), soft delete로 할지(REMOVE 제거 + 상태 업데이트) 중 하나를 확정해야 한다.

마지막 문장만 수정이 필요해 보여: 지금 문장에는 “soft delete 전략으로 통일” 흐름인데, 결론에서 “hard delete 전략으로 선택”했다고 되어 있어서 서로 충돌해.

  • soft delete로 통일할 거면: “REMOVE/ALL을 제거(또는 최소화)하고, Author 탈퇴도 delYn 업데이트로 처리한다.”
  • hard delete로 갈 거면: “Post의 delYn soft delete는 조회 정책으로만 쓰거나, 삭제 정책을 hard delete로 변경한다.”
profile
Eazy하게

0개의 댓글