티스토리 뷰


비합리적이거나 애매한 가정에 기반해서 코드가 작성되거나 다른 개발자가 잘못된 일을 하는 것을 막지 못할 때 코드는 오용되기 쉽다. 코드를 잘못 사용할 수 있는 몇 가지 일반적인 경우는 다음과 같다.

  • 호출하는 쪽에서 잘못된 입력을 제공
  • 다른 코드의 부수 효과(입력 매개변수 수정 등)
  • 정확한 시간이나 순서에 따라 함수를 호출하지 않음
  • 관련 코드에서 가정과 맞지 않게 수정이 이루어짐

위와 같은 상황을 막기 위해선 코드를 오용하기 어렵게 설계하고 작성하는 것이 중요하다.
아래에서 코드를 쉽게 오용할 수 있는 경우를 살펴보고, 오용하기 어렵게 만드는 기법을 알아보자.


불변 객체로 만드는 것을 고려하라

객체를 불변으로 만드는 것이 항상 가능하지도 않고, 또 항상 적절한 것도 아니다. 하지만 가변적인 객체는 코드의 복잡성을 늘리고 문제를 일으킬 수 있기 때문에, 기본적으로는 불변적인 객체를 만들되 필요한 곳에서만 가변적이 되도록 하는 것이 바람직하다.


가변 클래스는 오용하기 쉽다.

클래스를 가변적으로 만드는 가장 일반적인 방법은 세터(setter) 함수를 제공하는 것이다.


아래는 세터 함수로 쉽게 오용될 수 있는 예이다.

class TextOptions {
    private Font font;
    private Double fontSize;

    TextOptions(Font font, Double fontSize){
        this.font = font;
        this.fontSize = fontSize;
    }

    void setFont(Font font) { // <- 폰트는 setFont()를 호출해서 언제든지 변경할 수 있다.
        this.font = font;
    }

    void setFontSize(Double fontSize) { // 폰트의 크기는 setFontSize()를 호출해서 언제든지 변경할 수 있다.
        this.fontSize = fontSize;
    }

    Font getFont() {
        return this.font;
    }

    Double getFontSize() {
        return this.fontSize;
    }

}

만약 다른 개발자에 의해 setFont와 setFontSize가 무작위로 호출되어 값이 변한다면 의도와는 전혀 다른 결과가 나올 것이고, 이를 찾아내기도 쉽지 않다. 이와 같은 상황을 막기 위한 방법으로는 두 가지를 들 수 있다.


객체를 생성할 때만 값을 할당하라

모든 값이 객체의 생성 시에 제공되고 그 이후로는 변경할 수 없도록 함으로써 클래스를 불변적으로 만들 수 있고 오용도 방지할 수 있다. 여기에 final 키워드를 사용한다면 해당 변수를 변경하는 코드를 실수로라도 추가하는 것을 방지할 수 있게 된다.


아래는 세터 함수를 제거하고, 객체 생성시에만 값을 할당하도록 한 예이다.

class TextOptions {
    private final Font font;
    private final Double fontSize;

    TextOptions(Font font, Double fontSize){ // 멤버 변수는 생성 시에만 설정된다.
        this.font = font;
        this.fontSize = fontSize;
    }

    Font getFont() {
        return this.font;
    }

    Double getFontSize() {
        return this.fontSize;
    }

}

불변성에 대한 디자인 패턴을 사용하라

클래스에서 세터 함수를 제거하고 멤버 변수를 final로 표시하면 클래스가 불변적이 되고 버그를 방지할 수 있다. 하지만 만약 일부 값이 반드시 필요하지 않거나 불변적인 클래스의 가변적 버전을 만들어야 한다면, 빌더 패턴을 사용할 수도 있을 것이다.


class TextOptions {
    private final Font font;
    private final Double fontSize;

    TextOptions(Font font, Double fontSize){ // 멤버 변수는 생성 시에만 설정된다.
        this.font = font;
        this.fontSize = fontSize;
    }

    Font getFont() {
        return this.font;
    }

    Double getFontSize() {
        return this.fontSize;
    }

}

class TextOptionsBuilder {
    private final Font font;
    private Double? fontSize;

    TextOptionsBuilder(Font font) { // 빌더는 생성자를 통해 필수 값을 받는다.
        this.font = font;
    }

    TextOptionsBuilder setFontSize(Double fontSize) { // 빌더는 세터 함수를 통해 필수적이지 않은 값을 받는다.
        this.fontSize = fontSize;
        return this;
    }

    TextOptionsBuilder build() { // 모든 값이 정해지고 나면 호출하는 쪽에서는 TextOptionsBuilder 객체를 얻기 위해 build를 호출한다.
        return new TextOptions(font, fontSize);
    }

}

객체를 깊은 수준까지 불변적으로 만드는 것을 고려하라

클래스가 실수로 가변적으로 될 수 있는 일반적인 경우는 깊은 가변성때문인데, 이 문제는 멤버 변수 자체가 가변적인 유형이고, 다른 코드가 멤버 변수에 엑세스할 수 있는 경우에 발생할 수 있다.


class TextOptions {
    private final List<Font> fontFamily; // fontFamily는 여러 폰트를 가지고 있는 리스트다.
    private final Double fontSize;

    TextOptions(List<Font> fontFamily, Double fontSize){ 
        this.fontFamily = fontFamily;
        this.fontSize = fontSize;
    }

    Font getFontFamily() {
        return this.fontFamily;
    }

    Double getFontSize() {
        return this.fontSize;
    }

}

위 코드가 있을 때, 다른 코드에서 getFontFamily를 호출해서 값을 변경하면 TextOptions 클래스가 참조하는 fontFamily에도 영향을 끼치게 된다.

List<Font> fontFamily = textOptions.getFontFamily();

// textOptions 클래스가 참조하는 리스트와 동일한 리스트를 수정한다.
fontFamily.clear();
fontFamily.add(Font.COMIC_SANS);

실제로 위와 같은 문제는 찾아내기 매우 어렵고 이상한 버그를 일으킬 것이다. 아래는 이를 막기 위한 두 가지 방법이다.


방어적으로 복사하라

클래스가 참조하는 객체가 클래스 외부의 코드에서는 참조할 수 없도록 하면 이 문제를 방지할 수 있는데, 클래스가 생성될 때 게터(getter) 함수를 통해 객체가 반환될 때 객체의 복사본을 만들면 된다.


class TextOptions {
    private final List<Font> fontFamily; // 이 클래스만이 참조하고 있는 fontFamily의 복사본
    private final Double fontSize;

    TextOptions(List<Font> fontFamily, Double fontSize){ 
        this.fontFamily = List.copyOf(fontFamily); // 생성자는 리스트를 복사하고 그 복사본에 대한 참조를 갖는다.
        this.fontSize = fontSize;
    }

    Font getFontFamily() {
        return List.copyOf(this.fontFamily); // 리스트의 복사본을 반환한다.
    }

    Double getFontSize() {
        return this.fontSize;
    }

}

하지만 위의 경우는 복사하는 데 더 많은 비용이 들 수 있고, 클래스 내부에서 발생하는 변경을 막아주지는 못한다.


불변적 자료구조를 사용하라

불변적 자료구조를 사용한다면, 생성되고 나면 아무도 내용을 변경할 수 없다는 것이다. 이것은 방어적으로 복사본을 만들 필요 없이 객체를 전달할 수 있는 것은 의미한다.


class TextOptions {
    private final ImmutableList<Font> fontFamily; // 클래스 내에서도 ImmutableList의 내용을 변경할 수 없다.
    private final Double fontSize;

    TextOptions(ImmutableList<Font> fontFamily, Double fontSize){ 
        this.fontFamily = fontFamily; 
        this.fontSize = fontSize;
    }

    Font getFontFamily() {
        return this.fontFamily;
    }

    Double getFontSize() {
        return this.fontSize;
    }

}

불변적인 자료구조를 사용하는 것은 클래스가 깊은 불변성을 갖도록 보장하기 위한 좋은 방법 중 하나다. 방어적으로 복사해야 하는 단점을 피하고 실수로라도 클래스 내의 코드에서 변경되지 않도록 보장한다.



요약

  • 코드가 오용되기 쉽게 작성되고 나면 어느 시점에선가는 오용될 가능성이 크고 이것은 버그로 이어질 수 있다.
  • 코드가 오용되는 몇 가지 일반적인 사례는 다음과 같다.
    • 호출하는 쪽에서 잘못된 입력을 제공
    • 다른 코드에서 일어나는 부수 효과
    • 함수 호출 시점이 잘못되거나 올바른 순서로 호출되지 않는 경우
    • 원래의 코드에 연관된 코드를 수정할 때 원래의 코드가 내포한 가정과 어긋나게 수정하는 경우



Reference
톰 롱. 『좋은 코드, 나쁜 코드』. 제이펍, 2022.

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/01   »
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 31
글 보관함