Home > Code Review > πŸ’»[Code Review] `Product` 클래슀의 `updateDetails` λŒ€ν•œ μ½”λ“œ 리뷰.

πŸ’»[Code Review] `Product` 클래슀의 `updateDetails` λŒ€ν•œ μ½”λ“œ 리뷰.
Code review OOP SOLID

πŸ›οΈ μ•„ν‚€ν…μ²˜ 및 섀계 (Architecture & Design)

  • Product 클래슀의 updateDetails λ©”μ„œλ“œλŠ” Rich Domain Model(ν’λΆ€ν•œ 도메인 λͺ¨λΈ) νŒ¨ν„΄μ„ μ™„λ²½ν•˜κ²Œ κ΅¬ν˜„ν•œ μ‚¬λ‘€μž…λ‹ˆλ‹€.
    • μ΄λŠ” 도메인 주도 섀계(Domain-Driven Design, DDD) 의 핡심 원칙 쀑 ν•˜λ‚˜λ‘œ, λΉ„μ¦ˆλ‹ˆμŠ€ λ‘œμ§μ„ 도메인 객체 내뢀에 μΊ‘μŠν™”ν•˜μ—¬ λ‹€μŒκ³Ό 같은 이점을 μ œκ³΅ν•©λ‹ˆλ‹€:
      • Domain : λΉ„μ¦ˆλ‹ˆμŠ€ κ·œμΉ™κ³Ό 데이터 λ³€κ²½ λ‘œμ§μ„ 자체적으둜 관리
      • Service : λΉ„μ¦ˆλ‹ˆμŠ€ ν”„λ‘œμ„ΈμŠ€ μ‘°μ •(Orchestration)μ—λ§Œ 집쀑
      • μΊ‘μŠν™” : 객체의 μƒνƒœ 변경을 μ œμ–΄λœ λ°©μ‹μœΌλ‘œλ§Œ ν—ˆμš©
      • 응집도 ν–₯상 : κ΄€λ ¨λœ 데이터와 둜직이 ν•œ 곳에 λͺ¨μž„

μ΄λŸ¬ν•œ μ„€κ³„λŠ” SOLID 원칙 쀑 단일 μ±…μž„ 원칙(SRP) κ³Ό μΊ‘μŠν™”(Encapsulation) λ₯Ό λ™μ‹œμ— λ§Œμ‘±ν•˜λŠ” 이상적인 κ΅¬μ‘°μž…λ‹ˆλ‹€.


βœ… ν΄λž˜μŠ€λ³„ 상세 리뷰 (Detailed Class Review)

package com.kobe.productmanagement.domain;

import com.kobe.productmanagement.common.BaseTimeEntity;
import com.kobe.productmanagement.common.Category;
import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.util.ArrayList;
import java.util.List;

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Product extends BaseTimeEntity {

	@Id
	@Column(length = 26) // ULIDλŠ” 26자둜 κ³ μ •λ˜λ―€λ‘œ 길이λ₯Ό μ§€μ •ν•΄μ£ΌλŠ” 것이 μ’‹μŠ΅λ‹ˆλ‹€.
	private String productId;

	@Column(nullable = false)
	private String productName;

	@Column(nullable = false)
	private Integer productRegularPrice;

	@Column(nullable = false)
	private Integer stockQuantity;

	@Enumerated(EnumType.STRING)
	@Column(nullable = false)
	private Category category;

	@Column(nullable = false)
	private Integer productCostPrice;

	@Column(nullable = false)
	private String productSupplier;

	@Column(nullable = false, unique = true)
	private String barcodeNumber;

	// μ–‘λ°©ν–₯ 관계λ₯Ό μœ„ν•œ μ½”λ“œ
	@OneToMany(mappedBy = "product", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
	private List<Stock> stocks = new ArrayList<>();

	@Builder
	public Product(String productId,
	               String productName,
	               Integer productRegularPrice,
	               Integer stockQuantity,
	               Category category,
	               Integer productCostPrice,
	               String productSupplier,
	               String barcodeNumber
	) {
		this.productId = productId;
		this.productName = productName;
		this.productRegularPrice = productRegularPrice;
		this.stockQuantity = stockQuantity;
		this.category = category;
		this.productCostPrice = productCostPrice;
		this.productSupplier = productSupplier;
		this.barcodeNumber = barcodeNumber;
	}

	public void increaseStockQuantity(int quantity) {
		this.stockQuantity += quantity;
	}

	public void decreaseStockQuantity(int quantity) {
		int restStock = this.stockQuantity - quantity;
		if (restStock < 0) {
			throw new IllegalArgumentException("μž¬κ³ λŠ” 0개 미만이 될 수 μ—†μŠ΅λ‹ˆλ‹€. ν˜„μž¬ 재고 :" + this.stockQuantity);
		}
		this.stockQuantity = restStock;
	}

	// OOP[μΊ‘μŠν™”/Encapsulation] + SOLID[SRP/단일 μ±…μž„ 원칙]
	public void updateDetails(String productName,
	                         Integer productRegularPrice,
	                         Integer stockQuantity,
	                         Category category,
	                         Integer productCostPrice,
	                         String productSupplier,
	                         String barcodeNumber
	) {
		if (productName != null) {
			this.productName = productName;
		}
		if (productRegularPrice != null) {
			this.productRegularPrice = productRegularPrice;
		}
		if (stockQuantity != null) {
			this.stockQuantity = stockQuantity;
		}
		if (category != null) {
			this.category = category;
		}
		if (productCostPrice != null) {
			this.productCostPrice = productCostPrice;
		}
		if (productSupplier != null) {
			this.productSupplier = productSupplier;
		}
		if (barcodeNumber != null) {
			this.barcodeNumber = barcodeNumber;
		}
	}
}

1. Product.java(Domain Entity) - ⭐️ 핡심 뢄석

πŸ”’ μΊ‘μŠν™”(Encapsulation) μ™„λ²½ κ΅¬ν˜„

  • 데이터 보호: λͺ¨λ“  ν•„λ“œκ°€ private으둜 μ„ μ–Έλ˜μ–΄ μ™ΈλΆ€μ—μ„œ 직접 μ ‘κ·Ό λΆˆκ°€
  • μ œμ–΄λœ μ ‘κ·Ό: updateDetails λ©”μ„œλ“œλ₯Ό ν†΅ν•΄μ„œλ§Œ μƒνƒœ λ³€κ²½ κ°€λŠ₯
  • 자체 검증 둜직: null 체크λ₯Ό ν†΅ν•œ 선택적 μ—…λ°μ΄νŠΈ κ΅¬ν˜„
  • λΉ„μ¦ˆλ‹ˆμŠ€ κ·œμΉ™ λ‚΄μž¬ν™”: μƒν’ˆ 정보 변경에 λŒ€ν•œ λͺ¨λ“  κ·œμΉ™μ΄ 도메인 객체 내뢀에 μœ„μΉ˜

🎯 단일 μ±…μž„ 원칙(SRP) μ€€μˆ˜

  • Product의 μ±…μž„: μžμ‹ μ˜ μƒνƒœμ™€ κ΄€λ ¨λœ λΉ„μ¦ˆλ‹ˆμŠ€ κ·œμΉ™ 관리
  • Service Layerμ™€μ˜ μ—­ν•  뢄리:
    • ServiceλŠ” ν”„λ‘œμ„ΈμŠ€ μ‘°μ •λ§Œ λ‹΄λ‹Ή
    • ProductλŠ” μƒνƒœ λ³€κ²½ 둜직만 λ‹΄λ‹Ή
  • 응집도 ν–₯상: κ΄€λ ¨ 데이터와 둜직이 ν•œ 곳에 집쀑

πŸ›‘οΈ 방어적 ν”„λ‘œκ·Έλž˜λ°(Defensive Programming)

if (productName != null) {
    this.productName = productName;
}
  • Null Safety: 각 νŒŒλΌλ―Έν„°μ— λŒ€ν•œ null 체크둜 NPE(Null Pointer Excepiton) λ°©μ§€
  • λΆ€λΆ„ μ—…λ°μ΄νŠΈ 지원: null이 μ•„λ‹Œ κ°’λ§Œ μ„ νƒμ μœΌλ‘œ μ—…λ°μ΄νŠΈ
  • μ•ˆμ „ν•œ μƒνƒœ λ³€κ²½: λ¬΄νš¨ν•œ λ°μ΄ν„°λ‘œ μΈν•œ 객체 μƒνƒœ μ˜€μ—Ό λ°©μ§€

πŸ—οΈ μΆ”κ°€ λΉ„μ¦ˆλ‹ˆμŠ€ λ©”μ„œλ“œλ“€

public void decreaseStockQuantity(int quantity) {
    int restStock = this.stockQuantity - quantity;
    if (restStock < 0) {
        throw new IllegalArgumentException("μž¬κ³ λŠ” 0개 미만이 될 수 μ—†μŠ΅λ‹ˆλ‹€. ν˜„μž¬ 재고 :" + this.stockQuantity);
    }
    this.stockQuantity = restStock;
}
  • λΉ„μ¦ˆλ‹ˆμŠ€ κ·œμΉ™ κ°•μ œ: 재고 κ°μ†Œ μ‹œ 음수 λ°©μ§€ 둜직
  • λͺ…ν™•ν•œ μ˜ˆμ™Έ λ©”μ‹œμ§€: λΉ„μ¦ˆλ‹ˆμŠ€ κ·œμΉ™ μœ„λ°˜ μ‹œ ꡬ체적인 였λ₯˜ 정보 제곡
  • 도메인 지식 ν‘œν˜„: μ‹€μ œ λΉ„μ¦ˆλ‹ˆμŠ€ μš”κ΅¬μ‚¬ν•­μ„ μ½”λ“œλ‘œ λͺ…μ‹œμ  ν‘œν˜„

πŸ† 총평

βœ… ν›Œλ₯­ν•œ 점듀

  1. Rich Domain Model의 μ™„λ²½ν•œ κ΅¬ν˜„
    • λ‹¨μˆœν•œ Data Containerκ°€ μ•„λ‹Œ λΉ„μ¦ˆλ‹ˆμŠ€ λ‘œμ§μ„ ν¬ν•¨ν•œ μ§€λŠ₯적 객체
    • Anemic Domain Model μ•ˆν‹°νŒ¨ν„΄μ„ μ™„λ²½νžˆ νšŒν”Ό
  2. SOLID μ›μΉ™μ˜ μ‹€μ²œμ  적용
    • SRP: 각 λ ˆμ΄μ–΄μ™€ 객체의 μ±…μž„μ΄ λͺ…ν™•νžˆ 뢄리
    • OCP: μƒˆλ‘œμš΄ μ—…λ°μ΄νŠΈ κ·œμΉ™ μΆ”κ°€ μ‹œ κΈ°μ‘΄ μ½”λ“œ μˆ˜μ • 없이 ν™•μž₯ κ°€λŠ₯
  3. μœ μ§€λ³΄μˆ˜μ„± κ·ΉλŒ€ν™”
    • μƒν’ˆ 정보 λ³€κ²½ 정책이 λ°”λ€Œμ–΄λ„ Product 클래슀만 μˆ˜μ •ν•˜λ©΄ 됨
    • Service Layer의 μ½”λ“œλŠ” μ „ν˜€ κ±΄λ“œλ¦΄ ν•„μš” μ—†μŒ
  4. μ•ˆμ •μ„±κ³Ό 견고성
    • Null Safetyλ₯Ό ν†΅ν•œ 방어적 ν”„λ‘œκ·Έλž˜λ°
    • λΉ„μ¦ˆλ‹ˆμŠ€ κ·œμΉ™ μœ„λ°˜ μ‹œ λͺ…ν™•ν•œ μ˜ˆμ™Έ 처리

🎯 μ•žμœΌλ‘œμ˜ λ°œμ „ λ°©ν–₯

μ΄λŸ¬ν•œ 섀계 νŒ¨ν„΄μ„ μ§€μ†μ μœΌλ‘œ μ μš©ν•˜λ©΄μ„œ λ‹€μŒκ³Ό 같은 κ³ κΈ‰ νŒ¨ν„΄λ“€λ„ κ³ λ €ν•΄λ³Ό 수 μžˆμŠ΅λ‹ˆλ‹€:

  • Domain Event: μƒν’ˆ 정보 λ³€κ²½ μ‹œ 이벀트 λ°œν–‰μœΌλ‘œ λ‹€λ₯Έ λ°”μš΄λ””λ“œ μ»¨ν…μŠ€νŠΈμ™€μ˜ 연동
  • Value Object: 가격, 재고 μˆ˜λŸ‰ 등을 Value Object둜 λΆ„λ¦¬ν•˜μ—¬ λ”μš± κ²¬κ³ ν•œ 도메인 λͺ¨λΈ ꡬ좕
  • Specification Pattern: λ³΅μž‘ν•œ λΉ„μ¦ˆλ‹ˆμŠ€ κ·œμΉ™ 검증을 μœ„ν•œ λͺ…μ„Έ νŒ¨ν„΄ λ„μž…

ν˜„μž¬μ˜ updateDetails λ©”μ„œλ“œλŠ” 객체지ν–₯ μ„€κ³„μ˜ λͺ¨λ²” 사둀이며, 이런 방식을 μ§€μ†ν•΄μ„œ μ μš©ν•œλ‹€λ©΄ ν™•μž₯ κ°€λŠ₯ν•˜κ³  μœ μ§€λ³΄μˆ˜ν•˜κΈ° μ‰¬μš΄ μ—”ν„°ν”„λΌμ΄μ¦ˆκΈ‰ μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ„ ꡬ좕할 수 μžˆμ„ κ²ƒμž…λ‹ˆλ‹€.