2장 설계원칙 / DI와 서비스 로케이터는 이제부터 내가 개발하면서 지켜야할 설계 원칙들을,
그리고 마지막으로 3장 주요 디자인 패턴은 앞으로 설계하게 될 패턴들 중 자주 사용되는 패턴들을 맛보기로 보여준다.
개발자는 소프트웨어를 만든다. 소프트웨어는 사용자가 요구하는 기능을 제공하여야 한다. 그런데 요구사항은 변한다. 변화 가능한 유연한 구조를 만들어주는 핵심 기법 중 하나가 바로객체 지향이다. 그리고 이 책은 객체지향을 위한 관점들을 제시한다.
물론 프로그래밍 기법에는 객체 지향만 있는 것은 아니다.
소프트웨어를 구현한다는 것은 결국 최종적으로는 소프트웨어를 구성하는 데이터와 데이터를 조작하는 코드를 작성하는 것이다. 데이터를 조작하는 코드를 별도로 분리해서 함수나 프로시저와 같은 형태로 만들고, 각 프로시저들이 데이터를 조작하는 방식으로 코드를 작성할 수 있으며, 이처럼 프로지서로 프로그램을 구성하는 기법을 절차지향 프로그래밍이라고 한다.
하지만 절차지향으로 짜여진 프로그램의 규모가 커지면, 데이터 타입이나 의미를 변경해야 할 때, 함께 수정해야 하는 프로시저가 증가하며, 같은 데이터를 프로시저들이 서로 다른 의미로 사용하는 경우가 발생한다.
절차지향과 달리 객체 지향은 데이터 및 데이터와 관련된 프로시저를 객체라고 불리는 단위로 묶는다. 객체는 프로시저를 실행하는데 필요한 만큼의 데이터를 가지며, 객체들이 모여 프로그램을 구성한다.
모든 프로시저가 데이터를 공유하는 절차지향과 달리 객체 지향은 객체 별로 데이터와 프로시저를 알맞게 정의해야 하기 때문에 프로그램의 규모가 작을 때는 절차지향방식보다 복잡한 구조를 갖는다.
하지만, 객체지향적으로 만든 코드에서는 객체의 데이터를 변경하더라도 해당 객체로만 변화가 집중되고 다른 객체에는 영향을 주지 않기 때문에, 요구사항의 변화가 발생했을 때 절차지향 방식보다 프로그램을 더 쉽게 변경할 수 있는 장점이 있다.
객체 지향의 핵심 주제는 ‘책임할당’, ‘캡슐화’, ‘다형성과 추상화’, ‘조립을 통한 재사용’ 이다.
객체의 핵심은 결국 기능을 제공하는 것이며, 이는 객체마다 자신만의 책임이 있다는 의미를 갖는다. 객체가 갖는 책임(혹은 기능)의 크기가 작아질수록 객체 지향의 장점인 변경의 유연함을 얻을 수 있게 된다(이는 SOLID 중 단일 책임의 원칙으로 이어진다.) 객체 지향적으로 프로그램을 구현하다보면, 다른 객체가 제공하는 기능을 이용해서(한 객체의 코드에서 다른 객체를 생성하거나 다른 객체의 메서드를 호출) 자신의 기능을 완성하게 되는데, 이를 그 객체에 의존한다고 표현한다. 객체 지향은 기능을 제공하는 여러 객체들이 모여서 완성된 어플리케이션을 구성하게 된다.
객체지향은 객체가 내부적으로 기능을 어떻게 구현하는지를 감추어서(캡슐화), 한 곳의 변화가 다른 곳에 미치는 영향을 최소화한다. 객체지향을 처음 접하는 사람들은 절차지향 방식의 습관 때문에 데이터 중심적인 코드를 만들기 쉽다. 이런 습관을 고치는데 도움이 되는 규칙이 두 가지 있다.
첫번째로 Tell, Don’t Ask 즉, 데이터를 물어보지 않고, 기능을 실행해달라고 말하라는 규칙이다.
## 만료일자 데이터를 가져와서 직접 만료일자를 확인
if(member.getExpiryDate () != null && member.getExpiryDate().getDate() < System.currentTimeMillis()){}
## 기능 실행을 요청하는 방식
if(member.isExpired()){}
두번째로는 데미테르 법칙으로, 이 법칙은 다음의 규칙으로 구성된다.
1) 메서드에서 생성한 객체의 메서드만 호출
2) 파라미터로 받은 객체의 메서드만 호출
3) 필드로 참조하는 객체의 메서드만 호출
이를 토대로 객체 지향 설계과정을 살펴보면,
1) 제공해야 할 기능을 찾고 또는 세분화하고, 그 기능을 알맞은 객체에 할당한다.
a) 기능을 구현하는데 필요한 데이터를 객체에 추가한다. 객체에 데이터를 먼저 추가하고 그 데이터를 이용하는 기능을 넣을 수도 있다.
b) 기능은 최대한 캡슐화해서 구현한다.
2) 객체간에 어떻게 메시지를 주고받을지 결정한다.
3) 과정1)과 과정2)를 개발하는 동안 지속적으로 반복한다.
추가적으로, 다형성이란 한 객체가 여러 타입을 가질 수 있다는 것을 뜻한다. 자바는 클래스 다중 상속을 지원하지 않는 언어이므로 인터페이스를 이용해서 객체가 다형을 갖게 된다. 따라서 자바에서 다형성이란 개념은 인터페이스 상속(타입 상속)과 구현상속(기능 재사용)을 모두 포함한다.
추상화는 데이터나 프로시저 등을 의미가 비슷한 개념이나 표현으로 정의하는 과정이다. (추상화가 익숙치 않을 때는, 변화되는 부분을 추상화하면 된다. 보통 요구사항이 바뀔 때 변화하는 부분은 이후에도 변경될 소지가 있기 때문이다.)
위 예시를 보면 구현 클래스는 추상 타입을 상속받아 생성되며, 다형성에 의해 LogCollector를 구현한 클래스의 collect() 코드는 실제 구현한 클래스의 collect() 메서드를 호출한다(overriding). 이 때 구현된 클래스들을 ‘콘크리트 클래스’라고 한다.
그런데 상속을 통한 재사용시에는 상위 클래스 변경의 어려움, 클래스의 불필요한 증가, 상속의 오용 등의 단점이 있다. 이 문제를 해소하기 위해 (상속에 비해 상대적으로 런타임 구조가 복잡해지고 구현이 어렵다는 단점이 있으나,) 객체 조립을 이용한다.
객체 조립은 여러 객체를 묶어서 더 복잡한 기능을 제공하는 객체를 만들어내는 것이다. 객체 지향은 책임에 따라 객체들이 세분화되는 특징을 갖는다. 따라서 객체지향적으로 구현을 하면 자연스럽게 많은 객체들이 만들어지고, 이 과정에서 조립과 위임(내가 할 일을 다른 객체에게 넘김)을 통해 객체를 재사용하게 된다.
상속을 사용할 때는, 재사용이라는 관점이 아닌 기능의 확장이라는 관점에서 상속을 적용해야 한다. 또한 명확한 IS-A관계가 성립되어야 한다.
소프트웨어는 크게 두 개의 영역으로 구분된다. 하나는 고수준 정책 및 저수준 구현을 포함한 어플리케이션 영역이고 또 다른 영역은 어플리케이션이 동작하도록 각 개체들을 연결해주는 메인 영역이다.
메인 영역에서 객체를 연결하기 위해 DI(의존성 주입)가 사용된다.
DI(의존성 주입)를 통해서 의존 객체를 관리할 때에는 객체를 생성하고 각 객체들을 의존 관계에 따라 연결해주는 조립 기능이 필요하다. 객체 조립 기능이 분리되면, 이후에 XML 파일을 이용해서 객체 생성과 조립에 대한 정보를 설정하고 이 XML 파일을 읽어와 초기화해주도록 구현을 변경할 수 있는데, 스프링 프레임워크가 이러한 기능을 제공하는 DI 프레임워크이다. (생성자 방식, 설정 메서드 방식)
객체 지향적으로 설계하는데 기본이 되는 설계원칙인 SOLID는,
단일 책임 원칙(Single repponsibility principle)
개방-폐쇄 원칙(Open-closed principle)
리스코프 치환 원칙(Liskov substitution principle)
인터페이스 분리 원칙(Interface segregation principle)
의존 역전 원칙(Dependency inversion principle)
단일 책임원칙과 인터페이스 분리원칙은 객체가 커지지 않도록 막아준다.
객체가 많은 기능을 가지게 되면, 객체가 가진 기능의 변경 여파가 그 객체의 다른 기능에까지 번지게 되고 이는 다시 다른 기능을 사용하는 클라이언트에게까지 영향을 준다. 객체가 단일 책임을 갖게하고 클라이언트마다 다른 인터페이스를 사용하게 함으로써 한 기능의 변경이 다른 곳에까지 미치는 영향을 최소화할 수 있고, 이는 결국 기능 변경을 보다 쉽게 할 수 있도록 만들어 준다.
리스코프 치환 원칙과 의존 역전 원칙은 개방 폐쇄 원칙을 지원한다.
리스코프 치환원칙은 기능의 명세에 대한 내용으로, 명시된 명세에서 벗어난 값, 익셉션, 기능 등을 수행하면 안된다는 것을 의미한다. 그리고 개방 폐쇄 원칙이란, 사용되는 기능의 확장에는 열려 있고, 기능을 사용(호출)할 코드(클래스)의 변경에는 닫혀있다는 의미이다. (비슷한 if/else 블록이 존재하거나, 다운캐스팅(instanceof)이 이루어진다면 개방 폐쇄원칙이 깨지진 않았는지 의심해볼 수 있다.) 즉, 개방 폐쇄 원칙은 변화되는 부분을 추상화하고 다형성을 이용함으로써 기능 확장을 하면서도 기존 코드를 수정하지 않도록 만들어준다. 여기서, 변화되는 부분을 추상화할 수 있도록 도와주는 원칙이 바로 의존 역전 원칙이고, 다형성을 도와주는 원칙이 리스코프 치환 원칙이다.