사내 세미나 - Getter와 Setter를 함부로 사용하면 안되는 이유;;

date
May 28, 2023
slug
moen-seminar-getter-setter
status
Public
category
컨퍼런스
tags
Methodology
summary
getter와 setter를 지양하자!!
type
Post
Created Time
May 28, 2023 10:18 AM
파일과 미디어

들어가기 앞서

지난 번에 작성했던 사내 세미나 - 테스트 코드에 대해 알아보자 세미나의 다음 편으로 진행한 세미나이다.
notion image
 

Getter와 Setter의 사용을 금지하라

'리팩토링' 책의 저자로 유명한 Martin Fowler는 객체지향 생활체조 원칙을 소개하면서 다음과 같이 쓴 적이 있다.
Getter, Setter의 사용을 금지하라
대체 왜 이런 얘기를 했을까?
이 이유에 대해 한번 알아보려고 한다.
 

Getter와 Setter란?

들어가기 앞서, Getter와 Setter가 무엇인지부터 알아보면 좋을 것 같다.
Getter와 Setter는 다른 객체가 자신의 속성에 직접 접근하는 것을 막기 위해 사용하는 메소드이다.
 

Getter와 Setter를 통해 객체 안의 데이터를 보호할 수 있다

Getter와 Setter 덕분에 객체 내부의 프로퍼티를 private으로 유지하면서 객체 밖에 프로퍼티의 값을 전달하거나, 수정하는 것이 가능하다.
class Human {
    public String name;
}

Human human = new Human();
human.name = "John";
public으로 열어두었다면, 객체 내부의 값을 마음대로 다음과 같이 조작해버릴 수 있을 것이다.
 

lombok을 사용하면 많은 변수들의 getter setter 생성도 간편!

멤버 변수의 개수가 많아진다면 이것들에 대한 getter와 setter를 일일이 만들어주어야 한다. 그러면 코드도 길어지고 귀찮아진다.
하지만 lombok을 사용한다면 클래스 선언 위에 @Getter@Setter만 적어줄 경우 lombok이 자동으로 객체 안의 변수들에 대한 getter와 setter를 생성해준다.
@Getter
@Setter
class Human {
    private String name;
    private int age;
}

Human human = new Human();
human.setName("John");
human.setAge(20);
 

데이터의 유효성을 검증할 수 있다

클래스 안의 변수를 public이 아니라 Setter를 사용해서 수정한다면 데이터의 유효성을 검증할 수 있다.
class Human {
    private String name;
    private int age;

    public void setAge(int age) {
        if (age < 0) {
            throw new IllegalArgumentException("나이는 0보다 작을 수 없습니다.");
        }
        this.age = age;
    }
}

Getter와 Setter, 이렇게 좋은데 왜 쓰지 말아야 하는걸까? 🤷‍♀️

지금까지 Getter와 Setter를 사용할 때 얻을 수 있는 장점에 대해 알아보았다.
그럼에도 불구하고 마틴 파울러씨는 대체 왜 Getter와 Setter를 사용하지 말라고 했을까?

들어가기 앞서..

그 이유에 대해 본격적으로 알아보기 전에 먼저 객체지향 프로그래밍에 대해 이야기 해보려고 한다.
객체지향 프로그래밍이란, 복잡한 어플리케이션을 객체들로 나누어서 생각하고 그 객체들 간의 상호작용을 통해 어플리케이션을 만드는 프로그래밍 방법이다.
그렇기 때문에 객체들은 각자가 자신의 책임을 다하고, 각자가 협력을 통해 어플리케이션을 만들어간다.
그 말인 즉슨.. 어떤 객체가 자신의 책임을 다 하지 않는다면, 그 책임은 다른 객체에게 넘어가서 과도한 책임을 지게 될 것이다.
이건 마치.. 내가 내 방 청소를 해야 하는데 안 해서 부모님이 내 방을 청소하시게 되는 것과 비슷한듯?
그리고 그렇게 되면 보통 어떻게 되던가.
notion image
"이게 방이야, 돼지우리야! 이럴거면 방 빼!"
이런 상황에서는 그냥 부모님한테 혼나는 선에서 끝나지만, 객체지향의 세계에서는 혼내줄 사람이 없다. 그저 코드가 점점 망가져갈 뿐이다..
 

Setter를 쓰지 말아야 할 이유

이제 Setter를 쓰지 말아야 할 이유에 대해 알아보자.
 

Lombok의 Setter는 모든 프로퍼티에 대해 setter를 생성한다

아까 lombok을 칭찬했던 것이 기억난다. 하지만, lombok의 setter는 모든 프로퍼티에 대해 setter를 생성한다.
그 말인 즉슨 내가 수정을 원치 않는 프로퍼티들마저도 setter 함수가 생성이 된다는 뜻이다.
@Setter
class Human {
    private String name;
    private String juminNumber;
}
사람에게 이름은 변경될 수 있지만 (개명), 주민번호는 변경될 수 없다.
그런데 lombok의 setter는 모든 프로퍼티에 대해 setter를 생성하기 때문에, 주민번호에 대한 setter가 생성이 되어 결국 주민번호를 변경할 수 있게 된다.
추가로, lombok의 setter는 그냥 정말 값을 바꾸기만 하는 녀석이기 때문에, 값의 유효성도 체크하지 않는다. 내가 human.setName(null)을 한다고 해도 아무런 문제가 없다는 뜻이다.
이 두 가지의 큰 문제는 결국 추후에 버그 발생의 씨앗이 된다.
 

Setter 함수로는 값을 수정하는 이유를 드러내지 못한다

@Setter
class Member {
    private int memberType;
}

Member octoping = new Member();
octoping.setMemberType(1);
위의 코드에서 1은 어떤 의미를 가질까?
이 코드는 setter를 통해 memberType의 값을 1로 바꾸었다. 하지만 이 값을 바꾼 개발자의 의도를 알 수가 없다.
물론 이건 매직 넘버를 사용한 문제이기도 하지만, setter의 문제이기도 하다..
 

Setter 함수를 사용하면 도메인 로직이 분산된다

setter를 이용해서 해당 객체의 바깥에서 그냥 값을 수정하게 된다면 해당 객체의 로직 (책임)이 다른 객체들로 분산될 수밖에 없다.
class MemberService {
    // ...

    public void resign(long id) {
        Member member = memberRepository.findById(id);

        if(member.getMemberRole() == MemberRole.ADMIN) {
            throw new IllegalStateException("관리자는 탈퇴할 수 없습니다.");
        }

        member.setMemberStatus(MemberStatus.RESIGNED);
        member.setResignedAt(LocalDateTime.now());

        memberRepository.save(member);
    }
}
다음 코드의 경우, Member가 직접 자신의 상태를 탈퇴 상태로 변경하는 책임을 다하지 않았기 때문에 MemberService가 그 책임을 대신 하고 있다.
MemberService는 현재 이 회원이 현재 탈퇴가 가능한 상태인지 직접 체크하고 있고, 가입 상태를 탈퇴로 변경하고 탈퇴 시간을 저장까지 해주고 있다.
Member가 자기 방 청소를 안 해서 MemberService가 대신 해주고 있는 격이다.
 

Getter를 쓰지 말아야 할 이유

이제 Getter를 쓰지 말아야 할 이유에 대해 알아보자.
 

Getter 함수는 객체지향성을 해친다

기왕 변수를 private로 숨기고 getter로 그 값을 그대로 드러내면 무슨 의미가 있을까?
객체가 다른 객체의 상태를 직접 조회하고 그것을 기반으로 결정을 내리는 것은 객체지향의 핵심을 거스르는 행위이다.
public List<Human> findBirthdayMan(List<Human> humanList) {
    LocalDateTime now = LocalDateTime.now();

    return humanList.stream()
            .filter(human ->
                human.getBirthday().getMonth() == now.getMonth() &&
                human.getBirthday().getDayOfMonth() == now.getDayOfMonth()
            )
            .toList();
}
다음 코드는 사람들 중 오늘이 생일인 사람을 찾기 위한 방법으로, humanList를 돌면서 그 사람의 생일의 월과 일을 직접 물어봐서 오늘과 비교하는 방식을 사용하고 있다.
 
한마디로.. 다음과 같다.
너 생일이 몇 월이야?
 
이상하지 않은가? 그냥 직접 오늘인지 생일인지를 물어보면 되는데 말이다.
이처럼 객체가 다른 객체의 상태를 직접 조회하고 그것을 기반으로 결정을 내리는 것은 객체지향의 핵심을 거스르는 행위이다.
 

Getter 함수를 사용하면 변경에 취약해진다

객체의 상태를 알아내기 위해 직접 그 객체의 변수에 접근하는 것은 코드를 변경에 크게 취약하게 만든다.
한 객체가 다른 객체의 멤버 변수가 무엇이 있는지를 알게 된다면 서로가 서로에게 강하게 결합되게 된다.
그 결과로, 한 쪽이 수정되면 다른 한 쪽은 오류가 발생할 수 밖에 없다.
이를 다음의 헬스맨과 언더아머 단속반의 예제로 알아보자.
@Getter
class HealthMan {
    private int 삼대중량;
    private String clothes;
}

class UnderArmourPolice {
    public void check(HealthMan 헬린이) {
        if(헬린이.get삼대중량() < 500 && healthMan.getClothes().equals("언더아머")) {
            throw new IllegalStateException("감히 3대 500미만이 언더아머를 입어?");
        }
    }
}
위의 코드는 언더아머 단속반이 헬스맨의 삼대중량을 직접 확인한 후 500 미만인지 비교한 후, 헬스맨의 옷이 언더아머라면 프로그램을 터트려버리는 코드이다.
 
어찌 보면 문제가 크게 없어보이지만.. 만약 헬스맨이 다음과 같이 자신의 코드를 살짝 바꾼다면 어떻게 될까?
@Getter
class HealthMan {
    private int squat;
    private int benchPress;
    private int deadLift;

    private String clothes;
}
삼대중량이 아닌 각각의 운동의 중량을 따로 저장하게 되었다.
이 경우 언더아머 단속반은 헬스맨의 삼대중량을 직접 확인할 수 없게 되었고, 곧바로 컴파일 에러에 직면하게 될 것이다.
notion image
 

Getter 함수를 사용하더라도 값을 변경할 수 있다

우리가 멤버 변수를 private로 숨기고, getter를 통해 제공하는 것은 객체의 바깥에서 그 값을 함부로 수정할 수 없도록 하는 행위이다.
하지만.. 원시 타입인 녀석들은 괜찮지만 그렇지 않은 녀석들은 call-by-reference이기 때문에 getter로 값을 제공해주더라도 값을 변경할 수 있다.
class Human {
    List<Human> children = new ArrayList<>();

    public List<Human> getChildren() {
        return children;
    }
}

Human octoping = new Human();

octoping.getChildren().add(new Human());
octoping.getChildren().add(new Human());

octoping.getChildren().size(); // 2
이와 같이 children을 추가하는 함수를 바깥에 제공하지 않았음에도, getChildren()을 통해 children을 가져와서 add()를 통해 값을 추가할 수 있게 되어버렸다.
 

그러면 어떻게 하라고요;; 😡

지금까지 getter와 setter를 함부로 쓰면 안되는 이유에 대해 알아보았다.
그러면 이제 아마 이런 생각이 점점 들 것이다.
"Getter와 Setter를 안 쓰면 코딩을 어떻게 하라는 걸까?"
이제 이 부분에 대해 알아보자.
 

Tell, Don't Ask

앞에서 Getter와 Setter의 사용을 금지하라고 하셨던, Martin Fowler께서는 이런 말도 하셨다.
"물어보지 말고 그냥 시켜라"
 
무언가를 하기 위해 객체에게 현재 상태를 물어보지 말고, 그냥 그 객체에게 시키라는 뜻이다.
 

비즈니스 로직을 짤 때 Getter로 값을 가져와서 구현하지 말라

아까 헬스맨과 언더아머 단속반의 이야기를 다시 가져와보자.
 
언더아머 단속반에게 필요했던 것은, HealthMan의 '삼대중량'이라는 변수의 값이 아니라 벤치프레스, 스쿼트, 데드리프트를 합친 값이 궁금했던 것이다.
다시 말해서 우리가 프로그래밍을 할 때 정말로 필요한 것은, '이 객체의 특정 멤버 변수의 값'이 아니라 '이 객체의 현재 상태'이다.
그렇기 때문에 getter로 다른 객체의 내부를 직접 헤집어 상태를 가져오기보다 그 객체에게 자신의 상태를 나에게 달라고 메시지를 보내자.
 
class HealthMan {
    // this..
    private int 삼대중량;

    // or this..
    private int squat;
    private int benchPress;
    private int deadLift;

    public int calculateBigThree() {
        // this
        return this.삼대중량;

        // or this
        return squat + benchPress + deadLift;
    }
}
getter로 '삼대중량'이라는 변수나 'squat' 등의 변수의 값을 직접 끄집어내어 합치는 것이 아니라, HealthMan에게 직접 삼대 중량의 값을 계산해주라고 메시지를 보내는 것이다.
 
그럴 경우 HealthMan의 내부 상태가 어떻게 바뀌든지 언더아머 단속반은 알 필요가 없다. 오직 HealthMan에게 삼대중량을 계산해달라고만 요청하면 되기 때문이다.
 
(+ 추가로 언급해서, HashMap, ArrayList와 같은 정말 값을 주고 받기 위한 데이터 클래스의 경우나, 정말 해당 객체의 특정 값이 필요한 경우는 예외다. 다시 말해, 비즈니스 로직을 구현할 때에 Getter로 직접 값을 끄집어와서 구현하지 말자는 의미이다)
 

Setter를 사용하지 말고, 상태를 바꾸라고 메시지를 보내라

앞과 마찬가지로 우리에게 필요한 것은 ‘이 객체의 상태를 바꾸는 것’이지, ‘이 객체의 특정 멤버 변수의 값을 바꾸는 것’이 아니다.
해당 객체의 상태를 바꾸라고 명령하는 함수를 만들고, setXXX 대신 그 의도를 알 수 있는 이름을 함수에 붙이자
 
class MemberService {
    // ...

    public void resign(long id) {
        Member member = memberRepository.findById(id);

        if(member.getMemberRole() == MemberRole.ADMIN) {
            throw new IllegalStateException("관리자는 탈퇴할 수 없습니다.");
        }

        member.setMemberStatus(MemberStatus.RESIGNED);
        member.setResignedAt(LocalDateTime.now());

        memberRepository.save(member);
    }
}
다음과 같이 MemberService가 직접 Member의 상태를 바꾸는 것이 아니라, Member에게 탈퇴하라고 메시지를 보내는 것이다.
 
class Member {
    // ...

    public void resign() {
        if(this.memberRole == MemberRole.ADMIN) {
            throw new IllegalStateException("관리자는 탈퇴할 수 없습니다.");
        }

        this.memberStatus = MemberStatus.RESIGNED;
        this.resignedAt = LocalDateTime.now();
    }
}

class MemberService {
    // ...

    public void resign(long id) {
        Member member = memberRepository.findById(id);

        member.resign();

        memberRepository.save(member);
    }
}
 

이 모든 건 캡슐화

지금까지 Getter와 Setter를 함부로 쓰면 안되는 이유, 그리고 Getter와 Setter를 안 쓰고 어떻게 코딩을 해야 하는지에 대해 알아보았다.
 
이 모든 건 캡슐화가 제대로 지켜지지 않아서 벌어진 일이었다.
하지만 조금 이상하지 않은가? 우리는 캡슐화를 지키기 위해 Getter와 Setter를 사용하던 것이 아닌가.
 
맞다. 하지만 결국 이 모든 것은 getter와 setter를 남용해서 캡슐화가 결국 어겨졌기 때문에 발생한 일이다.
 

마무리하며

마무리하면서.. 혹시나 누군가가 이렇게 생각했을 수도 있다 생각한다.
💩: 우리는 컨트롤러에서 데이터 받아와서 그냥 DB 쿼리에 태우기만 해서 비즈니스 로직이고 뭐고 자바 코드에 없는데요?
 
이러면.. 이러면 정말 안된다고 생각한다.
이러면 안되는 이유는 또 세미나 한 개 분량이 나오기 때문에 이 글에서 다루지는 않겠지만, 분명 많은 회사들이 직면하고 있는 상황이라 생각된다.
 
최대한 비즈니스 로직은 쿼리에서 빼내서 자바 코드로 옮기는 것이 좋다고 생각한다. 그리고, 그렇게 자바 코드로 작성하면서 오늘 다룬 내용을 지키면서 작성하도록 하자.
 

© Octoping 2022 - 2024