왜 Entity에 setter를 사용하지 말아야 할까?

lango·2022년 7월 24일
13

스프링(Spring)

목록 보기
2/5
post-thumbnail

들어가며

 최근 Spring boot와 더불어 JPA 학습에 열심을 다하고 있다. 그러다 엔티티(Entity) 클래스를 작성하던 중, 문득 Setter 생성 여부에 대해서 궁금한 점이 생겼다. 누구나 Setter에 대해서 조금만 찾아보면 Setter를 두는 것은 바람직하지 않다는 내용을 손쉽게 찾아볼 수 있다. 다른 분들이 되도록 사용하지 않으니 필자도 최대한 setter를 지양하고 있었는데, 그냥 넘어갈 수 없겠구나라는 생각이 들어 Setter를 지양해야 하는 이유에 대해서 살펴보고 기록하려 한다.

 필자도 setter를 사용하면 안된다는 것은 아니지만 지양해야 한다는 생각을 가지고 있다. 이 주장의 합리적인 근거를 구체화하기 위해 왜 Setter가 나쁜 것인지, 최종적으로 Setter 사용하지 않는 것이 좋은 것인지를 간단하게 정리해보자.




왜 setter를 지양해야 할까?

setter를 아무생각없이 사용하게 되었을 때 발생하는 대표적인 문제 2가지를 살펴보자.

🤔 사용한 의도를 쉽게 파악하기 어렵다.

Post post = new Post();
post.setId(1L);
post.setUserId("member1");
post.setTitle("제목입니다.");
post.setCont("내용입니다.");

위 코드의 경우 게시글 정보 Entity인 post를 set 메서드를 통해 값을 변경하는데 게시글의 값을 생성하는 구문인지, 변경하는 구문인지 정확한 의도를 파악하기 어렵다. 더군다나 객체의 내부 값이 많거나 복잡하다면, 더더욱 한눈에 알아보기 힘들 것이다.

🧐 일관성을 유지하기 어렵다.

public Post updatePost(Long id) {
    Post post = findById(id);
    post.setTitle("제목을 수정합니다.");
    post.setCont("내용을 수정합니다,");
    return post;
}

다음으로 위 코드의 경우 게시글을 변경하는 메소드인데, public으로 작성된 setter 메소드를 통해 어디서든 접근이 가능하기에 의도치 않게 post의 값을 변경하는 경우가 발생할 수 있다. 그렇다면 결국 post 객체의 일관성은 무너지게 된다.


setter 없이 어떻게 데이터를 수정할까?

setter의 경우 JPA의 Transaction 안에서 Entity 의 변경사항을 감지하여 Update 쿼리를 생성한다. 즉 setter 메소드는 update 기능을 수행한다.

여러 곳에서 Entity를 생성하여 setter를 통해 update를 수행한다면 복잡한 시스템일 경우 해당 update 쿼리의 출처를 파악하는 비용은 어마어마할 것이다.

그렇다면 어떻게 setter를 배제할까? 아니 setter를 어떤 방식으로 대체하는지 알아보자.

✅ 사용한 의도나 의미를 알 수 있는 메서드를 작성하자.

@Getter
@Entity
public class Post {

    private Long id;
    private String userId;
    private String title;
    private String cont;
    
    // ... 이하 생략
    
    public void updatePost(Long id, String title, String cont) {
        this.id = id;
        this.title = title;
        this.cont = cont;
    }
    
}
post.updatePost(1L, "수정할 제목입니다.", "수정할 내용입니다.");

위와 같이 Entity 내부에 updatePost라는 메서드를 작성하여 사용한다면, setter 메소드를 작성하여 사용하는 것보다 행위의 의도를 한눈에 알기 쉽다.

따라서 setter를 public으로 열어두고 사용하는 것보다는, 별도로 변경이라는 의미가 담긴 메서드를 통해 update 처리를 객체지향스럽게 쓰는 게 좋다.

✅ 생성자를 통해 값을 넣어 일관성을 유지하도록 하자. (feat. @Builder)

또한, Entity의 일관성을 유지하기 위해 생성시점에 값을 넣는 방식으로 setter를 배제할 수 있다.

@Getter
@Entity
public class Post {

    private Long id;
    private String userId;
    private String title;
    private String cont;

    @Builder
    public Post(Long id, String userId, String title, String cont) {
        this.id = id;
        this.userId = userId;
        this.title = title;
        this.cont = cont;
    }
    // ... 이하 생략
}
Post post = Post.builder()
		.id(1L)
        .userId("member1")
        .title("제목입니다.")
        .cont("내용입니다.")
        .build();

setter를 배제하기 위해 여러 생성자를 작성할 수도 있는데, 위와 같이 lombok의 @Builder 애노테이션을 통해 많은 생성자를 사용할 필요 없이 setter의 사용을 줄일 수 있다. 이로 인해, 빌더 패턴을 통해 post 객체의 값을 세팅할 수 있게 된다.


@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class Post {

    private Long id;
    private String userId;
    private String title;
    private String cont;

    protected Post(Long id, String userId, String title, String cont) {
        this.id = id;
        this.userId = userId;
        this.title = title;
        this.cont = cont;
    }
    
}

그리고 위와 같이 생성자의 접근제어자를 protected로 선언하면 new Post() 작성이 불가하기 때문에 객체 자체의 일관성 유지력을 높일 수 있다.

그리고 lombok에서 제공하는 @NoArgsConstructor 애노테이션을 사용하여 더 편하게 작성할 수 있다. 위 코드에서는 access = AccessLevel.PROTECTED 옵션을 부여하여 무분별한 객체 생성에 대해 한번 더 체크할 수 있도록 하였다.

💡 생성자를 private으로 선언하면 안되는 이유
private 접근제한자로 선언하게 된다면 JPA에서 오류를 발생시킨다. 이로 인해 JPA에서 프록시 객체를 만들어 해당 프록시 객체가 직접 만든 class 객체를 상속하기 때문에 public 이나 protected 까지 허용된다.


Setter 사용 여부에 대한 나의 생각

 처음 말했던 것처럼, Entity를 작성할 때 무조건 Setter를 배제해야 한다는 강박관념을 가질 필요는 없다고 생각한다. 상황이나 경우에 따라 이미 많은 Setter가 핵심 비즈니스에 여러 군데 침투되어 있다고 하면, 그리고 Setter 없이는 유지보수나 운영이 쉽지 않은 환경이라면 별도의 마이그레이션이나 리팩터링 일정이 잡히지 않는 이상 Setter를 그대로 사용해야 하는 것이 나을 수도 있다.

 반면에, Setter를 사용할지 말지 선택해야 하는 상황이라면 Setter를 사용하기보다는, 별도의 의미를 부여할 수 있는 메서드를 사용할 것 같다. 빌더 패턴이나 정적 팩토리 메서드와 같은 방법들로도 충분히 Setter를 대체할 수 있으니 고민할 이유가 없다고 생각한다. 단순히 하나의 필드를 변경하더라도 값을 바꾸는 이유를 드러내지 않기 때문에 메서드의 결과를 쉽게 예측할 수 없어 추가적인 비용이 들어가게 된다. 그리고 다른 객체들로 책임이 분산된다.

class Comment {
	private String content;
    public Comment(String balance) {
        this.balance = balance;
    }
    public void setContent(String content) {
        this.content = content;
    }
    public void changeComment(String content) {
        this.content = content;
    }
}
 
Comment comment = new Comment("댓글을 작성합니다.");

// 아래 두 메서드 중 무엇이 더 좋을까?
comment.changeComment("댓글을 수정합니다.");
comment.setContent("댓글을 수정합니다.");

👍 메서드의 행위를 명확하게 만들자.

 단순히 하나의 필드만 변경하더라도 Setter도 사용할 수 있고, 별도의 의미를 가진 비즈니스 메소드를 작성할 수도 있지만, 위 코드에서 보이는 setContent 메서드를 사용할 수도 있겠지만, 필자는 보다 댓글 변경이라는 행위를 명확하게 예측할 수 있는 changeComment 메서드를 사용할 것이다. 이 것은 굉장히 중요하지만 정답이 정해져도 않아서 더욱 결정이 힘들다.

🤝 다른 동료와의 협업을 고려하자.

 또한, 협업과 관련하여 중요하게 고려해야 할 점은 바로 외부에서 접근 가능한 Setter의 맥락 공유 필요성이다. 위 코드에 선언한 setContent 메서드와 changeComment 메서드는 모두 Public Setter로 지정되어 외부에서 아무 제약없이 사용할 수 있으니 이것을 사용하는 개발자들은 골치가 아파질 수 있다. 다른 동료가 리뷰를 해준다고 가정했을 때, Setter로 작성된 애매한 일관성을 가지는 setContent 메서드보다는 changeComment 메서드가 가독성 측면에서나 유지보수 차원에서 수월하게 피드백을 줄 수 있지 않을까?




마치며

 이번 기회에 Setter와 관련된 내용들을 배우며 마구잡이로 Setter를 사용하면 안된다는 인식에서 벗어날 수 있는 안목을 가질 수 있었다. Setter 메소드를 작성하게 되더라도, 어떤 동작을 하는지 미리 파악할 수 있다면 Setter를 두어도 충분할 수 있다. 다만, 애플리케이션 개발시 복잡한 객체 구조를 가지고 있을 경우 Setter 메소드를 작성하는 것보다 별도의 의미를 가진 메서드를 작성하는 방식이 더 효율적일 수 있다는 것을 염두에 두어야 함을 느꼈다.

 사실 혼자 개발한다면 크게 걱정할 일은 아니라고 보여진다. 나는 혼자 개발하기보다는 동료와 함께 개발해야 할 경우가 더 많다고 생각이 드는데, 이러한 코드 레벨에서의 고민들이 결국 협업에 영향을 미치게 된다고 생각이 든다. Setter를 사용하든, 사용하지 않든, 코드의 가독성을 높여서 다른 개발자와의 협업 간 불필요한 코드 리딩 시간을 줄일 수 있다면 이를 위한 협업 컨벤션을 따르는 것이 적절하지 않을까?

 이번 글을 통해서 여러가지 복합적인 고민들이 새롭게 생겨나서 배울 게 많아지기도 했지만 귀한 경험이었고, 배울 수 있음에 감사함을 느낀다.




참고문서

profile
찍어 먹기보단 부어 먹기를 좋아하는 개발자

2개의 댓글

comment-user-thumbnail
2023년 12월 22일

jpa 개발시 방향을 잡는데 도움이 되었습니다 감사합니다!

1개의 답글