반응형

📝절차지향 vs 객체지향

절차지향 vs 객체지향의 경우 인프콘에서 컨퍼런스한 내용을 기반으로 작성했습니다

 

절차지향과 객체지향에 차이를 설명하기에 앞서 예제를 준비했습니다. (Java 기반 코드)

장바구니의 전체 금액 >= 할인 기준금액일 경우 할인 프로모션을 적용시키는 시스템을 만드려고 합니다.

 

절차지향의 경우는 아래와 같습니다

// 할인
public class Promotion {
    private Long cartId;
    private Long basePrice;
    
    public Money getBasePrice() {
    	return basePrice;
    }
    public void setCart(Long cartId) {
   	 this.cartId = cartId;
    }
}

// 장바구니
public class Cart {
    private List<CartLineItem> items = new ArrayList<>();
    
    public Long getTotalPrice() {
    	return items.stream().mapToLong(CartLineItem::getPrice).sum();
    }
    public int getTotalQuantity() {
    	return items.stream().mapToInt(CartLineItem::getQuantity).sum();
    }
}

// 할인 프로세스
public class PromotionProcess {
    public void apply(Promotion promotion, Cart cart) {
        if (isApplicableTo(promotion, cart)) {
            promotion.setCart(cart);
        }
    }
    private boolean isApplicableTo(Promotion promotion, Cart cart) {
    	return cart.getTotalPrice() >= promotion.getBasePrice();
    }
}

절차지향의 경우 할인, 장바구니의 데이터영역과 할인 프로세스를 처리하는 프로세스영역으로 나누어져있습니다.

 

 

객체지향의 경우는 아래와 같습니다.

// 할인 + 할인프로세스
public class Promotion {
    private Cart cart;
    private Long basePrice;
    
    public void apply(Cart cart) {
    	if (cart.getTotalPrice() >= basePrice) {
    	this.cart = cart;
    	}
    }
}

// 장바구니
public class Cart {
    private List<CartLineItem> items = new ArrayList<>();
    
    public Long getTotalPrice() {
    	return items.stream().mapToLong(CartLineItem::getPrice).sum();
    }
    public int getTotalQuantity() {
    	return items.stream().mapToInt(CartLineItem::getQuantity).sum();
    }
}

절차지향과 다르게 객체지향은 데이터와 프로세스 영역으로 나누어져있지 않습니다. 할인과 관련된 것들끼리 다 모여있고 장바구니 관련된 것끼리 다 모여있습니다.

 


절차지향과 객체지향의 차이를 이야기할 때는 변경이라는게 중요합니다. 어떤 변경에 어떤 방식이 유연한지에 대한 것입니다. 위에 예제 기반으로 더 진행하자면 이번에는 최소금액 <= 장바구니금액 <= 최대금액일 경우에 할인이 적용되게끔 변경해달라는 요청이 있을 때 처리하는 방법입니다.

 

 

절차지향의 경우 아래와 같습니다.

// 할인 적용 프로세스
public class PromotionProcess { 
    public void apply(Promotion promotion, Cart cart) {
        if (isApplicableTo(promotion, cart)) {
            promotion.setCart(cart);
        }
    }
    
    private boolean isApplicableTo(Promotion promotion, Cart cart) {
    	return cart.getTotalPrice() >= promotion.getMinPrice() 
        &&     cart.getTotalPrice() <= promotion.getMaxPrice()
    }
}

// 할인
public class Promotion {
    private Long cartId;
    private Long minPrice;
    private Long maxPrice;
    
    public Long getMinPrice() {
        return minPrice;
    }
    public Long getMaxPrice() {
    	return maxPrice;
    }
    public void setCart(Long cartId) {
        this.cartId = cartId;
    }
}

절차지향의 경우 할인 적용시킬 기준 금액인 최소금액과 최대금액을 추가시켰습니다. Promotion과 PromotionProcess 두군데에서 수정이 이루어졌습니다.

 

 

객체지향의 경우 아래와 같습니다.

public class Promotion {
    private Cart cart;
    private Long minPrice;
    private Long maxPrice;
    
    public void apply(Cart cart) {
    	if (cart.getTotalPrice() >= basePrice &&
    		cart.getTotalPrice() >= maxPrice) {
        	this.cart = cart;
    	}
    }
}

객체지향의 경우 Promotion에 Promotion관련 필드와 로직이 다 들어있기 때문에 Promoiton 한군데에서만 수정이 이루어졌습니다.

 


변경사항이 또 들어왔습니다. 이번에는 할인 조건을 하나 더 추가하는데 장바구니의 상품수  >= 프로모션 상품수일 경우 할인을 해줍니다. 할인을 받기 위해서는 설정한 프로모션 상품수보다 장바구니에 더 많이 담으면 할인을 해준다는 것이죠

 

 

절차지향의 경우 아래와 같습니다.

// 할인 프로세스
public class PromotionProcess {
    public void apply(Promotion promotion, Cart cart) {
        if (isApplicableTo(promotion, cart)) {
    		promotion.setCart(cart);
    	}
    }
    
    private boolean isApplicableTo(Promotion promotion, Cart cart) {
        switch (promotion.getConditionType()) {
            case PRICE:
            	return cart.getTotalPrice() >= promotion.getBasePrice();
            case QUANTITY:
                return cart.getTotalQuantity() >= promotion.getBaseQuantity
        }
    	return false;
    }
}

// 할인
public class Promotion {
    public enum ConditionType {
    	PRICE, QUANTITY
    }

    private Long cartId;
    private Long basePrice;
    private int baseQuantity;

    public ConditionType getConditionType() {
    	return conditionType;
    }
    public Money getBasePrice() {
   	 return basePrice;
    }
    public int getBaseQuantity() {
    	return baseQuantity;
    }	
    public void setCart(Long cartId) {
    	this.cartId = cartId;
    }
}

절차지향의 경우는 가격 또는 개수에 따른 할인 조건이 두개라서 CondtionType을 할인 모델에 추가시켰습니다. 그리고 할인을 위한 기준 금액과 기준 개수가 필요하기 때문에 basePrice와 baseQuantity를 만들어줬습니다.

 

이럴경우 되게 코드가 지저분해보이며 모델도 추가해줘야하고 프로세스도 수정해야하는 여러군데에서 수정이 필요합니다. 또 할인 유형이 생기는 경우 더 늘어나게 됩니다.

 

객체지향의 경우 아래와 같습니다.

// 할인
public class Promotion {
    private Cart cart;
    private DiscountCondition condition;
    
    public void apply(Cart cart) {
    	if (condition.isApplicableTo(cart)){
        	this.cart = cart;
        }
    }
}

// 장바구니
public class Cart {
	private List<CartLineItem> items = new ArrayList<>();

	public Long getTotalPrice() {
		return items.stream().mapToLong(CartLineItem::getPrice).sum();
	}
	public int getTotalQuantity() {
		return items.stream().mapToInt(CartLineItem::getQuantity).sum();
	}
}

// 할인 인터페이스
public interface DiscountCondition {
	boolean isApplicableTo(Cart cart);
}

// 가격 할인 구현체
public class PriceCondition implements DiscountCondition {
    private Long basePrice;
    
    @Override
    public boolean isApplicableTo(Cart cart) {
    	return cart.getTotalPrice() >= basePrice;
    }
}

// 개수 할인 구현체
public class QuantityCondition implements DiscountCondition {
    private int baseQuantity;
    
    @Override
    public boolean isApplicableTo(Cart cart) {
    	return cart.getTotalQuantity() >= baseQuantity;
    }
}

객체지향의 경우 다형성을 이용해야합니다. 그렇기 때문에 interface를 만들어줍니다. Promotion의 apply에서는 interface를 받게끔 해줍니다.

 

파일은 많아집니다만 이런식으로 객체지향식으로 설계가 되어있는 상태에서 다른 할인 유형에 대한 처리가 필요한 경우 DiscountCondition을 상속받아 구현하면 됩니다.

 

Promotion에서 apply는 수정할 필요도 없습니다. DiscountCondition을 상속받은 구현체만 껴주기만 하면 됩니다. 그리고 Promotion을 건들 필요가 없기 때문에 실질적으로 DiscountCondition을 상속받아서 만들어주기만 하면 됩니다. 파일 수정도 한군데에서만 일어나게 됩니다. 이곳저곳 수정할 필요도 없고 DiscountCondition을 무조건 받아야하기 때문에 절차적인 것처럼 임의대로 막 만들수도 없어 개발이 체계적으로 보입니다.

 


위와 같은 상황인 같은 기능을 다른로직으로 갈아껴야하는 상황에는 객체지향이 유리합니다만 무조건적으로 객체지향이 유리하지는 않습니다. 새로운 요구사항을 추가해봅시다. 장바구니에 대한 할인이였는데 이번에는 장바구니에 있는 하나의 상품에 대한 할인 적용을 만들어봅시다. 이번에는 상품인 Item에 대한 모델은 따로 안 만들고 할인에 대한 프로세스에만 집중해봅시다.

 

 

절차지향의 경우 아래와 같습니다.

// 할인 프로세스
public class PromotionProcess {
	public void apply(Promotion promotion, Cart cart) {
		if (isApplicableTo(promotion, cart)) {
			promotion.setCart(cart);
		}
	}

	private boolean isApplicableTo(Promotion promotion, Cart cart) {
		switch (promotion.getConditionType()) {
			case PRICE:
				return cart.getTotalPrice() >= promotion.getBasePrice();
			case QUANTITY:
				return cart.getTotalQuantity() >= promotion.getBaseQuantity();
		}
		return false;
	}

	// 새로 추가된 상품에 대한 할인 적용
	public boolean isApplicableTo(Promotion promotion, CartLineItem item) {
		switch (promotion.getConditionType()) {
			case PRICE:
				return item.getTotalPrice() >= promotion.getBasePrice();
			case QUANTITY:
				return item.getTotalQuantity() >= promotion.getBaseQuantity();
		}
		return false;
	}
}

절차지향의 경우 상품을 매개변수로 받아야 하기 때문에 또 다른 함수를 만듭니다. 다른 파일 건들 필요 없이 간단하게 PromotionProcess에만 추가하면 됩니다.

 

 

객체지향의 경우 아래와 같습니다.

// 할인
public class Promotion {
    private Cart cart;
    private DiscountCondition condition;
    
    public void apply(Cart cart) {
        if (condition.isApplicableTo(cart)) {
            this.cart = cart;
        }
    }
}

// 할인 인터페이스
public interface DiscountCondition {
    boolean isApplicableTo(Cart cart);
    boolean isApplicableTo(CartLineItem item);
}

// Price 할인 구현체
public class PriceCondition implements DiscountCondition {
    private Long basePrice;
    
    @Override
    public boolean isApplicableTo(Cart cart) {
	    return cart.getTotalPrice() >= basePrice;
    }
    
    @Override
    public boolean isApplicableTo(CartLineItem item) {
    	return item.getPrice() >= basePrice;
    }
}

// Quantity 할인 구현체
public class QuantityCondition implements DiscountCondition {
    private int baseQuantity;
    
    @Override
    public boolean isApplicableTo(Cart cart) {
    	return cart.getTotalQuantity() >= baseQuantity;
    }
    
    @Override
    public boolean isApplicableTo(CartLineItem item) {
    	return item.getQuantity() >= basePrice;
    }
}

객체지향의 경우 Interface의 함수를 추가해야하고 이걸 무조건 구현해야하기 때문에 이걸 상속받은 구현체들의 코드는 다 추가되어야합니다. 또한 여기선 Promotion에 코드를 추가 안 했지만 Promotion 코드도 추가해야하기 때문에 총 4개의 파일을 수정해야합니다.

 

 

 


위와 같이 로직이 아예 다른 것을 추가할 때 객체지향의 경우 더 불리합니다. (장바구니할인 → 상품할인) 또 다른 예로는 타입 계층 전체 수정이 있는 경우입니다. Cart와 Promotion 모델을 합쳐서 CartWithPromotion 모델을 만들어야하는 예를 들어봅시다.

 

 

절차지향의 경우 아래와 같습니다.

public class PromotionProcess {

    public CartWithPromotion convertToCartWithPromotion(
	Promotion promotion,
    Cart cart) {
        CartWithPromotion result = new CartWithPromotion();
        result.setTotalPrice(cart.getTotalPrice());
        result.setTotalQuantity(cart.getTotalQuantity());
        result.setPromotionBasePrice(promotion.getBasePrice());
        result.setPromotionBaseQuantity(promotion.getBaseQuantity());
        return result;
    }
}

절차지향의 경우는 되게 간단하게 처리가 가능합니다. 두개의 모델을 받아서 그냥 새로운 모델에 넣어서 반환하면 됩니다.

 

 

 

객체지향의 경우 아래와 같습니다.

public class Promotion {
    private Cart cart;
    private DiscountCondition condition;
    ...
    
    public CartWithPromotion convertToCartWithPromotion() {
        CartWithPromotion result = new CartWithPromotion();
        result.setTotalPrice(cart.getTotalPrice());
        result.setTotalQuantity(cart.getTotalQuantity());
        
        if (condition instanceof PriceCondition) {
            result.setPromotionBasePrice(
            ((PriceCondition)condition).getBasePrice());
        }
        if (condition instanceof QuantityCondition) {
            result.setPromotionBaseQuantity(
            ((QuantityCondition)condition).getBaseQuantity());
        }
    	return result;
    }
}

객체지향의 경우는 되게 복잡하게 처리해야합니다. Interface로 다형성을 유지해야하기 때문에 어떤 구현체가 들어올지 몰라 instanceof를 통해 클래스를 확인한 이후에 각각 다르게 처리해야합니다. 복잡하고 되게 어색합니다.

 


최종적으로 정리해 간단 요약하면 아래 표와 같습니다.

절차적인 설계 객체지향 설계
포맷 변경을 위한 데이터 변환 (데이터 합치기) 규칙에 기반한 상태 변경 (체계적) [복잡한 설계에 유리]
데이터 중심 행동 중심
데이터 노출 데이터 캡슐화
기능 추가에 유리 (아예 새로운 기능 → 장바구니가 아닌 상품 할인 기능이 필요한 경우) 타입 확장에 유리 (본질적인 기능은 같지만 다양하게 확장 가능 → 인터페이스로 인한 할인 확장)
데이터와 프로세스가 명확히 분리 데이터 프로세스 같이 존재 (클래스 안에 할인 관련된 데이터와 할인 프로세스가 같이 들어있다)

 

 

 

  • 도메인 레이어의 경우 객체지향이 좋습니다. (어떤 할인 내용을 적용시킬지 = 할인이라는 공통 분모)
  • 프레젠테이션 레이어의 경우 데이터를 반환하기 때문에 절차지향에 좋습니다.
  • 서비스 레이어의 경우 비즈니스로직순차적 처리가 필요하기 때문에 절차지향에 좋습니다. (서비스에 어떤 할인을 적용시킬지 이런 내용인 인터페이스를 상속받은 구현체가 들어가서 절차지향에 객체지향이 들어간 형태입니다.)
  • 퍼시스턴스 레이어의 경우 DB 쿼리문 조회하고 이런 부분은 순차 처리가 필요하기 때문에 절차지향에 좋습니다. 

 

이미 우리는 상황에 맞게 절차지향과 객체지향을 섞어 쓰고 있습니다. 즉, 우월하다 이런 건 없고 상황에 맞게 잘 써야합니다.

 

 

📝 인터페이스 vs 추상클래스

 

인터페이스

public interface Animal {
    void eat();  // 메서드 선언 (구현 X)
    void sleep();
}

public class Dog implements Animal {
    @Override
    public void eat() {
        System.out.println("Dog eats.");
    }

    @Override
    public void sleep() {
        System.out.println("Dog sleeps.");
    }
}

인터페이스의 특징은 아래와 같습니다.

  • 다중 상속 가능
  • 행동의 규약 정의 → 해당 메소드는 반드시 구현해야 함
  • 무엇을 해야하는 가를 정의한다.

 

 

추상클래스

public abstract class Animal {
    private String name;  // 멤버 변수

    public Animal(String name) {
        this.name = name;
    }

    public void breathe() {
        System.out.println(name + " is breathing."); // 일반 메서드
    }

    public abstract void eat();  // 추상 메서드
}

public class Dog extends Animal {
    public Dog(String name) {
        super(name);
    }

    @Override
    public void eat() {
        System.out.println("Dog eats.");
    }
}

추상클래스의 특징은 아래와 같습니다.

  • 단일 상속
  • 추상클래스에서 이미 구현되어있기 때문에 상속받아서 직접 구현 안 해도 된다.
  • 기본적인 공통기능에 사용하며 만약 커스텀이 필요한 경우 @overrid로 재정의해서 사용하면 좋다.
  • 공통메서드를 사용하거나 커스텀이 필요한 경우 @override 재정의가 가능하기 때문에 interface처럼 규약에 덜 얽매인다.

 

 

🔗 참고 및 출처

https://www.youtube.com/watch?v=usOfawBrvFY&t=446s

https://cactuslog.tistory.com/

 

 

 

반응형