처누

[201 Created] 2주차 getter와 setter는 왜 쓰지말라는걸까 본문

우테코

[201 Created] 2주차 getter와 setter는 왜 쓰지말라는걸까

처누 2023. 10. 31. 17:16

getter와 setter를 왜 사용할까? 

 객체 지향의 원칙 중 하나는 정보 은닉이다. 이러한 이유 때문에 자바에서는 클래스를 작성할 때 필드를 private으로 숨기고 데이터의 조회나 변경이 필요한 경우 getter와 setter 메서드를 사용하여 데이터의 값을 조회하거나 수정할 수 있다.

 

getter와 setter의 사용을 왜 지양하라는걸까?

 최근 우테코 6기 오픈카톡방에서도 자주 나왔던 얘기 중 하나이다. 나 같은 경우는 코딩테스트 공부하면서 getter와 setter를 정말 유용하게 사용한 경우가 많았는데 왜 지양하라는거지?라는 생각을 했고, 이번 기회에 제대로 알아보려한다.

 

 우선 getter와 setter자체가 정보 은닉과 모순되는 말이 아닐까 생각한다. 필드를 private으로 숨겼는데 그 값을 getter로 조회하고 setter로 수정한다는건 정보 은닉과는 반대되기 때문이다.

 

우선 가장 많이들 언급하는 setter의 사용을 지양해야 하는 이유 먼저 살펴보자.

setter의 사용을 지양해야 하는 이유

 아래 코드는 왜 setter사용을 지양해야하는지에 이해를 시켜준 코드이다.

class Account {
	private long balance;
    
    public Account(long balance) {
    	this.balance = balance;
    }
    
    public void setBalance(long balance) {
    	this.balance = balance;
    }
}

Account myAccount = new Account(500);
myAccount.setBalance(1000);

출처 : https://colabear754.tistory.com/173

 

이 코드는 myAccount의 잔액을 1000으로 설정하고 있다. 그런데 잔액이 왜 1000이 되었는지 알지 못한다.

계좌에 입금을 해서 잔액이 1000이 된건지 계좌에서 인출을 하고나서 잔액이 1000이 남았는지 알 수 없다.

이렇게 setter를 사용하면 객체의 속성이 갖는 값을 바꾼 이유를 명확하게 알 수 없다.

 

다음과 같은 코드도 하나의 예시이다.

public class AccountService {
    ...
    
    public void withdraw(long id, long amount) {
        Account account = accountRepository.findById(id).orElseThrow();
        long newBalance = account.getBalance() - amount;
        
        if (newBalance < 0) {
            throw new IllegalArgumentException("잔액이 부족합니다.");
        }
        
        account.setBalance(newBalance);
    }
    
	...
}

출처 : https://colabear754.tistory.com/173

 

이 코드는 AccountService에서 계좌의 잔고가 충분한지 확인하고 입금을 하는 클래스의 일부분이다.

원래라면 Account 객체에서 자신의 잔고를 관리하는 책임을 져야하지만 이 코드에서는 AccountService가 관리하고 있다.

Account가 할 일을 AccountService가 대신 하고 있는 것이다.

 

Setter 대신 명확한 의도를 가진 메소드 사용

 바로 위 코드에서 계좌에 출금할 금액을 전달해서 출금하라는 메서드를 만들면 아래와 같은 코드가 된다.

class Account {
    private long balance;
    
    public void withdraw(long amount) {
        if (amount > balance) {
            throw new IllegalArgumentException("잔액이 부족합니다.");
        }
        
        balance -= amount;
    }
}
 
@Service
public class AccountService {
    ...
    
    public void withdraw(long id, long amount) {
        Account account = accountRepository.findById(id).orElseThrow();
        account.withdraw(amount);
    }
    
    ...
}

출처 : https://colabear754.tistory.com/173

 

 이러면 출금을 하겠다는 의도가 명확해진다. 출금 로직이 변경되어도 withdraw()의 내용만 수정하면 된다.

 

getter의 사용을 지양해야 하는 이유

getter는 값의 수정을 하지도 않고 단순히 조회하는데 왜 지양하라고 하는 것인지 궁금했다.

 

단순히 getter를 사용하여 값을 조회하는게 아닌 getter로 값을 조회하고 그 값이 조건에 맞는지 확인하여 비즈니스 로직을 수행하게 된다. 아래 코드를 보자.

public void withdraw(long id, long amount) {
    Account account = accountRepository.findById(id).orElseThrow();
    long newBalance = account.getBalance() - amount;
 
    if (newBalance < 0) {
        throw new IllegalArgumentException("잔액이 부족합니다.");
    }
 
    account.setBalance(newBalance);
}

 

출처 : https://colabear754.tistory.com/173

 

여기서 보면 Account에서 잔액을 조회해 인출할 금액만큼 뺀 후 그 금액이 음수가 되는지 확인하는 로직을 수행한다.

그냥 인출할 금액을 전달해서 잔액이 충분한지만 물어보면 되는데 3가지의 로직이나 수행하는 것이다.

 

 또한, 요구사항이 변경 됐을 경우에 취약하다.

 

 예를들어, 고객의 통신 요금에 따른 고객 등급을 출력하는 코드가 있다고 생각하자. 그런데 갑자기 요구사항이 바껴서 통신 요금이 아닌 데이터 사용량에 따라 고객 등급을 출력한다고 했을 때, 더 이상 존재하지 않는 통신 요금을 getter를 사용하여 조회하려고 하면 당연히 에러가 발생하게 된다. 이러한 경우 다른 코드들 까지 모두 수정해야 한다.

 

getter로 조건을 검사하지 말고 결과를 반환하자

앞에 예제처럼 중간에 요구사항이 바껴 에러가 발생하는 경우에는 결과를 반환하게 만들면 된다. 고객의 등급을 알기 위해 고객의 상태를 알 필요가 없고, 고객 객체에서 고객 등급을 계산해 반하게 해달라고 하면 된다.

 

 

 

참고 : https://colabear754.tistory.com/173