Getter도 Setter도 쓰지 말라고??

반응형

스프링에서 DTO를 만들면 어김없이 나오게 되는 `Getter`, `Setter`를 많이들 사용하고 계실 겁니다.

물론 그만큼 이 2가지를, 특히나 `Setter`를 사용하지 말라는 얘기도 정말 많이 들으셨겠죠.

저도 마찬가지입니다.

 

그래서 오늘은 왜 이 2개를 쓰지 말라고 하는지 정리해보려고 합니다.


우선 `Getter`와 `Setter`를 자주 사용하는 이유는 편하게 내부 필드에 접근하고 사용하기 위해서입니다.

 

객체 지향에서 지켜져야 하는 것 중 하나가 정보 은닉입니다.

정보 은닉은 객체의 구체적인 정보를 외부에 노출하지 말라는 것인데, 그 이유는 '객체 간에 서로 모르게 하는 것이 서로 간에 의존성을 없애서 유연성을 확보하는' 가장 좋은 방법이기 때문입니다.

 

아무튼 이러한 정보 은닉을 지키기 위해 자바에서는 필드를 `private`로 선언해서 외부에서 접근이 불가능하게 만들고 `public` 메서드를 통해 필드에 접근하고 사용하고 있습니다.


그러면 이게 왜 문제인 걸까요?

 

우선 근본적으로 각각의 필드에 직접 접근만 안 했다 뿐이지, Getter, Setter와 같은 메서드를 통해 하나하나의 필드에 접근할 수 있으면 사실 정보 은닉 원칙이 지켜졌다고 보기가 어렵습니다.

 

또한 객체 지향 프로그래밍은 다양한 객체들이 협력하여 하나의 애플리케이션으로 동작하도록 하는 프로그래밍 방법입니다.

이 객체지향 프로그래밍의 장점은 다른 객체의 세부 정보를 굳이 알 필요 없이 원하는 응답을 위한 요청만 보내면 된다는 것입니다.

 

이런 관점에서 생각해 보면 `Getter`를 사용해서 객체의 필드 값을 조회하고, 그 값을 비즈니스 로직에서 프로세스를 거쳐서 수정하여 다시 필드에 `Setter`를 통해 저장을 하는 것은 객체의 독립성을 유지하지 못하고 장기적으로 유연성을 확보할 수도 없습니다.

객체의 특정 필드 값을 메서드에 인자로 넘기기 위한 경우처럼 정말로 해당 값이 필요한 상황에서는 `Getter`를 쓸 수 밖에는 없긴 합니다.

 

`Setter`를 쓰지 말아야 하는 이유

`Setter`를 쓰지 말라는 말은

외부에서 필드의 값을 변경할 때, 단순하게 `setOOO`이라는 메서드로 바꾸지 말고, 해당 값을 변경하려는 목적을 잘 표현하는 메서드를 작성해서 사용하세요.

라는 의미입니다.

 

축구팀의 예산에 대한 예제로 설명해 보겠습니다.

 

우선 `Setter`를 쓰지 말아야 하는 첫 번째 이유는 값을 변경하는 목적을 알 수 없기 때문입니다.

public class Team {
    private int budget;	// 팀의 예산

    public Team(int budget) {
        this.budget = budget;
    }

    public int getBudget() {
        return budget;
    }

    public void setBudget(int budget) {
        this.experiencePoints = experiencePoints;
    }
}

Team myTeam = new Team(1000);
myTeam.setBudget(2000);

 

위의 예제에서 처음에 팀의 예산을 1000으로 설정해 놓고, 그 후에 2000으로 변경되었습니다.

하지만 이러한 단순 `Setter`를 이용한 변경은 어떤 이유로 값이 변경되었는지 명확히 알 수가 없습니다.

 

또한 단순 `Setter`를 통해 값을 변경하면 관련 로직(또는 책임)이 해당 객체가 아닌 다른 객체(주로 서비스 레이어)에서 전가될 수 있습니다.

@Service
public class TeamService {

    private final TeamRepository teamRepository;

    public TeamService(TeamRepository teamRepository) {
        this.teamRepository = teamRepository;
    }

    public void addBudget(long teamId, int amount) { // 예산 배정
        Team team = teamRepository.findById(teamId)
            .orElseThrow(() -> new IllegalArgumentException("팀을 찾을 수 없습니다."));

        int newBudget = team.getBudget() + amount;

        team.setBudget(newBudget);
        teamRepository.save(team); // 변경된 예산을 저장
    }

    public void spendBudget(long teamId, long points) { // 예산 사용
        Team team = teamRepository.findById(teamId)
            .orElseThrow(() -> new IllegalArgumentException("팀을 찾을 수 없습니다."));

        int newBudget = team.getBudget() - amount;

        if (newBudget < 0) {
            throw new IllegalArgumentException("예산이 부족합니다.");
        }

        team.setBudget(newBudget);
        teamRepository.save(team); // 변경된 예산을 저장
    }
}

 

원래대로라면 도메인의 속성을 변경하기 위해서는 도메인 영역에서 구현되어야 맞습니다.

하지만 위의 코드를 보면, 도메인 영역에서 해당 필드를 처리하는 게 아니라 비즈니스 로직을 처리하는 서비스 레이어에서 `Getter`를 통해 값을 가져와서 처리한 후 `Setter`로 다시 값을 세팅하는 것을 볼 수 있습니다.

즉, 도메인 로직이 응용 영역으로 분산되어 있는 것이죠.

 

그래도 현재 코드에서는 도메인 값에 대한 규칙(예산은 0보다 작을 수 없다)에 대한 검증을 가지고 있기 때문에 그렇게 큰 문제는 없을 것으로 생각됩니다.

하지만 만약에 또 다른 규칙 `예산을 배정받을 때, 각 팀의 예산은 리그 정책에 따라 10000을 넘을 수 없다`가 생겼다고 해보겠습니다.

@Service
public class TeamService {

    private final TeamRepository teamRepository;

    public TeamService(TeamRepository teamRepository) {
        this.teamRepository = teamRepository;
    }

    public void addBudget(long teamId, int amount) { // 예산 배정
        Team team = teamRepository.findById(teamId)
            .orElseThrow(() -> new IllegalArgumentException("팀을 찾을 수 없습니다."));

        int newBudget = team.getBudget() + amount;
        if (newBudget > 10000) {	// 추가 로직
            throw new IllegalArgumentException("예산이 제한 금액을 초과합니다.");
        }

        team.setBudget(newBudget);
        teamRepository.save(team); // 변경된 예산을 저장
    }

    public void spendBudget(long teamId, long points) { // 예산 사용
        Team team = teamRepository.findById(teamId)
            .orElseThrow(() -> new IllegalArgumentException("팀을 찾을 수 없습니다."));

        int newBudget = team.getBudget() - amount;

        if (newBudget < 0) {
            throw new IllegalArgumentException("예산이 부족합니다.");
        }

        team.setBudget(newBudget);
        teamRepository.save(team); // 변경된 예산을 저장
    }
}

 

위의 코드처럼 예산 배정 부분에서도 제한이 생기겠지요.

 

이렇게 도메인 규칙을 잘 지키며 코드를 작성한다면 문제가 생기지 않을 수 있습니다.

하지만 규칙이 지금처럼 2개가 아니라 20개, 30개까지 늘어난다면 어떻게 될까요??

 

혹은 팀을 운영하는데 여러 가지 업무가 생기고 이처럼 예산을 변경하는 부분이 더 많이 생긴다면??

 

모든 부분에서 이 규칙이 코드 안에 잘 들어있기를 기도할 수밖에 없습니다...(기도메타)

 

그리고 만약 해당 규칙이 변경되었다면 이 규칙이 사용된 곳을 하나하나 찾아서 다 바꿔주는 끔찍한 일이 발생하게 됩니다.

 

즉 이렇게 `도메인 로직이 한 곳에 모여있지 않으면 코드를 수정하고 유지보수하는데 매우 많은 노력이 필요하게 됩니다.`


그렇다면 `Setter`안에 해당 규칙을 넣으면 되는 거 아닌가 하는 생각이 들 수도 있습니다.

public void setBudget(int budget){
    if (budget > 10000) {	// 추가 로직
        throw new IllegalArgumentException("예산이 제한 금액을 초과합니다.");
    }

    if (budget < 0) {
        throw new IllegalArgumentException("예산이 부족합니다.");
    }

    this.budget = budget;
}

 

이렇게 말이죠.

 

하지만 이 방법도 추천되지 않는 방법입니다.

 

우선 위에서도 얘기했던 것처럼 단순한 `setOOO` 이름의 메서드는 해당 목적을 제대로 나타낼 수 없습니다.

예를 들어, 단순히 예산의 `배정`과 `사용`이 아니라 `선수 방출로 인한 수익`, `선수 영입으로 인한 예산 사용`, `경기장 수리로 인한 예산 사용` 등 다양한 상황들에 대해서 각각의 목적을 나타내는 이름의 메서드를 사용하는 것이 가독성 측면이나 추후 유지보수 측면에서 이점이 많이 있습니다.

 

또한 모든 규칙을 하나의 메소드 안에서 관리하게 되면 해당 메서드에 너무 많은 책임을 지우게 되는 것이고, 추후에 유지보수 측면에 있어서도 매우 비효율적입니다.

 

또한 그런 사람은 상대적으로 적겠지만 `Setter`를 모든 필드에 달아주게 되면 해당 객체에 접근하는 사용자(개발자)가 바꾸지 말아야 되는 값이 무엇인지, 바꿔도 되는 값이 무엇인지 헷갈리기 때문에 실수할 확률도 커지게 됩니다.


`Setter` 문제에 대한 해결법

그렇다면 어떻게 이 문제들을 해결할 수 있을까요?

바로 위에서 얘기했던 것처럼 해당 값을 변경하려는 목적을 잘 표현하는 메서드를 작성해서 사용해야 합니다.

 

도메인의 값을 변경할 때 고통으로 필요한 도메인 규칙을 도메인 영역 안에서 구현하고 지키게 함으로써, 도메인 영역 외의 영역들에서 더 이상 공통 도메인 규칙에 대해 신경 쓰지 않아도 되게 만드는 것입니다.

public class Team {
    private int budget; // 팀의 예산

    public Team(int budget) {
        if (budget < 0) {
            throw new IllegalArgumentException("예산은 음수일 수 없습니다.");
        }
        this.budget = budget;
    }

    public int getBudget() {
        return budget;
    }

    public void spendBudget(int amount) {
        if (amount > budget) {
            throw new IllegalArgumentException("예산이 부족합니다.");
        }
        this.budget -= amount;
    }

    public void addBudget(int amount) {
        if (amount < 0) {
            throw new IllegalArgumentException("추가 예산은 음수일 수 없습니다.");
        }
        
        if (amount + this.budget > 10000) {
        	throw new IllegalArgumentException("예산이 제한 금액을 초과합니다.");
        }
        this.budget += amount;
    }
}

 

이렇게 해당 도메인 안에서 조건을 검사하게 만듦으로써 도메인 규칙에 대해 서비스 단에서는 더 이상 신경 쓰지 않아도 됩니다.

@Service
public class TeamService {

    private final TeamRepository teamRepository;

    public TeamService(TeamRepository teamRepository) {
        this.teamRepository = teamRepository;
    }

    public void spendTeamBudget(long teamId, int amount) {
        Team team = teamRepository.findById(teamId)
            .orElseThrow(() -> new IllegalArgumentException("팀을 찾을 수 없습니다."));
        
        team.spendBudget(amount); // 팀의 예산 사용 로직을 캡슐화
        teamRepository.save(team); // 변경된 예산을 저장
    }

    public void addTeamBudget(long teamId, int amount) {
        Team team = teamRepository.findById(teamId)
            .orElseThrow(() -> new IllegalArgumentException("팀을 찾을 수 없습니다."));
        
        team.addBudget(amount); // 팀의 예산 추가 로직을 캡슐화
        teamRepository.save(team); // 변경된 예산을 저장
    }
}

 

이 방식으로 메서드를 작성함으로써

  • 객체가 스스로 자신의 상태를 관리하므로 외부에서 직접 조작할 수 없습니다
  • 예산 변경 로직을 내부로 감추어 외부에서 부적절한 상태 변경을 방지합니다 (정보 은닉)
  • 상태 변경 규칙이 객체 내부에 캡슐화되어 있어, 향후 변경이 필요한 경우 한 곳에서만 수정하면 됩니다. 

`Getter`를 쓰지 말아야 하는 이유

그렇다면 이번에는 `Getter`를 쓰지 말아야 하는 이유에 대해서 알아보겠습니다.

 

아무래로 `Setter`보다는 쓰지 말아야 하는 이유가 크게 와닿지 않을 수 있는데요.

 

정확히는 객체의 특정 값을 가져와서 사용해야 하는 `조회`를 위한 `Getter`보다는 비즈니스 로직 상에서 조건을 `검사`하기 위한 `Getter`를 지양하라는 말이 더 정확할 거 같습니다.

public void addBudget(long teamId, int amount) { // 예산 배정
    Team team = teamRepository.findById(teamId)
        .orElseThrow(() -> new IllegalArgumentException("팀을 찾을 수 없습니다."));

    int newBudget = team.getBudget() + amount;
    if (newBudget > 10000) {	// 추가 로직
        throw new IllegalArgumentException("예산이 제한 금액을 초과합니다.");
    }

    team.setBudget(newBudget);
    teamRepository.save(team); // 변경된 예산을 저장
}

 

위의 코드에서는 예산을 가져와서 추가하려는 예산을 더한 이후에 규칙에 어긋나지 않는지 검사하고 있습니다.

 

하지만 이런 검사도 결국 내가 추가하려는 금액을 더했을 때 제한 금액을 초과하는지 안 하는지 `True`, `False`만 알려주면 되는 건데 굳이 여러 과정을 거칠 필요가 없죠.

public boolean checkAddBudget(int amount){
	if(this.budget + amount > 10000){
    	return false;
    }
    return true;
}

 

위의 예제처럼 메서드를 작성하면 `Getter`를 통해 값을 가져오지 않아도 제한 금액을 초과하는지 체크할 수 있습니다.

 

또한 `Getter`를 통해 조건을 검사하면 이후 변경에 있어서 취약합니다.

예를 들어, 각 구단의 예산 규모에 따라 구단 등급을 나누는 로직이 있다고 해보겠습니다.

@Getter
class Team{
	private int budget
}

@Service
public class TeamService {
	...
    
    public void printTeamGrade(Team liverpool){
    	if(liverpool.getBudget() >= 9000){
        	System.out.println("A");
        }else if(liverpool.getBudget() >= 6000){
        	System.out.println("B");
        }else{
        	System.out.println("C");
        }
    }
    ...
}

 

이런 식으로 코드가 있다고 치면, 물론 등급을 나누는 조건이 바꾸면 해당 로직이 사용된 모든 부분을 바꿔줘야 하는 번거로움이 생기게 됩니다.

또한 만약 `Team` 클래스의 필드가 `budget`뿐만 아니라 `playerCount`라는 선수의 숫자까지 추가되고 이것도 등급의 구단을 나누는데 조건으로 추가되면 조회하여 조건을 검사하고 있던 코드를 모두 바꾸는 일은 더욱 복잡해지게 됩니다.

 

이것도 역시 도메인이 구단의 등급을 결정하는 책임을 지지 않아서 발생한 문제라고 볼 수 있습니다.

 

'Getter' 문제에 대한 해결법

그렇다면 어떻게 해결해야 할까요?

'Getter`로 조건을 검사하지 말고 결과를 반환하게 해야 합니다.

이 방법대로 구단 등급 나누는 메서드를 도메인 안에서 구현해서 등급을 반환하게 하면 아래와 같이 수정할 수 있습니다.

class Team {
    private int budget; // 팀의 예산
    ...
    public String checkGrade(){
        if(liverpool.getBudget() >= 9000){
        	return "A";
        }else if(liverpool.getBudget() >= 6000){
        	return "B";
        }else{
        	return "C";
        }
    }
	...
}

@Service
public class TeamService{
	...
    public void printTeamGrade(Team liverpool){
    	System.out.println(liverpool.checkGrade());
    }
    ...
}

 

이렇게 더 이상 서비스 레이어에서 등급을 계산하기 위해 값을 `Getter`로 가져올 필요 없이 도메인 영역에서 계산된 등급을 받아오기만 하면 됩니다.


지금까지 `Getter`와 `Setter`를 쓰면 안 되는 이유와 그러면 어떻게 해야 하는지에 대해서 알아보았습니다.

 

즉 결론적으로 말하면, 너무 포괄적이고 제약이 없는 메서드를 사용하게 하면 위험하니 구체적이고 안전한 메서드를 만들어서 그것을 사용하게 해야 한다라고 할 수 있습니다.

 


오늘의 내용은 너무 잘 정리된 글들이 있어서 해당 내용을 참고해서 작성했습니다.

https://colabear754.tistory.com/173

 

[OOP] Getter와 Setter는 지양하는게 좋다

목차 들어가기 전에 얼마 전 사내에서 Getter와 Setter를 함부로 사용하면 안되는 이유에 대한 세미나가 있었다. Setter에 대한 이야기는 워낙 많이 알려져있었지만 Getter에 대한 이야기는 잘 하지 않

colabear754.tistory.com

https://velog.io/@backfox/setter-%EC%93%B0%EC%A7%80-%EB%A7%90%EB%9D%BC%EA%B3%A0%EB%A7%8C-%ED%95%98%EA%B3%A0-%EA%B0%80%EB%B2%84%EB%A6%AC%EB%A9%B4-%EC%96%B4%EB%96%A1%ED%95%B4%EC%9A%94

 

setter 쓰지 말라고만 하고 가버리면 어떡해요

부트캠프 과정을 잘 견뎌내고 팀프로젝트에서 백엔드를 맡은 엄준식(27)씨.백엔드 커리큘럼을 유난히 즐거워했던 준식씨였기에 자신이 맡은 파트가 꽤 마음에 드는 모양이다.준식씨: 됐다! 게시

velog.io

 

반응형
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유