본문 바로가기
일지/헬키

[사고쳤다 / Mongodb Atlas] 헬키 개발일지 12. aggregate + $out의 위험성과 백업의 중요성 (feat. 200번째 글)

by 리나그(ReenAG) 2024. 9. 2.
728x90
반응형

 블로그에 쓰는 200번째 글이다. 그래서 좀 좋은 걸 들고 오고 싶어서 나름 11편에 예고했던 대로, three.js관련 내용을 들고 오려고 했다. 기왕이면 기분 좋게 200회를 맞이하는 게 좋지 않은가? (비공개글도 포함인지라 여러분은 뭔 소린가 싶을 수도 있다.) 근데 사고를 쳐도 정말이지 거하게 쳐버렸고, 내 팀원들에게 어떤 사고를 쳤는지 설명할 자료도 필요해서 관계로 이 글을 쓰려고 한다. 어떤 사고를 쳤고, 경과가 어떻게 되었는지 기록해두려고 한다. 결국 지옥을 내다모는 건 언제나 자신인가... 싶은 하루였다.

 

다시 돌아보자면 : 내가 친 사고에는 3가지 중요한 중심 원인이 있었는데,

1. aggregate의 $out에 대한 몰이해

2. aggregate를 코드로 하려고 했음(사실, 이럴 필요가 없다는 것을 배웠다. 나중에 후술 할 것이다.)

3. backup이 존재하지 않음

이다. 솔직히 이 중 1개라도 없었다면 아예 프로덕션 데이터베이스를 날리는 이런 어처구니 없는 사태를 적어도 어느 정도 복구하거나 방지할 수 있었을 텐데... 백업은 대체 왜 안 해놓은 거지 나 개발 잔데? 바본가? 아아아아아아아 군대에서 밥먹듯이 했던 작업인데? 미쳐버린 건가?

 

 10시정도에, 나는 우리 팀의 mongodb에 aggregate를 이용해서 totalworkoutoneweek -> totalworkout에 더하는 파이프라인을 만드려고 했다. 나는 당시에 aggregate 정확히는 $out을 정확히 이해하고 있지 못했다. 

 

 aggregate는 그 일련의 동작을 "파이프라인"이라 부른다. $lookup, $project, $unwind 등의 연산을 순차적으로 진행한 레코드들을 반환하기 때문이다. 순차적이라는 게 포인트인데, 예를 들어 이런 파이프라인이 있을 수도 있겠다.

 

1. User에서 Photo schema를 $lookup해서 같은 id를 가지는 스키마를 합친다. -> JOIN

2. 앞에서 받은 schema를 $unwind 해서 도큐먼트를 해제해서 인수로 만든다. -> {... } js operator

3. $project로 합쳐진 인수 중 원하는 것만 가져온다. -> SELECT

 

 이 경우에, 만든 컬렉션은 붕떠서 db에 영향을 주지 못한 채 쿼리만 하고 끝나게 된다. 이런 식으로 열람만 하고 끝내는 것도 aggregate의 사용법 중 하나다. 

 

 오늘의 문제는 그 마지막에 오는 $out 연산이다. 거창할 거 없고 sql이라던가 orm의 COMMIT과도 일맥상통한다. 인수로 받은 db, collection 명에 해당하는 자리에 연산된 레코드를 쓴다. 앞에 이야기한 파이프라인의 맨 마지막에 $out 연산을 이용하면 내가 만든 컬렉션을 실제로 db에 쓰게 된다.

 

 나는 이 "쓴다"라는게 아예 replace, 기존에 존재하는 컬렉션이 있다면 이를 아예 대체해 버린다는 특성이 있을 걸로는 생각하지 못하고 작업을 하게 되었고, 그게 큰 문제 중에 하나가 되었다. 존재하던 컬렉션과 합쳐지는 방식으로 동작할 것으로 생각했던 것이다.

 본래는 이 착각이 그렇게까지 큰 문제가 되지 않을 수도 있었는데, 나는 이것을 보통 aggregate의 제일 앞에 쓰는 $match연산자와 같이 쓴 게 문제가 되었다.

 

$match"이 조건에 해당하는 도큐먼트에만 작업"한다는 의미이다. 그래서 이것과 $out을 같이 써버린 나는 user table이 날아가는 마술을 경험했다.

$match로 어떤 특정한 도큐먼트만 선택된 채로, 그것만 남아 있는 컬렉션이 원래의 컬렉션을 밀어버리고 남아버린 것이다.

 

 atlas를 보면서 user의 상당수가 날아갔음을 인지한지 얼마간은 뭐지? 싶었다. 그러다가 갑자기 이해가 탁 되면서 날아갔나 싶었고, 결과는... 진짜 날아간 것이었다. 날아가고 10분간은 이걸 되돌릴 방법을 찾았었다. 근데 정말 놀랍도록 그런 속 편한 방법 따윈 없었다. 그래서 망했구나 싶었을 때 팀원에게서 전화가 왔고, 뭐... 날렸다는 이야기를 제외하고 해 줄 말이 없었다. 그래도 웹 캐시라던가 뭐 복구할 방법을 1시간 정도 더 찾아보고 확실히 없다 싶었고, 스키마에 남아있는 다른 정보를 이용해서 유저를 강제로 복구하는 것이 한계였다.

 그것도 팀원이 로그가 있다고 이야기 하기 전까지는 생각도 못하고 있었다.

 

 그리고 그 로그에서 데이터 뽑아내서 oid에서 타임스탬프를 뽑아내면서 별의 별짓을 다했지만 100% 복구하는 것은 일단 불가능했고, 이 짓하면서 원래 하려던 작업도 못하게 되었다.

https://steveridout.com/mongo-object-time/

 

MongoDB ObjectId to Timestamp Converter

Did you know that each MongoDB ObjectId contains an embedded timestamp of its creation time? From the mongo shell, you can use getTimestamp() to retrieve the timestamp from the ObjectId, but there's no built in function to generate an ObjectId from a times

steveridout.com

 

 2번 요인, aggregate를 굳이 코드로 할 필요 없다는 것은 atlas인터페이스를 쓰자는 것이다. atlas에서 데이터를 조작할 때, 보통은 collection browser를 제일 많이 쓰겠지만, 탭을 살짝 옮기면 aggregate도 있다. 나는 여기서 aggregate를 테스트만 할 수 있는 줄 알아서 내가 만든 어드민 툴에 그 파이프라인 코드를 넣어서 관리하고 있었다. atlas에서 작업하면 preview sample을 볼 수 있고, 결정적으로 $out의 동작방식이 replace라는 설명을 볼 수 있다...

 

 

 이걸 로그에서 유저 사용시간을 복구하면서 겨우 알았다. 너무 자신이 바보 같았다. 

 

3번째는 굳이 설명할 필요도 없이 백업이 없다는 것이다. 일단 관련한 것은 곧 작업하려 갈 예정이다.

더보기

하아... 마지막으로, 그냥 푸념 한 스푼 넣자면... 코로나도 걸린 것 같다는 것이다. 이 글 쓰면서 지금도 목이 칼칼하다. 오늘은 팀원들과 대면도 못했다. 머리 아프고, 피곤한 채로 작업하는데 미칠 것 같았다... 지금 한바탕 자고 나서 괜찮아지긴 했다만. 내 탓이라 누구한테 말할 것도 없다. 언제나 지옥으로 내모는 건 자신인데 뭐 누가 해 줄 말이 있을 리가. 악으로 깡으로 버텨야지! 싶다.

 

... 팀원들한테 정말 미안하다. 수도 없이 이야기 했지만... 후... 다행히 지금 테스트 규모가 크지 않아서 다행이지 안그랬으면 정말 큰일 날뻔 했다.

728x90
반응형