아래 질문에 대한 답변을 이곳에 남긴다. (객체 지향과 디자인 패턴 책의 일부 내용을 각색했다.)
질문 내용: "절차 지향은 데이터를 중심으로 프로시저를 구현한다. 객체 지향은 기능 중심으로 구현한다."가 무슨 의미인가?
원문: https://www.facebook.com/groups/itbook4u/permalink/1012985252066531/
어려운 질문이다. 아래 코드로 얘기를 풀어보자.
someServiceCheck() {
...
if (new Date().after(member.getExpirationDate()))) { ... }
...
}
// Member의 일부
private Date expirationDate;
public Date getExpirationDate() {
return expirationDate;
}
public void setExpirationDate(Date expDate) {
expirationDate = expDate;
}
위 코드는 메서드(즉, 프로시저)의 일부 코드다. 이 코드는 회원 만료 여부를 확인하기 위해 member.getExpirationDate()로 만료일을 구한다. 즉, someServiceCheck() 메서드는 member의 expirationDate 데이터를 사용하고 있다. 현재 시점에서 expirationDate 데이터는 someServiceCheck()와 Member가 공유하고 있다.
만료일을 1년 늘려주는 코드는 어떻게 될까? 아래와 같이 구현해 볼 수 있을 것 같다.
renewContract() {
Date date = member.getExpirationDate();
Date renewedDate = ... // date에 1년 더한 값
member.setExpirationDate(renewedDate);
}
이제 Member의 expirationDate를 공유하는 프로시저는 someServiceCheck()와 renewContract()로 늘어났다. 여기서 Member는 객체일까? 아니다. 정확히 말하면 Member는 객체라기 보다는 데이터를 담고 있는 구조체에 가깝다. 즉, 두 함수가 데이터를 공유하고 이를 기준으로 구현하는 전형적인 절차지향 방식이다.
만료 여부를 확인하는 코드가 많아지거나 만료 데이터를 변경하는 코드가 많아질수록 아래 그림처럼 expirationDate라는 데이터를 중심으로 프로시저를 구현하게 된다.
이렇게 절차 지향은 데이터를 중심으로 코드를 구현한다. 개별 프로시저가 일부 기능을 구현하지만, 그 기능의 완성은 데이터 공유에 있다.
절차 지향은 데이터를 중심으로 프로시저를 끈끈하게 묶어준다. 여기서 재앙이 시작된다. 예를 들어, 만료 없이 무한정 서비스를 받을 수 있다는 것을 표현하기 위해 expirationDate에 null을 할당하기로 했다고 해 보자. 이 순간 데이터를 공유하는 모든 프로시저가 영향을 받는다. 기존 코드에 null 검사를 추가해야 하고, null이면 에러가 아니라 만료일이 없도록 로직을 수정해야 한다. 또는 null 대신 9999년 12월 31일을 만료일자로 주기로 했다고 해 보자. 이 경우 남은 기간을 중심으로 환불 금액을 구하는 refund() 함수와 기타 만료일을 중심으로 중요 로직을 수행하는 코드들이 영향을 받게 될 것이다.
객체 지향은 데이터 구조체가 아닌 기능을 중심으로 프로그램이 엮인다. 예를 들어, Member를 구조체가 아닌 기능을 제공하는 객체로 바꿔보면 다음과 같이 바뀐다.
public class Member {
private Date expirationDate;
public boolean isExpired() {
return new Date().after(expirationDate);
}
public int getRestDay() {
... //
}
public boolean renewContract() { // 데이터와 관련된 일부 기능이 객체로 들어옴
.... //
}
}
이제 다른 기능들은 만료 여부를 확인하기 위해 expirationDate 데이터를 사용하지 않는다. 대신 Member가 제공하는 isExpired() 메서드를 사용한다. 예를 들면 다음과 같이 바뀐다.
someServiceCheck() {
...
if (member.isExpired())) { ... }
...
}
일부 기능은 Member 안으로 들어갔다. 계약 갱신 기능이 그렇다. 데이터와 밀접하게 연결된 기능을 데이터와 같은 객체의 기능으로 넣는다. 이렇게 함으로써 객체의 내부 구현(특히 데이터)를 외부에 노출하지 않을 수 있다. 즉, 캡슐화를 할 수 있다.
기능 구현을 캡슐화하면 내부 구현 변경을 조금 더 쉽게 할 수 있다. (Member 데이터가 아닌) Member 객체를 사용함으로써 만료 여부 로직을 변경할 때 다른 코드는 영향을 받지 않게 된다. 무한대로 사용할 수 있는 사용자의 만료 데이터를 null로 하든, 9999년 12월 31일로 하든, Member 객체를 사용하는 코드는 isExpired() 라는 기능을 사용하면 된다. 만료 데이터 저장 방식 때문에 영향을 받는 코드는 Member로 수렴함으로 변경이 그 만큼 용이해진다.
자바나 C#과 같은 언어가 아니라 C와 같은 언어를 사용해도 객체 지향적으로 코딩할 수 있다. 핵심은 데이터 중심이 아닌 기능 중심으로 구현하는 것이다. 즉, 여러 프로시저가 데이터를 공유하는 방식이 아니라 프로시저가 다른 프로시저를 사용하는 방식으로 구현을 하고, 데이터 공유를 적절히 제한하면 캡슐화 효과를 얻을 수 있다.