티스토리 뷰

getter/setter/property를 쓰지 않는다

 도메인 오브젝트로 설계한 Entity 또는 VO 클래스에는 getter/setter/property 사용을 지양해 상태노출을 최소화 하라는 지침이다.

숨은 의미

 객체지향 프로그래밍의 핵심 개념 중 캡슐화를 지키면서 객체에 메시지를 보내 스스로 상태에 대한 처리로직을 수행하도록 하라는 의미이다.

 

 이 지침은 데이터 전달을 목적으로 하는 DTO나 비즈니스 플로우 실행을 목적으로 하는 컨트롤러•서비스 유형의 무상태 클래스를 대상으로 하지 않는다.

객체에 메시지를 전달해라

 객체지향 프로그래밍은 객체의 '역할과 책임' 이라는 핵심 가치를 잘 유지할 때 그 의미가 살아난다. 이 역할과 책임의 대한 의의는 객체가 자신의 상태, 즉 정보에 대한 처리책임을 자신 스스로 가진다는 데 있다.

 

 간단히 말해서 객체가 가진 정보를 꺼내서 다른 객체가 처리하도록 구현하는 것이 아니라, 정보를 가지고 있는 객체가 업무 기준을 스스로 처리하는 메소드를 호출하는 형식으로 구현하라는 것이다. 이 처리에 대한 명령(메소드)을 호출하는 것을 메시지를 전달한다고 표현한다.

public class VipMemberService {

    private static final BigDecimal VIP_DEPOSIT_AMOUNT = new BigDecimal("1000000000");

    public void saveBenefit(Member member) {
        if (VIP_DEPOSIT_AMOUNT.compareTo(member.getTotalDepositAmount()) > 0) {
            throw new IllegalStateException("VIP 고객이 아닙니다.");
        }
        // ...
    }
}

 위 예시코드는 VIP 고객에 대해 혜택을 부여하는 로직을 보여준다. 혜택 부여 전 VIP를 판별하는 기준을 체크해 예외를 발생시키고 있는데, 고객의 수신 총 금액이 10억원 이상일 경우에 VIP로 판별하고 있는 것으로 보인다.

 

 위 코드의 문제는 Member 클래스가 가진 정보 (수신 총금액)를 직접 꺼내 업무적 판단을 위한 근거로 활용한다는 것이다. 이러한 코드를 구현할 경우, VIP의 판별기준이 변경되는 경우에 이 기준을 구현한 모든 코드를 수정해 주어야 한다. 만약 VIP의 판별기준이 체크카드를 개설하고, 신용카드 사용금액이 월 500만원 이상인 경우로 변경된다면 모든 if 조건문을 찾아 코드를 수정해야 한다.

 

 또다른 문제는 현재의 if 조건문이 VIP를 판별하는데 사용되지 않을 수도 있다는 점이다. 수신금액이 10억 이상이라는 기준은 VIP를 판별하기 위한 조건 외에도 여신심사 혹은 회원가입, 연체 패널티 경감 등의 프로세스에도 활용할 수 있다. 위 조건문의 심각한 문제는 단순히 Member 클래스의 정보를 꺼내 비교하는 것 외에 어떤 비즈니스적 의미를 표현하지 못한다는 것이다.

public class VipMemberService {

    public void saveBenefit(Member member) {
        if (!member.isVip()) {
            throw new IllegalStateException("VIP 고객이 아닙니다.");
        }
        // ...
    }
}

 getter를 제거한 로직으로 코드를 리팩토링 해 보았다. 가장먼저 눈에 띄는 큰 변화는 조건문의 문장을 굳이 해석해서 의미를 판단할 필요가 없어졌다는 점이다. !member.isVip() 라는 문장은 직관적으로 '멤버가 VIP가 아닌 경우' 로 쉽게 해석된다.

 

 두 번째는 변경에 대응하는 것이 용이해졌다는 점이다. VIP를 판별하는 기준이 바뀌더라도 Member 클래스의 isVip 메소드 로직을 수정해주기만 하면, 프로젝트에 모든 VIP 판별조건문에 적용된다.

 

 이렇게 객체의 정보를 객체가 직접 처리하는 코드를 구현해야 한다. 특히 조건문의 활용에서 첫 번째 코드와 같은 설계실수를 할 경우가 많다. 조건문을 사용하려는 본래의 목적에 집중해 본다면, 조건문의 판별기준이 자연스럽게 정보를 보유한 클래스의 내부로 이동할 것이다.

Entity와 VO, 그리고 DTO

 그렇다면 getter와 setter는 무조건 사용하지 말아야 하는것일까? 그건 아니다. 클래스는 역할과 책임이라는 어떠한 목적을 가지고 설계된다. Entity, VO 같은 도메인 클래스의 경우, 속성에 직접 접근해 값을 변경할 수 있는 setter의 노출은 최대한 자제해야 한다. 그러나 getter는 예외가 될 수 있다. 객체의 값을 외부로 표현(Presentation) 해 주어야 하는 경우에는 getter를 사용할 수 밖에 없다. 단, 이렇게 표현을 목적으로 getter를 노출하는 경우에는 값에 의한 업무적 판단이 일어나지 않도록 유의해야 한다.

 

 DTO(Data Transfer Object)는 데이터 전달을 목적으로 설계되는 객체이다. 데이터의 전달을 위해서는 당연히 프로퍼티의 값을 꺼내고, 수정할 수 있는 getter/setter가 필요할 수 밖에 없다. 대신에 DTO의 목적은 데이터의 전달에 있으므로, 내부에 비즈니스 로직을 구현해서는 안된다.

Unmodifiable Colletion의 활용

 일급 컬렉션에서 데이터를 추출할 때에는 컬렉션의 형태로 데이터가 리턴될 수 있다. 컬렉션에 들어있는 DTO의 setter를 제거하면 해당 DTO의 변조를 막을 수는 있다. 그런데 컬렉션을 final 로 선언해 리턴한다고 해도, 이는 컬렉션 변수의 재할당을 막아주는 역할밖에 하지 못한다. collection.add 와 같이 컬렉션의 레퍼런스 변수를 유지하면서, 컬렉션의 집합을 변형하는 행위는 허용된다는 것이다.

 

 컬렉션의 변조를 막기 위해 생성한 컬렉션을 변조불가컬렉션으로 정의할 수 있다

Collections.unmodifiableCollection(Collection<? extends T> c)

 위 API를 사용하면 해당 컬렉션은 수정할 수 없는 컬렉션이 된다. 결론적으로 일급컬렉션의 데이터 산출물을 수정하는 setter의 행위를 줄일 수 있는 것이다.

마치며

 이번 포스팅을 마지막으로 객체지향 생활체조 원칙에 대한 글을 모두 완성했다. 처음에는 따라하다 보면 개발습관을 개선할 수 있겠지 라는 막연한 기대를 가지고 도전해 보았다. 잘 이해가 안돼도 이런 방식으로 시작해보는것도 좋을 듯 하다. 자연스럽게 기존과 다른 코드를 작성하면서 다시 구조에 대해 고민해보고, 점점 코드설계가 객체지향적으로 개선되는 마법을 경험할 수 있을 것이다.

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday