-
Notifications
You must be signed in to change notification settings - Fork 0
[Backend] Tagging 구현을 위한 Entity 설계
우선, 어쩌다가 Tagging을 구현하게 되었는지 간략히 제 상황을 소개하고 지나가겠습니다. 저는 네이버 지도와 같이 여러 가게들에 대한 정보를 제공하는 프로젝트를 진행하고 있는데 요구 사항에 따르면 가게마다 매핑될 태그들이 있습니다.
이 태그들의 종류는 현재 요구사항에 따르면 총 3가지이고, 하지만 추후에 더 기능 확장이 될 수도 있습니다. 이에따라 확장성적인 측면도 고려를 해야하고 가게의 데이터 셋이 많아서 높은 성능과 최적화를 함께 진행해야 합니다.
이를 위해 어떻게하면 최적의 설계를 갖출 수 있을지 고민하다 설계한 Entity 다이어그램은 다음과 같이 크게 총 3종류가 있습니다. 지금부터는 이 3종류의 Architecture를 살펴보며 비교 분석해보겠습니다.

가장 단순하게 생각했던 Architecture부터 살펴보겠습니다. 첫번째 안으로 생각한 구조는 위 그림과 같이 가게와 Label 간에 M:N 연관 관계를 주는 것이었습니다. 이렇게 하면 JPA를 통해 굉장히 심플하게 구현할 수가 있습니다.
하지만, 이렇게 @ManyToMany 애너테이션을 Entity 간에 사용하게 되더라도 MySQL과 같은 RDB로 넘어가게 되면 M:N 관계를 표현할 수 없게 되기 때문에 내부적으로 Mapping table을 자동으로 만들어 1:N과 N:1 관계로 풀어 구현하게 됩니다. 바로 이 부분이 문제가 되는 부분인데, 이렇게 자동으로 생성된 Mapping table에는 추후에 요구 사항이 추가되어 해당 테이블에 추가 field를 생성해야 함에도 불구하고 생성하기가 곤란해지게 됩니다. 즉, 요구 사항이 변경되었을 때 확장성이 떨어지는 문제를 낳게됩니다.
위 문제는 굉장히 간단하게 해결할 수 있는데 Entity level에서 M:N으로 매핑되어 발생하는 문제이므로 이를 Entity level에서부터 매핑 테이블 역할을 하는 엔티티를 만들고 1:N과 N:1 관계로 풀어 구현해주면 됩니다. 이렇게 하면 추후에 요구 사항 변경에 따라 매핑 테이블에 추가 필드를 생성해야 할때도 쉽게 적용할 수 있습니다.

이전까지는 @ManyToOne 애너테이션, 즉 M:N 연관 관계 매핑의 문제점에 대해 알아봤습니다. 이를 해결하기 위해 이번 두번째 설계 구조에서는 Entiy level에서부터 매핑 테이블 역할을 하는 Entity를 만들고 1:N과 N:1 연관 관계 매핑을 통해 이 문제를 해결하고자 합니다.
그리고 더 나아가서, Object Oriented Programming 관점에서 역할과 구현을 구분하기 위해 한번 Label에 대해 역할과 구현을 위 그림과 같이 나눠봤습니다. 이렇게 인터페이스나 추상 클래스를 활용해서 추상화를 하게되면 생기는 장점으로 추후에 요구 사항이 변경되어 각각 Label마다 고유한 액션이 더 생기거나 Label끼리 구분되는 특별한 기능이 생길 때 유연하게 대처할 수 있다는 장점이 있습니다.
하지만, 저희 프로젝트에 서비스 도메인을 생각해봤을 때 Label은 정말 이름 그대로 검색 시 필터링하기 위한 이름표에 불과했고 그 이외에 부가적인 기능은 추가될 일이 없다고 생각되어 추상화까지 시킬 필요는 없겠다라는 생각이 들었습니다.
그리고, 추상화와 별개로 Mapping table을 두고 1:N, N:1 관계로 풀어 나타냈을 때 단점도 분명히 존재하는데 이는 다음에 나올 Value type을 활용한 방법보다 성능적인 면에서 떨어질 수 있다는 점입니다. 이렇게 매핑 테이블을 사이에 두게 되면 Join을 통해 여러 테이블을 함께 조회해야하는 방면에 다음에 나오는 것처럼 값 타입을 활용하면 가게 Entity만 조회해도 원하는 결과를 쉽게 얻을 수 있습니다.

바로 위 구조가 Value type을 활용한 Architecture입니다. 가게 Entity안에 Embedded type으로 넣게되면 별도의 매핑 테이블을 거치지 않아도 가게 Entity만 조회하여 특정 Label에 대해 필터링한 검색 결과를 쉽게 얻을 수 있습니다. 이는 앞서 말씀드린 것처럼 Mapping table을 활용했을 때보다 성능적인 부분에서 더 나은 Performance를 보여줄 수 있습니다.
하지만, 이 구조의 치명적인 단점은 확장성이 떨어진다는 점입니다. 이렇게 가게 Entity안에 몰아서 설계하게 되면, 혹여나 나중에 요구 사항이 바뀌어 새로운 Label을 추가해야 될 때 Value type 수정부터 시작해서 가게 테이블의 스키마까지 변경하는 문제가 생기게 됩니다. 이렇게 스키마가 바뀌게 되면, 기존에 존재하던 데이터들은 바뀐 스키마에 맞춰 별도의 업데이트도 진행해줘야 합니다.
지금까지 고민한 모든 점들을 종합해봤을 때, 저희 요구 사항에 가장 잘 맞는 설계는 아래 그림과 같다고 잠정적인 결론을 내렸습니다.

위에서 살펴본 것처럼 Value type을 활용하는 것이 아무래도 가게 Entity만 조회하면 되서 성능적인 performance가 뛰어나긴 하지만, 저희 프로젝트의 요구 사항인 추후 Label에 대한 확장성 고려가 반드시 필요하기 때문에 값 타입을 활용하는 방식은 적합하지 않다고 생각되었습니다.
그래서 결국은 Mapping table을 활용해서 1:N과 N:1로 풀 수 밖에 없는데, 이 구조에서도 값 타입을 활용한 것처럼 Join 횟수를 줄인다던지 해서 성능 최적화를 진행할 수 있지 않을까 고민하게 되었습니다.
그러던 중 떠올린 것이 캐싱입니다. 저희 프로젝트 자체가 가게의 Label을 활용해서 필터링하는 쿼리가 굉장히 빈번하게 사용되게 되는데 Label 종류가 그렇게 많지 않으므로 모든 경우의 수에 대해 캐싱해두고 전달하면 높은 성능을 낼 수 있을 것이라고 생각합니다. 하지만 여기서 만약 캐시를 사용한다면 어떤 캐시를 사용할지, 만약 로컬 캐시로 직접 구현한다면 동기화 이슈는 어떻게 처리할 것인지에 대한 고민이 반드시 필요할 것 같습니다. 그리고 아직 정확한 성능 비교는 진행하지 않았지만 직접 구현해가면서 캐싱 했을 때와 안했을 때 실제로 성능이 어떻게 차이나는지도 더 고민해볼 예정입니다.
저는 이 캐싱 방식을 통한 성능 개선이 저희 프로젝트에 굉장히 잘 들어맞는 개선 방향이라고 생각이 든 이유가, 마침 저희 프로젝트의 Label은 한번 지정해두게 되면 업데이트가 잘 되지 않는 성격을 가지고 있습니다. 이러한 부분은 캐싱 업데이트의 주기 또한 늦춰주므로 효과적인 개선이 될 것이라고 기대하고 있습니다.