본문 바로가기

SPRING/Spring 활용

[스프링부트| 스프링부트와 JPA 활용 1 | 웹 애플리케이션 개발 | 상품 도메인 개발] 상품 엔티티 개발(비즈니스 로직 추가)

@Entity // 이 클래스를 데이터베이스 테이블과 매핑할 엔티티 클래스임을 지정
@Inheritance(strategy = InheritanceType.SINGLE_TABLE) // 단일 테이블 상속 전략을 사용하여 이 클래스와 상속받는 모든 클래스를 하나의 테이블에 저장
@DiscriminatorColumn(name = "dtype") // 상속받는 클래스를 구분할 때 사용할 컬럼명을 'dtype'으로 지정
@Getter // 클래스 필드의 getter 메소드를 자동으로 생성 (Lombok 라이브러리)
public abstract class Item { // 이 클래스가 추상 클래스임을 명시, 직접 인스턴스화할 수 없고 상속을 통해서만 사용

    @Id // 해당 필드를 테이블의 기본 키(primary key)로 사용
    @GeneratedValue // 기본 키 값 생성 전략을 데이터베이스에 위임 (자동으로 증가하는 값)
    @Column(name = "item_id") // 데이터베이스의 컬럼명을 'item_id'로 지정
    private Long id;

    private String name; // 상품 이름
    private int price; // 상품 가격
    private int stockQuantity; // 재고 수량

    @ManyToMany(mappedBy = "items") // Category 엔티티와의 다대다 관계를 정의, Category 클래스의 'items' 필드에 매핑됨
    private List<Category> categories = new ArrayList<>(); // 카테고리 목록, 다대다 연관 관계를 위한 리스트

    //== 비즈니스 로직==
    public void addStock(int quantity) { // 재고를 추가하는 메소드
        this.stockQuantity += quantity; // 주어진 수량만큼 재고 수량을 증가
    }

    public void removeStock(int quantity) { // 재고를 감소시키는 메소드
        int restStock = this.stockQuantity - quantity; // 현재 재고에서 주어진 수량을 뺀 값
        if (restStock < 0) { // 계산 후 재고가 0 미만이면 예외 발생
            throw new NotEnoughStockException("need more stock"); // 재고 부족 예외 발생
        }
        this.stockQuantity = restStock; // 계산된 재고 수량을 현재 재고로 설정
    }
}

 

엔티티 내에 비즈니스 로직을 포함하는 것이 바람직한지에 대한 질문은 일반적으로 도메인 주도 설계(Domain-Driven Design, DDD)와 같은 설계 원칙에서 찾을 수 있습니다. 이 접근 방식은 엔티티가 단순히 데이터를 보관하는 것이 아니라 해당 데이터와 관련된 비즈니스 로직도 함께 캡슐화해야 한다고 제안합니다. 이는 객체 지향 프로그래밍의 핵심 원칙 중 하나인 캡슐화를 강조합니다.

엔티티 내 비즈니스 로직의 장점

  1. 캡슐화: 데이터와 이를 조작하는 행위를 함께 묶음으로써 객체의 캡슐화를 강화합니다. 이는 객체의 완결성을 보장하고, 외부에서 객체의 상태를 임의로 변경하는 것을 방지합니다.
  2. 재사용성 및 일관성: 특정 엔티티와 관련된 로직이 해당 엔티티 내에 정의되어 있으면, 이 로직이 필요할 때마다 일관되게 재사용할 수 있습니다. 다른 개발자들이 같은 기능을 다시 구현할 필요가 없어 일관성과 안정성이 향상됩니다.
  3. 도메인 모델의 강화: 도메인 모델이 비즈니스 로직을 내포하게 되면, 모델 자체가 더 풍부해지고 비즈니스 규칙을 명확하게 표현할 수 있습니다. 이는 도메인의 복잡성을 관리하는 데 도움이 됩니다.

주의할 점

  1. 과도한 로직 포함: 엔티티에 너무 많은 로직을 포함시키면 클래스가 과도하게 복잡해질 수 있습니다. 이는 유지보수를 어렵게 하고, 테스트의 어려움을 증가시킬 수 있습니다.
  2. 서비스 레이어와의 역할 구분: 특히 트랜잭션 처리나 여러 도메인 모델 간의 조합과 같은 상황에서는 서비스 레이어에서 비즈니스 로직을 처리하는 것이 더 적합할 수 있습니다. 엔티티는 가능한 한 간단하게 유지하고, 복잡한 비즈니스 로직은 서비스 레이어에서 처리하는 것이 좋습니다.
  3. 단일 책임 원칙: 엔티티가 비즈니스 로직 뿐만 아니라 데이터 접근 로직까지 포함하게 되면, 단일 책임 원칙을 위배할 수 있습니다. 엔티티는 주로 데이터의 상태를 표현하는 데 집중하는 것이 좋습니다.

결론

엔티티 내에 서비스 로직을 포함할지 여부는 해당 프로젝트의 도메인 복잡성, 팀의 선호도, 기존 설계 원칙 등 다양한 요소를 고려해 결정해야 합니다. 간단한 로직, 예를 들어 객체의 상태를 변경하는 로직은 엔티티 내에 포함하는 것이 적합할 수 있으며, 복잡하고 여러 모델에 걸쳐 있는 로직은 서비스 레이어에서 처리하는 것이 좋을 수 있습니다.

public class NotEnoughStockException extends RuntimeException { // RuntimeException을 상속받는 사용자 정의 예외 클래스

    public NotEnoughStockException() { 
        super(); // 부모 클래스의 기본 생성자 호출
    }

    public NotEnoughStockException(String message) { 
        super(message); // 오류 메시지를 포함하는 RuntimeException 생성자를 호출
    }

    public NotEnoughStockException(String message, Throwable cause) { 
        super(message, cause); // 오류 메시지와 원인 예외를 포함하는 RuntimeException 생성자를 호출
    }

    public NotEnoughStockException(Throwable cause) { 
        super(cause); // 원인 예외만을 포함하는 RuntimeException 생성자를 호출
    }

}

클래스 및 생성자 설명

  • 기본 생성자: 예외를 생성할 때 별도의 메시지나 원인을 제공하지 않습니다.
  • 메시지를 받는 생성자: 사용자가 지정한 오류 메시지를 예외에 포함시켜 생성합니다. 이 메시지는 예외가 발생했을 때 추적하기 쉽게 해줍니다.
  • 메시지와 원인을 받는 생성자: 오류 메시지와 함께 다른 예외를 원인으로 포함시켜 예외를 생성합니다. 이는 예외의 연쇄적인 발생을 추적할 때 유용합니다.
  • 원인만을 받는 생성자: 다른 예외를 원인으로 하여 새 예외를 생성합니다. 이는 주로 다른 예외를 감싸 처리하고자 할 때 사용됩니다.