목차
- 불변성이 지켜지는 안전지대
- 방어적 복사(=깊은 복사)
- Copy on Write(=얕은 복사)
- 방어적 복사 구현
- 마무리
불별성이 지켜지는 안전지대
안전지대라는 용어는 이 책에서 사용되는 용어입니다. 안전지대란 불변성이 지켜지는 코드 영역을 말합니다. 아래 그림을 보죠.
안전지대는 불변성이 지켜지기 때문에, 안전지대 안에서의 데이터는 신뢰할 수 있습니다.
하지만
- 안전지대 밖의 데이터
- 안전지대 밖에서 오는 데이터
- 안전지대 밖으로 나가는 데이터
들은 불변성이 지켜지지 않은 코드영역을 거치거나 돌아다녔기 때문에, 데이터가 바뀌지 않았음을 보장할 수 없습니다.
그럼 안전지대 밖의 코드영역은 어떤 것들이 있을 까요?
- 부수효과가 있는 영역
- 레거시 코드 영역
- 외부 라이브러리의 코드 영역
등등이 있습니다.
함수형 프로그래밍에서는 최대한 안전지대에서 만큼은 데이터 신뢰를 보장하기 위해, 2가지 복사 기법을 사용합니다.
- 방어적 복사
- Copy on Write
입니다. 지금부터 알아봅시다.
방어적 복사
방어적 복사는 받은 인자나 출력값에 대해 깊은 복사를 하는 기법을 말합니다. 깊은 복사에 대해 생소하시다면 인터넷에 검색하면 많은 자료가 있으니 찾아보시길 바랍니다. 이 글에서 정리할 것은 방어적 복사는 언제 사용하는가? 입니다.
책에서는 2가지 사용시점이 있다고 합니다.
- 데이터가 안전지대에 들어올 때
- 데이터가 안전지대에서 나갈 때
먼저 그림으로 봅시다.
안전지대는 소화기🧯로 그 밖의 영역은 화재🔥로 표현해 봤습니다. 그림으로는 개념은 이해할 수 있지만, 실전에서 써먹고자 한다면 역시 코드로 예제를 보는 것이 좋습니다.
예시 들기
아래의 가정을 해보겠습니다.
직원의 급여 계산을 하는 코드를 외부 라이브러리를 씁니다. 외부라이브러리 코드 중에 payrollCalc(employee)라는 함수를 사용할 겁니다.
payrollCalc(employee)를 쓴다고 했을 때, payrollCalc()로 넘어간 employee 데이터는 그 이후로부터 믿고 쓸 수 있을까? 그건 알 수 없습니다.
- JS 에서 함수 인자로 넘어가는 객체는 얕은 복사가 이뤄집니다. ('함수형 코딩을 읽고(1~6 챕터)'에서도 잠깐 언급드렸습니다)
- payrollCalc() 내부에서 어떤 행위를 하는지 알 수가 없습니다.
이때 앞서 본 그림과 같이 방어적 복사를 해봅시다.
const payrollCalcSafe(employee) {
// 안전지대에서 불안전지대로
let copy = deepCopy(employee);
let payrollChecks = payrollCalc(copy);
// 불안전지대에서 안전지대로
return deepCopy(payrollChecks);
}
방어적 복사의 한계점
하지만 방어적 복사는 딱 이 두 경우에만 사용하는 것이 좋습니다. 깊은 복사를 하는 만큼 데이터 복잡도가 높을수록 컴퓨팅 자원이 많이 들기 때문입니다. 물론 현대의 컴퓨터 성능은 훌륭하지만, 잦은 깊은 복사는 피하는 것이 좋습니다.
특히 여러 명의 사용자를 상대해야 하는 서버에서는 더더욱 조심히 써야겠죠. 그래서 그 외의 경우에는 Copy On Write 기법을 활용하는 것이 좋습니다.
Copy On Write
Copy On Write는 방어적 복사와 달리 얕은 복사를 기반으로 합니다. '함수형 코딩을 읽고 (1~6 챕터)'에서도 잠깐 설명했지만 얕은 복사는 가장 최상에 있는 데이터는 복사되지만, 그 아래 참조 데이터들은 여전히 기존 참조값을 가리킵니다.
그래서 구조적 공유를 활용하여, 필요한 부분만 사본을 떠놓는 기법을 구사하죠. 그럼 Copy On Write는 언제 사용하면 될까요?
책에서는 1가지 사용시점을 이야기합니다.
- 안전지대 안에서 사용
사용을 늘려가다 보면, Copy On Write 덕분에 안전지대가 확장이 됩니다.
코드로 확인해 보면 아래와 같습니다.
const modify = (employee, key, value) => {
// Copy On Write
let copiedEployee = { ...employee };
copiedEployee[key] = value
return copiedEployee
}
const payrollCalcSafe(employee) {
// 방어적 복사
let copy = deepCopy(employee);
let payrollCheck = payrollCalc(copy);
return deepCopy(payrollCheck);
}
const main = () => {
const employee = { ... };
const senior = modify(employee, 'age', 55);
const payroll = payrollCalcSafe(senior);
}
Copy On Write의 한계점
당연히 필요한 부분만 복사를 하고, 반영하는 Copy On Write 는 방어적 복사보다 컴퓨팅 자원을 적게 사용하는 장점이 있습니다. 다만 Copy On Writer를 하려면
- 코드로 제어가 가능한 부분이어야 한다
- 데이터의 구조를 알아야 한다
위의 조건이 충족되어야 합니다.
깊은 복사 구현
책에서는 Javascript로 깊은 복사를 제대로 구현하는 것은 쉽지 않다고 합니다. 불가능한 것이 아니라 그만큼 비용이 든다는 거죠. 그래서 Lodash 라이브러리의 .cloneDeep() 함수 사용을 추천합니다.
하지만, Lodash는 용량이 큰 무거운 라이브러리이기 때문에, cloneDeep() 쓰기 위해, 설치하는 건 좋습니다. 그래서 Lodash를 제대로 쓰고 있다면, cloneDeep() 함수를 쓰고, 그렇지 않다면, util 성으로 따로 직접 구현을 하는 게 좋을 것 같습니다.
코드를 직접 구현해 봤습니다.
const deepCopy = (obj) => {
if (!obj) {
return null;
}
if (typeof obj === 'object') {
const newObj = {};
for (const key in obj) {
newObj[key] = deepCopy(obj[key]);
}
return newObj;
}
if (Array.isArray(obj)) {
return obj.map(deepCopy);
}
return obj;
}
막상, 구현해 보니 위 코드도 모든 타입을 고려한 게 아니라는 걸 알게 됐습니다. Array나 Object 중에서도 key, value 형태만 가능하겠죠?
마무리
얕은 복사와 깊은 복사가 어떤 개념인지는 알고 있었지만, 실제로 잘 알고 있는 것이 아니었습니다. 이번 학습으로 인해 2가지를 명확하게 알게 된 것 같습니다.
- 언제, 어디서 방어적 복사(=깊은복사)를 사용하고, Copy on Write(=얕은 복사)를 사용하는지에 대한 기준!
- 각각의 기법을 사용했을 시 어떤 Trade Off 를 가져갈 수 있는지
그리고 더 나아가 신뢰를 보장하는 불변성 영역을 구축해 가기 위해, 이 두 기법의 사용을 적절하게 배치해야겠다는 생각을 하게 됩니다.
'Dev.' 카테고리의 다른 글
함수형 프로그래밍 직접 해본 경험 (5) | 2023.11.24 |
---|---|
함수형 코딩을 읽고 (1~6챕터) (0) | 2023.06.29 |
Playwright, Auth 자동화와 API Mocking (0) | 2023.06.13 |
테스트도 전략이다 (0) | 2023.05.27 |
E2E 테스트로 왜 Playwright 선택했는가? (0) | 2023.05.27 |