티스토리 뷰

반응형

소프트웨어 개발 과정에서 예기치 못한 상태 변경으로 인한 버그를 마주합니다. 이런 경우 디버깅도 힘들어지고 이후 객체의 개발 의도가 변경되고 비대해지면서 더 찾기가 힘들어집니다. 이런 문제를 개선하기 위해선 어떤 방법이 있을까요?

불변성

불변성이란 한번 생성된 객체는 이후 어떤 방법으로든 상태가 변경되지 않는 것을 말합니다. 불변 객체는 상태가 바뀌지 않기 때문엔 여러 스레드에서 동시접근 해도 안전하고, 예측가능하고 디버깅이 쉬운 코드 작성을 돕습니다.

보통 final class로 불변 클래스를 선언해주고 내부적으로도 상태를 변경하는 코드가 없어야 합니다. 다음은 불변을 위반한 상황입니다. 

val person = Person("1996-08-12", "golf", 27, 179, 72)

println(person.toString()) // 나이 : 27

person.age++

println(person.toString()) // 나이 : 28

 

val person은 불변하는 변수이지만 내부 상태가 변경 가능하기 때문에 변경이 외부 어디서든 자유롭게 변경이 가능하며 여러 스레드가 상태를 동시에 변경도 가능합니다. 가급적 피해야하는 구현입니다. 

 

이러한 솔루션을 위해 불변 객체를 선언해야하는데 대표적으로 Kotlin에 List가 있습니다. 

다음과 같이 코틀린에선 List 내부의 상태를 변경할 수 없습니다. 철저한 불변이라고 볼 수 있습니다. 

이러한 경우 멀티스레드의 경쟁조건과 외부 무분별한 변경으로 부터 우리 비즈니스를 보호할 수 있습니다. 변경되지 않는다는 강력한 전제로 인해 코드도 간결해집니다. 

 

그러면 이러한 불변 객체가 상태를 변경해야하는 요구사항이 생긴다면 어떻게 해야할까요?  불변 객체는 아까도 말씀드렸다 싶이 상태를 변경하지 못합니다. 그렇다고 해도 상태를 변경해야하는 경우 불변 객체를 깨버리는 경우 사이드 이펙트는 커질 수 있다. 

Defensive Copy 

defensive copy란 방어적 복사를 뜻합니다. defensive copy를 통해서 위 문제를 해결할 수 있다. 방법은 간단하다. 내부 상태를 똑같이 들고 있는 복사본을 새로운 객체에 담아 리턴하는 방식이다. 

 

아래 코드를 보자 

class Person(
    private val birth,
    private val name,
    private val age,
    private val height,
    private val weight
) {

    fun copy(): Person {
    	new Person(
            birth = this.birth,
            name = this.name,
            age = this.age,
            height = this.height,
            weight = this.weight
        )
    }
}

 

Person 클래스는 copy할 때 본인 클래스를 그대로 리턴하는것이 아닌 다른 인스턴스지만 똑같은 정보를 갖는 인스턴스를 반환합니다. 이럴 경우 디버깅 시 외부에서 변경될 이유가 없기 때문에 copy시 이전과 현재 상태만 확인해본다면 쉽게 오류 원인을 찾을 수 있을 것입니다. 

 

typescript 에서도 마찬가지입니다. 대표적으로 mutableList와 deep copy를 구현한 예제를 찾아봅시다. 

import _ from "lodash"

class MutableList<T> {
    
    private list: T[] = [];
    
    constructor(list: T[]) {
        this.list = list;
    }
    
    public add(item: T): MutableList<T> {
        const copyList = _.cloneDeep(this.list);
        copyList.push(item);
        return new MutableList(copyList);
    }
    
    public remove(index: number): MutableList<T> {
        const copyList = _.cloneDeep(this.list);
        copyList.splice(index, 1);
        return new MutableList(copyList);
    }
    
    // ... 이후 구현
}

 

위 예제는 lodash 라이브러리를 사용하여 list deep copy 후 필요한 연산 후 새로운 인스턴스를 반환해주고 있습니다. 마찬가지로 상태가 변경 되는게 아닌 상태가 다른 새로운 객체가 생기기 때문에 안정적이고 간결한 코딩이 가능해집니다. 

Deep Copy?

본 객체의 모든 필드와 그 필드들이 참조하는 객체들까지 모두 복사하여 새로운 객체를 생성하는 것을 말합니다. 즉, 원본 객체와 복사된 객체는 완전히 독립적인 상태가 되며, 하나의 객체를 수정해도 다른 객체에 영향을 미치지 않습니다.

 

반면, 얕은 복사(Shallow Copy)는 최상위 객체의 필드만 복사하며, 객체가 참조하는 하위 객체들은 참조만 복사합니다. 따라서 원본 객체와 복사된 객체는 하위 객체를 공유하게 되어, 하위 객체의 변경은 두 객체 모두에 영향을 미칠 수 있습니다.

 

위 예제 같은 경우 typescript에서 Deep copy를 사용했는데 이유는 다음과 같습니다. 

  • 상태 관리 : 프론트엔드 애플리케이션에서는 상태(state)를 관리하는 것이 중요합니다. 상태가 변경될 때마다 UI를 업데이트해야 하기 때문에, 상태 변경이 발생할 때 불변성을 유지하는 것이 중요합니다.

  • 참조 문제 방식: 얕은 복사는 객체의 참조를 복사하기 때문에 원본 객체와 복사된 객체가 동일한 하위 객체를 참조하게 됩니다. 이는 원치 않는 부작용(side effects)을 발생시킬 수 있습니다.

typescript가 많이 쓰이는 FE 진영에선 다음과 같은 이유 등으로 deep copy  및 defensive copy가 적극적으로 쓰이고 있습니다. 

 

그럼 단순 장점만 있을까요? 불변 객체 및 defensive copy는 다음을 주의하여야 합니다. 

  • 객체가 너무 많이 사용하여 memory 사용량이 증가 한다. 대량의 객체 생성이 readonly임에도 불구하고 defensive copy를 사용한다면 오히려 득보단 실이 많을 것이다. 이럴떈 불변 객체로 선언한 후 defensive copy를 미적용하는 것도 좋습니다.

  • 개발 생산성이 느려질 수도 있다. defensive copy를 적용하기 위해 immer, lodash 등 적절한 라이브러리를 선택해야할 것이고 defensive copy에 대한 이해와 구현 노력 비용이 들어간다. 한시가 급한 비즈니스면 패스하는 것도 좋은 선택입니다.

마치며

defensive copy와 deep copy 불변성에 대해서 알아보았습니다. FE 진영에서 정말 중요하고 특히 함수형 프로그래밍의 핵심 가치중 하나인 순수함수를 만들기 위해선 상태가 매우 중요하게 다뤄집니다. 외부의 영향을 안받는 순수함수를 만들고 또 외부에 영향 받지 않는 불변 객체를 만들어 복잡한 체이닝 함수에서 상태가 일관될 수 있도록 하고 변경 추적이 용이하게 만들기 위해 필수입니다. 

 

꼭 FE 개발자라면 공부해보시길 바랍니다. 

 

마침.

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/09   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
글 보관함