Hash 마이그레이션 작업 회고

유수민·2025년 3월 20일
0
post-thumbnail

따끈따끈한 이번 작업을 소개하려고 한다.
오늘의 주인공은 상용데이터의 Hash 마이그레이션이다.

📌 등장 배경

🌞 화창한 아침..운영 실무자에 의해 데이터 변경 요청이 떨어졌다

현재 요청된 내용은 프로덕트에 매핑된 속성을 변경하되 그 속성 옵션값은 그대로 유지해달라는 요청이다.
좀 더 구체적으로 말해보자면, 상품1에 속성으로 PRADA 컬러라는 속성이 있고 그 값으로 빨강, 주황이 있는데 상품 1의 속성을 GUCCI 컬러라는 속성으로 변경하되 값은 그대로 빨강, 주황으로 유지 해달라는 것이다.

  • 운영팀의 요청: 특정 상품의 속성을 다른 속성으로 변경해야 하지만, 속성 값은 유지해야 한다.
  • 문제: 기존 정책상 노출 속성은 수정할 수 없으므로, 마이그레이션 작업이 필요하다.

현재 정책상 노출 속성은 한번 상품에 매핑되면 수정불가한 정책을 가지고 있기 때문에 우리가 마이그레이션을 해줄 수밖에 없다.

내가 할일은
1. product_attribute에서 attribute를 변경해주고
2. item_attribute_option에서 attribute와 attributeOption을 변경해줘야한다.
바꿀 attributeOption은 기존의 attributeOption 값과 일치한 것을 찾아다가 변경해주면된다.

사실 여기까지보면 뭐.. 쿼리 돌려주면 되지 한다. 맞다 이 일은 어렵지 않다. 근데 문제는 hash이다. 이번 이야기에서 나오는 주제의 주인공은 HASH이다.

우리는 상품과 아이템의 중복확인을 hash로 구별한다.

  • 상품 중복 조건 : 카테고리 + 상품이름 + 노출속성목록 + 브랜드Id + 모델명
  • 아이템 중복 조건 : 상품 Id + 노출속성목록 + 속성값목록

이렇게 되어 있다.
이번에 이 조건들 중에 상품에는 노출 속성 목록이, 아이템에는 노출속성목록과 속성값 목록이 변경되기 때문에 Hash를 새로 변경해줄 수밖에 없다.
그럼 변경된 상품과 아이템만 변경하면되잖아? 하기에는 이번 작업부터 중복 조건이 일부 변경되었다. 위에 언급한 조건은 이번에 변경되는 조건에 해당된다.

에이 변경하면 되지..하기엔 단순히 몇백개가 아니고 몇십만 단위다..ㅎ 거기다가 상품 중복 조건이 이번 작업부터 변경된 조건이라 해시 충돌이 안날래야 안날 수가 없는 작업이다 ㄸㄹㄹ,,

💡 정리하자면, 상품과 아이템의 중복 여부를 확인하는 데 해시를 사용하는 이번 속성 변경하면서 그 기준이 되는 값 또한 변경이 되었기에 hash 작업이 필요하다

📌 Hash?

💡 해시
임의의 크기를 가진 데이터를 고정된 크기의 데이터로 변화시켜 저장하는 것

결론부터 말하자면, 동일한 입력값이면 항상 동일한 해시값이 생성되므로, 데이터를 직접 비교하지 않고도 빠르게 중복을 판별할 수 있다. 즉, 중복 체크하는데 성능적으로 해시가 더 빠르다고 판단한 것이다.

🐳 직접 비교 Vs 해시 비교

상품을 등록하는 걸 예시로 들어보자.
상품을 등록하기 전 위 중복조건에 의해 중복된 상품이 있는지 확인해야 한다.

만약 직접 비교해야 한다면?

EXPLAIN SELECT * FROM product
WHERE category_id = ?
AND product_name = ?
AND brand_id = ?
AND model_no = ?
AND displayed_attribute_ids = ?;

해당 쿼리를 돌려 보면

실행계획에 테이블 풀스캔하고 있음을 확인할 수 있었다.
즉, 상품을 추가할때 기존 상품과 하나씩 비교한다는 것이다. -> 최악의 경우 O(N)
풀 스캔하는 것을 보니 데이터가 많을수록 성능이 저하됨을 의미하는데 현재 우리 상품은 몇십만개이다. 그럼.. 성능은 아주 최악이 되는 것이다.

그럼 Hash의 경우는?

EXPLAIN SELECT * FROM product_identity pi
WHERE pi.hash = ?;

해당 쿼리를 돌려보면

const row lookup으로 인덱스를 확용한 상수 조회를 하고 있음을 확인할 수 있다.
해당 스캔 방식은 단일 행을 대상으로 하는 상수 값을 기반으로 하는데 Primary Key 또는 Unique Key에 대한 WHERE 절의 상수 값 비교가 있을 때 나타난다.
hash에 의해 unique를 걸 수 있기 때문에 인덱스를 걸 수 있는 것이다.
즉, 테이블의 유니크 인덱스 (uk_hash)를 사용하여 단 한개의 행만 조회하고 있다.
-> 시간 복잡도 O(1) ~ O(logN)

결과적으로 직접 비교보다 해시비교가 풀스캔 방지가 될 뿐만아니라 속도도 훨씬 빠르기 때문에 중복 검사에 해시를 적용하는 것이 성능상 최적이라는 것을 알 수 있다.

정리하자면,

  • 직접 비교
    풀스캔, 시간복잡도 : O(N), 데이터 많을수록 성능 저하
  • Hash 비교
    O(1) ~ O(logN), 인덱스를 활용해 빠른 조회 가능

📌 마이그레이션 작업에 대한 회고

작업에 대한 플로우를 말해보자면,

  1. 속성 변경에 대한 프로덕트-속성 매핑 데이터, 아이템-속성 매핑 데이터 변경
  2. 중복 조건에 의한 해시 충돌 테스트
  3. 충돌된 상품에 대한 실무자 전달 및 데이터 정리
  4. 프로덕트, 아이템 해시 테이블에 생성 -> 임시 테이블에 생성
  5. 기존 해시 테이블 백업 후 임시테이블을 기존 테이블 명으로 변환
  6. product, item 해시 캐시의 기존 데이터 삭제 후 새로 생성

상용 데이터를 다루기도 하고, 내 기준상 데이터가 많아서 사실 두렵기도 하고 어려움도 많았던 작업이었다. 이 작업을 하면서 흥미로웠던 구간은 기존의 데이터에 대한 해시 생성 구간이었다.

아이템만 해도 현재 활성화된 아이템이 몇십만개인데 이걸 어떻게 생성을 하느냐?
방법은 page를 활용해서 한번에 3000개씩 가져와서 배치를 돌리는 것이다.
( 이건 그냥 여담인데 아무생각없이 10개씩 가져오도록 했다가 반나절이되어도 안끝났다..ㅎ 그래서 확 3000개씩 가져오는 걸로 바꾸었더니 5도 안되어서 끝났다. ㅎㅎ 네트워크 비용에 대한 체감이 확 드는 순간이었다. )

🐳 내부 코드 분석

가장 흥미로웠던 코드를 분석해보자.

해당 코드는 대량 데이터를 한번에 삽입해야 할때 사용한다.
먼저 excute 내부 코드를 살펴보면, ( Spring 내부 코드 )

1) getTransaction() : 직접 트랜잭션을 가져오고,
2) commit() : 트랜잭션을 직접 커밋하고
3) rollbackOnException() : 트랜잭션을 수동으로 롤백하고
이를 통해 excute() 메소드는 트랜잭션 관리 로직을 직접 실행하는 메서드임을 확인할 수 있다.
-> 예외 발생시 전체 롤백이 되게 됨을 알 수 있다. 실제로 테스트할때 충돌로 인한 예외 발생시 전체 롤백된 것을 확인하였다.

대망의 batchUpdate 메소드를 살펴보자면, ( Spring 내부 코드 )
이 메소드는 JDBC 기반 배치 업데이트 메서드로 여러개의 데이터를 한번에 업데이트하는 기능을 함을 알 수 있다.


실제로 Sql에 해당 하는 코드가

이렇게 앞전에 선언해놓았고, 이것을 매개변수로 받아서 hash 생성을 한꺼번에 하는 것을 알 수 있었다.

🐳 회고

이 작업을 수행하면서, 역시 직접해봐야 좀더 와닿음을 다시 한번 확인하는 순간이었다. 내가 이번에 작업한 해시는 이전부터 중복 검사를 위해 해놓았던 조치였다. 그러나 내가 한게 아니다보니, 해시 작업에 대한 이해도가 낮았다.
이번에 해시 생성 작업을 하다보니, 왜 필요하고 어떤 코드를 사용했는지 나를 납득해가면서 작업을 하다보니 얻어가는 것도 많고 이해도도 높아짐을 알 수 있었다. 나중에 중복 검사가 필요할때 이 작업을 떠올리며 해시를 생각해내지 않을까? ㅎㅎ😄

처음에 이 작업이 주어졌을때, 상용 데이터를 만진다는 것에 대한 두려움이 있었는데, '이 연차에 어떤 누가 몇십만개의 상용 데이터를 다루는 경험을 하겠어?' 하는 마음으로 임했다. 그래도 무서웠다. 혹시 내가 잘못할까봐 덜덜 떨면서 했다. 며칠 지난 지금, 이 작업을 회고하면 간단한데? 싶다.

이 작업에서 가장 오래 시간이 걸렸던 부분은 해시 충돌 부분이었다.
사실 상품 해시 충돌은 예상했지만, 아이템 해시 충돌은 예상 못했다. 왜냐면 상품 해시 충돌된 것을 해결하면(상품 충돌난 아이템까지 삭제했기에) 아이템 중복 기준에 상품 ID가 있기 때문에 아이템 해시 충돌은 전혀 나지 않을 것이라 예상했기 때문이다. 그런데 아이템 해시 충돌이 발생했다.

앞선 코드에 보여줬던 것이 하나라도 충돌되면 멈추는 로직이기때문에 몇십만개의 아이템을 다 돌리면서 충돌날때마다 에러나는 상황은 너무나도 시간이 많이 들기때문에

에러나더라도 계속 돌아가도록 해당 부분을 try-Catch로 감싸서 충돌나서 에러가 나더라도 멈추지 않도록 했고, 페이지네이션으로 3000개씩 데이터를 돌렸더니 몇십만개의 데이터가 모두 거의 5분만에 다 돌아가서 모든 충돌을 잡아낼 수 있었다. 덕분에 빠르게 어떤 아이템들이 충돌났는지 확인할 수 있었다.

거의 40개의 아이템이 충돌났고, 상품으로 따지자면 3개 상품에 대한 아이템들이었다. 알고보니 임의로 수동으로 넣은 아이템 데이터에서 발생한 것이다. 따라서 해당 아이템들과 상품들을 삭제하니 해결되었던 문제였다.

해시 충돌된 상품들과 아이템들이 다행히 모두 주문이 나간적이 없거나 테스트 상품이라서 삭제하기 어렵지 않았다. 물론 해당 상품과 아이템들을 삭제하기 위해 그 하위 집단인 벤더아이템, 딜벤더아이템, 딜 등의 삭제 작업들을 추가로 해줘야 하지만 오더가 없었기에 스위칭이 아닌 삭제로 해결할 수 있었던 작업이었다. 이번 작업을 통해 실무자와 커뮤니케이션도 해야하고 배포 작업도 걸려있어서 부담이 많이 가는 작업이었다. 하지만, 정말 값진 경험이었다. 앞으로 중복검사나 데이터 마이그레이션이 필요할때 이번 경험이 큰 도움이 될 것 같다.

profile
배우는 것이 즐겁다!

0개의 댓글