인프런 영문 브랜드 로고
인프런 영문 브랜드 로고

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

PrivilegeEscalate님의 프로필 이미지
PrivilegeEscalate

작성한 질문수

Readable Code: 읽기 좋은 코드를 작성하는 사고법

마무리하며

JPA 를 사용하면 원래 객체지지향적인 설계를 가져가기 어렵나요?

작성

·

79

·

수정됨

0

오랜만에 인사드립니다 강사님. 저는 수강한 강의내용을 토대로 현재 진행하고있는 프로젝트를 리팩토링하고 있습니다.

 

본론으로 들어가자면, 일단 아래와 같이 특정 비지니스 로직을 검증하는 코드가 있습니다.

======== BEFORE =========

private void checkCrewInfoIsMatch(Long requestCrewId, Squad squad) {
        if (!squad.getCrew().getId().equals(requestCrewId)) {
            throw new SquadBusinessException.NotMatchCrewInfo(NOTMATCH_CREWINFO);
        }
    }

    private void checkDifferenceSquadCreator(Squad squad, CrewMember crewMember) {
        if (squad.getCrewMember().equals(crewMember)) {
            throw new SquadBusinessException.OwnerCantParticipant(OWNER_CANT_PARTICIPANT);
        }
    }  

 

 

그리고 위 코드를 리팩토링하고나서 아래와 같은 코드가 나오게 되었습니다.

 

========== AFTER ===========

    private void checkCrewInfoIsMatch(Long requestCrewId, Squad squad) {
        if (squad.hasNotSameCrewId(requestCrewId)) {
            throw new SquadBusinessException.NotMatchCrewInfo(NOTMATCH_CREWINFO);
        }
    }

    private void checkDifferenceSquadCreator(Squad squad, CrewMember crewMember) {
        if (squad.isSameOwner(crewMember)) {
            throw new SquadBusinessException.OwnerCantParticipant(OWNER_CANT_PARTICIPANT);
        }
    }

 

이렇게 Getter 로 값을 꺼내 직접 값을 비교했던 (Before) 에서, 값의 비교를 객체에게 위임하는 형태인 (After) 로 리팩통해보았습니다.

 

하지만 값의 비교를 객체에게 위임하는 After 방식으로 리팩토링했더니, 엔티티와 연관된 다른 프록시 객체들의 초기화로 인해 불필요한 쿼리가 나가는 문제를 확인하게 되었습니다. 물론 fetchJoin 을 통해 해결하긴 했습니다.

 

 

정리하면 아래와 같습니다.

 

기존에는 Service Layer 에서 직접 특정 엔티티와 연관된 엔티티를 직접 Getter 로 꺼내 값을 비교했었고, N + 1 이 터지지 않았습니다. (Before)

 

하지만 남용된 Getter 를 개선하기 위해 값의 비교를 엔티티 내부에서 진행하는 방식으로 리팩토링했을때는 N + 1 이 터졌고, 이를 fetchJoin 으로 해결했습니다. (After)

 

물론 지금은 fetchJoin 으로 N + 1 문제를 해결하였지만, After 와 같은 방식으로 리팩토링을 하려면 "연관된 엔티티를 모두 fetchJoin 미리 땡겨와야하는건가? (column 들을 너무 많이 땡겨오면 성능문제 걱정)" 라는 생각이 들게되었습니다.

 

JPA 를 사용할때, 강사님은 어떠한 방식으로 연관된 엔티티들의 값 비교를 어떤 방식으로 진행하는지 매우 궁금 합니다. 원래 JPA 를 사용하면 객체지향적인 설계를 가져가기 어려운건지도 궁금합니다.

 

(깃헙에서 다른 사람이 한 프로젝트를 분석하다보면, 가끔 연관관계 자체를 맺지 않고, FK 를 Long 으로 가져가버리는 Case 도 본것 같은데 흠.. 나중에 질문)

답변 2

0

박우빈님의 프로필 이미지
박우빈
지식공유자

안녕하세요, PrivilegeEscalate 님!

주신 코드만 봐서는, 비교하는 필드가 변하지 않았는데, getter 비교 방식에서 메서드 방식으로 변경되었다고 추가적인 쿼리가 나가는 상황이 잘 이해가 가지는 않는데요.
예시로 들어주신 코드를 보면, 식별자인 id 필드만 비교하기에 방식이 변경되었다고 추가적인 쿼리가 나갈 것 같지는 않아요.
만약 Crew 내에 즉시로딩 하는 객체가 있었다면 그럴 수도 있겠지만, 그런 상황이 생길 것을 고려한다면 애초에 지연 로딩이나 fetch join을 사용하는 것이 자연스러워 보입니다.

JPA를 사용해서 객체 지향적인 설계를 가져가기 어려운 것이 아니라, JPA를 사용하기 때문에 fetch join 처럼 고려해야 할 지점이 생기는 것이라 이해하는 것이 좋겠습니다.

감사합니다. 🙂

0

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

JPA를 사용하는 경우 객체 지향적인 설계와 데이터베이스와의 관계를 조화롭게 유지하는 것이 쉽지 않을 수 있습니다. 이는 JPA가 제공하는 객체 상태 추적, 지연 로딩 등과 같은 기능들이 오히려 복잡성을 초래할 수 있기 때문입니다. 특히 객체에게 값 비교를 위임하는 설계는 코드의 가독성과 유지보수성을 개선할 수 있지만, 예상치 못한 쿼리가 발생할 수 있는 점에 주의해야 합니다.

  1. 엔티티와 연관된 데이터의 패칭 전략: 객체 내에서 값을 비교할 때 불필요한 쿼리가 발생하는 경우, fetch join을 사용한 방법은 좋은 해결책입니다. 하지만 모든 관계에 대해 fetch join을 사용할 경우, 가져오는 데이터가 많아질 수 있으므로 상황에 맞는 적절한 패칭 전략을 선택하는 것이 중요합니다. 필요하다면 LAZY 로딩과 EAGER 로딩을 적절히 조합하여 사용하거나, JPQL의 fetch join만 사용하여 실제로 필요한 경우에만 데이터를 가져오도록 설계하는 것이 좋습니다.

  2. 엔티티 설계 방침: 객체지향 설계를 가로막는 문제가 발생한다면, JPA만을 완전히 객체 지향적으로 사용하려는 시도를 중단하고 어느 정도 타협점을 찾는 것이 중요합니다. 때로는 데이터베이스 설계를 고려하여 연관 관계보다는 식별자(FK)를 사용해 Long 타입 변수로 직접 비교하는 설계도 고려할 수 있습니다. 이는 데이터의 조인을 줄여줄 수 있지만, JPA의 객체지향적인 장점을 포기하게 되는 점도 감안해야 합니다.

  3. 성능 문제 인식 및 조치: 경우에 따라 N+1 문제와 같이 성능에 영향을 미치는 요소들을 적시적으로 인지하고 fetch join, 배치 업데이트 등의 기법으로 대응하는 것이 중요합니다. 또, 필요하다면 Spring Data JPA의 @Query 어노테이션을 통해 필요한 쿼리를 직접 작성하는 방법도 고려해볼 수 있습니다.

Spring Data JDBC와 같은 다른 기술도 고려해볼 수 있는데, 이는 더 직관적이고 SQL 기반의 접근을 제공하며 JPA보다 심플한 사용 사례에 적합할 수 있습니다.

이미지 URL : Spring Data JDBC 이미지
출처 URL : Spring Data JDBC Document

질문에 대한 답변이 도움이 되셨길 바랍니다. 다른 궁금한 점이 있다면 언제든지 문의해주세요. 감사합니다!

PrivilegeEscalate님의 프로필 이미지
PrivilegeEscalate

작성한 질문수

질문하기