안녕하세요. 이번 시간에는 자바/스프링 실용주의 프로그래밍 책을 읽고 정리해보고
제 생각을 공유하는 게시글을 작성해보겠습니다.

 

 

1부 객체지향

 

 

객체지향에서는 복잡한 문제를 역할과 책임에 따라 '개별 객체'로 분해합니다.

분해된 각기 다른 특성과 기능의 객체들이 상호작용하고 협력해 문제를 해결합니다.

 

 

01 절차지향과 비교하기

 

컴퓨터 공학에서 말하는 Procedure는 '함수'를 가리킵니다. (순차지향 = 어셈블리어, 절차지향 = C언어)

따라서 절차지향 프로그래밍은 함수 지향 프로그래밍.

 

아래는 자바를 사용하지만 함수 위주의 프로그래밍으로 코딩한 절차지향 코드의 예시.

 

@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;
    }
}

 

 RestaurantChainService 클래스에 calculateRevenue 함수를 실행시키기 위해 객체가 존재하는 코드가 되버림

 

Store, Order, Food를 클래스로 표현했지만 이 클래스에는 아무런 책임이 존재하지 않으며 데이터를 실어나르는 역할 정도만 하고 있음

 

위처럼 모든 비즈니스 로직이 서비스 컴포넌트에 들어가 있으며 서비스 함수를 실행하는 위주의 프로그래밍도 절차지향 프로그래밍임.

 

 

 


 절자치향 코드를 객체지향 코드로 수정해보기

@Service
@RequiredArgsConstructor
public class RestaurantChainService {
    private final StoreRepository storeRepository;

    public long calculateRevenue(long restaurantId) {
        Long<Store> stores = storeRepository.findByRestaurantId(restaurantId);
        long revenue = 0;
        for (Store store : stores) {
            // 비즈니스 로직을 객체가 처리하도록 수정
            revenue += store.calculateRevenue();
        }
        return revenue;
    }
}

 

기존에 데이터를 전달하기만 하던 객체가 행동을 갖게 됨.

-> 어떤 메시지(필요한 값이나 목표)를 받았을 때 어떤 일을 책임지고 처리한다. 라고 표현할 수 있다.

 

객체에 어떤 메시지를 전달할 수 있게 됐다. 

객체가 어떤 책임을 지게 됐다.

객체가 어떤 처리하는 방법을 스스로 알고 있다.

 

 

 


 

정리) 

서비스에 모든 비즈니스 로직이 들어가고 클래스는 그저 데이터를 저장하는 용도로 사용되면 절차지향 프로그래밍이다!

 


 

02 객체지향 == 책임 (객체지향 != 가독성)

 

 

객체지향은 가독성이 오히려 떨어질 수도 있음

다른 객체와 협력을 하면서 전체 로직도 분산됨.

협력 객체들의 내부 동작이 어떤지 알 수 없게 됨.

 

하지만 객체지향은 객체를 신뢰하는 것이 중요하다. 그것이 바로 캡슐화.

 

이러한 객체지향 방식의 프로그래밍 업무 방식이 소프트웨어 개발에 효율성을 높힘

 

객체들이 어떻게 협력할지, 어떤 책임을 맡을지 결정하고 나면 병렬적인 개발을 진행할 수 있기 때문.

책임이 나눠져 있으므로 이슈 추적도 용이하다.

 

 

책임은 계약이다. 책임은 어떻게 신뢰(검증)하는가? -> 테스트 코드의 필요성


정리) 객체지향은 책임, 신뢰, 협력이다.

 

객체지향은 각 객체가 책임을 갖고, 그 책임을 신뢰하며 서로 협력하는 프로그래밍이다.

 

객체지향을 추구해야하는 이유 : 병렬적인 개발, 이슈 추적이 용이해져 소프트웨어의 생산성을 높힐 수 있다.

 


 

03 객체지향은 객체를 추상화한 역할에 책임을 할당한다.

 

public interface Calculable {
    long calculateRevenue(); // 역할에 책임을 할당
}

publi class Store implements Calculable{ // 역할을 구현
    private List<Calculable> orders; // 역할에 의존
}

 

- 역할은 객체의 추상화 (인터페이스)

- 역할에 책임을 할당한다. (추상 메서드)

- 역할을 구현한다. (구현 클래스)

- 역할에 의존한다. (다형성)

 

객체지향은 엄밀히 말해 객체를 추상화한 역할에 책임을 할당하는 것.

 


정리) 

역할 -> 책임 -> 신뢰 -> 협력

 

역할 -> 객체의 추상화 (인터페이스)

책임 -> 객체가 구현해야할 행동 (추상 메서드)

신뢰 -> 캡슐화 (테스트 코드로 검증) 

협력 -> 객체들의 상호작용 (서비스 레이어)


 

04 객체지향은 실세계를 반영하지 않는다.

 

 

실세계에선 store가 스스로 가격을 계산하지 않음.

 

-> 자아를 가진 객체들이 서로 협력하는 방식으로 개발되는 것에 가까움

 

 


 

05 TDA 원칙 - Tell, Don't Ask

 

절차지향적 사고에서 벗어나 객체지향적으로 개발하는 방법 -> TDA 원칙을 따르기

 

- TDA 원칙 : 객체에게 값을 물어보지 말고 일을 시켜라!

 

 

아래는 TDA 원칙을 따르지 않는 코드

 

public class Shop {
    public void sell(Account account, Product product) {
        long price = product.getPrice(); // 객체에서 값을 꺼내옴
        long mileage = account.getMoney(); // 객체에게 값을 꺼내옴
        if (mileage >= price) { // 객체가 아닌 꺼내온 데이터로 작업을 진행한다.
            account.setMoney(mileage - price);
            System.out.println(product.getName + "를 구매했습니다.");
        }else{
            System.out.println("잔액이 부족합니다.");
        }
    }
}

 

 

수정된 TDA 원칙을 따르는 코드

public class Shop {
    public void sell(Account account, Product product) {
        if (account.canAfford(product.getPrice())) { //객체에게 작업을 시킴
            account.withdraw(product.getPrice()); //객체에게 작업을 시킴
            System.out.println(product.getName() + "를 구매했습니다.");
        } else {
            System.out.println("잔액이 부족합니다.");   
        }
    }
}

 

 

객체를 데이터 덩어리로 보지 말고 객체에게 책임을 위임해야함!

객체는 자아를 가진 것처럼 능동적으로 움직이는 존재가 되어야함.

 

 

 


 

 

 

궁금한 사항) 

 

객체지향이 그렇다는 건 알겠는데, 실무에 어떻게 적용합니까..?

 

실무에 적용할 때 위의 상황을 엔티티로 구현한다면 위의 객체지향 예시코드는 평균 시간복잡도가 O(N^3)이다.

Repository를 사용하여 효율적인 쿼리를 작성한 함수를 호출한다면 평균 O(logN^3)으로 줄일 수 있을 것이다.

 

 

실용주의 프로그래밍이라는 책 제목으로 유추해보았을 때 객체지향의 실무적인 해결책도 뒷 부분에 나오지 않을까 기대해본다.

 

 

일단 1장만 읽은 후, 찾아본 결과로 인프런 질문 게시판에서 비슷한 상황에 대한 질문으로 김영한 님의 답변을 찾아볼 수 있었다.

 

 

 

 

엔티티는 빈을 필드로 주입 받기 어렵고, 파라미터를 통해 빈을 받을 수 있으나 추천하는 방식은 아닌듯 하다.

 

일단 김영한님은 객체에게 온전한 책임을 갖게 하는 로직은 해당 객체 내부에서 처리할 수 있는 데이터에 한하여 엔티티의 책임을 구현하고
둘 이상의 객체가 협력하는 로직에 대해서는 함수로 처리하는 방식으로 진행하시는 듯하다.

 

하지만 그렇게 되면 객체의 역할이 축소되어 결국 다시 서비스 레이어에 의존하는 절차지향 프로그래밍이 되지 않을까하는 생각이 든다. 

 

 



참고 도서)
https://www.yes24.com/Product/Goods/126845564