캡스톤 프로젝트를 진행하면서 우아한 테크코스의 코드 컨벤션을 따르게 되었습니다.
그 중 Entity Layer단에셔 Setter를 지양하는 이유에 대해
제대로 알지 못하고 사용했던 것 같아 정리하게 되었습니다.
사용했던 코드컨벤션 링크
woowacourse-docs/styleguide/java at main · woowacourse/woowacourse-docs
우아한테크코스 문서를 관리하는 저장소. Contribute to woowacourse/woowacourse-docs development by creating an account on GitHub.
github.com
코드 편리성 라이브러리 : Lombok
- @Setter 사용을 금지하고 메소드로 값을 변경하기
setter 지양에 대해서는 주변에서 많이 들어보셨을 거라고 생각합니다.
그러면 왜 setter를 지양해야할까요??
그 이유에 대해서 정확하게 알지 못하는 것 같아서 포스팅을 작성하게 되었습니다.
자바 빈즈 패턴(Java Beans Pattern)
매개 변수가 없는 생성자(기본 생성자)로 객체를 만든 후에 setter 메서드를 호출하여 원하는 매개변수의 값을 설정하는 방식을 자바 빈즈 패턴이라고 합니다.
public class User {
private String name;
private int age;
// 기본 생성자
public User() {}
// setter 메서드
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
}
사용예시
User user = new User();
user.setName("Alice");
user.setAge(30);
하지만 이렇게 하나의 객체를 만들기 위해서 메서드를 여러번 호출하는 것은 수고스러운 일이며,
setter 메서드의 특성상 불변 객체를 만들 수 없게 됩니다.
Setter를 지양해야 하는 이유
1. Setter 메소드로 값을 변경 시에 의도를 파악하기 어렵다.
public class Product {
private double price;
public Product(double price) {
this.price = price;
}
public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
}
}
public class ShoppingCart {
public static void main(String[] args) {
Product product = new Product(100.0);
System.out.println("Original price: " + product.getPrice());
// 외부에서 setPrice로 임의로 값 변경
product.setPrice(80.0); // 할인 가격으로 설정
System.out.println("Discounted price: " + product.getPrice());
}
}
위와 같은 코드가 있을 때, setPrice가 어떻게 동작했는 지 Product 클래스에선 알 수 없습니다.
product.setPrice(1000); // 가격 설정이 어떤 의도로 이루어졌는지 알 수 없음
위 코드에선 할인 가격으로 설정하기 위해 setPrice를 썼지만, 다른 곳에선 돈을 지불하고, 남은 돈을 넣기 위해서 setPrice를 사용할 수도 있습니다. 이렇게 되면 어떠한 의도로 데이터를 변경하였는 지 파악하기가 어려워집니다.
2. 객체의 일관성을 유지하기 어렵다.
자바 빈 규약에 따르는 Setter는 public으로, 변경할 수 있는 상태가 됩니다.
다른 코드를 짜다가 가격을 수정해야하는 일이 생길 때, setter를 사용하여 가격을 수정하게 될 것입니다.
product.setPrice(1000); // 가격을 직접 설정하는 방식은 할인 적용과 구별되지 않음
할인 가격으로 변경하는 메서드인 setPrice의 의미가 없어지게 되고, 어디서든 price가 수정되기 때문에 객체의 일관성이 깨지게 됩니다.
Entity Layer 단에서의 Setter를 지양해야하는 이유
setter의 경우 JPA에서 영속성 컨텍스트(Persistence Context)를 관리하여 객체 상태를 추적합니다. 즉, Entity객체의 필드값이 변경하면, JPA는 자동으로 변경사항을 감지하여 Update 쿼리를 생성합니다. 즉, Setter는 Update 기능을 수행합니다.
문제는 여러 곳에서 Entity의 상태를 Setter를 통해 Update를 진행하는 것 입니다.
각각의 Setter 호출마다 JPA가 Update 쿼리를 실행하게 되면서 쿼리의 추적이 어려워지고, 불필요한 쿼리가 자주 발생하면서 비용이 커지고, 성능이 저하될 수 있습니다.
그렇다면 이러한 Setter를 대체하는 방식은 어떤 것들이 있을까요?
Setter를 대체하는 방식
1. 사용한 의도와 의미를 알 수 있는 메서드 작성법
@Entity
@Getter
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private double price;
//생략 (기본생성자)
// price 변경을 위해 명확한 의도를 드러내는 메소드 제공
public void applyDiscount(double discountRate) {
if (discountRate < 0 || discountRate > 1) {
throw new IllegalArgumentException("Discount rate must be between 0 and 1.");
}
this.price *= (1 - discountRate);
}
public void applyDiscountToProduct(Long productId, double discountRate) {
Product product = productRepository.findById(productId)
.orElseThrow(() -> new IllegalArgumentException("Product not found"));
// 명확한 의도를 드러내는 메소드로 가격 변경
product.applyDiscount(discountRate);
productRepository.save(product); // 변경 사항을 저장
}
앞선 코드와 같은 setter를 사용하기보단, 명확한 의도를 드러내는 메소드의 이름을 명명하여 사용하는 방식이 있습니다. 해당 방식을 사용하면 한눈에 어떤 기능을 하는 지 알아볼 수 있습니다.
별도의 명확한 의도를 가진 메소드를 통해 update를 진행하면서 객체지향적인 특성을 가지고 가야합니다.
2-1. 생성자를 이용하여 값의 일관성을 유지하기
생성자를 이용하여 객체를 초기화 해주는 방법도 있습니다.
@Getter
public class User {
private final String username; // 필수 필드
private final String email; // 필수 필드
private final int age; // 선택적 필드
// 모든 필수 필드를 초기화하는 생성자
public User(String username, String email, int age) {
this.username = username;
this.email = email;
this.age = age;
}
// 오버로딩된 생성자로 age 필드 생략 가능
public User(String username, String email) {
this(username, email, 0); // age 필드는 기본값 0
}
}
일관성이 보장되고, 객체 생성시 필요한 정보만 전달하면 되기 때문에 작은 객체에 대해서는 단순하게 동작합니다.
하지만 매개변수가 늘어나게 되면서 어떤 값이 어떤 필드를 의미하는 지 파악하는 것이 어려워지고, 필드가 많아질수록 가독성이 저하됩니다. 또한, 선택적 필드를 설정하려면 여러 생성자를 오버로딩 해야하기 때문에 코드가 복잡해집니다.
2-2. 빌더 패턴을 이용한 일관성 유지
빌더 패턴을 이용하여 필수 필드와 선택적 필드를 명확하게 구분지어서 유연하고 가독성 높은 방식으로 객체를 생성할 수 있습니다.
✅ 코드 편리성 라이브러리 : Lombok
Builder 패턴, Getter 메서드, 빈 생성자에 대해서는 Lombok 어노테이션으로 대체합니다.반복적인 코드 사용을 줄여 코드 편의와 가독성을 높이기 위함입니다.
@Getter
@Builder
public class User {
private final String username; // 필수 필드
private final String email; // 필수 필드
@Builder.Default
private final int age = 0; // 선택적 필드, 기본값 0으로 초기화
// 필요한 필드에 대한 유효성 검사
private User(String username, String email, int age) {
if (username == null || email == null) {
throw new IllegalArgumentException("Username and email must not be null.");
}
this.username = username;
this.email = email;
this.age = age;
}
}
// 빌더 패턴을 사용한 객체 생성 예시
User user1 = User.builder()
.username("john_doe")
.email("john.doe@example.com")
.age(25) // age 지정
.build();
User user2 = User.builder()
.username("jane_doe")
.email("jane.doe@example.com")
.build(); // age는 기본값 0으로 설정됨
Lombok의 @Builder와 @Getter를 통해서 반복적인 코드의 사용을 줄이고 가독성을 높여서 객체의 값을 셋팅할 수 있습니다.
기본적으로 Builder는 기본값이 지정되어있지 않으면 int 형은 0, boolean형은 false, 참조형은 null로 셋팅됩니다.
@Builder를 사용할 때 필드에 직접적으로 값을 지정해줄 수 없기 때문에 @Builder.Default를 사용하여 기본값을 지정해줍니다. (해당 어노테이션을 붙이지 않고 값을 지정하면 빌더가 이를 무시하고, 명시적으로 설정 되어있는 앞선 기본 값들이 셋팅됩니다.)
마치며
이렇게 Setter를 지양해야하는 이유에 대해서 알아보았습니다.
현업에서는 이미 Setter를 쓰는 경우가 부지기수 하다고 들은 적이 있는 데, 기존의 작성 된 코드들을 건들기 보단 새롭게 작성되는 비즈니스 로직에서 Setter의 사용을 고민하고 있다면 명확한 메소드를 통해서 의미를 전달해주는 것이 좋을 거 같다고 느꼈습니다.
하나를 쓰더라도 명확하게 알고 사용하는 것은 확연한 차이가 있음을 느끼며 포스팅을 마치겠습니다.
reference
(Java) 점층적 생성자 패턴 & 자바 빈즈 패턴
프로그래밍에서 Class를 설계하다 보면, 필수로 받아야 할 인자와 선택적으로 받아야 할 인자가 구분됩니다. 그리고 Java에서는 이렇게 설계된 다양한 형태의 Class들을 객체화하는 3가지 패턴이 존
wildeveloperetrain.tistory.com
왜 Entity에 setter를 사용하지 말아야 할까?
Entity를 작성할 경우 작성한 필드를 setter로 작성하는 경우가 많다. setter를 사용하면 해당 Entity의 값을 변경할 수 있기 때문에 객체의 일관성을 보장할 수 없다고 알고 있기에 당연하게 Entity에서
velog.io
'개발 > 프로젝트' 카테고리의 다른 글
[단잠] SSE와 RabbitMQ를 이용한 알림 도입기 [2편] (0) | 2024.11.27 |
---|---|
[단잠] 트러블슈팅: SSE(Server-Sent-Events) 첫 연결 이후 바로 Connection closed가 되는 현상 (0) | 2024.11.26 |
[단잠] 알림 도입기 (feat. AMQP, RabbitMQ, SSE) [1편] (4) | 2024.11.19 |
[단잠] 대학교 기숙사 메이트 매칭 및 대학 생활 커뮤니티 서비스 회고 (2) | 2024.11.14 |
[FitTrip] 캡스톤 프로젝트 / 함께하는 운동 커뮤니티 FitTrip 회고록 (8) | 2024.11.06 |