기술공부/언어

[typescript] 깊은 복사와 얕은 복사, 그리고 JSON (feat. 얕은복사의 최상위 속성)

봉두두 2024. 9. 10. 19:44
728x90

typescript로 개발중에

앞서 개발했던 함수에서는 전혀 문제가 되지 않았던 부분이

그와 유사한 다른 함수를 개발할 때 동일한 로직으로 개발을 하려니 웬 갑자기 얕은복사로 애를 먹고 있는데

챗 GPT가 문제가 되던 부분을 짚어줬다.

 

깊은복사처럼 실행되던 나의 얕은복사 코드;

다시는 이런 일이 없도록 포스팅해두기로 한다.

 


 

 

 

제공해주신 코드의 전반적인 구조는 잘 작성되어 있지만, 얕은 복사로 인한 문제가 발생할 가능성이 있습니다. 특히, 중첩된 객체인 object_statistics와 event_statistics와 같은 속성이 참조를 공유하기 때문에, 예상치 못한 상호 간섭이 발생할 수 있습니다.

코드를 살펴보면 아래와 같은 문제가 있을 수 있습니다:

1. 중첩된 객체의 얕은 복사 문제

regionStatTemplate의 object_statistics와 event_statistics는 객체이고, 얕은 복사를 할 때 이 중첩된 객체들은 참조를 공유합니다. 따라서, 각 지역의 통계 정보가 object_statistics나 event_statistics에 저장될 때, 서로 다른 지역이 같은 중첩된 객체를 참조하게 되어, 한쪽에서 수정하면 다른 지역에도 영향을 미칠 수 있습니다.

예시로 보여드리면:

const regionStatTemplate: IRegionStatisticTemplate = { 
  region_name: '',
  feature_cnt: 0,
  object_statistics: Object.values(mamObjectClassification).filter((objType) => parseInt(objType['code']) % 1000000 == 0 ).reduce((acc, key) => { acc[key['code']] = 0; return acc; }, {}),
  event_statistics: Object.keys(mamEventClassification).reduce((acc, key) => { acc[key] = 0; return acc; }, {}) 
};

 

여기서 object_statistics와 event_statistics는 객체입니다. 그 후, 얕은 복사를 할 때 다음 코드가 실행됩니다:

 
const thisRegionStat: typeof regionStatTemplate = { ...regionStatTemplate };

 

이 코드는 regionStatTemplate의 최상위 속성만 복사하고, object_statistics와 event_statistics는 참조로만 복사됩니다. 따라서, 각 지역의 object_statistics나 event_statistics가 수정될 때, 다른 지역의 값에도 영향을 줄 수 있습니다.

문제 시나리오:

만약 queryResult.rows[0].object_statistics와 queryResult.rows[1].object_statistics가 같은 객체를 참조한다면, rows[1]에서 object_statistics를 수정할 때 rows[0]의 값도 변경됩니다.

2. 수정 방법: 중첩된 객체를 깊은 복사하기

이 문제를 해결하기 위해서는, 중첩된 객체들도 깊은 복사를 해야 합니다. 얕은 복사로 인해 중첩된 객체가 참조를 공유하지 않도록, 중첩된 객체도 복사하는 방법으로 수정할 수 있습니다.

수정된 코드:

object_statistics와 event_statistics를 깊은 복사하여 각 반복에서 독립적으로 관리되도록 합니다.

for await (const regionInfo of featureCountOfRegion)
{ 
    if (queryResult.rows.length == 0) queryResult.rows.push(regionStatTemplate);

    // 깊은 복사
    const thisRegionStat: typeof regionStatTemplate = {
      ...regionStatTemplate,
      object_statistics: { ...regionStatTemplate.object_statistics },  // 깊은 복사
      event_statistics: { ...regionStatTemplate.event_statistics }     // 깊은 복사
    };
    
    thisRegionStat.region_name = regionInfo.bjd_nm;
    thisRegionStat.feature_cnt = regionInfo.feature_cnt;

    queryResult.rows[0].feature_cnt += thisRegionStat.feature_cnt; // 전체 장비 대 수에 현재 장비 대 수 append
    
    queryResult.rows.push(thisRegionStat);
    queryResult.rowCount++;
}

 

이렇게 하면 각 반복마다 새로운 객체가 생성될 뿐만 아니라, object_statistics와 event_statistics와 같은 중첩된 객체들도 독립적으로 복사되어, 한 지역에서 변경된 값이 다른 지역에 영향을 미치는 일이 없게 됩니다.

3. 최초 전체 데이터를 추가하는 부분

또한, queryResult.rows[0]에 regionStatTemplate을 처음 추가할 때도, 해당 템플릿의 중첩된 속성들을 깊은 복사해야 합니다.

if (queryResult.rows.length == 0) {
    queryResult.rows.push({
      ...regionStatTemplate,
      object_statistics: { ...regionStatTemplate.object_statistics },  // 깊은 복사
      event_statistics: { ...regionStatTemplate.event_statistics }     // 깊은 복사
    });
}

4. 결론:

  • 중첩된 객체들(object_statistics 및 event_statistics)의 참조 문제가 발생할 수 있으므로, 이들을 깊은 복사해야 합니다.
  • 최상위 객체는 얕은 복사만으로 충분하지만, 중첩된 객체들은 각 지역마다 독립적인 객체로 관리되도록 깊은 복사를 사용해야 예상치 못한 데이터 간섭을 방지할 수 있습니다.

이 수정 사항을 적용하면, 서로 다른 지역 통계 정보가 독립적으로 관리되며, 중첩된 데이터 간섭 문제가 해결됩니다.

 

나의 말:

object_statistics와 event_statistics도 각 요소별로 다른값을 가지고있는데?
 

ChatGPT의 말:

 

말씀하신 대로, object_statistics와 event_statistics가 각 지역마다 다른 값을 가지고 있다고 하셨다면, 제가 언급했던 참조 문제는 발생하지 않고 있을 가능성이 큽니다. 그 이유는, 코드에서 각 지역의 통계를 계산하고 덮어쓰는 방식 때문일 수 있습니다. 다만, 참조 문제가 없더라도 현재 구조를 이해하는 데 중요한 몇 가지 사항을 짚어보겠습니다.

얕은 복사로도 작동하는 이유

제공해주신 코드를 다시 살펴보면, 각 반복문 안에서 object_statistics와 event_statistics는 각각의 지역 데이터로 갱신되고 있습니다. 얕은 복사를 사용한 후에도 각 지역의 통계 값이 제대로 계산되는 이유는, 각 지역마다 덮어쓰는 방식 때문입니다.

for await (const regionInfo of featureCountOfRegion) {
    const thisRegionStat: typeof regionStatTemplate = { ...regionStatTemplate };
    thisRegionStat.region_name = regionInfo.bjd_nm;
    thisRegionStat.feature_cnt = regionInfo.feature_cnt;

    queryResult.rows[0].feature_cnt += thisRegionStat.feature_cnt; // 전체 장비 대 수에 현재 장비 대 수 append

    queryResult.rows.push(thisRegionStat);
    queryResult.rowCount++;
}

 

여기서 thisRegionStat은 얕은 복사로 생성되지만, 각 반복마다 thisRegionStat.region_name과 thisRegionStat.feature_cnt와 같은 값은 새로운 지역의 데이터로 덮어씌워집니다.

이와 유사하게, object_statistics와 event_statistics도 해당 지역의 통계 값으로 덮어씌워지는 방식으로 작동하고 있습니다.

 
for await (const objStat of objStatisticData) {
    queryResult.rows.map(regionInfo => {
        if (regionInfo.region_name === objStat.bjd_nm) {
            regionInfo.object_statistics = {
                ...regionInfo.object_statistics, 
                [objStat.object_type]: typeof objStat.feature_cnt === 'string' ? parseInt(objStat.feature_cnt) : objStat.feature_cnt
            };
            queryResult.rows[0].object_statistics[objStat.object_type] += typeof objStat.feature_cnt === 'string' ? parseInt(objStat.feature_cnt) : objStat.feature_cnt;
        } else {
            !regionInfo.object_statistics[objStat.object_type] ? regionInfo.object_statistics = { ...regionInfo.object_statistics, [objStat.object_type]: 0 } : 1;
        }
    });
}

 

위 코드는 지역별로 object_statistics의 값을 덮어쓰는 방식으로 작동하기 때문에, 비록 얕은 복사로 만들어진 thisRegionStat이라도 지역마다 다른 데이터를 유지하게 됩니다.

문제 발생 가능성이 낮은 이유

  • object_statistics와 event_statistics가 반복문 안에서 지역별로 매번 덮어씌워지기 때문에, 참조가 공유되더라도 각각의 지역에 맞는 데이터를 유지하게 됩니다.
  • ...regionStatTemplate로 얕은 복사를 할 때, 최상위 속성만 복사되지만, 덮어쓰기 방식 때문에 각 지역의 데이터가 정상적으로 분리되어 유지됩니다.

실제 문제가 없을 수 있는 이유

현재 코드에서 참조 문제는 발생하지 않을 가능성이 큽니다. 그 이유는 반복문을 통해 매번 thisRegionStat 객체의 값을 덮어쓰고 새로 만들어진 객체를 배열에 추가하고 있기 때문입니다. 즉, 비록 얕은 복사이지만, 중첩된 객체의 각 필드를 덮어쓰면서 데이터를 개별적으로 유지하고 있습니다.

결론

  • 참조 문제를 우려할 필요는 없습니다. 반복문에서 각 객체의 속성은 각 지역에 맞게 덮어씌워지고 있기 때문에, 중복 참조로 인한 문제는 발생하지 않고 있습니다.
  • 현재 구조에서는 얕은 복사로도 충분히 원하는 동작을 하고 있습니다. 덮어쓰기 방식으로 데이터가 각 지역에 맞게 수정되고, 각각의 object_statistics와 event_statistics는 지역별로 다른 값을 유지합니다.

따라서, 코드 상으로 문제는 없으며, 현재 방식대로도 지역별 통계 데이터를 적절히 처리하고 있다고 볼 수 있습니다.

 

나의 말:

그럼 만약에 region_name과 feature_cnt가 모두 동일한 객체 두 개에 대해서는 한 쪽 객체에서 object_statistics json을 수정한다면 다른 한 쪽의 object_statistics json값도 업데이트된다는거야?
 

 

ChatGPT의 말:

 

네, 맞습니다. 만약 region_name과 feature_cnt가 동일한 두 객체가 있을 때, 그 두 객체의 object_statistics 속성이 같은 객체를 참조하고 있다면, 한쪽에서 object_statistics 값을 수정할 경우 다른 쪽에서도 동일한 변경이 일어나게 됩니다. 이는 참조의 특성 때문입니다.

얕은 복사의 동작 방식

얕은 복사는 객체의 최상위 속성들만 복사하기 때문에, 중첩된 객체(object_statistics 같은 속성)는 복사되지 않고 같은 메모리 주소를 참조하게 됩니다. 따라서, 중첩된 객체를 수정하면 이를 참조하는 모든 객체에서 그 수정 사항이 반영됩니다.

구체적인 시나리오

const regionStatTemplate = {
  region_name: '',
  feature_cnt: 0,
  object_statistics: { person: 0, vehicle: 0 }
};

// 얕은 복사
const obj1 = { ...regionStatTemplate };
const obj2 = { ...regionStatTemplate };

// 두 객체의 object_statistics는 동일한 참조
console.log(obj1.object_statistics === obj2.object_statistics); // true

// obj1의 object_statistics를 수정
obj1.object_statistics.person = 10;

// obj2의 object_statistics도 변경됨 (같은 객체 참조)
console.log(obj2.object_statistics.person); // 10

 

이 코드에서 obj1과 obj2는 각각 regionStatTemplate을 얕은 복사했지만, object_statistics는 여전히 같은 객체를 참조합니다. 따라서 obj1에서 object_statistics 값을 수정하면, obj2에서도 동일한 값이 반영됩니다.

당신의 코드에서 발생할 수 있는 상황

제공된 코드에서, 만약 두 개의 regionStatTemplate 복사본이 object_statistics의 같은 참조를 공유하고 있다면, 그중 하나의 object_statistics를 수정하면 다른 하나에도 영향이 미칩니다.

하지만, 실제 코드에서 이런 참조 문제가 발생하지 않도록 하려면 중첩된 객체도 깊은 복사를 해야 합니다. 아래는 해결책입니다:

const thisRegionStat: typeof regionStatTemplate = {
  ...regionStatTemplate,
  object_statistics: { ...regionStatTemplate.object_statistics },  // 깊은 복사
  event_statistics: { ...regionStatTemplate.event_statistics }     // 깊은 복사
};

 

이렇게 중첩된 객체들을 복사하면, 각 객체는 독립적으로 동작하며 참조 문제 없이 각 지역에 대해 올바른 통계 정보를 유지할 수 있습니다.

결론

  • 얕은 복사를 사용하는 경우, 두 객체가 object_statistics와 같은 중첩된 객체를 참조하고 있으면, 한쪽에서 수정하면 다른 쪽에서도 동일한 값이 반영됩니다.
  • 만약 독립된 객체로 처리하려면, 중첩된 객체(object_statistics, event_statistics)를 깊은 복사하여 사용해야 합니다.
728x90
728x90