그럴 거면 JPA를 왜 써? : 내가 연관 관계를 대하는 방법 본문

그럴 거면 JPA를 왜 써? : 내가 연관 관계를 대하는 방법

JinHwan Kim 2024. 6. 1. 15:12

연관 관계를 피한다

난 JPA, ORM을 사용한다고 연관관계를 반드시 사용하진 않는다.

 

첫 번째는 엔티티 간 연관 관계가 객체 간 의존도를 크게 해서이다.

의존성이 커져 프로젝트가 통짜가 되어버리고, 조립이 아닌 나열식 전개로 점점 전환되는 프로젝트 코드를 경험한 적 있다.

더 작게는 테스트 코드가 복잡해지고, 개발 순서가 엮여 병렬로 작업을 처리하기 까다로웠다.

거기엔 엔티티의 연관관계가 큰 몫을 한다고 생각한다.

 

두 번째는 의도하지 않은 쿼리가 발생하거나, 직관적이지 않은 코드가 생기기 좋아서이다.

이를 테면 연관관계를 위한 ORM의 동작으로 코드 레벨에서는 눈에 보이지 않는 쿼리가 발생할 수 있다.

코드를 짤 때도 이를 생각해야 하기에 단순한 쿼리조차도 ORM 동작에 한번 더 고민하는 경우가 나오기 좋다.

특히 teamFromDB.member().name().value() 처럼 체이닝이 계속되는 것도 답답하다.

 

그보다는 관계가 필요한 테이블의 PK 값을 FK로 직접 필드에 두는 꼴을 더 좋아한다.

만약 "정말 A를 사용할 땐 B가 필요해"라고 한다면 A와 B가 결국 같은 엔티티여야 하는 것은 아닌지 고민한다.

 

그럴 거면 JPA를 왜 써?

나는 JPA를 ORM, RDBMS와 객체지향 간 패러다임 차이 극복을 위해서만 이점이 있다고 생각하지 않는다.

영속성 컨텍스트의 1차 캐시와 지연 쓰기로 쿼리가 줄고, 엔티티의 동일성 보장으로 혼란을 피할 수 있다.

또 Spring boot 와도 잘 어울려서, CRUD와 검색, 페이징, 정렬 등 거의 대다수의 기본 쿼리가 메서드 이름만으로 자동 생성된다.

 

무엇보다 관계를 두는 경우도 분명히 있다.

ORM과 연관 관계에만 갇히지 말고, JPA의 이점을 뽑아 사용하자가 포인트였다.

 

그럼에도 관계가 필요할 때

앞서 연관 관계를 우선으로 생각하지 않지만, 그러면서도 관계를 두는 것이 더 편한 명확한 상황도 있다.

그럴 땐 @OneToMany가 아닌 @ManyToOne 을 먼저 생각한다.

@ManyToMany 역시 중간 매핑 테이블로 나눠, @ManyToOne의 두 테이블로 쪼갠다.

 

@OneToMany는 RDBMS의 FK 위치와 다르게 코드를 구성한다.

그래서 의도하지 않은 쿼리, 비효율적인 코드가 발생하기 좋다.

 

예를 들어 JPA에선 한 엔티티의 여러 @OneToMany 필드는 FetchJoin이 안된다.

또 단일 필드라 하더라도 페이징이 불가능하다는 것을 인지할 수 있는 지식이 필요하다.

특히 연관관계의 '주'와 Cascade를 고민해야 하는 것은 번거로움을 넘어 위험하다고까지 느낀다.

 

연관관계를 두었던 경험

말로는 모호할 것 같아, 내가 연관 관계를 두었던 경험을 소개한다.

코어 데이터에 여러 날짜를 저장하고, 날짜로 데이터를 검색해야 하는 경우가 있었다.

검색 성능이 매우 중요한 경우였기에 인덱싱이 필요했고, 한 컬럼에 날짜를 여러 개 저장해 두는 역정규화는 불가능했다.

 

코어 데이터와 날짜 정보를 서로 다른 테이블로 분리하여 저장하고, 날짜 정보에 인덱스를 걸어 검색 속도를 높였다.

이때 날짜 데이터에는 코어 데이터가 반드시 필요했지만, 코어 데이터에서 날짜 정보는 반드시 필요한 정보가 아니었다.

이런 상황에서 날짜 엔티티에 코어 데이터를 @ManyToOne으로 연관관계를 두었다.

날짜 엔티티를 사용하려면 코어 데이터가 반드시 필요하기에, 쿼리도, 코드 전개도 관계를 두는 게 더 명확하다는 판단이었다.

 

사용할 데이터만, 필요한 쿼리만 

이렇게 연관 관계가 있을 때는 가급적 Lazy loading으로 데이터를 사용하는 경우에서만 쿼리 할 수 있도록 한다.

이때 한 쿼리로 관계 데이터 조회를 위한 쿼리가 여러번 발생하는 N+1 문제가 발생할 수 있겠다.

그럴 땐 Fetch join을 사용하는 메서드를 만들어, 엔티티 단위의 Join 문 한 쿼리로 데이터를 로드할 수 있도록 한다.

혹은 @BatchSize를 지정해서 WHERE IN으로 여러 엔티티를 한 쿼리로 가져오는 것도 방법이다.

 

관계 데이터가 불필요한 상황, Lazy loading이 더 효율적인 상황을 위한 Join 없는 메서드도 만들어 둔다.

코드를 짜는 사람이 데이터가 필요할 때 쿼리하는 것이 효율적 일지, 미리 한 번에 쿼리 하는 것이 효율적인지 판단해야 한다.

팀의 규칙이 명확해서, 메서드 네이밍만 보고도 '아 이건 fetch join이 한번에 처리되겠구나'가 되면 너무 좋다.

 

동적 쿼리가 필요한 상황에선

사실 가급적이면 단순한 로직과 명확한 쿼리를 더 좋아한다.

경우에 따라 1개 쿼리를 복잡하게 짜는 것보다 3개 쿼리를 깔끔하게 짜는 것이 관리하기 좋을 때도 많다고 느낀다.

차라리 비효율적으로 쿼리하고 애플리케이션에서 조합하는게 나을 때도 있었다.

 

가급적 JpaRepository와 @Query 선으로 끝내려고 하지만, 그럼에도 동적 쿼리를 피할 수 없는 상황은 꼭 있었던 것 같다.

그럴 땐 QueryDSL을 사용한다.

CriteriaAPI가 더 편한 적도 있었고, 일부러 QueryDSL를 의존성에 포함하지 않을 때도 있었다.

근데 요즘은 QueryDSL이 더 보편적인 것 같고, 다른 동료의 러닝커브나 반발감이 적지 않을까 싶다.

 

정리

지금 시점에서의 내 생각을 정리해보았다.

처음 JPA를 공부할 땐 문법을 달달 외우고 억지로 연관 관계를 맺으며 연습했던 것도 같은데, 요즘은 생각이 많이 변했다.

또 다른 팀원들과 함께하고 새로운 경험을 만나가며 계속 더 좋은 방향을 고민할 수 있었으면 좋겠다.

 

 

 

Comments