인프런 커뮤니티 질문&답변

권기준님의 프로필 이미지

작성한 질문수

Java/Spring 테스트를 추가하고 싶은 개발자들의 오답노트

마무리와 부록

n+1질문입니다!

23.11.29 09:40 작성

·

685

0

안녕하세요 강의 잘듣고 있는 수강생입니다.

헥사고날 아키텍처를 이번 토이프로젝트에 적용하면서

강의에서 알려주신대로 설계를 이렇게 유연하게 변경하면

n+1문제도 해결 할 수 있다고하셨는데

예를들면

MemberRepository impl에서

 

멤버 아답터만 주입받고있는상황에서

 

팀 엔티티랑 연관관계가있는 상황에서

N+1 문제를 해결하려면

MemberRepository impl 에서 memberRepository말고

TeamRepository도 주입받아서 한번에 다 불러와서 도메인 엔티티에 저장해야하나요?

 

아니면 서비스 계층에서 각각 레파지토리에서 불러온다음

MemberRepository에서 넘겨준다음 도메인 모델을 리턴할때 넣어줘야 하나요

 

n+1문제를 서비스계층에서 결합할지 레포지토리 계층에서 결합할지 궁금해서 질문드립니다.

 

답변 2

1

김우근님의 프로필 이미지
김우근
지식공유자

2024. 01. 30. 12:59

안녕하세요. 근래에 책을 집필할 기회가 생겨 그쪽에 힘을 실어주다 보니 다른 일에 신경 쓰지 못했습니다. 답변이 늦어 죄송합니다. 다만 해당 강의는 공식적으로 질의응답을 제공하지 않는 강의였다는 점을 이유로 늦어진 부분에 대해 양해 부탁드립니다.

Member Team 관계에서 팀과 팀원을 나타내는 정보가 아래와 같다고 가정합시다.

@Getter
@Builder
class Team {

    private long id;
    private String name;
}

@Getter
@Builder
class Member {

    private long id;
    private String name;
    private Team myTeam;
}

@Data
@NoArgsConstructor
@Entity(name = "team")
class TeamJpaEntity {

    @Id
    private String id;
    @Column
    private String name;

    public Team toModel() { ... }
}

@Data
@NoArgsConstructor
@Entity(name = "member")
class MemberJpaEntity {

    @Id
    private String id;
    @Column
    private String name;
    @ManyToOne
    @JoinColumn(name = "my_team_id")
    private TeamJpaEntity myTeam;

    public Member toModel() { ... }
}

이때 팀 정보와 팀원 정보를 같이 있는 도메인 객체인 TeamWithMembers 라는 도메인이 있다 가정합시다. TeamWithMembers은 다음과 같습니다.

class TeamWithMembers {

    private long id;
    private String name;
    private List<Member> members;
}

그리고 TeamWtihMembers의 JpaEntity는 없습니다. 그러면 이때 TeamWithMember를 어떻게 만들 수 있을지 고민해 봅시다.

방법.1 서비스에서 Team 정보, Member 정보를 Repository로부터 모두 불러와 TeamWithMembers 도메인을 만들어 사용한다.

이는 즉 아래와 같은 코드를 만들겠다는 의미입니다.

class MyService {

    // 의존성 주입되는 코드 생략

    public void doSomething() {
        Team team = teamRepository.getById(teamId);
        List<Member> members = memberRepository.findByTeamId(teamId);
        TeamWithMember teamWithMember = TeamWithMember.builder()
            .id(team.getId())
            .name(team.getName())
            .members(members)
            .build();
        // ...
    }
}

이 방법을 사용하면 트랜잭션 스크립트 같은 코드가 만들어질 수 있습니다. 그것 외에는 특별히 문제가 될 만한 점은 보이지 않습니다. 이러한 역할을 수행하는 것이 서비스의 역할인 것인지는 약간 의문이 들긴 하나 그렇다고 또 완전히 틀린 역할은 아닙니다. 추가로 염려되는 점이라면 코드를 이렇게 작성하다 보면 결국 TeamWithMembers를 만들어주는 역할을 전문적으로 하는 서비스가 만들어 질 수 있다는 것입니다.

방법.2 리포지토리에서 Team 정보, Member 정보를 Repository로부터 모두 불러와 TeamWithMembers 도메인을 만들어 사용한다.

이는 즉 아래와 같은 코드를 만들겠다는 의미입니다.

class TeamWithMembersRepositoryImpl implements TeamWithMembersRepository {

    // 의존성 주입되는 코드 생략

    public TeamWithMembers getById(long teamId) {
        Team team = teamJpaRepository.getById(teamId);
        List<Member> members = memberJpaRepository.findByTeamId(teamId);
        return TeamWithMember.builder()
            .id(team.getId())
            .name(team.getName())
            .members(members)
            .build();
    }
}

크게 문제없어 보입니다.

종합하면 이야기해 주신 상황은 Repository의 역할로 보입니다. 왜냐하면 Repository의 역할은 도메인 데이터를 저장하고 불러오는 것인데, TeamWithMembersRepositoryImpl은 그 역할에 정확히 부합한 일을 수행하고 있기 때문입니다.

사실 이 문제는 의존성 역전만 제대로 돼있다면 방법.1로 개발하던 방법.2로 개발하던 크게 상관이 없는 문제입니다. 그래서 개발자마다 해석이 다를 수 있고 저는 방법.2가 맞다 생각하지만, 누군가는 방법.1이 맞다 생각할 수도 있습니다.

헷갈린다면 어느날 갑자기 Jpa가 갑자기 사라진다고 생각해 보길 바랍니다. 그리고 “Jpa가 아닌 JdbcTemplate으로 대체해야 할 때 Domain, Service 코드를 안 건드려도 되는가?”를 고민해 보시길 바랍니다. 그리고 이 상황에서 방법.1이든 방법.2든 모두 안전합니다.

답변에 도움이 됐길 바랍니다.

0

인프런 AI 인턴님의 프로필 이미지

2023. 11. 30. 09:46

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

N+1 문제는 특정 엔티티를 조회할 때 연관된 엔티티를 함께 자주 접근하는 상황에서 발생합니다. 이를 해결하기 위해선 여러 방법이 있으며, 디자인 패턴에 관계 없이 적용할 수 있습니다.

  1. fetch join을 사용하여 연관 엔티티를 즉시 로딩: 이는 JPQL이나 @EntityGraph를 통해 구현할 수 있습니다.
  2. batch size를 설정하여 연관 엔티티를 지연 로딩할 때 N이 아닌 설정된 size 만큼의 쿼리로 데이터를 가져옵니다.

MemberRepository에서 Team 엔티티의 데이터를 처리해야 하는 경우, 서비스 계층보다는 리포지토리 계층에서 fetch join 등을 통해 데이터를 함께 로드하는 것이 객체의 책임과 역할을 고려했을 때 자연스럽습니다. 그러나 실제로 어디에서 결합하느냐는 프로젝트의 구조나 상황에 따라 달라질 수 있으며, 모든 상황에서 최적의 방법이 있는 것은 아닙니다.

리포지토리 계층에서 처리하여 도메인 엔티티에 저장하는 방식은 리포지토리 계층의 복잡성을 증가시키지만, 트랜잭션 내에서 데이터의 일관성을 유지할 수 있다는 장점이 있습니다. 반면, 서비스 계층에서 각각의 레포지토리를 호출하여 결합하면 불필요한 비즈니스 로직이 리포지토리에 침투하는 것을 방지하고 처음부터 쿼리의 목적과 범위를 명확히 할 수 있으나, 복잡한 서비스 메소드로 이어질 수 있습니다.

결국, 헥사고날 아키텍처에서 중요한 것은 유연성입니다. 레포지토리에서 직접 데이터를 결합하여 반환하는 것과 서비스 계층에서 결합하는 것 중 어떤 것이 더 유지보수하기 좋고, 시스템의 성능에 더 적합한지에 따라 결정하는 것이 좋습니다.

추가적인 학습을 원하시면 Spring Data JPA 문서에 자세한 설명이 있으니 참고하시기 바랍니다.