객체지향에게 역할과 책임이란

profile image 스이연 2025. 2. 11. 03:09

객체 지향 공부를 하게되면 역할과 책임에 대한 말이 항상 등장한다.
그만큼 매우 중요하다는 건 알지만 정확히 어떤 의미인지는 추상적인 개념이라 떠올리기가 어려웠다..
객체간의 역할과 책임에 대해 공부하게 되면 추후 객체지향에 대해 공부할 때 이해하기 수월할 거 같아 이번 기회에 공부해보고자 한다.

절차 지향과 비교하기

절차지향과 순차지향

  • 순차 지향 (Sequential oriented programming)
    • 코드를 위에서 아래로 읽는 순차적인 방법
  • 절차 지향 (Procedure oriented programming)
    • Procedure는 함수라는 의미로 함수를 위주로 생각하고 프로그램을 만드는 방법

음식 체인점을 관리하는 프로그램을 자바로 개발하는 예시

데이터 구조

class RestaurantChain(){
	private List<Store> stores;
}

@Getter
class Store {
	private List<Order> orders;
	private long rentalFee;
}

@Getter
class Order {
	private List<Food> foods;
	private double transactionFeePercent = 0.03;
}

@Getter
class Food {
	private long price;
	private long originCost;
}

절차지향처럼 동작하는 RestaurantChainService

@Service
@RequiredArgsConstructor
public class RestaurantChainService {
	private final StoreRepository storeRepository;
	
	public long calculateRevenue(long restaurantId) {
		List<Store> stores = storeRepository.findByRestaurantId(restaurantId);
		long revenue = 0;
		for(Store store : stores) {
			for(Order order : store.getOrders()){
				for(Food food : order.getFoods()){
					revenue += food.getPrice();
				}
			}
		}
		return revenue;
	}
	
	public long caluateProfit(long restaurantId) {
		List<Store> stores = storeRepository.fingByRestaurantId(restaurantId);
		long cost = 0;
		for(Store store : stores) {
			for(Order order : store.getOrders()) {
				long orderPrice = 0;
				for(Food food : order.getFoods()) {
					orderPrice += food.getPrice();
				}
				cost += orderPrice * order.getTransactionFeePercent();
			}
			cost += store.getRentalFee();
		}
		return calculateRevenue() - cost;
	}
}

 

위의 코드가 절차지향적 코드인 이유

: calculateRevenue , calculateProfit 메서드를 실행하기 위해 Store, Order, Food가 존재할 뿐이기 때문이다.

객체지향적으로 바꾼 코드

class RestaurantChain {
	private List<Store> stores;
	
	public long calculateRevenue() {
		long revenue = 0;
		for(Store store : stores) {
			revenue += store.calculateRevenue();
		}
		return revenue;
	}
	
	public long calculateProfit() {
		long income = 0;
		for(Store store : stores) {
			income += store.calculateProfit();
		}
		return income;
	}
}

class Store {
	private List<Order> orders;
	private long rentalFee;
	
	public long calculateRevenue() {
		long revenue = 0;
		for(Order order : orders) {
			revenue += order.calculateRevenue();
		}
		return revenue;
	}
	
	public long calculateProfit() {
		long income = 0;
		for(Order order : orders) {
			income += order.calculateProfit();
		}
		return income - rentalFee;
	}
}

class Order {
	private List<Food> foods;
	private double transactionFeePercent = 0.03;
	
	public long calculateRevenue() {
		long revenue = 0;
		for(Food food : foods) {
			revenue += food.calculateRevenue();
		}
		return revenue;
	}
	
	public long calculateProfit() {
		long income = 0;
		for(Food food : foods) {
			income += food.calculateProfit();
		}
		return (long)(income - calculateRevenue() * transactionFeePercent);
	}
}

class Food {
	private long price;
	private long originCost;
	
	public long calculateRevenue() {
		return price;
	}
	
	public long calculateProfit() {
		return price - originCost;
	}
}

 

절차 지향 코드를 객체 지향 코드로 바꿈으로써 비즈니스 로직을 객체가 처리하도록 변경했다.

그러자 Store, Order, Food 클래스가 갖고 있던 데이터를 그대로 전달하기만 했던 객체가 행동을 하게 되었다.

 

즉 객체는 어떤 요청이 들어왔을 때 어떤 일을 책임지고 처리한다는 책임이 생겼다.

객체에 어떤 메세지를 전달 할 수 있게 되었다.
객체가 어떤 책임을 지게 되었다.
객체는 어떤 책임을 처리하는 방법을 스스로 알고 있다.

절차지향 코드와 객체지향 코드 비교해보기

// 절차 지향
public long caluateProfit(long restaurantId) {
		List<Store> stores = storeRepository.fingByRestaurantId(restaurantId);
		long cost = 0;
		for(Store store : stores) {
			for(Order order : store.getOrders()) {
				long orderPrice = 0;
				for(Food food : order.getFoods()) {
					orderPrice += food.getPrice();
				}
				cost += orderPrice * order.getTransactionFeePercent();
			}
			cost += store.getRentalFee();
		}
		return calculateRevenue() - cost;
	}

 

Order에서 결제 수수료를 계산하는 로직을 RestaurantChain에서 처리하고 있다.

결제 수수료 정보를 가지고 있는건 Order인데 비즈니스 로직은 RestaurantChain이 처리하고 있는건 부자연스러운 일이다.

즉 RestaurantChain에 부적절한 책임이 할당된것이다.

 

// 객체 지향
class RestaurantChain {
	private List<Store> stores;
	
	public long calculateRevenue() {
		long revenue = 0;
		for(Store store : stores) {
			revenue += store.calculateRevenue();
		}
		return revenue;
	}
	
	public long calculateProfit() {
		long income = 0;
		for(Store store : stores) {
			income += store.calculateProfit();
		}
		return income;
	}
}

 

결제 수수료를 더이상 RestaurantChain에서 처리하지 않고 데이터와 비즈니스 로직이 한 곳에 잘 들어있다.

데이터 측면에서 봤을 때도 어떤 행위를 하기 위해 만들어진 행동과 데이터가 한 곳에 잘 응집됐다고 볼 수 있다.

= 응집도가 높다.

 

논리 흐름상 절차지향 코드가 더 양이 적고 가독성이 높을 수 있지만 객체 지향으로 코드를 작성하는 이유는 가독성을 높이기 위해서가 아닌 객체들이 각각 책임을 갖는것에 집중하기 위해서다.

객체들은 각자의 책임을 수행하기 위한 협력 객체가 무엇인지 잘 알고 있으며 그 밖에 필요한 값은 모두 각자가 갖고 있다.

 

다른 객체와의 협력이 강조되면서 전체 로직은 분산되었기 때문에 협력 객체들의 내부 동작이 어떤지는 알 수 없다.

= 캡슐화

책임과 역할

절차지향적 코드라고해서 책임이 없다는 의미로 보면 안된다.

다만 객체지향과의 차이는 절차지향에서는 책임을 함수로 나누고 함수에 할당한다. 객체지향에서는 책임을 객체로 나누고 객체에 할당한다.

책임을 객체에 할당한다고 꼭 객체 지향적이라고 할 수 있을까?

 

절차 지향적 코드도 구조체를 만들고 함수 포인터로 구조체에 함수를 넣으면 구조체 단위로 책임을 할당할 수 있기 때문에 책임을 객체에 할당했다고 해서 객체 지향적 코드라고 할 수는 없다.

 

기존 코드에서 인터페이스 구현

interface Calculable {
	long calculateRevenue();
	long calculateProfit();
}
class RestaurantChain implements Calculable {
	private List<Calculable> stores;
	
	@Override
	public long calculateRevenue() {
		long revenue = 0;
		for(Store store : stores) {
			revenue += store.calculateRevenue();
		}
		return revenue;
	}
	
	@Override
	public long calculateProfit() {
		long income = 0;
		for(Store store : stores) {
			income += store.calculateProfit();
		}
		return income;
	}
}

해당 코드는 객체에 할당되어 있던 책임을 인터페이스로 분할해서 역할을 만들었다.

그리고 그 객체들이 인터페이스라는 역할을 구현하게 했다. 즉 추상화의 원리를 이용해서 다형성을 지원하게 했다.

🔗 만든 역할

  • 매출 계산과 관련된 역할
  • 순이익 계산과 관련된 역할

 

객체지향에서는 책임을 객체에 할당하는 것이 아니고 객체를 추상화한 역할에 책임을 할당한다.

이는 절차지향에서 지원하지 못하는것이며 이러한 것이 다형성이라는 객체지향의 특징중 하나다.

구현과 역할을 분리하고 역할에 책임을 할당하는 과정은 매우 중요하다.

역할을 이용해서 통신하게 되면 실제 객체가 어떤 객체인지 상관하지 않아도 된다는 장점이 있다. 따라서 확장에 유연해진다.

음식점에서 주문을 음식 말고 상품까지 주문하게 할 경우

 

역할을 분리하지 않은 코드

class Order {
	private List<Food> foods;
	private List<BrandProduct> brandProducts; // 상품 변수 추가
	private double transactionFeePercent = 0.03;
	
	public long calculateRevenue() {
		long revenue = 0;
		for(Food food : foods) {
			revenue += food.calculateRevenue();
		}
		
		// 상품 가격 더함
		for(BrandProduct brandProduct : brandProducts) {
			revenue += brandProduct.calculateRevenue();
		}
		return revenue;
	}
	
	public long calculateProfit() {
		long income = 0;
		for(Food food : foods) {
			income += food.calculateProfit();
		}
		
		// 상품 가격 더함
		for(BrandProduct brandProduct : brandProducts) {
			income += brandProduct.calculateProfit();
		}
		return (long)(income - calculateRevenue() * transactionFeePercent);
	}
}

 

구현체가 아닌 역할에 집중하는 코드

class Order implements Calculable {
	private List<Calculable> items;
	private double transactionFeePercent = 0.03;
	
	@Override
	public long calculateRevenue() {
		long revenue = 0;
		for(Calculable item : items) {
			revenue += item.calculateRevenue();
		}
		return revenue;
	}
	
	@Override
	public long calculateProfit() {
		long income = 0;
		for(Calculable item : items) {
			income += item.calculateProfit();
		}
		return income - rentalFee;
	}
}

 

역할에 집중하니 코드를 크게 변경하지 않아도 기능을 확장할 수 있게 되었다.

이는 구체적인 것(클래스)이 아닌 추상적인 것(역할, 인터페이스)에 집중할 때 유연한 설계를 얻을 수 있게 된다는 뜻이다.

객체가 책임을 갖게 됐고 객체의 역할이 정해졌으며 어떤 목표를 달성하기 위해 서로 다른 객체와 협력하게 되었다.

 

즉 객체지향의 본질은 역할, 책임, 협력이다.

추상화, 다형성, 상속, 캡슐화는 본질이 아니고 역할, 책임, 협력을 잘 다루기 위해 존재하는 프로그래밍 언어일 뿐이다.

💡 역할, 책임, 협력에 대한 개념이 확실하게 잡혀있지 않았었는데 이번 기회에 조금이나마 어떤 개념인지감을 익힐 수 있었고 앞으로 자바를 사용하며 객체를 다룰 때 좀 더 객체 하나하나에게 역할을 부여하며 서로에게 협력할 수 있도록 고민하며 코드를 구현해야겠다는 생각이 들었다.

출처
자바/스프링 개발자를 위한 실용주의 프로그래밍

 

'Programming > Java' 카테고리의 다른 글

Interface와 Abstract  (1) 2025.06.15
JVM이 무엇이고 구조에 대해 알아보자  (0) 2025.05.10
원시타입과 참조 타입  (2) 2025.05.04
의존성에 대해서  (2) 2025.03.20
BufferedReader와 BufferedWriter  (2) 2025.02.15