setter 메서드는 왜 지양해야 한다고 할까

ifi9·2023년 3월 13일
0
post-custom-banner

setter 메서드

우선 모든 setter 메서드의 사용법이 지양되어야 한다는 것은 아니다. setter 메서드를 만들 때 방법 중 하나인 Lombok의 @Setter가 있는데, 여기서 주로 지양하여야 할 점이 발생한다고 생각한다.

setter 장점

setter 메서드에 대해서 이야기를 해보면 단점을 주로 이야기하므로, 장점에 대해서도 생각해 보았다.

public class Member {
	private Long id;
    private String name;
    private String addr;
    
    public void setName(String name) {
        if (name == null || name.trim().isEmpty()) {
            throw new IllegalArgumentException("Name may not be null or empty.");
        }
        this.name = name;
    }

    public void setAddr(String addr) {
        if (addr == null || addr.trim().isEmpty()) {
            throw new IllegalArgumentException("Addr may not be null or empty.");
        }
        this.addr = addr;
    }
    
    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public String getAddr() {
        return addr;
    }
}

먼저 private 인스턴스 변수를 캡슐화하여, 외부 접근을 제한한다는 것을 알 수 있다. 그리고 메서드 내부에서 매개변수로 받은 값을 검증 또한 가능하다. 무엇보다 호출만 가능하다면 어디서든 객체의 상태를 자유롭게 변경할 수 있는 유연성을 가지고 있다.

이러한 장점들이 있지만, 오히려 유연성에서 파생되어 나오는 단점들이 부각된다.

setter 단점

예전에 본 회사 소스 코드 일부에는 setter 메서드가 여기저기서 호출되어 있는 것을 볼 수 있었다.
문제는 여기저기라는 것이 내가 미처 보지 못해서 놓친 부분이 있을 수도 있다는 것이다. 그리고 이렇게 public으로 선언되어 있으면 객체의 보안성을 저해할 수도 있다. 또한 해당 코드를 작성한 사람의 의도를 파악하기가 힘들다는 점이 있었다. 이러한 문제점은 결국 일관성을 유지하기 어렵게 만든다.

Lombok @Setter

Lombok의 @Setter는 왜 지양되어야 하는지 생각을 해본 적이 있었다. 나는 모든 인스턴스 변수의 setter 메서드를 만든다는 점이 가장 큰 문제라고 생각하였다.

위의 예제 소스 코드를 보면 private Long id에 대해서는 setter 메서드를 작성하지 않았다. DTO로 사용하는 경우에는 DB에 저장된 식별자 ID 값을 불러오면 되는 것이지, Auto Increment 혹은 Sequence로 자동 증분 되는 값에 대해 굳이 변경할 수 있는 기능이 필요가 없다고 생각했기 때문이다.
마찬가지로 변경할 필요가 없는 다른 변수가 있을 수도 있다. 이것 또한 만들 이유가 없기 때문에 모든 인스턴스 변수의 setter 메서드를 만드는 @Setter의 사용은 지양되어야 한다고 생각한다.

다른 불필요한 setter 메서드를 만들지 않은 예시는 Collections에서도 볼 수 있다. 아래는 그중 하나인 ArrayList class의 소스 코드이다.

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
	...
    
	/**
     * The size of the ArrayList (the number of elements it contains).
     *
     * @serial
     */
    private int size;
    
	...

	/**
     * Returns the number of elements in this list.
     *
     * @return the number of elements in this list
     */
    public int size() {
        return size;
    }
	
	...
    
    /**
     * Inserts the specified element at the specified position in this
     * list. Shifts the element currently at that position (if any) and
     * any subsequent elements to the right (adds one to their indices).
     *
     * @param index index at which the specified element is to be inserted
     * @param element element to be inserted
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public void add(int index, E element) {
        rangeCheckForAdd(index);
        modCount++;
        final int s;
        Object[] elementData;
        if ((s = size) == (elementData = this.elementData).length)
            elementData = grow();
        System.arraycopy(elementData, index,
                         elementData, index + 1,
                         s - index);
        elementData[index] = element;
        size = s + 1;
    }
    
    ...
    
    /**
     * Private remove method that skips bounds checking and does not
     * return the value removed.
     */
    private void fastRemove(Object[] es, int i) {
        modCount++;
        final int newSize;
        if ((newSize = size - 1) > i)
            System.arraycopy(es, i + 1, es, i, newSize - i);
        es[size = newSize] = null;
    }
    
    ...
}

ArrayList의 변수인 size의 getter 역할을 하는 size() 메서드는 존재하지만, setter 역할을 하는 메서드는 찾아볼 수 없다. add 혹은 remove 과정 중에서나 size의 변경을 볼 수 있는데, 임의로 size를 변경하는 것이 가능하다면 의도치 않은 결과가 나올 것이기 때문이다.

setter 메서드의 대안

setter 메서드의 사용과는 다르게 객체의 불변성을 이루기 위한 방법들이 존재한다. 그중에서 많이 사용되는 방법들을 적어보았다.

생성자 방식

public Member(String name, String addr) {
	if (name == null || name.trim().isEmpty()) {
    	throw new IllegalArgumentException("Name may not be null or empty.");
	}
	if (addr == null || addr.trim().isEmpty()) {
		throw new IllegalArgumentException("Addr may not be null or empty.");
	}
	this.name = name;
	this.addr = addr;
}

필요한 인스턴스 변수를 매개변수로 사용하는 객체를 생성한다. 객체가 생성될 때 모든 필드를 초기화할 수 있으므로 객체의 불변성을 보장할 수 있다.

하지만 필요한 인스턴스 변수의 개수가 늘어난다면, 코드가 길고 복잡해질 수 있다. 그리고 모든 매개변수 타입이 동일한 것이 많다면, 그것을 확인하기에 불편하다. 위의 경우에는 name과 addr의 순서가 헷갈려서 객체 생성 시에 잘못된 값을 넣을 수도 있다는 문제점이 존재한다.

정적 팩토리 메서드

public static Member updateMemberInfo(String name, String addr) {
	if (name == null || name.trim().isEmpty()) {
    	throw new IllegalArgumentException("Name may not be null or empty.");
	}
	this.name = name;
	this.addr = addr;
}

생성자 방식과 비슷하게 생긴 것으로 정적 팩토리 메서드 패턴이 있다. 생성자 방식과 같이 객체를 생성한다는 같은 역할을 하지만 몇 가지 차이점이 있다.

정적 팩토리 메서드 만의 특징은 여러 가지가 있으나, 그중 몇 가지만 언급하고자 한다.
가장 먼저 메서드 명을 가진다는 점이 있어서 생성하고자 하는 목적을 담아낼 수가 있다. 그리고 정적 메서드를 사용하여 객체를 생성하기 때문에 객체 생성에 대한 캐싱이나 재사용을 할 수 있다. 그렇지만 정적 메서드만으로 생성하기 때문에 클래스 상속을 이용한 확장이 힘들다는 단점이 있다.

Builder 패턴

private Member(Builder builder) {
	this.name = builder.name;
    this.addr = builder.addr;
}

public static class Builder {
    private String name;
    private String addr;

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

    public Builder setAddr(String addr) {
        this.addr = addr;
        return this;
    }

    public Member build() {
        return new Member(this);
    }
}

Builder 패턴은 생성자 방식에 비해서 가독성이 좋다는 장점이 있다.
위의 소스 코드를 실제로 사용한다면 이러할 것이다.

public class SetterExample {
    Member member = new Member.Builder()
            .setAddr("addr")
            .setName("name")
            .build();
}

이처럼 매개변수의 순서에 상관없이 값을 입력할 수도 있다. 그렇지만 Member 객체를 위한 Builder 객체를 생성해야 하므로, 불필요한 메모리 사용이 발생할 수 있다는 단점이 존재한다.

생각하는 선택 기준

처음 Builder 패턴을 알게 되었을 때에는 불필요한 매개변수 setter 메서드의 존재가 너무나도 싫어서 Builder를 막 사용하는 경향이 있었다. 사용을 하다 보니 1~3개 정도 매개변수를 몇 개 사용하지 않는 경우에도 Builder 패턴을 사용하려는 나를 볼 수가 있었는데, 그제야 이러한 사용법이 옳은 것인지 고민을 해보게 되었다.

고민 후 정하게 된 나의 기준으로는 이러하다.
예시로 든 Member 객체를 생성할 때 필요한 인스턴스 변수는 name과 addr로 2개다. 이 경우에는 Builder 패턴을 사용하지 않고 생성자 패턴 혹은 정적 팩토리 메서드를 사용할 것이다. 생성자 패턴으로도 충분히 가독성이 좋으며, 불필요한 소스 코드의 작성은 필요 없다고 생각했기 때문이다.
Builder 패턴은 생성자 방식을 사용하였는데 가독성이 좋지 않다는 느낌이 온다면 그때부터 사용하는 것으로 정하였다.
setter 메서드의 경우에도 필요하다면 사용할 수 있다. 예시로 든 Builder class를 보면 내부에서 setter 메서드를 사용하여 값을 넣어주고 있는 것을 볼 수 있다.

결론

무엇을 사용하든 간에 중요한 것은 이루고자 하는 목적을 잘 나타내는 메서드를 만드는 것이다. 그때그때 자신의 기준 혹은 팀 컨벤션에 맞는 방식을 사용하도록 하자.

post-custom-banner

0개의 댓글