본문 바로가기

개발일지/광고데이터 연결 PJT - 차콜진센

20201219_시스템 구조 - 데이터베이스

드디어 코드에 대해 적어볼 예정이다. 

 

이번엔 최대한 루비를 루비답게 써보려했다. 그 전 코드들은 정말이지 엉성했다. 효율성 생각 없이, 대충 돌아가는 대로, 누가 보면 루비인지 아닌지도 모를 정도로 '일단 그냥' 짰다.

최근에 많이 느끼는건데, 그냥 코드를 짠다고 다가 아닌 것 같다. 시스템은 그냥 짜도 어떻게든 돌아는 간다(삐걱삐걱..). 하지만 만약 자신이 루비 개발자라면 루비의 정수를 최대한 누려가며 코드를 작성해야 하며, 간단한 코드라도 항상 질문을 해봐야 한다. 그 안에서 가장 효율적인 코드를 적용해야 하며 그 이유도 충분히 갖춰야 한다. 

지금까진 그러지 못했다. 지금까지라 해도 고작 반년 좀 지난거지만, 여하튼, 반성해야 한다. 

 

 

목표는 간단하다. 크론잡으로 메인 스크립트를 매시간 실행시키기.

그래서 구조도 간단하다. DB 테이블 2개, 메인 스크립트, 크론 yaml파일 등으로 구성되어있다.

기술 스택은 아래와 같다.

  • ruby
  • mysql2
  • docker
  • kubernetes

이번 편에선 먼저 데이터베이스 구조 및 발생했던 이슈들에 대해 설명 후 다음 편부터 메인 스크립트에 대해 설명해 보겠다. 


DB Schema

 

# migration file
create_table "daily_reports", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci", force: :cascade do |t|
    t.date "date", null: false
    t.integer "platform_id", null: false
    t.string "advertiser_id", null: false
    t.string "advertiser_name"
    t.string "order_id", null: false
    t.string "order_name"
    t.string "schedule_id", null: false
    t.string "schedule_name"
    t.string "creative_id", null: false
    t.string "creative_name"
    t.text "article_url"
    t.integer "imp", default: 0, null: false
    t.integer "click", default: 0, null: false
    t.integer "conversion", default: 0, null: false
    t.decimal "net", precision: 20, scale: 6, default: "0.0", null: false
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
    t.index ["date", "platform_id", "advertiser_id", "order_id", "schedule_id", "creative_id"], name: "daily_reports_unique_index", unique: true
    t.index ["date"], name: "index_daily_reports_on_date"
    t.index ["platform_id", "advertiser_id"], name: "index_daily_reports_on_platform_id_and_advertiser_id"
    t.index ["platform_id", "order_id"], name: "index_daily_reports_on_platform_id_and_order_id"
  end

 

(table은 2개지만 비슷한 구조이기에 대표 테이블인 daily reports 테이블만 소개한다)

 

우리가 데이터를 수집해야 할 웹사이트들은 구글 애드, 야후 애드, 도코모 등 배너광고나 검색광고 등을 운용하는 플랫폼이다. 

전편에서 말했듯이, 각 플랫폼은 공통적으로 캠페인-광고그룹-크리에이티브(배너 등)의 구조로 이루어져 있다. 각 요소의 데이터들은 아래와 같이 각 칼럼에 매핑된다.

 

advertiser id, name -> 챠콜진센(현재 운용 중인 광고의 광고주 이름)

order id, name -> 캠페인

schedule id, name -> 광고그룹

creative id, name -> 크리에이티브

article url -> 크리에이티브를 통해 연결되는 페이지 (기사광고)

 

 

Issue#1. Table option

 

migration파일을 보게되면, option 부분에 charset을 utf8mb4가 아닌 utf8로 세팅한 것을 알 수 있다. 이는 어쩔 수 없는 선택이었다. 왜냐하면 utf8mb4일 경우 우리가 설정한 unique index의 길이가 mysql의 index 최대 길이인 3072 bytes를 넘어 버렸기 때문이다. utf8mb4로 index를 생성하려 하면 아래와 같은 에러가 발생했다.

 

ERROR 1709 (HY000): Index column size too large. The maximum column size is 3072 bytes.

 

좀 더 자세히 말해보자면, 

utf8mb4의 경우, 한 글자가 4 bytes이다. 위 migration 파일의 string type은 mysql의 varchar(255)에 해당되며 255*4 bytes = 1020 bytes로 계산된다. 우리가 설정하려는 unique index에는 4개의 string 칼럼이 포함되어 있기에 이 4개의 칼럼만으로도 이미 최대 길이인 3072 bytes를 넘어선다. 

 

이를 해결하기 위한 방법으로 총 3개의 옵션이 있었다. 

첫 번째는 string의 length를 조절하는 것이다. 255자가 아닌 약 190자 정도로 조절한다면 index 생성이 가능했다. 하지만 문자의 길이를 조정하는 옵션은 위험부담이 컸기에 일단 제쳐두었다.

두 번째 옵션은 index 구성을 변경하는 것이었다. 예를들어 advertiser id, creative id를 인덱스 구성에서 빼는 형식이다. 하지만 CTO의 반대에 부딪혔다. unique index이기 때문에 반드시 저 형태로 구성하길 원하셨다. 이 옵션도 제쳐둔다.

세 번째 옵션이 바로 utf8mb4가 아닌 utf8로 설정하는 것이었다. utf8은 한 글자당 3 bytes이다. 아슬아슬하게 index 최대길이 규정에 맞아 들어간다. CTO와 상의한 결과 각 플랫폼의 id 등에 이모지가 들어갈 확률이 극히 낮다고 판단하였고 이에 세 번째 옵션으로 진행하기로 했다.

참고로 collation은 기본값인 utf8_general_ci가 아닌 utf8_unicode_ci을 채택했다. utf8_general_ci는 속도가 우선시되는 반면 정확한 비교, 정렬이 불가능한데, 우리의 시스템은 빡세게 돌아가는 서버가 아니기에 정확성이 높은 unicode_ci를 선택하게 되었다.

 

 

Issue#2. net column type

 

net cloumn의 type은 처음에는 integer였다. 하지만 각 플랫폼의 net 데이터가 소수점이었기에 integer로 저장할 경우 조금씩 오차가 생기고 있었다. 이에 황급히 net의 컬럼 타입을 decimal로 바꾸게 되었다. 

mysql에서 소수점을 표현하기 위한 선택지는 크게 2가지가 있는데, 고정소수점 타입인 decimal과 부동소수점 타입인 float, double이다. 

고정소수점 타입인 decimal은 실수의 값을 정확히 표현하기 위해 사용되며, 소수부의 자릿수를 고정하여 사용한다. precision은 소수 부분을 포함한 실수의 총 자릿수를 나타내며 scale은 소수 부분의 자릿수를 나타낸다. 

부동소수점의 경우는 실수의 값을 대략적으로 표현할 때 사용된다 한다. 

net은 발생비용, 즉 통화단위이므로 대략적인 값을 나타내는 부동소수점보다는 정확한 실수 값을 표현할 수 있는 고정소수점 타입, 즉 decimal이 적절하다 판단하여 decimal로 타입 변환을 하였다. 

 

 

다음 편에서 계속...