Table of contents
Setter를 쓰지 말아야 한다고 주장하는 사람들이 어떤 의미에서 쓰지 말라고 하는지는 100% 동감한다. 하지만 단순히 “Setter를 쓰지 말라”는 말은 우리를 본질에서 멀어지게 만든다. 그렇다면 어떤 맥락에서 Setter를 쓰지 말라는 소리가 나오는 걸까?
빈약한 도메인 모델
회사가 소프트웨어를 개발하는 이유는 돈을 벌기 위함이다. 여기서 돈을 버는 로직은 결국 회사의 관심사인 “도메인”이 된다. 그렇다면 회사에서는 이 핵심 관심사를 잘 유지하고 개선하며 발전시켜나가야 비즈니스가 성장하고 앞으로도 계속 성장할 수 있을 것이다.
아래 코드는 간단한 커머스 도메인을 나타낸 코드이다.
public class User {
private int id;
private String name;
private double balance;
// getters and setters
}
public class Product {
private int id;
private String name;
private double price;
// getters and setters
}
public class Order {
private int id;
private int userId;
private int productId;
private int quantity;
// getters and setters
}
public class OrderService {
public void processOrder(User user, Product product, int quantity) {
double totalPrice = product.getPrice() * quantity;
if (user.getBalance() >= totalPrice) {
user.setBalance(user.getBalance() - totalPrice);
Order order = new Order();
order.setUserId(user.getId());
order.setProductId(product.getId());
order.setQuantity(quantity);
// save order to database
} else {
throw new InsufficientFundsException();
}
}
}
이 코드를 보면 User, Product, Order가 존재한다는 것은 알 수 있다. 하지만 모델 자체는 단순 데이터 모델일 뿐, 이 모델만으로 회사의 핵심 비즈니스를 파악하기는 어렵다.
예를 들어, 회사가 성장함에 따라 VIP 고객 유치를 위한 새로운 로직이 추가돼야 한다고 가정해보자. 결국 가격 책정을 하는 코드를 찾아내서 아래와 같은 코드를 추가 작성해야 할 것이다.
public class OrderService {
public void processOrder(User user, Product product, int quantity) {
double price = product.getPrice();
// 새로운 로직: VIP 고객 할인 적용
if (user.isVip()) {
price = price * 0.9; // 10% 할인
}
double totalPrice = price * quantity;
if (user.getBalance() >= totalPrice) {
user.setBalance(user.getBalance() - totalPrice);
Order order = new Order();
order.setUserId(user.getId());
order.setProductId(product.getId());
order.setQuantity(quantity);
order.setTotalPrice(totalPrice);
// save order to database
} else {
throw new InsufficientFundsException();
}
}
// 새로운 메소드: VIP 상태 변경
public void changeVipStatus(User user, boolean isVip) {
user.setVip(isVip);
// update user in database
}
}
지금 상태에서는 코드가 적고 주석도 잘 적혀 있어 그럭저럭 이해할 수 있다. 하지만 이 코드는 점차 유지보수가 이뤄지고 여러 개발자들의 손을 거치면서 형체를 알아보기 힘들 정도로 변질될 가능성이 크다.
풍부한 모델
그렇다면 풍부한 모델이란 무엇일까? 사실 이에 대해서는 주관적이며, 정해진 부분은 없다. 다만 대체로 객체지향 모델에서 이야기하는 부분과 맞닿아 있다고 생각한다.
그 자체로 완전한 객체이다.
의도를 드러내는 코드이다.
이와 반대되는 경우가 앞서 본 빈약한 모델 예제다. 그렇다면 아래 풍부한 모델 예제를 보자.
public class User {
private int id;
private String name;
private Money balance;
private List<Order> orders;
public User(int id, String name, Money initialBalance) {
this.id = id;
this.name = name;
this.balance = initialBalance;
this.orders = new ArrayList<>();
}
public void placeOrder(Product product, int quantity) {
Money totalPrice = product.calculatePrice(quantity);
if (balance.isLessThan(totalPrice)) {
throw new InsufficientFundsException();
}
Order order = new Order(this, product, quantity);
balance = balance.subtract(totalPrice);
orders.add(order);
}
// Other methods...
}
public class Product {
private int id;
private String name;
private Money price;
private Money cost;
public Product(int id, String name, Money price, Money cost) {
this.id = id;
this.name = name;
this.price = price;
this.cost = cost;
}
public Money calculatePrice(int quantity) {
return price.multiply(quantity);
}
public Money calculateMargin(int quantity) {
return calculatePrice(quantity).subtract(cost.multiply(quantity));
}
// Other methods...
}
public class Order {
private int id;
private User user;
private Product product;
private int quantity;
private Money totalPrice;
private Money margin;
public Order(User user, Product product, int quantity) {
this.user = user;
this.product = product;
this.quantity = quantity;
this.totalPrice = product.calculatePrice(quantity);
this.margin = product.calculateMargin(quantity);
}
// Other methods...
}
public class Money {
private BigDecimal amount;
private Currency currency;
// Constructor, arithmetic methods (add, subtract, multiply), comparison methods...
}
public class OrderService {
public void processOrder(User user, Product product, int quantity) {
user.placeOrder(product, quantity);
// Save order to database
}
}
이 예제를 통해 다음과 같은 사실들을 알 수 있다.
유저는 주문을 할 수 있다.
소지 금액이 부족하면 주문은 실패한다.
소지 금액이 충분하면 금액이 차감되고 주문은 성공한다.
상품은 수량에 따른 마진 및 금액을 계산할 수 있다.
이를 통해 OrderService는 도메인 모델을 조정하는 역할만 수행하면 되므로 더욱 간단해진다.
앞서 본 빈약한 모델 예제처럼 요구사항을 추가해보자.
public interface Membership {
BigDecimal getDiscountRate();
}
public class RegularMembership implements Membership {
@Override
public BigDecimal getDiscountRate() {
return BigDecimal.ZERO;
}
}
public class VipMembership implements Membership {
@Override
public BigDecimal getDiscountRate() {
return new BigDecimal("0.10"); // 10% discount
}
}
public class User {
private int id;
private String name;
private Money balance;
private List<Order> orders;
private Membership membership;
public User(int id, String name, Money initialBalance) {
this.id = id;
this.name = name;
this.balance = initialBalance;
this.orders = new ArrayList<>();
this.membership = new RegularMembership();
}
public void placeOrder(Product product, int quantity) {
Money totalPrice = product.calculateDiscountedPrice(quantity, membership);
if (balance.isLessThan(totalPrice)) {
throw new InsufficientFundsException();
}
Order order = new Order(this, product, quantity, membership);
balance = balance.subtract(totalPrice);
orders.add(order);
}
public void upgradeMembership() {
this.membership = new VipMembership();
}
public void downgradeMembership() {
this.membership = new RegularMembership();
}
// Other methods...
}
public class Product {
// ...
public Money calculateDiscountedPrice(int quantity, Membership membership) {
Money totalPrice = price.multiply(quantity);
BigDecimal discountRate = membership.getDiscountRate();
return totalPrice.subtract(totalPrice.multiply(discountRate));
}
public Money calculateMargin(int quantity, Membership membership) {
Money discountedPrice = calculateDiscountedPrice(quantity, membership);
Money totalCost = cost.multiply(quantity);
return discountedPrice.subtract(totalCost);
}
// ...
}
public class Order {
// ...
public Order(User user, Product product, int quantity, Membership membership) {
this.user = user;
this.product = product;
this.quantity = quantity;
this.totalPrice = product.calculateDiscountedPrice(quantity, membership);
this.margin = product.calculateMargin(quantity, membership);
}
// ...
}
public class OrderService {
public void processOrder(User user, Product product, int quantity) {
user.placeOrder(product, quantity);
// Save order to database
}
}
보듯이 OrderService는 변경 없이, 도메인 모델만 수정하여 핵심 로직을 추가해냈고, 추가된 로직도 알아보기 쉽게 구성되었다. 이 코드를 통해서 멤버쉽은 업그레이드 될 수도, 다운그레이드 될 수도 있으며 멤버쉽에 따른 할인도 존재한다는 사실을 알기 쉽다.
그래서?
결국 애초에 질문이 잘못되었다고 생각한다. 객체지향 관점에서 보면 불완전한 객체 혹은 의도를 표현하지 못하는 객체, 그리고 도메인 주도 설계 관점에서는 도메인을 표현하지 못하는 모델, 즉 “빈약한 모델”을 작성하지 말라는 것이 핵심이다. Setter 자체는 빈약한 모델이나 불완전한 객체에서 자주 보이는 케이스일 뿐이다. Setter를 남용하다 보면 빈약한 모델을 만들 위험이 높아진다는 점이 문제다.
간혹 “setter를 쓰지 말라”는 주장과 함께 의도를 드러내는 네이밍을 사용하라는 이야기가 나온다. 이 부분은 어느 정도 공감한다. 하지만 보통 블로그에서 다루는 예시 코드를 보면 다음과 같은 경우가 많다.
public class Post {
...
private String content;
// bad!
public void setContent(String content) {
this.content = content;
}
// good!
public void updateContent(String content) {
this.content = content;
}
// good!
public void changeContent(String content) {
this.content = content;
}
}
이 경우에는 사실상 setter와 동일하다고 생각한다. 변경한다는 관점에서는 차라리 setter라는 네이밍이 더 직관적일 수 있다. setter를 update나 change로 바꾼다고 해서 setter가 아니게 되거나 의도가 더 드러난다고 보기는 어렵다. 그렇기 때문에 위와 같은 경우라면 setter를 써도 무방하다고 본다. 오히려 저런 예시들은 “setter에 대한 반골 기질”이 만들어낸 결과물처럼 느껴지기도 한다.
반면 setter를 쓰지 말아야 하는 경우는 다음과 같다.
public class Account {
private int balance;
// bad!
public void setBalance(int balance) {
this.balance = balance;
}
public void updateBalance(int balance) {
this.balance = balance;
}
public void changeBalance(int balance) {
this.balance = balance;
}
// good!
public void withdraw(int money) {
if(money > balance) {
throw new IllegalArgumentException("계좌 잔액이 부족합니다.");
}
this.balance -= money;
}
}
public class Main {
public static void main(String[] args) {
//bad!
Account account1 = new Account(1_000);
//700원 인출을 가정하고, 300원으로 set한다. update or change도 동일한 의미를 갖는다.
account1.setBalance(300);
// good!
Account account2 = new Account(1_000);
//700원을 인출한다.
account2.withdraw(700);
}
}
계좌에서 금액을 인출하는 예제다. 여기서 update나 change 같은 메서드명은 setter와 크게 다를 바가 없으며, 의도도 명확하게 전달하지 못한다는 점이 훨씬 더 와닿을 것이다.
결론
결론적으로 “Setter를 쓰지 말아야 한다”가 아니라, “Setter가 만들어내는 결과물”에 집중해야 한다고 생각한다. 빈약한 모델을 방지하기 위해서는 도메인의 핵심 로직을 객체 안으로 잘 녹여야 하며, 단순히 데이터를 옮겨 담는 수준에 그치지 않는 모델링이 필요하다.