최근 회사에서 개발중이던 프로젝트에서 부하테스트를 하던 중, 예상치 못한 버그가 발생했습니다.

원인을 파악하는 과정에서 JS의 Number와 Array에 대해 다시한번 공부하게 되었던 이야기를 해보려고 합니다.

사실 부하 테스트 수준으로 요청이 몰리는 경우가 아니면, 거의 발생하지 않는 점 그리고 개발중인 제품의 특성상 그만큼의 트래픽이 발생하지는 않을 것이라는 점에서

해당 이슈가 급하게 해결되어야 하는 문제가 아니라고 판단을 하였고, 해결 방법을 정하기 위해 팀 내에서 의견을 모으고 있습니다.

첫 번째 원인

첫 번째 원인은 어떤 도메인 객체를 생성할 때, 메서드가 호출 된 시간을 yyyyMMddHHmmssSSS 형식으로 사용해 데이터에 그룹아이디 라는 속성을 부여하는 방식에 있었습니다.

부하테스트 수준으로 요청이 몰리는 경우, 대부분의 요청은 그룹이 제대로 분리가 되었지만 간헐적으로 서로 다른 스레드에서 처리된 요청이 밀리세컨드 단위까지 일치하였고

서로 관련이 없는 객체에 동일한 그룹아이디 속성 값이 부여되는 문제가 있었습니다.

이 문제는 각 요청을 처리한 스레드의 아이디를 그룹아이디 속성에 ### 형식으로 추가하는 방식으로 해결을 하거나, 아니면 그룹아이디를 완전 새로운 방식으로 만드는것이 해결책이 될 수 있을것 같습니다.

두 번째 원인

두 번째 원인은 그 값을 이용해 클라이언트 사이드에서 Number(value)를 이용해 파싱하는데 있었습니다.

Javascript의 모든 숫자는 IEEE 754(double-precision 64-bit binary format) 을 따릅니다.

쉽게 얘기하면 JS의 모든 숫자는 Double 형식이라고 생각하면 됩니다.

브라우저 콘솔에서 0.1 + 0.2 === 0.3을 적으면 false가 출력되는 예제를 한번쯤 봤을 것입니다.

0.1 + 0.2 를 출력해보면 0.30000000000000004 라는 값이 나옵니다.

배정밀도 부동소수의 가수부는 총 52비트 이기 때문에, 이 방식으로 정확히 표현가능한 최대 정수값은 2^52 - 1 이고 JS에서는 이 값을 Number.MAX_SAFE_INTERGER 라는 상수로 표현하고 있습니다.

이 값을 10진수로 표현하면 9,007,199,254,740,991 이 되고, 해당 값보다 큰 값을 입력하는 경우 동일한 값이 아닌 근사 값으로 표현 됩니다.

문제는 제가 파싱하려한 yyyyMMddHHmmssSSS 형식은 총 17자리 이었기 때문에 발생했었습니다.

밀리세컨드 단위가 일치하지는 않지만, 간소한 차이로 생성된 객체의 경우 Number로 파싱하는 과정에서 동일한 근사값을 갖게되었고

데이터를 파싱하는 과정에서 서로 다른 그룹이, 동일한 그룹으로 묶이는 문제가 발생했었습니다.

이 문제는 JS에서 Array가 갖는 독특한 성질을 이해한뒤, 파싱과정 없이 바로 object의 key로 등록하는 방식으로 해결 되었습니다.

근데 Array 좀 이상하네?

설명하자면 구구절절한 이유로, 클라이언트 사이드에서 그룹 아이디를 파싱한 아주 큰 값을 Array의 index로 지정하여 값을 할당하는 코드가 있었습니다.

근데 이렇게 할당한 값은 신기하게도 배열에 들어있지만, 찾을 수 없는 상태(non-indexible)의 값이 됩니다.

const arr = []
// 아래와 같이 0번째 인덱스에 1 이라는 값을 할당합니다.
arr[0] = 1
// 이제 이 배열은 길이가 1이 되었습니다. 이제 4번째 인덱스에 1을 또 할당하면
arr[4] = 1
console.log(arr)
// 이 배열을 출력하면 [1, empty x 3, 1] 라고 출력됩니다. 당연히 길이는 5 입니다. 
// 이번에는 2**32 - 1 번째 인덱스에 또 값을 할당하고, 배열을 다시 출력해봅니다.
arr[2**32 - 1] = 1
console.log(arr) // [1, empty × 3, 1, 4294967295: 1]
console.log(arr.length) // 5
console.log(arr[2**32 - 1]) // 1
console.log([...arr]) // [1, 1]

네 뭔가 이상합니다. 갑자기 배열안에 key, value 쌍이 생겼습니다.

분명 존재하는 값인데, 배열은 저 값을 모른체 합니다.

ECMA JS spec - array을 읽어보면 exotic object라는 표현이 나옵니다.

Array는 Object 이지만 숫자로된 키 값을 인덱스로 취급해주는 조금 특이한 녀석입니다.

근데 또 숫자로 된 키 값도 아무 숫자나 되는것도 아닙니다. 부호가 없는 32비트 정수형 으로 표현이 가능한 범위의 숫자만 가능합니다.

따라서 JS 스펙상 배열내에서 표현 가능하고 탐색 가능한 최대 인덱스는 2^32 - 1, 즉 4,294,967,295입니다.

만약 4,294,967,295보다 큰 값 또는 문자열을 인덱스로 하는 할당 연산이 요청되면, 해당 값들은 object의 key로 할당 됩니다.

놀랍게도 JS는 object에도 마치 배열의 인덱스를 지정해서 값을 할당하듯, 속성을 할당 할 수 있습니다.

const o = {}
o["key"] = 1
console.log(o) // { key: 1 }
console.log(o["key"]) // 1

만약 배열 내에서 키로 등록된 값들을 찾고 싶다면 아래와 같이 Object.keys() 함수를 이용할 수도 있고

Object.values()와 스프레드 연산자를 활용하면 배열에 들어있는 모든 값을 꺼낼 수도 있습니다.

console.log(Object.keys(arr)) // [0, 4, 4294967295]
console.log([...arr]) // [1, 1]
console.log([...Object.values(arr)]) // [1, 1, 1]

카테고리:

업데이트:

댓글남기기