네로개발일기

개발자 네로의 개발 일기, 자바를 좋아합니다 !

반응형

코드를 작성할 때 도메인에서 사용하는 용어는 매우 중요하다. 
도메인에서 사용하는 용어를 코드에 반영하지 않으면
그 코드는 개발자에게 코드의 의미를 해석해야 하는 부담을 준다.
도메인 모델의 Entity나 value에 공개 set 메서드만 넣지 않아도 일관성이 깨질 가능성이 줄어든다.
공개 set 메서드를 사용하지 않으면 의미가 드러나는 메서드를 사용해서 구현할 가능성이 높아진다.
예를 들어, set 형식의 이름을 갖는 공개 메서드를 사용하지 않으면
자연스럽게 cancel이나 changePassword 처럼 의미가 더 잘 드러나는 이름을 사용하는 빈도가 높아진다.
- 도메인 주도 개발 시작하기, 최범균-

 

'setter를 지양하라'라는 말을 구체적으로 서술하면

외부에서 필드의 값을 변경하기 위해 접근할 때,

단순하게 setXXX라는 이름의 메서드 하나만 있는 것이 아니라,

필드의 값을 변경하려는 목적을 파악하여

그 목적을 잘 표현하는 메서드를 제공하라.

 

 

예제

'회원' 객체는 회원 이름과 회원 상태 속성을 가지고 있다.

setState 메서드는 회원의 상태를 변경하는 '공개 set 메서드'이다.

public class 회원 {
    
    private String name;
    private String state;
    
    public String getName() {
        return name;
    }
    
    public String getState() {
        return state;
    }
    
    public void setState(String state) {
        this.state = state;
    }
}

회원관리 서비스는 회원의 상태를 조정하여 회원을 차단하거나, vip로 승격한다.

public class 회원관리 {
    
    public void blockMember(회원 회원) {
    
        if (회원.getName().equals("jyjeon")) { // 공통 도메인 규칙
            throw new IllegalArgumentException("닉네임이 jyjeon인 사람의 정보는 변경할 수 없습니다.");
        }
        if (회원.getState().equals("vip")) { // 회원 차단 도메인 규칙
            throw new IllegalArgumentException("vip는 차단할 수 없습니다.");
        }
        
        회원.setState("blocked");
    }
    
    public void upgradeMemberToVip(회원 회원) {
        if (회원.getName().equals("jyjeon")) { // 공통 도메인 규칙
            throw new IllegalArgumentException("닉네임이 jyjeon인 사람의 정보는 변경할 수 없습니다.");
        }
        if (회원.getState().equals("blocked")) { // vip 상향 도메인 규칙
            throw new IllegalArgumentException("차단된 사용자는 vip가 될 수 없습니다.");
        }
        
        회원.setState("vip");
    }
}

회원의 상태(state 필드 값)를 변경하기 위해서는 회원 도메인의 규칙을 지켜야 합니다.

- 특정 닉네임을 가진 회원의 상태는 변경할 수 없다.

- vip는 차단할 수 없다.

- 차단된 회원은 vip가 될 수 없다.

 

공개 set 메서드의 문제점 1) 도메인 로직의 분산

공개 set 메서드는 도메인의 의미나 의도를 표현하지 못하고
도메인 로직을 도메인 객체가 아닌 응용 영역이나 표현 영역으로 분산시킨다.
도메인 로직이 한 곳에 응집되지 않으므로 
코드를 유지보수할 때에도 분석하고 수정하는 데 더 많은 시간이 필요하다.
- 도메인 주도 개발 시작하기, 최범균 -

"도메인 로직이 응용 영역이나 표현 영역으로 분산된다."의 의미를 알아보자.

 

도메인 모델 패턴에서, 사용자의 요청을 처리하고 사용자에게 정보를 보여주는 표현 영역이라 한다.

MVC 패턴은 http 요청을 받고 응답하는 컨트롤러 클래스들은 표현 영역이라 할 수 있다.

사용자가 요청한 기능을 실행하는 영역을 응용 영역이라 한다. MVC 패턴은 비즈니스 로직을 수행하는 서비스 클래스가 응용 영역이다.

 

특정 도메인을 개념적으로 표현하고, 도메인의 핵심 규칙이 있는 영역을 도메인 영역이라고 한다. 도메인 영역은 도메인 객체 하나를 가리킬 수도 있지만, 연관이 있는 상위 도메인과 하위 도메인들을 한 다발로 묶어 Aggregate(애그리거트)라는 군집을 가리킬 수도 있다.

 

예제에서 '회원의 상태를 변경하기 위해서 회원이 특정한 이름을 가지고 있는지 확인해야 한다.'는 규칙이 있다. 도메인의 속성을 변경하기 위한 핵심 규칙이기 때문에 이 규칙은 도메인 영역에서 구현되어야 한다.

 

하지만, 예제에서는 도메인 규칙을 표현하는 코드가 비즈니스 로직을 수행하는 '회원관리' 클래스에 작성되어 있다. 다시말해, 도메인 로직이 응용 영역으로 분산되어 있다.

 

1. 서비스가 커지면서 회원의 상태를 변경하는 곳이 29억 군데라면? 29억 군데에 규칙을 표현해야 한다.

2. 회원의 상태를 변경하기 위한 규칙이 추가되거나 삭제된다면? 29억 군데의 비즈니스 로직을 전부 수정해야 한다.

 

공개 set 메서드를 사용할 때, 도메인 로직이 한 곳에 응집되지 않으므로 코드를 유지 보수할 때에도 분석하고 수정하는 데 시간이 필요하다. 

 

 

단순히 값을 변경하는 공개 set 메서드가 일으킬 문제에 대해 알아보았다.

그럼 값을 변경하는 것에 더해서 공통 규칙을 set 메서드에 넣어주면 해결이 될까?

// 회원.java
public void setState(String state) {
    if (this.name.equals("jyjeon")) {
        throw new IllegalArgumentException("닉네임이 jyjeon인 회원의 정보는 변경할 수 없습니다.");
    }
    
    this.state = state;
}
public class 회원관리 {
    
    public void blockMember(회원 회원) {
        if (회원.getState().equals("vip")) {
            throw new IllegalArgumentException("vip는 차단할 수 없습니다.");
        }
        
        회원.setState("blocked"); // 공통 규칙은 set 메서드에 있다.
    }

    public void upgradeMembetToVip(회원 회원) {
        if (회원.getState().equals("blocked")) {
            throw new IllegalArgumentException("차단된 사용자는 vip가 될 수 없습니다.");
        }
        
        회원.setState("vip"); // 공통 규칙은 set 메서드에 있다.
    }
}

 

하지만, 'vip는 차단할 수 없다.' '차단된 회원은 vip가 될 수 없다.' 라는 규칙도 도메인 규칙이고, 도메인 영역에 있어야 한다. 지금은, 그 책임을 응용 영역에서 해결하고 있다.

 

그렇다면, 특정 조건에 따른 도메인 규칙도 set 메서드를 넣으면 해결이 될까?

public void setState(String state) {
    if (this.name.equals("jyjeon")) {
        throw new IllegalArgumentException("닉네임이 jyjeon인 회원의 정보는 변경할 수 없습니다.");
    }

    if (state.equals("blocked")) {
        if (this.state.equals("vip")) {
            throw new IllegalArgumentException("vip는 차단할 수 없습니다.");
        }
    } // 회원을 차단할 때의 도메인 규칙?

    if (state.equals("vip")) {
        if (this.state.equals("blocked")) {
            throw new IllegalArgumentException("차단된 사용자는 vip가 될 수 없습니다.");
        }
    } // vip로 상향할 때의 도메인 규칙?

    this.state = state;
}
public class 회원관리 {
    public void blockMember(회원 회원) {
        회원.setState("blocked");
    }

    public void upgradeMemberToVip(회원 회원) {
        회원.setState("vip");
    }
}

이런 코드로 해결하는 것은 좋지 않다. 하나의 공간에 너무 많은 책임이 있다.

 

문제점 1 (도메인 로직의 분산)의 해결책

필드의 값을 변경하려는 목적을 제대로 파악하여
그 목적을 잘 표현하는 메서드를 제공해라.
public class 회원 {
    private String name;
    private String state;

    public String getName() {
        return name;
    }
    
    public String getState() {
        return state;
    }

    public void blockMember() {
        verifyName();
        if (this.state.equals("vip")) {
            throw new IllegalArgumentException("vip는 차단할 수 없습니다.");
        }

        this.state = "blocked";
    }

    public void upgradeMemberToVip() {
        verifyName();
        if (this.state.equals("blocked")) {
            throw new IllegalArgumentException("차단된 사용자는 vip가 될 수 없습니다.");
        }

        this.state = "vip";
    }

    private void verifyName() { // 공통 도메인 규칙
        if (this.name.equals("jyjeon")) {
            throw new IllegalArgumentException("닉네임이 jyjeon인 회원의 정보는 변경할 수 없습니다.");
        }
    }
}
public class 회원관리 {
    public void blockMember(회원 회원) {
        회원.blockMember(); // 도메인 관련 로직을 도메인 영역에 위임
    }
    
    public void upgradeMemberToVip(회원 회원) {
        회원.upgrageMemberToVip(); // 도메인 관련 로직을 도메인 영역에 위임
    }
}

이 코드가 기존의 문제점을 어떻게 해결해줄까?

1. 공통 도메인 규칙이 응용 영역으로 분산된다.

회원 도메인의 상태를 변경할 때 공통으로 필요한 도메인 규칙을 도메인 영역 안에서 구현하고 지키게함으로써, 도메인 영역 외의 영역들에서는 더이상 공통 도메인 규칙에 대해 신경 쓸 필요가 없다.

헌데, 도메인 영역 안의 메서드라 해도, 이런 식의 코드는 공통 도메인 규칙을 검사하는 것을 깜빡할 수도 있다.

public class 회원 {
    private String name;
    private String state;

    public String getName() {
        return name;
    }

    public void blockMember() {
        if (this.state.equals("vip")) {
            throw new IllegalArgumentException("vip는 차단할 수 없습니다.");
        }

        setState("blocked");
    }

    public void upgradeMemberToVip() {
        if (this.state.equals("blocked")) {
            throw new IllegalArgumentException("차단된 사용자는 vip가 될 수 없습니다.");
        }

        setState("vip");
    }

    private void setState(String state) {
        verifyName(); // 공통 도메인 규칙과 필드에 값을 할당하는 로직을 묶어서 메서드로 뺀다.
        this.state = state;
    } 

    private void verifyName() {
        if (this.name.equals("jyjeon")) {
            throw new IllegalArgumentException("닉네임이 jyjeon인 회원의 정보는 변경할 수 없습니다.");
        }
    }

    public String getState() {
        return state;
    }
}

공통 도메인 규칙과 필드에 값을 할당하는 코드를 setState()라는 메서드로 묶어 추출했다. 필드에 값을 할당하고자 한다면 직접 값을 할당하지 말고 내부적으로 다시 setState()를 호출하도록 만들면, 값 할당과 함께 공통 도메인 규칙을 검사하는 로직이 실행된다.

 

2. 상황별 도메인 규칙이 응용 영역으로 분산된다.

각 상황별로 각각의 공개 메서드를 만들어서 그 안에 도메인 규칙을 구현하고, 그 각각의 메서드를 외부에 제공한다.

응용 영역에서 도메인 영역과 소통할 때, 도메인 모델과 관련된 모든 규칙들은 도메인 모델 안에서 알아서 구현하도록 했기 때문에, 응용 영역은 어떠한 도메인 규칙에 대해서도 신경쓸 필요가 없다. (캡슐화)

 

도메인과 관련된 어떤 처리가 필요할 때는 도메인 영역에서 제공하는 공개 메서드들 중에 목적에 맞는 메서드를 호출하고, 응용영역은 비즈니스 로직에만 집중할 수 있습니다.

 

비즈니스 로직에서 도메인 규칙을 가지고 있음 -> 응용 영역으로 도메인 영역의 책임이 분산된다. -> 응용 영역에 너무 많은 책임을 가지게 된다.

공개 set 메서드의 문제점 2) 잘못 정의한 메시지

Never Trust User (사용자를 믿지 마라)

'객체 지향의 사실과 오해 (조영호)'라는 책에서 객체와 객체 간에 주고 받는 메시지의 역할을 강조한다.

메시지를 수신받은 객체는 우선 자신이 해당하는 메시지를 처리할 수 있는지 확인한다.
메시지를 처리할 수 있다는 이야기는 객체가 해당 메시지에 해당하는 행동을 수행해야 할 책임이 있다는 것을 의미한다.
따라서 근본적으로 메시지의 개념은 책임의 개념과 연결된다.
송신자는 메시지 전송을 통해서만 다른 객체의 책임을 요청할 수 있고, 수신자는 오직 메시지 수신을 통해서만 자신의 책임을 수행할 수 있다.
따라서 객체가 수신할 수 있는 메시지의 모양이 객체가 수행할 책임의 모양을 결정한다.
- 객체지향의 사실과 오해, 조영호 -

객체가 객체에게 어떤 행동을 지시하는 유일한 방법은 메시지를 주고받는 것이다.

set메서드를 가지고 있는데 메시지 상으로 'XXX를 바꾼다'라는 메시지를 받아 수행할 수 있다.

하지만, 실제로는 그 메서드를 실행했을 때 문제가 생길 수 있음에도 실행 권한을 주어 버렸으므로 문제가 발생하는 것이다.

 

문제점 2 (잘못 정의한 메시지) 해결책

도메인 모델의 entity나 value에 공개 set 메서드만 넣지 않아도 일관성이 깨질 가능성이 줄어든다.
공개 set 메서드를 사용하지 않으면 의미가 드러나는 메서드를 사용해서 구현할 가능성이 높아진다.
예를 들어, set 형식의 이름을 갖는 공개 메서드를 사용하지 않으면
자연스럽게 cancel이나 changePassword처럼 의미가 더 잘 드러나는 이름을 사용할 빈도가 높아진다.
public class Post {

    private Long id;
    private String title;
    private String content;
    private Member writer;
}

1. id는 유일한 식별자이다. 통상적으로 식별자 값은 데이터의 무결성과 연관관계 등 고려해야 할 제약조건이 많아 수정하지 않는 것을 권장한다.

2. title을 바꾸기 위해서는 새 title이 '영어 대/소문자, 숫자, 특수문자를 포함하여 10자 이상'을 만족하는지 검사한다.

검사를 마친 후, title를 바꾸도록 메서드를 구현하고 setTitle() 보단 changeTitle()로 실행의도를 분명히 들어낸다.

3. writer의 값을 바꾸는 것은 금지되어 있다. 관련 메서드는 제공하지 않는다.

 

public class Post {
    
    private Long id;
    private String title;
    private String content;
    private Member writer;
    
    public changeTitle(String newTitle) {
        checkTtile(newTitle);
        this.title = newTitle;
    }
    
    private void checkTitle(String title) {
        Pattern titlePattern = Pattern.compile("^(?=.*[A-Za-z])(?=.*[0-9])(?=.*[$@$!%*#?&])[A-Za-z[0-9]$@$!%*#?&]{10,}$");
        Matcher matcher = titlePattern.matcher(title);

        if (!matcher.find()) {
            throw new IllegalArgumentException("title의 작성 양식에 맞지 않습니다.");
        }
    }
}

 

 출처  setter 쓰지 말라고만 하고 가버리면 어떡해요 - 여우

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

 

728x90
반응형
blog image

Written by ner.o

개발자 네로의 개발 일기, 자바를 좋아합니다 !