본문 바로가기

코딩/Java, SpringBoot

[SpringBoot] 엔티티에는 setter를 두지 않는다. 확장성 있는 함수구성

왜 엔티티에 setter를 두지 않는지 몇개의 블로그를 읽고 정리했다.


https://velog.io/@aidenshin/%EB%82%B4%EA%B0%80-%EC%83%9D%EA%B0%81%ED%95%98%EB%8A%94-JPA-%EC%97%94%ED%8B%B0%ED%8B%B0-%EC%9E%91%EC%84%B1-%EC%9B%90%EC%B9%99

 

JPA 엔티티 작성 - Setter 금지

엔티티를 작성함에 제가 생각하는 몇가지 원칙(?)이 있습니다.그중 엔티티(객체)의 Setter 사용 금지 원칙(?) 에 대해 알아보겠습니다.엔티티를 작성할 때 습관적으로 모든 필드에 Setter를 생성하는

velog.io

<정리 : 엔티티에 setter를 두지 않는 이유>

  1. setter는 메서드의 의도를 알기 힘들다. setter가 아닌 상황에 맞는 메서드를 만들어서(사실 기능은 setter와 동일) 사용해라. 더 직관적이고 규모가 커짐에 따라 관리하기 쉬워진다.
  2. 객체 일관성을 유지하기 위해 객체 생성시점에만 값을 넣을 수 있게 한다. 더불어 생성자를 protected로 생성하면 객체의 일관성을 더 완벽하게 유지할 수 있다. (protected로 설정하는 이유는 JPA 기본 스펙 상 기본 생성자가 필요한데 protected로 제어하는 것 까지 허용되기 때문입니다.) 롬복을 사용한다면 @NoArgsConstructor(access = AccessLevel.PROTECTED) 어노테이션으로 쉽게 protected생성자를 만들 수 있다.
  3. "무분별한 Setter 사용금지는 엔티티 뿐 아니라 객체 생성 및 변경 시 모두 해당하는 부분입니다. 객체의 일관성을 유지할 수 있어야 프로그램의 유지 보수성을 끌어 올릴 수 있기 때문에 일급 컬렉션 사용 등 많은 개발자들이 객체의 일관성을 유지하기 위해 노력을 기울이고 있다고 생각합니다." 라고 쓰여져 있다.

위에서 객체의 일관성이라는 말이 나왔다 객체의 일관성은 무엇인지 알아보자. https://codingdog.tistory.com/entry/%EC%83%9D%EC%84%B1%EC%9E%90%EC%99%80-%EA%B0%9D%EC%B2%B4%EC%9D%98-%EC%99%84%EC%A0%84%ED%95%9C-%EC%83%81%ED%83%9C

 

java 생성자와 객체의 완전한 상태

 lombok에 빌더 어노테이션에 대해 생각해 보다가, 문득 builder를 왜 쓰는지가 궁금했습니다. 디자인 패턴에 대해서 하나도 모르는 저는 빌더 패턴을 3편에서 4편 정도를 쓸 듯 싶습니다. 먼저, '완

codingdog.tistory.com

윗 글에서 설명하고 있듯이 객체의 로직상 필요한 멤버가 항상 값이 채워져 있는 상태로 객체가 존재할 때 객체가 일관성이 있다고 한다. setter로 값을 채울경우 생성 후 setter를 호출하기 전까지 일관성이 일시적으로 깨지게 된다. 그리고 사람의 실수에 따라서도 일관성이 깨질 수 있다. 그럼 어떻게 객체의 일관성을 유지할 수 있을까?

아래 글에서 빌더패턴으로 객체의 일관성을 유지할 수 있다고 말한다.

 

 https://okky.kr/article/12599

 

OKKY | 자바빈즈패턴을 사용할때 객체의 일관성이 일시적으로 무너진다는 뜻을 모르겠습니다

디자인 패턴 중에 빌더패턴을 공부하고 있습니다 이해안되는 부분이 무엇이냐면 객체에 대한 필드값을 설정할때 setter 을 활용해서 값을 채워 넣는데 이때 필드가 많으면 많을수록 setter 의 호출

okky.kr

객체의 일관성 : 필수인 모든 멤버가 값이 채워져있는 상황.

자바 빈즈 패턴 : 생성자의 인자가 없고 setter메소드만 존재하는 방식으로 객체를 구성

이런 경우를 객체의 일관성이 깨지는에 이것을 빌더패턴으로 방지할 수 있다. 대신 설명자료에 있듯이 멤버가 final이어야 한다.

 

 


 

https://www.inflearn.com/questions/16235

 

생성 메서드 setter 질문 - 인프런 | 질문 & 답변

entity에는 Setter가 존재하면 안된다는 글을 많이 보았습니다. 때문에 생성자를 통해서만 entity가 만들어지도록 샘플프로젝트를 만들어왔었는데요. 물론  교육이라 setter를 사용하신다고 생각하는

www.inflearn.com

윗 글에서는 아래와 같이 김영한 님의 답변을 볼 수 있었다.(배민 개발 ~ 리더인가 팀장이다)

안녕하세요. bk님^^

객체를 생성할 때는 3가지 방법중 하나를 사용합니다.

- 생성자

- 정적 팩토리 메서드

- Builder 패턴

엔티티에 따라 이 방법중 상황에 따라서 하나를 선택하고, 파라미터에 객체 생성에 필요한 데이터를 다 넘기는 방법을 사용합니다. 그리고 정적 팩토리 메서드나, Builder 패턴을 사용할 때는 생성자를 private 처리합니다. 객체 생성이 간단할 때는 단순히 생성자를 사용하고, 만약 객체 생성이 복잡하고, 의미를 가지는 것이 좋다면 나머지 방법 중 하나를 선택합니다.

그러면 setter가 없는데, 엔티티를 어떻게 수정할까요?

이것은 setter를 만들기 보다는 의미있는 변경 메서드 이름을 사용합니다. 예를들어서 고객의 등급이 오른다면 member.levelUp() 같은 메서드가 있겠지요? 이 메서드는 내부의 필드 값을 변경하고요.


https://yoonbing9.tistory.com/28

 

엔티티에 setter를 사용하면 안되는 이유와 대체방안

엔티티에 setter를 열어두면 어떻게 될까? 보통 스프링은 여러가지 클래스 종류로 나눌수 있지만 대표적으로 Controller, Service, Repository, Domain 등 으로 분류할 수 있다. 엔티티는 도메인에 해당하며

yoonbing9.tistory.com

<정리>

  • 보통 스프링은 여러가지 클래스 종류로 나눌수 있지만 대표적으로 Controller, Service, Repository, Domain 등 으로 분류할 수 있다. 엔티티는 도메인에 해당하며 여기서 만약 setter를 열어뒀다고 가정해보자.
  • 엔티티에 setter로 필드 상태를 변경하고 트랜잭션이 종료되면 변경감지에 의해 update문이 나가게 된다.
  • 그런데 Controller에서도 Entity를 생성하여 setter를 호출하고,,, Service에서도 호출하고,,
  • 시스템이 복잡하다면 운영시 해당 Update문이 어디서 누구에 의해 발생했는지 추적하기가 굉장히 어려워진다.

위의 문제는 Entity를 repository, service layer에서만 존재하게 하면 해결가능하다. 애초에 Entity의 범위를 한정하고 DTO클래스를 사용하는 이유가 위와 같은 이유이다. 즉 setter를 사용하지 않는 타당한 이유는 아니다. 그럼에도 위의 글을 가져온 것은 아래의 내용 때문이다. 아래의 내용이 무슨말인지 잘 모르겠기 때문임...

  • 도메인 모델 패턴에서는 이런 메서드를 도메인에 구현하여 응집도 있게 개발한다.
  • 서비스에서는 이런 도메인의 비즈니스 메서드를 조립하고 트랜잭션 단위로 잘 묶는 것이 포인트이다.

https://khdscor.tistory.com/15

 

JPA setter을 만들지 않아야 하는 이유

나는 JPA 엔티티를 만들면 항상 getter, setter을 만들었었다. 하지만 setter는 사용하지 않는 게 좋다고 한다. 이 글은 그 이유를 간단하게 기록해본 것이다. JPA에서 데이터 타입은 크게 값 타입과 엔

khdscor.tistory.com

 

JPA에서 데이터 타입은 크게 값 타입과 엔티티 타입으로 나뉜다. 

여기서 값 타입은 기본값 타입, 임베디드 타입, 컬렉션 값 타입 으로 나뉘고 기본값 타입은 자바 기본 타입(int, double), 래퍼 클래스(Integer), String 로 나뉜다.

엔티티를 구성할 때 값 타입(기본값(int, double, 래퍼클래스, String), 임베디드, 컬렉션)으로 구성하는 경우가 많은데 각 값 타입은 다른 엔티티들과 공유를 하면 위험하다. 한 엔티티에서 다른 엔테티의 값을 변경하면 위험하기 때문이다. 

일반적인 자바 기본 타입은 값을 대입하면 복제가 이뤄진다. 하지만 임베디드 타입같은 객체형식의 값타입은 값을 대입하면 복제가 아닌 참조가 일어나서 대입한 값이 변하면 원래의 값도 변하게 된다. 이는 매우 위험한 상황이기 때문에 조심하야 한다. 

이를 위한 해결법은 첫번째로 참조가 아닌 복사의 방식을 이용하면 된다. 하지만 이는 임시적인 방식이 뿐 객체의 공유 참조는 피할 수 없다. 즉, 근본적인 해결책이 필요한데 가장 단순한 방법은 객체의 값을 수정하지 못하게 막으면 된다. 이를 불변객체라 하는데 값 타입은 될 수 있으면 불변 객체로 설계해야 한다.

불변 객체로 설계하는 가장 간단한 방식은 생성자로만 만들고 setter를 만들지 않으면 된다.

그렇게 하면 값을 수정할 수 없으므로 공유한다 해도 문제가 발생하지 않는다. 

참고로 흔히 사용되는 Interger, String 도 불변객체이다. 

불변객체의 의미 = 스레드 세이프


결국 블로그의 내용을 정리하면 아래 두가지 이유때문에 setter를 사용하지 않는다.

1. 코드의 가독성, 유지보수

2. 불변객체


사실 이 글을 쓴 이유는 아래의 내용 때문이다.. 어쩌다 보니 글이 길어졌다.

아래는 MyOrder 프로젝트의 Demand 엔티티의 코드이다. Demand = 주문, order 를 뜻한다. order가 예약어라서 Demand라는 단어를 선택했다. 주문은 status로 waiting, accepted, completed, rejected 4가지의 상태를 가지고 있다. 엔티티에서 setter를 두면 안된다고 배웠다. 그래서 아래와 같이 메소드로 함수를 정의했다. 여기서 말하고 싶은 것은 changeTo(DemandStatus status){}로 함수를 구성한 것이 아니라 changeToAccepted(){}로 함수를 구성했다는 것이다. 이렇게 하는게 더 직관적이고 나중에 상태를 바꾸는 동작마다 다른 동작을 추가하기 쉽다. 예를 들어 changeToAccepted()가 실행되었을 때 더 필요한 로직을 쉽게 추가할 수 있다. 만약 changeTo(DemandStatus status){}로 함수를 구성했다면 if, else 안에서 추가된 로직을 넣어야 할 것이다. 즉 아래와 같이 작성하는 것이 더 확장성 있다고 생각한다.

(어디서 배운게  아니고 단순 내 생각이라 틀릴 수 있다.

 public boolean changeToAccepted() {
        if (status == DemandStatus.WAITING) {
            status = DemandStatus.ACCEPTED;
            return true;
        }
        return false;
    }
    
    public boolean changeToRejected() {
        
        if (status == DemandStatus.WAITING) {
            status = DemandStatus.REJECTED;
            return true;
        }
        return false;
    }
    
    public boolean changeToCompleted() {
        
        if (status == DemandStatus.ACCEPTED) {
            status = DemandStatus.COMPLETED;
            return true;
        }
        return false;
    }