객체지향 프로그래밍
객체지향 프로그래밍은 어떤 클래스가 필요한지를 먼저 고민하는것이 아닌 어떤 객체가 필요한가를 고민해야한다. 이를 위해서는 어떤 객체들이 어떤 상태와 행동을 가지는지를 결정해야 한다. 그 다음으로 객체를 독립적인 존재가 아닌 기능 구현을 위해 상호작용하는 존재로 바라봐야 한다. 이는 설계를 유연하고 확장성 있게 만드는데 도움을 준다.
컴파일 타임 의존성과 런타임 의존성
다음과 같은 코드를 보자.
1 | public class Parent { |
1 | public abstract class Child { |
1 | Parent parent = new Parent(new Son()); |
Parent와 Child는 서로 의존성이 존재한다. 하지만 실제로는 Parent가 callChild 메소드를 수행하기 위해서는 Son이나 Daughter가 필요하다는 점이다. 그렇지만 Parent 클래스 내에서는 그 어떠한 것에도 의존하지 않는다. 이를 의존하는 부분을 찾기 위해서는 Parent를 생성하고 연결하는 부분을 찾아야 한다. 이렇듯 컴파일 타임 의존성과 런타임 의존성이 다르면 코드를 이해하는데 힘들어진다. 반면에 이를 통해 코드는 유연성과 확장성을 가진다. 그리고 이를 가능케 하는 것이 상속이다.
상속
상속을 통해 코드는 유연성을 가지게 되었다. Son과 Daughter는 Parent가 호출하는 child.toString()
의 메세지를 이해할 수 있게 되었다. 그로인해 Parent와 협력할 수 있게 되었다. 또한 Parent와 협력하는 인스턴스가 Son이라면 Son의 toString을, Daughter라면 Daughter의 toString을 호출하게 된다. 이를 다형성이라 한다.
다형성은 실행될 메서드를 런타임에 결정한다는 특징이 있다. 즉, 메세지와 메소드를 실행 시점에 바인딩 하게 된다. 이를 동적 바인딩이라 한다. 반면에 컴파일 타임에 바인딩하는 것을 정적 바인딩이라 한다.
인터페이스
만약 자식이 없는 부모가 있다고 가정하자. 그렇다면 코드를 어떻게 변화시킬 것인가? 가령 다음과 같은 방법도 있을 수 있겠다.
1 | public class Parent { |
하지만 결코 좋아보이지 않는다. 자식의 존재를 표현하기 위해 이를 추상화하고 유연성과 확장성을 가져간 코드의 장점을 다 해쳐놓는다.
그럼 다음과 같은 방법은 어떨까
1 | public class NoneChild extends Child{ |
이것도 하나의 방법이 될 것이다. 추상화를 통해 확장성있는 코드를 작성할 수 있다.
하지만 상속은 문제가 있다. 캡슐화가 약해진다는 점이다. 즉, 상속은 자식 클래스와 부모 클래스를 강하게 결합시킨다. NoneChild를 호출 시키면 Hey, NONE
가 반환되는데, 반환되는 메세지의 의미가 매우 불명확하다. 이 문제를 해결하기 위해서는 Child를 인터페이스로 바꾸는 방법이다. 이를 통해 더 유연한 코드를 작성할 수 있게 된다.
합성
Parent는 Child의 코드를 재사용한다. 이 방법은 Parent와 Child가 인터페이스로 약하게 결합된다는 점이다. 그리고 인터페이스에 정의된 내용만을 통해 코드를 재사용하는 방법을 합성(Composition)이라 한다.
합성은 두 가지의 장점이 있다. 첫째는 인터페이스에 정의된 내용으로만 상호작용하기 때문에 캡슐화를 효과적으로 할 수 있다. 그리고 의존하는 인스턴스를 쉽게 교체할 수 있는 장점이 있다.