Skip to content

[♻️Refactor] 생명주기(Lifecycle) 기반 JPA 연관관계 매핑 최적화 (Item 엔티티 중심)#142

Open
minsubyun1 wants to merge 1 commit intodevfrom
refactor/141/domain-relationship
Open

[♻️Refactor] 생명주기(Lifecycle) 기반 JPA 연관관계 매핑 최적화 (Item 엔티티 중심)#142
minsubyun1 wants to merge 1 commit intodevfrom
refactor/141/domain-relationship

Conversation

@minsubyun1
Copy link
Copy Markdown
Member

@minsubyun1 minsubyun1 commented Mar 1, 2026

⛓️‍💥 Issue Number

🔎 배경 및 문제점

기존 NexERP의 도메인 모델은 다대다 관계를 풀어내기 위해 중간 테이블을 만들어 1:N (@OneToMany)과 N:1(@ManyToOne) 양방향 관계로 풀어내어, 객체 지향적인 탐색(부모 객체 -> 자식 컬렉션을 직접 조회)과 비즈니스 로직의 직관성을 확보하도록 했습니다. 이는 제가 기존에 ORM JPA 연관관계를 공부하면서 학습했던 방식과 일치하기도 했고, 이론적으로 올바른 형태라고 생각했습니다. 김영한 - 자바 ORM 표준 JPA 프로그래밍

그러나, 기술진을 통해 받은 피드백의 주요 내용을 정리하자면 @oneToMany를 거는 것 자체가 잘못된 것은 아니지만, 라이프 사이클적으로 실질적인 이유가 있지 않은 이상, @oneToMany가 걸린 양방향이 많아질수록 마스터-트랜잭션 (Item - InventoryItem) 간의 데이터가 폭발하거나, 도메인 간 의존성이 높아져 코드 수정이나 확장이 더 어려울 수 있다는 것이었습니다.

생명주기(Lifecycle) 관점의 엔티티 매핑 재평가 및 조치

그래서 연관관계를 맹목적으로 끊기보다, 기존에 있던 관계에 있어서 데이터의 생성/소멸 주기와 마스터/트랜잭션 데이터 여부를 기준으로 수정 타겟/현행 유지/추후 과제 정도로 단계를 나누어 평가를 진행했습니다.

🛠 수정 타겟: Item <-> InventoryItem / LogisticsItem (수정 완료)

  • 평가 기준 (생명주기 불일치): Item은 서비스의 기준 정보입니다. 그리고 InventoryItem 등은 매일 누적되는 내역 정보이죠. 즉, 내역이 지워진다고 품목이 지워지지는 않습니다. 그 역도 성립하지 않죠. 또한, 저희는 내역 자체는 ItemHistory로 별도로 관리하고 있는 상황이며, 매 입고나 출하 처리 때마다 연관관계의 주인인 InventoryItem이나 LogisticsItem과 연결된 Id를 통해서 매번 Item의 수량 반영을 하고 있기 때문에 @oneToMany가 없이도 사이드 이팩트 없이 코드가 멀쩡하게 돌아갑니다.
  • 조치 내용: 생명주기가 완전히 다른 두 도메인 간의 강한 결합을 끊기 위해 Item 엔티티 내의 @OneToMany 리스트 매핑을 일괄 삭제하고, 연관된 서비스 계층의 코드들을 모두 테스트 완료했습니다.

🔒 현행 유지 01. Inventory <-> InventoryItem

  • 평가 기준 (생명주기 일치): 입고 전표(Header)와 입고 품목(Line)은 생명주기가 정확히 일치합니다. 전표가 생성/삭제될 때 품목들도 운명을 함께하기 때문입니다.
  • 조치 내용: 이 관계는 캡슐화와 무결성 유지를 위해 설정을 유지하는 것이 객체지향적 설계에 부합하다고 판단했습니다.

🔒 현행 유지 02. Company (1) <-> (N) Member

  • 평가 기준 (우선순위 조정): 우선 회사와 멤버 및 프로젝트 관계를 생각해보면, 회사가 폐업할 시 프로젝트와 직원 데이터도 의미가 없어진다고 생각합니다. 또한, 한 회사의 직원이 수십만명 수준으로 늘어나지 않는 이상 메모리 문제도 없다고 생각해서, 지금 당장 수정할 필요성은 못 느꼈습니다.
  • 조치 내용: 동일하게 유지

🔒 현행 유지 03. Project(1) <-> Member_Project(N)

  • 평가 기준 (생명주기 일치): 프로젝트가 삭제되면 해당 프로젝트의 모든 참여 명단 (Member_Proejct)도 의미가 없어지며, 함께 삭제되어야 한다고 생각했습니다.
  • 조치 내용: 동일하게 유지

🔗 추후 과제 Member(1) <-> Member_Project(N) 단방향 전환

  • 평가 기준 (불일치): 직원은 프로젝트 참여 여부와 관계없이 존재하고, 한 명의 직원이 시간이 흐름에 따라 수십, 수백 개의 프로젝트에 참여할 수 있죠. 그래서 Member 엔티티가 오히려 List<Member_Project>를 가지고 있으면 Item 엔티티에서 조치했던 것처럼 메모리 비대화 위험이 어느 정도 있다고 판단했습니다.

  • 추후 조지할 내용: 현재 저희 NexERP 규모로서는 해당 위험이 발생할 가능성이 낮긴 해서 현행을 유지하지만, 추후 조치가 가능하다면, @onetomany 삭제 후 관련된 모든 Repository 쿼리를 MemberProjectRepository에서 직접 하도록 하여 필요한 조회 기능을 유지하는 것이 바람직해 보입니다.

💡 고민한 지점

이론의 정석과 실무적 트레이드오프의 균형
참고 자료를 모두 보아도, 사실 Bad 케이스나 장단이 있을 뿐 정석은 없다고 말씀해 주십니다.
모든 프로젝트의 라이프 사이클이 다를 것이고, 그에 따라 각 도메인에 대한 매핑 평가도 달라질 것입니다. 그래서 JPA 표준 설계 방식과 연관관계 지양 전략 사이에서 처음 고민을 하게 되었습니다. 정석이란 존재하지 않기 때문에 본인이 평가한 엔티티 간 라이프 사이클에 따라 매핑을 하는 것이 바람직하다고 생각하게 됐습니다. 앞으로는 모든 엔티티를 그냥 연결하기 보다, 마스터 데이터와 내역 데이터 또한 엄격히 분리해서 설계하여 유연하면서도 단단한 모델을 구축하면 좋을 것 같습니다.

📸 참고자료

김영한 - 자바 ORM 표준 JPA 프로그래밍
제미니 - JPA 연관관계 어떻게 걸까요?


✅ PR 유형

어떤 변경 사항이 있나요?

  • 새로운 기능 추가
  • 코드 리팩토링
  • 버그 수정
  • 코드에 영향을 주지 않는 변경사항(오타 수정, 탭 사이즈 변경, 변수명 변경, git template 수정)
  • 테스트 추가, 테스트 리팩토링
  • 빌드 부분 혹은 패키지 매니저 수정
  • 파일 혹은 폴더명 수정/삭제

✅ PR 체크 리스트

  • PR이 다음 요구 사항을 충족하는지 확인하세요.
  • 리뷰어 설정을 했나요?
  • Lables를 설정했나요?
  • 커밋 메시지 컨벤션에 맞게 작성했습니다.
  • 변경 사항에 대한 테스트를 했습니다.(버그 수정/기능에 대한 테스트)
  • test workflow가 정상적으로 작동했습니다.
  • 병합 위치가 올바른 브랜치인지 확인하셨나요?

@minsubyun1 minsubyun1 requested a review from zldzldzz March 1, 2026 11:39
@minsubyun1 minsubyun1 self-assigned this Mar 1, 2026
@minsubyun1 minsubyun1 added the ♻️ refactor 리팩토링 제안/작업 label Mar 1, 2026
@minsubyun1 minsubyun1 linked an issue Mar 1, 2026 that may be closed by this pull request
2 tasks
@zldzldzz
Copy link
Copy Markdown
Member

zldzldzz commented Mar 8, 2026

해당 PR의 리뷰를 자세히 하기 위해 리뷰가 늦은 점 죄송합니다.
아래는 제가 학습한 내용을 바탕으로 해당 PR의 리뷰를 해보려고 합니다. 말씀처럼 정답은 없는 관점이라 개인적인 의견입니다.

DDD에서 애그리거트 경계를 고려하면 하나의 애그리거트 내부에서는 객체 참조를 허용하되 애그리거트 간에는 직접 객체 참조보다 ID 참조를 우선적으로 고려하는 방식이 일반적으로 권장됩니다.
이렇게 하면 애그리거트 간 결합도를 낮추고 각 경계의 독립성과 캡슐화를 유지할 수 있습니다. 또한 직접 연관관계에 과도하게 의존하지 않게 되어 JPA 실무에서는 지연 로딩 관리 부담, 순환 참조, fetch 전략 복잡도와 같은 문제를 줄이는 데도 도움이 됩니다.

또한 이러한 방식은 향후 모듈 분리나 MSA로 확장할 때도 유리합니다. 애그리거트 간이 객체 연관관계가 아니라 ID 중심으로 연결되어 있으면 서비스 분리 시에도 관계를 비교적 명확하게 유지할 수 있기 때문입니다.

반면 ID 참조를 사용할 경우 DB 차원의 참조 무결성에만 의존하기 어려워지고 잘못된 ID 저장 가능성이나 도메인 검증 누락으로 인한 고아 데이터 발생 가능성도 존재합니다. 따라서 이 경우에는 애플리케이션이 정합성 책임을 더 많이 가져가야 하며 생성/수정 시점의 검증 로직을 더 명확하게 설계해야 합니다.

저는 이러한 장단점을 고려했을 때 결국 핵심은 애그리거트 경계를 어디에 두는가라고 생각합니다. 애그리거트는 하나의 트랜잭션 안에서 원자적으로 변경되어야 하거나 도메인상 항상 함께 유지되어야 하는 규칙을 기준으로 결정해야 합니다. 예를 들어 저희 서비스에서 “물류는 하위 모든 물류 물품이 완료된 후에만 완료 상태가 될 수 있다”는 규칙이 있습니다. 이는 동일한 일관성 경계 안에서 관리되어야 할 불변식 후보라고 볼 수 있습니다.

결과적으로 여러 애그리거트가 함께 참여하는 흐름은 하나의 큰 객체 관계로 묶기보다 애플리케이션 서비스나 오케스트레이션 패턴 등을 통해 협력시키고 애그리거트 내부에서는 객체 관계를 활용해 불변식을 명확하게 표현하는 것이 실수를 줄이고 유지보수성을 높이는 방향이라고 생각합니다.

참고 자료:
애그리거트 모범 사례
인프런 질문과 답변

Copy link
Copy Markdown
Member

@zldzldzz zldzldzz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

당시의 작성할 때의 관계에 대해 깊은 지식 없이 관계를 추가했던 것 같습니다. 죄송합니다.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

♻️ refactor 리팩토링 제안/작업

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[♻️Refactor] 도메인 모델 관계 개선 (@onetomany)

2 participants