코드를 이용한 모델링은 효율적일까?

salmonavocado's avatar
Feb 01, 2024
코드를 이용한 모델링은 효율적일까?

들어가며

최근 마무리지었던 프로젝트의 데이터 모델링 과정에서 나를 꽤 괴롭히던 문제가 있었는데 바로 코드 모델링이다.
프로젝트 진행 배경 상황은 1차 개발을 마무리하고 변경된 데이터를 기반으로 새로운 데이터를 쌓아추가 기능을 개발할 예정이었다. 즉, 새로운 데이터를 기존에 개발한 로직에서 크게 벗어나지 않는 선으로 적재하는 개발이 필요했다.
문제는 한글값으로 들어오는 데이터를 그대로 사용할 것인가, 코드값을 부여하여 메타데이터를 함께 관리하여 적재할 것인가였다.
 
 

코드란?

코드란 있는 그대로 사용하기 불편한 정보를 약속된 형태로 압축한 간단한 기호체계이다. 속성 단위로 정보를 입력하는 데이터 모델에서는 속성의 값을 기호로 변환한 것이 코드이다.
기호화한 것이라고 모두 코드라고 할 수는 없는데, 코드인 것과 코드가 아닌 것을 구분하기 위해서는 식별자인 것과 코드인 것을 구분해야한다.
식별자란 일반적으로 기본 키(PK)와 동일하며 엔터티의 개별 인스턴스를 유일하게 식별할 수 있는 장치이다. 반면 코드는 분류를 위한 도구로 IT 조직 내에서 자료 사전으로 관리되며 메타데이터를 생성하여 코드를 관리하는 데이터 시스템을 구축해야한다.
식별자와 코드를 구분하기 위해 식별자에는 ID번호 등의 이름을 사용하고 코드값에는 코드를 붙인다.
 
💡
메타데이터란? ”데이터에 대한 데이터”
많은 사람들이 위와 같이 메타데이터를 정의하지만 나에게는 와닿지 않아 아래와 같이 재정의 해보았다.
”메타데이터란 데이터의 관리 편의성을 제공해 데이터의 정확성을 높여주고 사용자가 데이터를 이해하는데 도움을 주는 보조/관리 데이터다.“
정의에 따르면 메타데이터가 존재하지 않을경우 관리의 어려움이 생긴다. 예를 들어 사명을 당근마켓에서 당근으로 변경한 경우, 메타데이터가 존재한다면 메타데이터에서만 값을 바꿔주면된다. 반면에 메타데이터가 없다면 해당 값이 사용되는 모든 데이터를 찾아 일일이 값을 변경해줘야하는 번거로움이 생긴다. 이 경우 데이터의 정확성도 흔들리는 위험이 존재한다.
또한 메타데이터가 없다면 해당 데이터의 유형, 데이터들 사이의 관계 등을 파악하기 어렵다. 따라서 데이터 모델링시에 메타데이터 필수적인 요소로 보아도 무방하다.
 
 
 

본연의 값을 사용하자 vs 코드값을 사용하자

우선 팀 내에서는 한글값으로 들어온 있는 그대로의 데이터를 사용하자파와 메타데이터를 생성해 코드값으로 관리하자파로 나뉘었다. 각 입장별 주장을 살펴보자.
  • 한글값 사용하자 Say…
    • 관리의 어려움 : 코드를 관리하는 메타데이터가 많아지고 프로젝트 규모에 비해 관리의 어려움이 커진다.
    • 직관성 : 코드값을 사용해 메타데이터 관리시 코드값을 관리하는 메타데이터 개수만큼 JOIN 연산이 필요한데, 불필요한 JOIN을 줄이고 데이터를 직관적으로 이해할 수 있는 한글값을 사용하자. 무엇보다 원본데이터에 코드를 적용하고 실제 의미를 파악하기 위해서는 별도의 변환 과정이 필요한데 이 또한 위험 부담이 존재한다.
    • 성능의 미미한 차이 : 범주화 연산 (집계)이 필요하기는 하지만, 한글값 대신 코드값을 사용했을 때의 성능의 차이는 미미할 것이다.
  • 코드값 사용하자 Say…
    • 한글값 위험성의 존재 : 값 변경시 메타 데이터에서만 수정 vs 모든 데이터에서 값을 바꿔주기 중 전자가 더 유리하다.
    • 성능 개선 : 추가 기능 개발을 고려했을 때, 현재보다 집계 연산이 더 많아질텐데, 한글값보다는 코드값을 사용하는 것이 쿼리 속도가 더 빠를 것이다. 또한 짧고 간결한 기호를 사용해 저장 공간 효율성을 도모할 수 있다.[코드값 기반 쿼리 결과][한글값 기반 쿼리 결과]
      • 코드값 기반 쿼리 결과
        코드값 기반 쿼리 결과
        한글값 기반 쿼리 결과
        한글값 기반 쿼리 결과
        테스트 데이터 사이즈의 규모가 작아 차이는 미미하지만 데이터가 점점 더 쌓일수록 이 차이는 더 커질 것이라고 예상한다. 단, 실제 서비스는 MySQL DB에 데이터를 적재하고 있는데 테스트는 Athena에서 진행하여 설득력을 살짝 잃었다 . . (MySQL에서 테스트를 진행 한 결과로 추후 보완 예정)
         
  • 열정적인 중재자, 팀장님 Say…
    • 선택의 문제다. 테이블 간의 연관도(Join, Group By 등)를 파악하고 성능적으로 더 좋고, 효율이 더 높고, 관리가 더 편한 것을 선택하라.
 
결과적으로 1차 개발을 완료한 현재 상태에서 추가적인 기능 개발을 고려하여 코드값을 쓰는 것이 효율이 높을 것으로 판단하여 코드값을 생성하고 메타데이터를 관리하였다. (강경한 코드값을 사용하자파로서 설득하는데 힘들었지만 꽤나 기쁜 일이었다.)
 
 
 

코드값을 생성해보자

해당 프로젝트에서 코드로 보이는 숫자,기호로 변환하여 메타데이터로 관리할 데이터는 크게 7가지였지만, 코드값 개념에 부합하는 코드 데이터는 2가지고 그 외 5가지는 식별값이었다.
 
spark.sql로 코드값을 생성하고 관리하는 로직 개발 과정을 소개하고자한다. 코드로 관리할 데이터는 대업종>중업종>소업종 개념으로 대업종 내의 중업종이, 중업종 내에 소업종이 존재한다.
 
💡
예시)
음식 > 치킨 > 양념치킨
음식 > 치킨 > 후라이드
왼쪽부터 대업종, 중업종, 소업종 컬럼. 각 업종 별 한글명은 캡쳐본에서 제외
왼쪽부터 대업종, 중업종, 소업종 컬럼. 각 업종 별 한글명은 캡쳐본에서 제외
 
코드값을 위와 같이 부여하였다. 대업종은 대업종 컬럼값 별 코드값, 중업종은 중업종 컬럼값 별 코드값 , 마지막으로 소업종은 대업종 + 중업종 + 소업종 컬럼값 별 코드값으로 unique한 코드값을 부여했다.
즉, 대업종과 중업종 코드의 직접적인 관계는 없지만 소업종에서 대-중-소의 관계를 만들어 소업종의 값만 보아도 해당 데이터의 대-중-소업종을 알 수 있는 구조이다.
 
코드를 보면서 이해를 해보자.
기존_업종코드_메타데이터= spark.read.parquet("") max_id = int(기존_업종코드_메타데이터.select(max("대업종")).collect()[0][0]) 업데이트_대업종 = df.select("대업종").distinct()\ .subtract(기존_업종코드_메타데이터.select("대업종").distinct()) \ .withColumn("대업종", lpad(row_number().over(Window.orderBy("대업종")) + max_id , 2, "0"))\ ... 중략 ... 대업종_업데이트_메타데이터 = 기존_업종코드_메타데이터.union(업데이트_대업종)
기존 메타데이터에서 대업종 코드와 값만 가져와 새로운 값이 들어올 경우 새로운 코드를 부여하는 과정이다. 대업종의 경우 값이 최대 100개 이하로 유지될 것으로 예상하여 2자리 수로 포맷을 맞추었다. 중업종도 위와 같은 로직으로 3자리 수로 포맷을 맞추어 업데이트한다.
 
 
max_id_소업종 = 기존_업종코드_메타데이터.withColumn("r", row_number().over(Window.partitionBy("대업종", "중업종").orderBy(desc("소업종"))))\ .filter("r=1")\ .selectExpr("대업종", "중업종", "max_id_소업종")\ .withColumn("max_id_소업종", substring(col("max_id_소업종"), 6, 2).cast("Integer")) 신규추가_업종코드_메타데이터 = df.select("대업종", "중업종", "소업종").distinct()\ .subtract(기존_업종코드_메타데이터.select("대업종", "중업종", "소업종"))\ .join(업데이트_대업종, "대업종", "left")\ .join(업데이트_중업종, "중업종", "left")\ .join(기존_업종코드_메타데이터, ["대업종", "중업종", "소업종"], "left")\ .join(max_id_소업종, ["대업종", "중업종"], "left")\ .select("대업종", "중업종", "max_id_소업종")\ .withColumn("소업종", when(expr("소업종 is null and max_id_소업종 is not null"), lpad(row_number().over(Window.partitionBy("대업종", "중업종")\ .orderBy("소업종")) + col("max_id_소업종"), 2, "0")) .when(expr("소업종 is null and max_id_소업종 is null"), lpad(row_number().over(Window.partitionBy("대업종", "중업종")\ .orderBy("소업종")), 2, "0")) .otherwise(col("소업종")))\ .withColumn("소업종", when(expr("len(소업종) < 7"), concat(col("대업종"), col("중업종"), col("소업종")))\ .otherwise(col("소업종")))\ 업데이트_업종코드_메타데이터 = 기존_업종코드_메타데이터.union(신규추가_업종코드_메타데이터)
다음은 소업종을 업데이트하는 과정이다.
기존 업종코드 메타데이터에서 대/중업종 별로 partition을 나누어 가장 큰(최신의) 소업종 코드를 찾는다. 이후 ⓐ기존에 존재한 대/중업종의 소업종만 새로 생긴 경우, 해당 범위의 가장 큰 소업종 코드 다음에 이어서 생성 ⓑ기존에 없던 새로 생긴 대/중업종에 소업종도 새로 생긴 경우에는 1부터 새로 값을 부여하는 분기문을 통해 소업종의 코드값을 업데이트한다.
마지막으로 새로 코드값을 부여한 소업종들을 기존 포맷에 맞춰주는 작업을하면 코드값 생성이 끝난다.
 
 
 

마치며

데이터 개발을 마친 시점에서 추가 기능 개발 없이 현행 유지라는 기획의 변화가 생겼다. 다시 원점으로 돌아가 문제에 대한 고민을 했고, 새로 개발한 데이터 처리 로직은 현행 프로젝트에 적용하기엔 무겁다고 생각하여 메타데이터 사용을 최소화하고 한글값을 사용하는 것으로 변경하였다. .
 
“코드를 이용한 모델링은 효율적일까?”
 
이 질문에 대한 답은 “그럴 수도 있고 아닐 수도 있다”라고 생각한다. 절대적인 것은 없으며 각각의 상황에 맞춰 최선을 다해 개발하는 것이 최고의 효율을 내는 것이 아닐까?
 
전에 실장님께 데이터를 잘 다룰 수 있는 왕도가 있는지 여쭤봤는데, 실장님께서는 “데이터를 많이 보면된다”라고 하셨다. 실제로 이번에 데이터를 정말 많이, 오래, 깊게 보면서 데이터에 대한 이해도가 많이 올라갔고 이런저런 경우의 수에 따른 결과가 머릿 속에서 그려지는 경험을 했다. 실장님의 말을 몸소 이해한 순간이었다. 이번 프로젝트를 통해서 데이터 모델링 왕초보에서 초보로 한단계 성장한 것 같다.
 
 
 

📢 문제를 해결하기 위해 도움을 준 책

  • 프로젝트 성패를 결정짓는 데이터 모델링 이야기 - 김상래
  • 김기창의 데이터 모델링 강의 - 김기창
 

written by salmonavocado🥑
 
Share article

salmonavocado