DEV/Architecture

헥사고날 아키텍처 시리즈 8. 영속성 계층에 ORM 적용하기

행운개발자 2024. 3. 18. 11:40
728x90

객체 중심의 개발의 문제점

애플리케이션이 성장하면서 그 내부의 복잡도는 점점 커집니다. 개발자들이 도메인 영역을 객체지향 중심적으로 잘 구성한다고 하더라도, 복잡한 객체가 될수록 객체의 행위를 데이터베이스에 저장하는 과정도 함께 복잡해집니다. 객체지향은 행동을 중심에 두고, 관계형 데이터베이스는 데이터를 중심에 두기 때문에 이 둘 사이의 불일치가 존재합니다. 유연하고 확장 가능하도록 객체지향 설계를 향해 나아갈수록 데이터 중심의 데이터베이스와 거리가 멀어집니다. 추상화, 캡슐화, 정보은닉, 상속, 다형성 등 애플리케이션 개발에서 사용되는 다양한 장치들이 데이터베이스에는 정확하게 매핑되는 개념이 없기 때문입니다.

객체를 관계형 데이터베이스에 저장하고 조회하는 과정에서 개발자는 무수히 많은 코드를 작성하고 시간을 소비해야합니다. SQL을 작성하고, 조회된 결과를 객체로 변환하고, 객체의 상속관계를 테이블에 저장하기도 합니다. 객체에 대한 CRUD용 SQL 작업은 반복적이고 지루하고 꼭 필요합니다. 이를 자동화해주는 도구를 직접 만들어도 프로젝트가 복잡해지면 결국 한계를 마주하게 됩니다. 객체 모델링이 세밀하게 진행될수록 객체를 데이터베이스에 저장하거나 조회하는 과정이 점점 더 어려워지고, 복잡한 SQL을 작성하게 됩니다. 연관관계를 가지고 있는 도메인 정보를 한번에 조회하려면 객체에 연관관계를 가진 객체를 필드로 추가해야함은 물론이고 findUserWithTeam()와 같은 Repository 계층의 메서드와 SQL도 함께 확인하고 수정해야합니다.

연관관계를 가진 객체를 탐색할 때 객체는 참조를 사용하고 데이터베이스는 외래 키를 통한 조인을 사용합니다. 만약 객체의 참조를 가지고 있는 것인 아니라 객체의 Id만 가지고 있다면 데이터베이스 테이블에 맞추어 개발하는 형태가 됩니다. Id만 필요한 경우가 아니어서 결국 객체를 조회해야하는 상황이라면 객체 지향의 장점을 잘 활용할 수 없는 상태가 됩니다.

ORM의 도입

객체와 관계형 데이터베이스 간의 차이를 중간에서 해결해주는 ORM(Object-Relational Mapping) 프레임워크를 도입해야 할 때입니다. ORM은 이러한 과정을 도와서 SQL 작성 없이 객체를 데이터베이스에 직접 저장하게 도와주고, 객체와 관계형 데이터베이스 사이의 차이를 해결해줍니다. 물론 ORM 프레임워크를 사용하더라도 어떤 SQL이 실행될지 고민하고 생각하면서 개발해야 합니다.

ORM을 사용하면 참조를 사용하고 참조가 null일 때에는 AOP를 사용해서 객체를 조회할 수 있게 됩니다. 연관된 객체를 함께 조회할지 아니면 실제 사용하는 시점에 조회할지도 간단한 설정으로 정의할 수 있습니다. ORM을 적용하면 CRUD SQL을 직접 작성하지 않아도 되고, 조회 결과가를 객체로 매핑하는 작업도 대부분 자동으로 처리됩니다. SQL과 쿼리 힌트가 필요하면 이를 사용할 수 있는 방법도 제공합니다.

ORM을 사용할 때 마음 가짐

ORM은 데이터베이스 기술이라기보다는 애플리케이션의 데이터를 객체지향 관점에서 다룰 수 있도록 도와주는 객체지향 기술입니다. 하지만 객체 관점에서 복잡한 문제를 다루다보면 데이터베이스 관점의 최적화를 놓치기 쉽습니다. 객체 지향 관점과 데이터베이스 최적화 관점 두 가지 모두 놓치지 말아야합니다.

객체 비교

데이터베이스는 기본 키의 값으로 각 ROW를 구분합니다. 반면에 객체는 동일성 비교(==)와 동등성 비교(equals)하는 두 가지 비교 방법이 있습니다. JPA에서는 같은 트랜잭션일 때 하나의 영속성 컨텍스트에서 같은 객체가 조회되는 것을 보장합니다. 만약 분산처리 환경이나 여러 개의 트랜잭션이 하나의 영속성 컨텍스트를 사용하거나 할 때에는 도메인 객체에 동일성 비교(==)까지는 적용하지 못할 수 있습니다. 이럴 때에는 도메인 객체의 id 또는 이름 + 이메일 조합만으로 동등성(equals) 비교까지만 수행해도 괜찮습니다. 대신에 hashcode 메서드까지 새로 정의해서 Map에서도 사용할 수 있도록 보장하는 것도 빼먹지 않아야 합니다.

헥사고날 아키텍처에 적용

헥사고날 아키텍처에서는 애플리케이션 계층의 도메인 객체와 영속성 계층의 엔티티 객체가 분리됩니다. 영속성 게층의 Mapper를 통해서 엔티티를 도메인 객체로 바꿀 수 있습니다. 그런데 도메인 객체와 엔티티 객체를 분리하면서 엔티티 객체만이 가질 수 있는 지연로딩과 같은 기능도 도메인 객체에서 잃어버리게 됩니다.

도메인 객체와 엔티티를 분리하는 목적에 대해서 다시 생각해볼 필요가 잆습니다. 도메인 객체에서는 지연로딩과 같은 ORM에서 제공하는 기능을 잃어버리는 것이 아니라, 사용하지 않아도 되도록 의도적으로 분리를 하는 것이 목적입니다. 도메인 객체는 지연로딩을 사용하는지 안하는지도 모르고, 그저 Mapper를 통해서 조립된 객체를 사용할 뿐입니다.

일반적으로 도메인 객체에 연관된 객체를 필드에 추가하는 것은 해당 객체가 영속성 계층에서 조립되어 애플리케이션 계층으로 넘어온다는 뜻이 됩니다. 값이 없으면 없는대로 의미를 가지고, 있으면 있는대로 의미를 가지고, 연관된 엔티티 중 일부만 객체의 참조에 들어있다면 그 자체로 또 의미를 가져야 합니다. 비즈니스의 요구사항에 따라서 도메인 객체를 구성하는 의도가 먼저 정해지고 이를 뒷받침하도록 ORM 기술을 사용해야합니다.

영속성 계층에서는 처리하기 힘든 아주 복잡한 비즈니스 로직이 있다면, 관련된 모든 데이터를 조회해서 도메인 객체에 저장하고, 도메인 객체에서 비즈니스 로직을 처리하는 방법을 사용할 수 있습니다. 데이터 모델링만으로 풀기에는 너무 복잡한 요구사항이라면, 영속성 계층은 입력을 조회하고 결과를 저장하는 부분만 담당하고 나머지는 애플리케이션 로직에서 코드로 푸는 것이죠. 우선 이렇게 풀고나서 그 다음에 속도가 안나면 최적화를 고민해보는 것도 좋습니다. 우아한것보다 일단 동작하도록 개발을 1차 마무리하는게 더 중요하기 때문입니다.

 

다음 주제는

헥사고날 아키텍처에 대해서 여기까지 정리한 방법대로 프로젝트를 좀 더 진행해보려고 합니다. 실제로 프로젝트로 적용해보면서 부족한 부분이 있으면 보충하겠습니다. 헥사고날 아키텍처를 처음 적용했던 프로젝트는 private으로 숨겨놨고, JPA를 적용한 두 번째 프로젝트는 아래 링크에서 진행해보고 있습니다.

728x90