이 글을 쓰게 된 배경
최근에 현업에서는 연관관계 매핑을 지양한다는 말을 들었습니다. 저는 처음 백엔드 공부를 시작할 때 JPA부터 시작했기 때문에 당연히 모든 사람들이 연관관계 매핑을 사용한다고 생각했고, 다른 방법에 대해서 생각해본 적이 없었습니다. 지금까지 아무런 의심없이 연관관계 매핑을 사용하고 있었던 제 과거를 반성하는 의미에서 이 글을 쓰며 연관관계 매핑의 트레이드오프에 대해서 알아보겠습니다.
추후에 프로젝트에서 어떤 방법을 채택했는지에 대해서도 적겠습니다.
다룰 내용
- 연관관계 매핑이란
- 연관관계 매핑의 장점
- 연관관계 매핑의 문제점
- 연관관계를 사용하지 않을 때
- 채택 과정
연관관계 매핑이란?
연관관계 매핑이란 객체와 객체 간의 연결이 필요할 때 두 객체간의 관계에 맞게 매핑해 주는 것을 말합니다. 객체지향에서는 두 객체간의 관계를 객체 간 참조로 표현합니다. 하지만 데이터베이스에서는 두 객체간의 관계를 외래 키를 사용해서 표현합니다.
이 두가지 개념의 충돌을 피하고자 주로 ORM을 사용하여 해결합니다.
연관관계 매핑을 지원하는 ORM은 여러가지가 있지만 저는 JPA를 사용하기 때문에 JPA 기준으로 설명하겠습니다.
연관관계 매핑의 장점
연관관계 매핑은 ORM의 핵심적인 기능 중 하나입니다. 연관 관계 매핑의 장점은 여러가지가 있지만 핵심적인 장점만 뽑자면 다음과 같습니다.
객체지향적인 개발
연관관계 매핑을 사용하면 객체 간의 참조를 통해서 데이터베이스의 데이터에 접근 할 수 있습니다.
SQL을 사용하지 않고 데이터베이스에 데이터에 접근하기 때문에 코드 상에서 객체를 다루는 것처럼 사용할 수 있습니다.
효율적인 개발
ORM을 사용하면 연관관계 매핑이 자동으로 처리되기 때문에 복잡한 JOIN 쿼리를 작성하는데 드는 시간을 줄일 수 있습니다.
데이터베이스 의존성이 떨어지기 때문에 서비스단에서의 로직에 더 신경쓸 수 있습니다.
유지보수성 증가
객체 간의 관계를 매핑해 놓으면 데이터베이스 테이블 구조의 변경이 있어도 객체와의 매핑만 수정하면 됩니다.
JPA가 자동으로 관리해 주기 때문에 코드상에서의 수정이 거의 없습니다.
성능
JPA는 연관 엔티티를 1차 캐시에서 관리하므로, 동일한 엔티티를 다시 조회하지 않아 성능이 향상될 수 있습니다.
Lazy Loading이나 fetch join을 사용하면 데이터를 필요한 시점에 가져오거나, JOIN FETCH로 한번에 가져올 수 있습니다.
JPA가 쿼리를 최적화하여 필요에 따라 적절히 실행합니다.
연관관계 매핑의 문제점
불필요한 조인
연관된 엔티티를 로드하려면 내부적으로 JOIN 쿼리가 실행되며, 데이터가 많아질수록 성능이 저하될 수 있습니다.
다대일(@ManyToOne), 일대다(@OneToMany) 등의 관계가 많을수록 JOIN이 여러 번 중첩될 수 있습니다.
특히 복잡한 관계일수록 JOIN의 깊이가 증가해 성능 저하를 초래할 수 있습니다.
N+1 문제
Lazy Loading 설정이 잘못되면 다수의 SELECT 쿼리가 발생하여 성능 문제가 생길 수 있습니다.
팀에 팀원이 여러명 있을 때 팀(1)을 조회하면 팀에 속해있는 팀원(N)의 정보를 조회하기 위해서 추가 쿼리가 발생합니다.
각 쿼리는 데이터베이스 커넥션을 사용합니다. 이 커넥션의 비용이 크기 때문에 N개의 커넥션이 생기면 성능이 크게 저하됩니다.
트랜잭션 관리 문제
연관관계 매핑된 엔티티를 사용할 때 트랜잭션 범위 밖에서 접근하면 지연 로딩이 동작하지 않아 오류가 발생할 수 있습니다.
JPA는 트랜잭션 내에서만 영속성 컨텍스트 기능을 지원하기 때문에 변경 감지, 지연 로딩, 캐싱 등의 기능이 필요하다면 트랜잭션 내에서 데이터를 조회해야 합니다.
연관관계 매핑을 사용하지 않을 때
연관관계 매핑을 사용하지 않을 때는 key값을 가지고 데이터를 직접 조회하는 방법을 사용하면 됩니다.
@Query("SELECT c FROM CartItem c WHERE c.cartKey IN :cartItemKeys")
List<CartItem> findAllByCartItemKeys(@Param("cartItemKeys") List<Long> cartItemKeys);
위 코드는 key값을 가지고 내 장바구니에 있는 아이템들을 조회하는 쿼리입니다.
이 작업을 위해서 해당 유저가 가지고 있는 cartItemKey 리스트를 알고있어야 합니다. (로직이 복잡해지만 쓸모없는 쿼리가 안나감)
연관관계 매핑을 사용하지 않을 때의 장단점
장점 | 엔티티를 독립적으로 관리 | 테이블 간의 관계가 느슨해져 칼럼 변경이 비교적 자유로워짐 | N+1이나 불필요한 조인이 발생하지 않음 |
단점 | 비즈니스 로직의 복잡도가 증가 | 무결성 보장이 어려움(key값으로만 연결되어 있기 때문에 안전장치가 없음) | 객체지향적인 설계 불가 |
채택 과정
중간에 참여한 프로젝트가 연관관계 매핑을 사용하지 않고 키 값을 사용하는 방식으로 되어 있었습니다.
저는 키값을 사용하는 것은 좋지만 연관관계 매핑을 하지 않고 키 값을 사용해야만 하는 뚜렷한 근거가 있었으면 좋겠다고 생각해서 팀원에게 사용 이유를 물어본 결과 연관관계 매핑을 사용하면 외래키가 많아져서 복잡해진다는 이유였습니다.
물론 테이블의 관계가 복잡해 질 수 있지만 비즈니스 로직이 더 깔끔해 지기 때문에 이건 합당한 이유가 아니라고 생각했습니다.
저는 연관관계 매핑을 사용하지 않았을 때 생길 수 있는 문제를 팀원에게 설명하기 위해서 대표적으로 객체지향이 깨지는 예시를 팀원에게 설명해야 했습니다.
다음은 제가 생각한 예시입니다.
주문 테이블이 있고 주문이 상품과 수량 총 금액 칼럼을 가지고 있는 경우입니다.
먼저 키 값을 사용할 때는 상품의 키값을 가져와서 저장하기 때문에 서비스단에서 주문이 가지고 있는 키 값을 통해 상품을 찾는 쿼리를 날리고 그 상품의 가격과 수량을 연계해서 총 금액을 찾아야 합니다. 일단 이렇게 되면 주문은 총 금액을 얻기 위해서 무조건 서비스단에서 총 금액을 계산해야하는 로직을 수행해야 하기 때문에 비즈니스 로직이 길어집니다. 그리고 상품의 가격이 변경되면 항상 수동으로 총 금액을 바꿔줘야 합니다. 상품이 바꼈는데 주문도 수정해야 하는 경우가 생기기 때문에 객체지향의 캡슐화가 깨진다고 할 수 있습니다.
개인적으로는 결국 크게 보면 연관관계 매핑을 사용하는 것은 객체지향 vs 성능 중에 하나를 선택하는 것이라고 생각했습니다. 당연히 뒤따라 오는 부가적인 장단점들이 있을 것입니다.
채택 결과
'Spring' 카테고리의 다른 글
스프링 부트에서 예외와 처리 방법 (2) | 2024.12.30 |
---|---|
Spring Security 필터에서 발생한 인증/인가 예외 처리하는 방법 (5) | 2024.12.30 |
커넥션 풀 vs 복잡한 로직 시간 비용 (4) | 2024.12.22 |
트랜잭션 by JPA (6) | 2024.12.17 |
Spring Boot에서 Querydsl 사용 (2) | 2024.12.04 |