객체지향 시스템을 잘 짜려면, 기능을 구현하는 객체들의, 동적으로 변화하는 모습을 고안하고 코드설계가 이어져야 한다.
다시 말해, 동적인 모델이 정적인 모델을 주도해야 한다.
지난 포스트를 요약해 보자면, 객체지향을 하기 위해서는
이 포스트에서는 4번째 단계인 코드를 작성하는 단계에 대해 다룬다.
단순한 (그러나 미래에 매우 복잡해질 수도 있는?) 영화 예매 시스템을 통해 컴파일타임 측면의 '클래스'를 만들 때는 어떤 것을 고려해야 하는지 고민해 본다
presentation - domain - persistence 레이어에 모두 객체지향적인 설계를 적용할 수는 없다. 객체지향 설계는 domain레이어에 한정된다.
객체지향을 처음 배운 사람이 위의 영화 예매 프로그램을 만들고자 한다면 아마,
데이터를 어디에서든 쓸 수 있도록 모두 준비하고 이 기능에는 이거, 저 기능에는 저거.. 가져와서 쓰는 방식이다. 그러나 이미 데이터가 다 만들어진 이후 프로세스를 고려하는 것은 객체지향적인 설계라고 말할 수 없다.
데이터와 프로세스가 분리된 코드, 데이터가 다 만들어진 이후 프로세스를 고려하는 것, 조건문으로 type checking을 반복적으로 하는 것은 객체지향적인 설계라고 할 수 없다. 위와 같은 절차적 설계의 문제는 무엇이고, 어떻게 개선할 수 있을까?
흔히 하는 설계에 대한 오해는, 마치 아파트를 짓거나 자동차를 만들 때처럼, 프로그램을 짤 때에도 설계를 시작하고 설계가 다 끝나면 코드를 짜야 한다고 생각하는 것이다
(Software가 괜히 soft가 아니다)
망한 서비스가 아니라면, 서비스가 굴러가는 동안 리팩토링은 계속해서 해야 할 것이다.
프로그래머는 리팩토링을 할 때마다 좋은 설계에 대한 고민을 해야 한다.
좋은 설계를 하나의 단어로 정의하자면 '변경'하기 쉬운 설계이다.
설계와 관련된 SOLID 등의 원칙은 모두 변경과 관련된 것이고, 디자인 패턴의 목적 또한 대부분 변경을 감추는 것이다.
면접 준비할 때 밥먹듯 나오는 응집도, 결합도, 캡슐화 또한 변경과 관련된 개념들이다.
좋은 설계를 하기 위한 몇 가지 선택지가 있다.
이 코드는 다시 볼 코드인가 아닌 코드인가? 요구사항은 어떤 방향으로 변경되고 있는가?
이러한 질문들을 잘 고려하여 심플한 절차적 코드를 짤 것인지, 인터페이스와 추상클래스를 활용하여 복잡하지만 유연성을 가진 코드를 짤 것인지는 상황에 맞게 선택해야 한다.
좋은 설계란 변경이 쉬운 코드라는 것을 알았으니 응집도, 결합도, 캡슐화, 즉 변경의 관점에서
위의 절차적 코드의 문제점을 찾아 보고 어떻게 개선해야 할지 고민해 보자.
응집도란 "모듈 내부 요소들이 서로 관련있는 정도" 라는 뜻이다.
이를 변경의 관점에서 설명한다면 "모듈 내부 요소들이 함께 변경되는 정도" 가 되겠다.
모듈이 이런 이유, 저런 이유들로 계속 변경된다면 응집도가 낮은 코드, 모듈이 변경되는 이유들이 모두 동일하다면 응집도가 높은 코드이다.
SOLID중 SRP(단일 책임 원칙)은 각 클래스는 변경될 이유가 하나여야 함을 의미하며 응집도에 관한 원칙이다.
만약 영화 예매 시스템에서 영화 가격 계산을 담당하는 로직이 할인 조건, 할인 정책 등 서로 다른 이유로 계속해서 변경된다면 응집도가 낮은 코드라는 뜻이고,
(실무에서는 해당 클래스에서 conflict가 많이 나 번거로워 질 것)
이를 할인 조건, 할인 정책이 변경되면 구현체만 추가하거나 조건, 정책 등 분리된 클래스만 변경하도록 수정하는 것이 바람직하다.
결합도란 "한 모듈이 다른 모듈에 의존하는 정도"이다. (다른 모듈에 대해 알고 있는 정보의 양이라고도 표현한다.)
변경에 관점에서는 "한 모듈을 변경할 때 다른 모듈이 함께 변경되는 정도" 이다.
ISP(인터페이스 분리 원칙)은 변하는 것과 변하지 않는 것을 구분해야 한다는 원칙이다. (느슨한 결합도를 유지하라는 것)
만약 영화 예매 시스템에서
int price;
...
int getPrice();
...
int calculateDiscount(int price);
이런 코드가 있는데, price의 타입이 변경될 여지가 있다면 많은 메소드들의 반환타입과 인자타입이 변경되어야 할 것이다.
getter/setter를 마구잡이로 만드는 것이 좋지 않은 이유도, 결합도가 강해지기 때문이다.
셋 중 가장 중요한 개념
캡슐화는 상태(데이터)와 행동(메소드)를 하나로 묶은 것인데, 묶어서 뭘 하냐면 구현(=변경) 을 감춘다
객체 뿐만 아니라 자주 변경되는 타입을 인터페이스로 감추는 것도 캡슐화라고 말할 수 있다.
DIP(의존 역전 원칙)은 항상 추상화된 것에 의존하라는 것이다.
(공통의 인터페이스를 다루는 객체들은 협력 가능하기 때문에 변경의 범위가 줄어든다)
위의 영화 예매 시스템에서 할인 정책을 그냥 사용하기만 하는 '결제'를 담당하는 클래스가 있다고 가정하자. 이 아이의 입장에서는 할인 정책이 무엇인지,
얘가 정액 할인을 하는지 비율로 할인하는지... 알아야 할 필요가 없다.
따라서 할인하는 정책들이 자주 변경되고 있다면 이를 '할인 정책'으로 추상화하여
변화하는 타입을 숨기는 것이 바람직하다.
절차적 코드는 '할인 정책', '상영', '영화' 등의 개념들이 드러나 있지 않고 알고리즘 속에 숨어 있어서
그 히스토리를 알아야만 이해 가능하다.
반면 객체지향적으로 잘 짜여진 코드라면 변경이 잘 고려되어 있어서 코드 수정이 필요할 때 부담을 훨씬 덜 수 있다.
그러나 시스템에 어떤 변경이 있을 지 모르기 때문에, 처음부터 객체지향적인 설계를 하기에는 어려움이 있다.
따라서 리팩토링을 할 때마다 변경을 위한 고민을 하는 것이 바람직하다.
앞으로 요구사항이 변경되고, 추가될 때마다 스트레스 받고 짜증 내지 말고 성장의 기회로 삼아보도록 하자(?)
(망한 시스템은 요구사항이 바뀌지도 않는다)
Jopro 님 강의