안녕하세요😎 백엔드 개발자 제임스입니다 :)
오늘은 제가 진행했던 프로젝트 "채식이들"에서 구현했던 기술을 설명하겠습니다. 당시 핵심 기능이었던, 채식 유형에 따른 제품 조회 기능을 구현하기 위해 많은 고민을 했습니다.
먼저 기능의 요구사항을 알아보겠습니다.
저희는 채식주의자의 개인 채식 유형을 고려했습니다. 즉, 채식주의자의 채식 유형에 해당하는 제품만 노출되도록 구현하기로 했습니다. 여기서 제품은 한 가지의 채식 유형만 갖습니다. 회원 또한 동일합니다.
그렇다면 회원과 제품 도메인에 단순히 채식유형을 값으로 저장한 뒤, SQL 조건문으로 (채식) 유형에 해당하는 제품을 간단하게 노출시키면 되지 않을까?
문제는 채식 유형이 먹을 수 있는 식재료가 아래 이미지와 같이 계층적인 관계를 나타낸다는 것입니다.
예를 들어, 락토를 선택한 회원이 제품을 조회한다고 하겠습니다. 그럼 락토에 해당하는 제품이 나오게 됩니다. 그러나 위 이미지를 참고한다면, 식재료가 겹치는 비건과 프루테리언의 제품도 조회되어야 합니다. (오보는 '난류'가 포함되기 때문에 이를 먹지 못하는 회원에게는 오보 제품이 출력되선 안됩니다.)
채식 유형들의 섭취 가능한 식재료를 보다시피, 복잡한 관계라는 것을 알 수 있습니다.
고민 Point
1. 채식 유형을 DB에 저장하지 않고, 제품 출력 시 계층적인 형태로 출력할 순 없을까?
2. 식재료가 겹치는 유형들의 제품을 리소스 요청 비용을 줄이며 출력하는 방법은 무엇일까?
기능 구현에 앞서 저희는 위와 같이 고민했습니다.
먼저 9가지의 채식 유형은 고정 데이터였기 때문에 불필요하게 메모리를 낭비하고 싶지 않았습니다. 하지만 마땅한 방법이 떠오르지 않았습니다. 또 프로젝트 기간이 짧았기 때문에, 기능 검증을 우선으로 생각하여 테이블로 생성하는 전략을 수립했습니다.
그럼에도 두 번째 고민까지 해결하고 싶었습니다. 여기서 채식 유형의 식재료들이 Tree 형태인 계층 구조를 나타낸다는 것을 파악하게 되었습니다. 따라서 각 유형에 레벨을 부여했습니다. 이렇게 설계한 테이블을 통해서 '자기 참조 계층형 조회 쿼리'로 제품을 제공하는 것을 결정했습니다.
자기 참조 계층형 조회 쿼리 구현
1. 구현 아이디어
먼저 채식 유형 테이블을 만들었습니다. 그리고 테이블에는 각 유형의 이름과 레벨에 대한 칼럼이 존재합니다. 저희는 레벨을 통해 상. 하위 구조를 표현했습니다.
방식은 이렇습니다. 먼저 '선택된 채식 유형'과 '선택된 채식 유형의 레벨보다 낮은 레벨을 가진 유형'을 조회하는 방식입니다.
선택된 채식 유형이 '플렉시테리언'이라면 조회되는 제품의 유형은 플렉시테리언, 폴로-페스코, 페스코, 폴로, 락토-오보, 락토, 오보, 비건, 프루테리언입니다.
2. '선택된 채식 유형'과 '보다 낮은 레벨의 유형' 조회 쿼리 구현
위 문제를 해결하기 위해 Native Query로 서브쿼리를 직접 구현했습니다..
SELECT vegetarian_type
FROM vegetarian
WHERE
levels < (SELECT levels FROM vegetarian WHERE vegetarian_type = '선택된 채식 유형')
OR vegetarian_type = '선택된 채식 유형'
where문 이하에 명령어를 보도록 하겠습니다. 먼저 levels를 나타내는 부등식이 있습니다. 해당 명령어를 통해서 선택된 채식 유형의 레벨보다 낮은 레벨의 유형들이 조건에 포함됩니다. 그리고 아래 OR 연산자를 통해서 선택된 채식 유형도 포함시키고 있습니다.
결과는 오보, 비건, 프루테리언 유형이 출력되는 것을 알 수 있습니다.
이제 이를 이용해서 핵심 기능을 구현할 것입니다. 즉, 유형이 아닌 제품이 출력되어야 합니다.
3. 핵심 기능 구현 (선택된 채식 유형이 먹을 수 있는 제품 출력)
- @Param 애너테이션을 통해서 매개변수의 인자로 선택된 채식 유형을 받아옵니다.
- IN 연산자 사용
위에서 선택된 채식 유형 이하 레벨의 유형까지 조회되는 것을 확인했습니다. 핵심 기능의 SQL Query에서는 WHERE 조건문을 통해 조회된 유형들에 해당하는 제품을 출력하는 것입니다. 하지만 조회된 유형은 한 개 이상의 여러 개입니다. 각각의 채식 유형이 조건절에 와야 하는 것입니다. 따라서 IN 연산자를 사용합니다.
4. 테스트 결과
회원이 채식 유형 중 오보를 선택했을 때, [오보, 비건, 프루테리언] 유형의 제품이 성공적으로 출력되었습니다.
마지막으로
저희는 해당 기능을 Native Query로 SQL을 직접 작성하여 구현했습니다. 하지만 Native Query 와 서브 쿼리 구현에는 단점이 여러 존재했습니다. 그중 가장 크게 느껴지는 것은 가독성이 좋지 않다는 점입니다. (앞으로 리팩토링 할 생각에 벌써 기대되네요^^)
또 프로젝트 확장을 하려 하니, 수정해야 할 부분이 너무 많았습니다. 예를 들어서 필드명을 하나라도 바꿀 경우 건드려야 할 부분이 한두 개가 아니었습니다. (한 번은 대소문자 때문에 엄청 애먹은 기억이 있습니다..) 무엇보다 컴파일 시점에 에러를 잡을 수 없다는 것이 크게 불편합니다. 또 특정 DB에 의존한다는 단점도 있었습니다.
뿐만 아니라 서칭을 하다보니 서브쿼리가 성능 저하를 초래할 수 있다는 것을 알게되었습니다.
따라서 앞으로 2가지의 리팩토링 도전 미션을 생각하고 있습니다. (1) 첫 번째는 해당 쿼리들을 QueryDSL로 구현하는 것입니다. (2) 두 번째는 채식유형을 테이블로 만들지 않고, 시간복잡도를 최대한 고려한 자료구조를 활요하는 방법입니다.
'개발 > Programming' 카테고리의 다른 글
[프로그래밍] 자주 사용하는 정규식 표현 정리 (3) | 2023.03.08 |
---|---|
[IDE] MacOS 터미널(CLI)에서 IntelliJ 바로 실행하기 (2) | 2023.02.27 |
[아키텍처] 실제로 겪게 된 순환 참조 문제 (2) | 2022.10.03 |
[빌드] ./gradlew: /bin/sh^m: bad interpreter: no such file or directory 오류 발생 그리고 해결 (7) | 2022.09.02 |
[배포] AWS를 통한 배포 방법 알아보기(EC2 서버 실행) (6) | 2022.08.09 |