본문 바로가기

TIL(Today I Learned it)

TIL.40 Service 클래스에서 Setter 사용의 문제점

Spring Boot에서 Service 클래스에서

객체의 필드를 변경하기 위해 set 메서드를 사용하는 것은

보안 취약점으로 이어질 수 있습니다.

특히, 객체가 외부로부터 입력된 데이터에 의해 수정될 때

불변성을 유지하지 못해 예상치 못한 동작을 초래하거나

무결성 문제를 일으킬 수 있습니다.

1. Service 클래스에서 Setter 사용의 문제점

  1. 불변성 결여 : Setter 메서드를 사용하면 객체의 필드를 언제든지 수정할 수 있게 됩니다. 이는 객체가 불변(immutable)하지 않게 되고, 특히 객체가 여러 곳에서 공유되거나 스레드 간에 공유될 때 불변성을 보장하지 않기 때문에 동시성 문제와 같은 이슈가 발생할 수 있습니다.
  2. 비즈니스 로직 무결성 문제 : Service 클래스에서는 주로 비즈니스 로직을 다루게 됩니다. Setter를 이용해 필드를 외부에서 수정할 수 있게 되면, 데이터의 일관성에 문제가 생길 수 있습니다. 예를 들어, 특정 필드가 무결성을 유지하기 위한 규칙이 있는데, Setter를 이용하면 이 규칙이 깨어질 수 있습니다.
  3. 객체의 예기치 않은 상태 변화 : Setter를 사용하면 외부에서 객체의 필드를 직접 변경할 수 있습니다. 이는 객체가 예상치 못한 상태에 빠지게 할 수 있으며, 결과적으로 시스템의 동작을 예측하기 어렵게 만듭니다.

2. 객체를 불변으로 만들기

객체의 불변성(immutable)

객체가 생성된 이후에는 그 상태가 변경되지 않음을 의미합니다.

이는 특히 멀티스레드 환경에서 매우 유용합니다.

불변 객체를 사용하면 스레드 간 동기화 문제를 최소화할 수 있습니다.

1) 불변 객체 설계

불변 객체를 설계하려면 다음과 같은 접근 방식을 사용해야 합니다.

  • 모든 필드를 privatefinal로 선언합니다.
  • 객체를 생성할 때 모든 값을 초기화하고, 그 이후에는 필드를 수정하지 않도록 합니다.
  • Setter 메서드를 제공하지 않습니다.
public class User {
    private final String name;
    private final String email;

    public User(String name, String email) {
        this.name = name;
        this.email = email;
    }

    public String getName() {
        return name;
    }

    public String getEmail() {
        return email;
    }
}

위 예제에서 User 클래스는

필드가 final이므로 객체가 생성된 이후에는 변경될 수 없습니다.

이로 인해 데이터가 변경되지 않음을 보장할 수 있습니다.

3. 빌더 패턴 사용

객체 생성 시 필드가 많으면 생성자를 사용하는 방식이 복잡해질 수 있습니다.

빌더 패턴은 이러한 문제를 해결하면서도

불변성을 유지할 수 있는 방법입니다.

1) 빌더 패턴 예제

빌더 패턴은

클래스 내부에 Builder라는 정적 클래스를 두어

단계적으로 객체를 생성할 수 있게 합니다.

public class Product {
    private final String name;
    private final String description;
    private final double price;

    private Product(Builder builder) {
        this.name = builder.name;
        this.description = builder.description;
        this.price = builder.price;
    }

    public static class Builder {
        private String name;
        private String description;
        private double price;

        public Builder setName(String name) {
            this.name = name;
            return this;
        }

        public Builder setDescription(String description) {
            this.description = description;
            return this;
        }

        public Builder setPrice(double price) {
            this.price = price;
            return this;
        }

        public Product build() {
            return new Product(this);
        }
    }

    public String getName() {
        return name;
    }

    public String getDescription() {
        return description;
    }

    public double getPrice() {
        return price;
    }
}

이 빌더 패턴을 사용하면

객체 생성 시의 가독성을 높이고,

생성된 객체는 불변성을 유지합니다.

4. DTO 사용

Service 계층에서 직접 도메인 객체를 수정하는 대신,

DTO를 사용하여 데이터를 전달하는 것이 좋습니다.

이를 통해 데이터 수정 로직을 도메인 객체와 분리할 수 있습니다.

1) DTO 사용 예제

예를 들어, 사용자 정보를 업데이트하는 경우

User 객체를 직접 수정하지 않고,

UserUpdateDTO를 사용하여 데이터를 전달합니다.

public class UserUpdateDTO {
    private String name;
    private String email;

    // Getter and Setter
}

Service 클래스에서는

DTO를 받아서

도메인 객체를 갱신합니다.

public void updateUser(Long userId, UserUpdateDTO userUpdateDTO) {
    User user = userRepository.findById(userId);
    if (user != null) {
        user = new User(userUpdateDTO.getName(), userUpdateDTO.getEmail());
        userRepository.save(user);
    }
}

이렇게 하면 객체의 무결성을 유지하고,

DTO는 단순히 데이터를 전달하는 역할만 하기 때문에

Service에서 객체의 상태를 안전하게 관리할 수 있습니다.

5. 생성자를 통한 값 설정

객체 생성 시 필요한 모든 필드를 생성자를 통해 설정함으로써,

객체의 상태가 불완전한 상태로 존재하지 않도록 보장할 수 있습니다.

이를 통해 객체가 올바른 상태로 생성되도록 강제할 수 있습니다.

public class Order {
    private final String orderId;
    private final String product;
    private final int quantity;

    public Order(String orderId, String product, int quantity) {
        if (quantity <= 0) {
            throw new IllegalArgumentException("Quantity must be greater than zero");
        }
        this.orderId = orderId;
        this.product = product;
        this.quantity = quantity;
    }

    public String getOrderId() {
        return orderId;
    }

    public String getProduct() {
        return product;
    }

    public int getQuantity() {
        return quantity;
    }
}

위의 예제에서는 생성자를 통해 필드를 설정하고,

이를 통해 유효성 검사를 할 수 있습니다.

이를 통해 잘못된 데이터가 객체로 들어오는 것을 방지할 수 있습니다.

6. 요약

  • Setter의 사용은 보안 및 불변성 측면에서 취약합니다. 객체의 상태를 외부에서 쉽게 변경할 수 있어 문제가 발생할 수 있습니다.
  • 불변 객체를 만들어 객체가 생성된 후에는 상태가 변경되지 않도록 하는 것이 좋습니다. 이를 통해 데이터 무결성과 시스템의 안정성을 높일 수 있습니다.
  • 빌더 패턴을 사용하면 복잡한 객체 생성을 쉽게 관리하면서 불변성을 유지할 수 있습니다.
  • DTO 사용을 통해 도메인 객체와 데이터 전달을 분리함으로써, 데이터 수정 로직을 안전하게 처리할 수 있습니다.
  • 생성자를 통한 값 설정으로 객체가 올바른 상태로 생성되도록 강제하고, 유효성 검사를 통해 잘못된 데이터를 방지합니다.

이러한 방식들은 객체지향 원칙SOLID 원칙을 지키면서

시스템의 안전성과 유지보수성을 높이기 위한 좋은 방법들입니다.

728x90