개발 이야기 안하는 개발자

Good Code, Bad Code (7) _ 코드를 오용하기 어렵게 만들라 본문

Book/Good Code, Bad Code

Good Code, Bad Code (7) _ 코드를 오용하기 어렵게 만들라

07e 2025. 9. 27. 15:43
반응형

 

클래스를 인스턴싱하고 데이터를 수정하는 행위는 생각보다 빈번하게 일어날 것이다.

이 행위가 내가 방금 로직을 작성하고 바로 사용한다면 괜찮을 거다.

 

이후에 누군가가 내 코드를 수정한다거나, 아니면 내가 제작한 인스턴스를 가져와서 참고하게 된다면 해당 데이터가 어디서 바뀌었는지, 어떤 의도로 바뀌었는지 찾아보기 어려울 것이다.

 

따라서 해당 쳅터에서는 클래스의 역할을 어떻게 규정하고 코드를 오용하지 않게 하는 방법을 소개한다.

 

오용하기 힘들게, Set 함수는 최대한 자제할것.

기존에 있던 Text 클래스가 있고 SetFont 와 SetSize 가 있다고 가정하자.

만약에 Text 인스턴싱을 얕은 복사를 통해 여러곳에서 사용하게 된다면 문제가 생긴다.

 

A 위치에 A 텍스트가 있다.

B 위치에 얕은 복사를 진행한 A 텍스트가 또 존재한다.

 

B위치에서 Set을 진행하면 A위치의 텍스트도 같이 Set한 데이터가 될탠데,
이런 문제를 B위치를 작업하는 작업자는 알 수가 없다.

 

즉, A 위치를 개발한 개발자가 오용하기 쉽게 Text 클래스를 정의한 문제이다.

 

 

위 같은 상황을 방지하기 위해선 다음 패턴을 추천한다.

- 빌더 패턴 추천

실 기능을 담당하고 Get 밖에 없는 Product 객체가 있다.

이 객체는 생성자에서 해당 멤버 변수들을 받을 뿐, 이후부턴 절대 수정이 불가능하다 (외부에서 수정 불가)

const 나 Final 을 붙이면서 값을 수정 못하게 지정하게 된다.

 

그리고 이 Product를 제작하는 Builder 가 있다.

이 빌더는 해당 Product를 어떻게 제작할지에 대한 내용이 포함되어 있다.

 

예를들어, Font 만 추가한다면 Font값을 추가한 Product를 생성한다.

또는 Font와 Size가 둘다 필요로 한다면 둘다 추가한 Product를 생성한다.

 

즉, 값은 Set을 할 수 없고 위 같이 Text에 문제 상황에서 값 수정을 막아버리는 형식으로 B 위치 작업자에게 값 수정을 못한다라고 알리는 방식이다. 

 

- 쓰기 시 복사 패턴

이 패턴은 위 텍스트 문제에서 B위치를 제작하는 작업자를 위한 패턴이라고 볼 수 있다.

A 클래스 내부에 값을 복사하여 새로운 객체를 반환하게 해주는 메소드를 포함하여 값을 Set하게 될 경우 새로운 객체를 반환하게 해주는 메소드를 추가하는 방식이라고 볼 수 있다.

class A {
private:
    TArray<int32> MyArray; // 공유 가능한 배열

public:
    A() {
        MyArray = {1, 2, 3}; // 초기화
    }

    // 읽기 전용 접근 (const 참조 반환)
    const TArray<int32>& GetArray() const {
        return MyArray;
    }

    // COW 패턴: 배열의 복사본을 만들어 수정 후 반환
    TArray<int32> ModifyArrayWithCOW(int32 NewElement) const {
        // 원본 배열 복사
        TArray<int32> ArrayCopy = MyArray;
        // 복사본 수정
        ArrayCopy.Add(NewElement);
        return ArrayCopy; // 수정된 복사본 반환
    }
};

 

 

참조 반환 문제

Get 을 통해 값을 복사 반환 받는게 아니고 참조를 받게 된다면 이것 또한 문제가 생길 수 있다.
예를들어 List를 Get으로 받은 상태에서 List의 원소를 수정하게 되면 원본 클래스에도 오염이 생긴다.

 

C++에선 이를 방지하기 위해선 const 를 사용 해야 한다.

또는 다른 언어에선 Immutable을 붙인 변수명을 사용해야한다.

 

 

가독성 오염

List<List<int>> 같은 방식에선 오용하기 쉽다 (어떤 데이터가 어떤 데이터인지 헷갈리기 때문)

마찬가지고 Pair 도 그러하다.

 

따라서, 이런 방식은 주석도 괜찮지만 문서화가 필요한 경우가 생긴다.

이런 경우에는 데이터를 객체화해서 가독성을 높이는게 제일 좋은 방법이다. (struct)

 

 

 

데이터에 대해 진실의 원천을 하나만 가져가야 한다.

기본데이터는 코드에 제공하는 데이터를 뜻한다.

파생 데이터는 기본 데이터를 가공해서 나온 데이터를 뜻한다.

 

특정 클래스에 데이터를 추가해야하 할때 파생 데이터는 클래스를 인스턴싱 하는 쪽에서 계산하는게 아니라 클래스 자체에서 계산하도록 해야 오용을 막을 수 있다.

class TestText
{
    int A;
    int B;
    int Minus;

    TestText(int a, int b, int aminusb)
    {
        A = a;
        B = b;
        Minus = aminusb;
    }
}

TestText(3, 5, 3-5);

위 코드에서 3-5 같은 경우는 해당 클래스를 사용하는 사람에게 실수를 유발할 가능성을 부여한다.

따라서 이런 내용은 해당 클래스 내부에 넣는게 옳다.

class TestText
{
    int A;
    int B;
    int Minus;

    TestText(int a, int b)
    {
        A = a;
        B = b;
        Minus = a - b;
    }
}

TestText(3, 5);

 

 

논리에 대해 진실의 원천을 하나만 가져야 한다.

코드의 한 부분에서 수행되는 일이 다른 부분에서 수행되는 일과 일치해야 한다.

 

Logger - 데이터 직렬화 => 기록

Loader - 데이터 읽기 => 데이터 역직렬화

 

이렇게 되어있는 각 2개의 클래스가 있는 상태에서 Logger의 내용이 바뀌어서 로직이 수정되었다면
Loader도 이에 대응하는 로직을 작성해 주어야하는데, 이를 놓칠 수 있다.

 

따라서, 이 2개의 내용을 하나로 합쳐서 실수 하지 않도록 하는게 좋은데, 여기서 가장 좋은 방법은 직렬화, 역직렬화 기능을 가지고 있는 클래스를 새로 만드는 것이다.

 

InitListForamt - 직렬화() , 역직렬화()

 

그래서 Logger와 Loader는 로직이 수정된다.

Logger - InitListFormat.직렬화 => 기록

Loader - 읽기 => InitListForamt.역직렬화

반응형