Java, Immutable - 불변 객체 활용해보기

이전에 만든 Rectangle 클래스를 활용해 불변 객체를 이용한 사이드이펙트를 방지하는 코드 예제를 만들어본다.

...V1 은 불변객체가 없는 경우 발생되는 문제점을 확인하고 ...V2 이름의 코드는 발생된 문제점을 해결하는 과정이다.

Rectangle.class

import java.util.Objects;

public class Rectangle {

    private float width;
    private float height;

    public Rectangle(float width, float height) {
        this.width = width;
        this.height = height;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Rectangle rectangle = (Rectangle) o;
        return Float.compare(width, rectangle.width) == 0 && Float.compare(height, rectangle.height) == 0;
    }

    public float getWidth() {
        return width;
    }

    public void setWidth(float width) {
        this.width = width;
    }

    public float getHeight() {
        return height;
    }

    public void setHeight(float height) {
        this.height = height;
    }

    @Override
    public String toString() {
        return "Rectangle width=" +
                width +
                ", height=" + height;
    }
}

다음과 같이 불변이 아닌 일반 변수로 선언하고 객체를 수정할 수 있도록 getter, setter 메소드를 만들어둔다.

그리고 PlayerV1 클래스를 만들어 name, rectangle 변수를 만든다

Palyer.class

public class PlayerV1 {
    private String name;
    private Rectangle rectangle;

    public Player(String name, Rectangle rectangle) {
        this.name = name;
        this.rectangle = rectangle;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Rectangle getRectangle() {
        return rectangle;
    }

    public void setRectangle(Rectangle rectangle) {
        this.rectangle = rectangle;
    }

    @Override
    public String toString() {
        return "Player{" +
                "name='" + name + '\'' +
                ", rectangle=(" + rectangle +
                ")}";
    }
}

PlayerMainV1.class

public class PlayerMainV1 {
    public static void main(String[] args) {
        Rectangle rect = new Rectangle(99,40);

        Player playerA = new Player("A", rect);
        Player playerB = new Player("B", rect);

        // PlayerA 와 PlayerB 같은 크기를 갖고 있음
        System.out.println(playerA);
        System.out.println(playerB);

        // PlayerB 의 크기를 변경해야함
        Rectangle rect2 = playerB.getRectangle();
        playerB.getRectangle().setWidth(200);
        playerB.getRectangle().setHeight(150);

        System.out.println("Changed PlayerB Width=200, Height=150");
        System.out.println(playerA);
        System.out.println(playerB);
    }
}

출력 결과

PlayerV1{name='A', rectangle=(Rectangle width=99.0, height=40.0)}
PlayerV1{name='B', rectangle=(Rectangle width=99.0, height=40.0)}
Changed PlayerB Width=200, Height=150
PlayerV1{name='A', rectangle=(Rectangle width=200.0, height=150.0)}
PlayerV1{name='B', rectangle=(Rectangle width=200.0, height=150.0)}

Player2의 크기를 수정하였더니 Player1 덩달아 같이 수정되었다.
객체를 활용해 값을 수정한 경우 발생되는 사이드이펙트는 찾기가 어렵고 수정도 어렵게 만드는 요인이 된다.


PlayerMain 개선하기

앞서 발생한 사이드 이펙트 오류를 최소화하기 위해 불변 객체를 만들어주어야 한다.
Rectangle 클래스에서 멤버 필드 두 개를 final 키워드를 사용하여 불변 객체로 만들어 준 후 코드를 다시 작성한다.

Rectangle.class

private final float width;
private final float height;

추가로 setWidth, setHeight 멤버 필드를 수정하는 메소드도 제거한다.

Player.class

해당 클래스는 수정사항이 없다.

PlayerMainV2.class

public class PlayerMainV1 {
    public static void main(String[] args) {
        Rectangle rect = new Rectangle(99,40);

        Player playerA = new Player("A", rect);
        Player playerB = new Player("B", rect);

        // PlayerA 와 PlayerB 같은 크기를 갖고 있음
        System.out.println(playerA);
        System.out.println(playerB);

        // PlayerB 의 크기를 변경해야함
        // playerB.getRectangle().setWidth(200); // 컴파일 오류, 개발자는 여기서 불변 객체인 것을 알아야 한다.
        // playerB.getRectangle().setHeight(150); // 컴파일 오류, 개발자는 여기서 불변 객체인 것을 알아야 한다.
        playerB.setRectangle(new Rectangle(200, 150));

        System.out.println("Changed PlayerB Width=200, Height=150");
        System.out.println(playerA);
        System.out.println(playerB);
    }
}

출력 결과

Player{name='A', rectangle=(Rectangle width=99.0, height=40.0)}
Player{name='B', rectangle=(Rectangle width=99.0, height=40.0)}
Changed PlayerB Width=200, Height=150
Player{name='A', rectangle=(Rectangle width=99.0, height=40.0)}
Player{name='B', rectangle=(Rectangle width=200.0, height=150.0)}

PlayerB의 Rectangle 크기와 높이가 변경되고, 추가로 사이드 이펙트가 예상되었던 PlayerA의 Rectangle 값이 변경되지 않았다