# chapter02 객체지향
# 목표
개발자가 반드시 정복해야 할 객체 지향과 디자인 패턴 스터디를 진행하며 공부한 내용을 정리한다.
# 1. 시작은 절차 지향
초기의 프로그래밍은 절차 지향적 프로그래밍 방식을 활용했다. 절차 지향 프로그래밍은 소프트웨어를 구성하는 데이터와 데이터를 조작하는 코드를 별도로 분리한 함수
혹은 프로시저
의 형태로 만들어 작성하고 있다.
이것을 그림으로 표현하면 아래와 같다.
이러한 절차 지향은 결국 데이터가 중심
이 되어 구성된다. 또한 여러 프로시저
에서 데이터를 공유하여 사용하고 있다. 마치 데이터
와 프로시저
가 풀어지지 않는 강한 결합으로 연결
되어 있는 형태처럼 보인다.
만약 수정된 요구사항으로 인해 데이터 4의 타입을 변경한다고 가정한다. 해당 데이터를 사용하고 있는 프로시저 4, 5, 6번
을 연쇄적으로 점검
해야 한다. 현재는 간단한 그림으로 표현하여 작아 보이지만 프로그램의 규모가 커질 수록 이러한 문제는 더 큰 파급력
을 가져오고 해당 데이터를 서로 다른 의미로 사용할 가능성이 높아진다.
결국 절차 지향은 변화하는 요구사항에 유연하게 대처할 수 없기 때문에 코드 수정을 어렵게 만든다. 어려운 코드 수정은 많은 개발 비용을 동반한다.
# 2. 객체 지향의 등장
이러한 절차 지향의 단점을 극복하기 위해 객체를 지향하는 개발 방법론인 객체 지향이 등장한다.
객체 지향은 객체가 중심이 되어 객체 간의 협력을 통해 프로그램을 구성한다. 객체 지향은 객체 별로 데이터를 나타내는 속성
과 프로시저를 나타내는 행위
로 구성되어 있는데, 각각의 객체는 자신의 책임
에 맞게 데이터와 행위
를 정의해야 한다. 이러한 방법은 절차 지향에 비해 언제 변경 될지 모르는 요구사항을 반영하기 위해 복잡한 설계와 구조를 동반
한다.
객체 지향 구조는 아주 큰 장점을 가지고 있다.
만약 객체 3의 데이터의 타입이 변경 되었다고 가정한다. 이러한 변화는 객체 3 내부에서만 영향을 끼치도록 구성해야 하기 때문에 다른 객체에게 변화의 책임을 나누지 않는다. 즉 잘 설계된 객체
는 변화에 대응하기 쉽기 때문에 큰 유연함을 제공
하고 있다.
# 3. 객체는 책임을 가진다.
객체 지향은 기능을 제공하는 여러 객체 들이 모여 하나의 프로그램을 구성한다. 각각의 객체는 책임을 가지고 있다. 이러한 책임은 필요한 기능 목록을 정리한 뒤 적절히 부여해야 한다. 한 가지 주의해야 할 점은 객체가 갖는 책임의 크기가 작을 수록 좋다
는 것이다.
아래는 이전 자동차 미션을 해결하기 위해 객체에게 책임을 부여한 뒤 그에 따른 크기를 비교한 것이다.
만약 모든 책임이 Application
에 있다면 많은 정보인 데이터를 공유해서 사용
하기 때문에 절차 지향에 가까워 진다. 하지만 각각의 객체에게 적절히 책임을 분담한 뒤 메시지를 통해 협력
을 이루면 기능의 세부 내용이 변경될 때 변경해야 하는 부분을 책임을 가진 한 곳으로 집중
할 수 있다.
정리하면 앞서 언급한 것 처럼 책임의 크기가 작을 수록 변경에 대한 유연함을 얻을 수 있다.
이에 대한 자세한 예제는 후에 진행되는 단일 책임 원칙(SRP)
에서 확인할 수 있다.
# 4. 객체간의 의사소통
객체는 메시지 전송
을 통해 의사소통한다. 객체는 제공된 인터페이스의 오퍼레이션을 호출하는 것을 메시지 전송
이라고 표현한다.
아래 Car의 move 메서드는 이동 전략에게 이동 여부를 물어보기 위해 메시지를 전송하고 있다.
public class Car {
...
private final MovingStretagy movingStretagy;
...
public void move() {
// movingStretagy가 참조하는 객체에게 메시지 전송
if (movingStretagy.isMovable()) {
position++;
}
}
...
}
# 5. 객체는 협력을 통해 의존성을 가진다.
각각의 객체는 다른 객체를 생성
하거나 메서드를 호출(메시지 요청)
하여 협력
을 이룬다. 이러한 협력은 객체 간의 의존성
을 가졌다고 표현한다.
아래 그림을 살펴보면 RacingCarController는 Domain에 해당하는 Cars와 Count에게 메시지를 요청하기 때문에 RacingCarController
가 Cars와 Count에 의존
한다고 표현할 수 있다.
이러한 의존성은 의존하는 객체가 변하면 본인도 함께 변경될 가능성이 있다
는 것을 내포한다.
의존성은 꼬리에 꼬리를 문 것처럼 전파되는 특성
이 있다. 아래와 같이 독립된 Car를 나타내는 Car의 변경
은 의존하고 있는 Cars와 RacingCarController에 영향
을 끼칠 가능성이 있다.
특히 이러한 의존이 순환해서 발생할 경우 순환 의존
이 발생한다. 결국 변경에 대한 파급력이 나비효과
처럼 자신에게 돌아올 수 있다.
한 예시로 MVC 패턴 중 Domain에서 출력과 관련된 메시지를 View에게 전송
과 동시에 View에서 Domain 내부의 비즈니스 로직 실행을 위해 메시지를 전송
할 경우 서로 순환 참조하게 되어 변경에 매우 취약한 구조
가 된다. 이러한 순환 의존이 발생하지 않도록 하는 원칙 중 하나로 의존 역전 원칙(DIP)
이 있다. 이에 대한 내용은 후에 진행되는 발표에서 확인할 수 있을 것이다.
의존을 정리하면 아래와 같다.
- 내가 변경되면
나에게 의존하고 있는 코드에 영향
을 준다. - 나의 요구가 변경되면
내가 의존하고 있는 타입에 영향
을 준다.
# 6. 캡슐화
객체 지향의 가장 큰 장점은 한 곳의 구현 변경이 다른 곳에 변경을 가하지 않도록 해준다. 객체 지향은 캡슐화를 통해 한 곳의 변화가 다른 곳에 미치는 영향을 최소화 할 수 있다.
캡슐화는 객체가 내부적으로 기능을 어떻게 구현 했는지 감추는 것이다.
public class Car {
private String name;
private int position;
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public int getPosition() {
return this.position;
}
public void setPosition(int position) {
this.position = position;
}
}
위 코드는 언뜻보면 private
으로 객체의 속성을 숨기는 것 처럼 보이지만 getter/setter
로 인해 모든 정보를 오픈한 상태이다. 즉 캡슐화가 전혀 이루어지고 있지 않다.
객체를 캡슐화 시키기 위해서는 내부 기능이 어떻게 동작 되는지 숨겨야 한다. 예를들어 Car의 위치를 비교한다고 가정한다. 이것을 이루기 위해서는 getPostion()
을 활용하여 객체 외부에서 비교하기 보다 Car 객체에게 같은 위치인지 물어보는 것
이 바람직하다.
public class Car {
private String name;
private int position;
...
public boolean isSame(int position) {
return this.position == position;
}
}
# 6.1 캡슐화를 위한 두 가지 규칙
이러한 캡슐화를 이루기 위해 도움이 되는 두 개의 규칙이 존재한다.
Tell, Don’t Ask
: 데이터를 물어보지 않고 기능을 실행해 달라고 말하라.데미테르의 법칙(디미터 법칙 Law of Demeter)
:- 메서드에서 생성한 객체의 메서드만 호출한다.
- 파라미터로 받은 객체의 메서드만 호출한다.
- 필드로 참조하는 객체의 메서드만 호출한다.
위와 같은 규칙을 잘 지키면 데이터 중심이 아닌 기능 중심의 코드
를 작성하도록 유도하기 때문에 기능 구현의 캡슐화를 향상 시켜 준다.
# 7. 객체 지향 설계 과정
- 제공해야 할 기능을 찾고 또는 세분화한다. 그 기능을 알맞은 객체에게 할당한다.
- 기능을 구현하는데 필요한 데이터를 객체에 추가한다. 객체에 데이터를 먼저 추가하고 그 데이터를 이용하는 기능을 넣을 수 있다.
- 기능은 최대한 캡슐화해서 구현한다.
- 객체 간에 어떻게 메시지를 주고받을 지 결정한다.
- 과정1과 과정2를 개발하는 동안 지속적으로 반복한다.
이것은 우리가 미션을 해결할 때 필요한 요구사항을 정리하는 것부터가 객체 지향을 설계하는 기본이 된다. 아래는 실제 미션을 진행할 때 작성한 예시이다.
객체 설계는 한 번에 완성되지 않고 구현을 진행해가며 점진적으로 완성된다. 그렇기 때문에 가장 핵심이 되는 것은 변경에 유연한 구조를 갖도록 노력해야 한다. 캡슐화도 유연함을 제공해주는 기법 중 하나이다.
# References
최범균 지음, 『개발자가 반드시 정복해야 할 객체지향과 디자인 패턴』, 인투북스(2014), p29-57.