inflearn logo
강의

강의

N
챌린지

챌린지

멘토링

멘토링

N
클립

클립

로드맵

로드맵

지식공유

토비의 클린 스프링 - 도메인 모델 패턴과 헥사고날 아키텍처 Part 1

N+1 관련해서 질문있습니다.

2

Jubuseong

작성한 질문수 1

0

안녕하세요. 우선 좋은 강의 제작해주신 토비님께 항상 감사하고 있어요. 이제 배운지 1년된 왕초보입니당..

혼자 배워보면서 개인 프로젝트를 만들고 있는데 JPA를 사용하고 있어요.

제가 궁금한 것이... N+1 관련한 문제입니다. 아 일단 프로젝트 주제는 복식부기 가계부에요.

 

@Entity
...
public class Journal extends BaseEntity {
  @ManyToOne(fetch = FetchType.LAZY)
  @JoinColumn(name = "ledger_id", nullable = false, updatable = false)
  @OnDelete(action = OnDeleteAction.CASCADE)
  private Ledger ledger;

  ...
  @OneToMany(mappedBy = "journal", fetch = FetchType.LAZY, cascade = CascadeType.ALL,
  orphanRemoval = true)
  private List<EntryLine> entries = new ArrayList<>(2);

  ...
  public EntryLine getEntryLine(EntrySide side) {
    switch (side) {
      case CREDIT : this.entries.stream().filter(line -> line.isCredit()).findFirst()
                    .orElseThrow(...);
      case DEBIT  : this.entries.stream().filter(line -> line.isDebit()).findFirst()
                    .orElseThrow(...);
      default : throw new ...
    }
  }

  ...
  // Service에서 저장되기 전에 호출
  public void validateSavable() {
    ...
    validateJournalSave();
  }
  
  private void validateJournalSave() {
    AccountType debit = getEntryLine(EntrySide.DEBIT).getAccountType();
    AccountType credit = getEntryLine(EntrySide.CREDIT).getAccountType();

    if(!this.transactionType.isValidPlacement(debit, credit)) {
      throw new ...
    }
  }
}

Journal Class에서 EntryLine List에 접근하고 있어요.

 

그리고 EntryLine Class는 이렇게 생겼어요.

@Entity
...
public class EntryLine extends BaseEntity {
  @ManyToOne(fetch = FetchType.LAZY)
  @JoinColumn(name = "journal_id", nullable = false, updatable = false)
  private Journal journal;

  @ManyToOne(fetch = FetchType.LAZY)
  @JoinColumn(name = "account_id", nullable = false)
  private Account account;

  ...
  // private package 접근제어자 사용
  // Account는 Category를 참조중이에요.
  AccountType getAccountType() {
    return this.account.getCategory().getAccountType();
  }
}

 

거래가 저장되기 전에 Journal : validateJournalSave() 에서 this.transactionType에 따라 차변과 대변에 올바르게 위치하고 있는지 검사한 후 저장하고 있는데 이것을 생성과 수정할 때 두 곳에서 사용하고 있어요.

 

Ledger에 5개 Category가 있고, Account는 그 Category를 참조하고 Category에서만 AccountType이 있어요.

Journal이 각 EntryLine의 AccountType을 얻기 위해

Journal -> EntryLine -> Account -> Category -> getAccountType() 이렇게 흘러가네요.

이렇게 접근해도 설계상 괜찮은걸까요?

 

Journal을 저장할때는 @Query 사용해서 Fetch Join으로 필요한 Account를 가져오고 있는 상황이에요.

Journal이라는 엔티티가 비즈니스 로직 수행을 위해서 다른 엔티티의 필드까지 깊게 참조?? 가져오도록 설계하는게 옳은건지 모르겠어요.

java spring spring-boot jpa 리팩터링 ddd

답변 1

0

인프런 AI 인턴

안녕하세요, 인프런 AI 인턴입니다. Jubuseong님!

프로젝트에서 N+1 문제와 관련된 질문을 주셨고, 이를 해결하기 위한 접근 방식에 대한 조언을 드리겠습니다.

N+1 문제에 대한 이해

N+1 문제는 주로 JPA에서 발생하며, 하나의 쿼리를 통해 기본 엔티티를 로드한 후 관련된 여러 엔티티를 각각의 쿼리로 로드하는 시나리오입니다. 이는 많게는 N+1개의 쿼리를 실행하게 되어 비효율적일 수 있습니다.

현재 설계에서의 문제점

Journal 엔티티가 EntryLine 리스트를 통해 AccountType 정보를 가져오기 위해 여러 엔티티를 탐색하는 부분은 깊은 객체 탐색을 유발할 수 있습니다. 특히, @ManyToOne(fetch = FetchType.LAZY) 설정으로 인해 각 EntryLine 객체별로 AccountCategory에 접근할 때 추가적인 쿼리가 발생하여 N+1 문제를 야기할 수 있습니다.

해결책 제안

  1. Fetch Join 사용: 이미 사용 중이신 @Query 및 Fetch Join을 통해 Journal과 관련된 EntryLine 및 그 이상의 계층(Ledger, Account, Category)을 한번의 쿼리로 가져올 수 있습니다. 이를 통해 N+1 문제를 해결할 수 있으며, 관련 객체가 모두 초기화된 상태로 작업을 수행할 수 있습니다.

  2. DTO 사용: 깊은 탐색이 필요하지 않다면, 관련 데이터만 포함한 DTO를 사용하는 것도 좋은 방법입니다. DTO는 필요한 데이터만 포함하므로 성능 상 이점이 있으며, 필요 없는 객체 탐색을 예방할 수 있습니다.

  3. 쿼리 성능 모니터링 및 튜닝: 툴을 사용하여 JPA 쿼리가 생성하는 실제 SQL을 모니터링하고, 성능 상 병목이 없는지 확인하세요. 쿼리를 최적화하거나 인덱스를 추가해 성능을 개선할 수 있습니다.

추가적인 리소스

저는 질문자님의 이해를 돕고자 지식공유자님의 기존 답변을 참고하여 유사한 답변 링크를 추천해드리고 있습니다.
현재 베타 기능으로 답변이 만족스럽지 않을 수 있는 점 양해 부탁드립니다. 🙏
추가적으로 궁금한 점이 있으시면, 이어서 질문해 주세요. 곧 지식공유자께서 답변해 주실 것입니다.

도메인 모델에서 관계와 규칙을 구분하는 방법

0

55

2

헥사고날 아키텍처에서의 배치, 시큐리티, 비동기 이벤트 처리는 어떻게 하나요?

0

126

2

어댑터에서 도메인에 직접 의존하는 경우에 대해

0

127

2

Member 도메인이 PasswordEncoder를 받는 구조 질문 있습니다.

0

114

2

MemberService와 EmailSender 책임 분리에 대한 질문

0

107

2

NonNullApi를 NullMarked로 대체하라고 합니다.

0

130

2

39. 문서와 코드 다듬기 updateInfo 테스트 질문 있습니다.

0

77

2

Repository Adapter 설계에 대해 피드백을 부탁드립니다

0

111

2

헥사고날 part2 강의 출시 예정일 문의 드립니다.

0

255

2

PT 문의사항

0

101

1

초기 어플리케이션 구동 시 compose.yml 파싱 오류

0

151

2

애플리케이션의 JPA 리턴과 도메인 모델

0

129

2

애그리거트 루트의 하위 도메인들의 depth가 깊어질 때 문의

0

136

2

페이징 처리를 해야한다면 어떻게 해야할까요?

0

193

2

애그리거트의 repository

0

119

2

Domain Expert가 정확히 어떤 역할을 하는 사람인가요?

0

236

1

회원 애플리케이션 서비스 테스트 (1)

0

107

2

정적 팩토리 메서드 관련 질문드립니다!

0

110

2

spotbug + @NonNullApi 로만 Null 방어가 될까요?

0

129

2

required 포트에 관해서

0

93

2

혹시 다음 편은 언제쯤 오픈할까요?

0

168

2

서비스 단위 테스트 코드 작성

0

98

2

domain 모듈에 entity를 정의한다고 했을때

0

101

2

여러 엔티티의 조합으로 리포트를 제공해야할 때

0

80

2