이 글은 Thinking in Java(Bruce Eckel 저)를 참고하여 작성했습니다.
_____
객체라는 개념은 데이터와 기능성을 개념적으로 함께 묶을 수 있게 해준다. 예를 들어 금전 출납원, 고객, 계정, 거래 등은 각각 프로그램 내에서 객체[1]로 표현될 수 있다. 이러한 엔티티, 즉 객체는 자신의 속성과 행동을 정의하는 특정 클래스에 속한다. 즉 동일한 속성과 동일한 행동을 갖는 객체들을 나타낸 것이 클래스이며 클래스는 하나의 데이터 타입이라고 할 수 있다.
한편 앞서 추상화의 장점에서도 언급했듯 코드의 중복은 좋지 않다. 예를 들어 유사한 기능을 갖는 클래스가 있음에도 매번 어렵게 새로운 클래스를 만드는 것은 좋지 않다. 하지만 코드의 재사용은 객체지향 프로그래밍 언어가 제공하는 최대 장점 중 하나이고, 이를 구현할 수 있는 방법 중 하나[2]가 상속(inheritance)이다.
상속이란 기존 클래스를 이용해서 만든 후 필요한 부분만 따로 추가, 변경하는 것이다. 즉 기존 클래스와 새로 만드는 클래스의 관계를 설정하여 코드를 재사용할 수 있는 것이다. 이것을 상속 관계라 하며 기존 클래스를 베이스(base) 혹은 수퍼(super) 클래스 또는 부모(parent) 클래스라고 하고, 기존 클래스를 이용해서 새로 만들어지는 클래스를 파생(derived) 클래스 혹은 서브(sub) 클래스 또는 자식(child) 클래스라고 한다. 중요한 점은 파생 클래스는 베이스 클래스와 동일한 인터페이스[3]를 갖는다는 것이다. 따라서 베이스 클래스의 객체에게 보낼 수 있는 메시지를 파생 클래스의 객체에게도 보낼 수 있다. 파생 클래스에는 베이스 클래스에 없는 인터페이스를 추가할 수 있지만 그보다 중요한 것은 베이스 클래스에서 상속받은 인터페이스의 행동(구현부)을 변경하는 것이다. 이것을 오버라이딩(overriding)이라고 한다.
파생 클래스에 행동을 추가하지 않고, 상속받은 행동을 변경하는 이유는 다형성(polymorphism) 때문이다. 파생 타입의 객체를 처리할 때 그 객체의 타입을 베이스 타입으로 지정[4]하면 유리할 때가 많다. 특정 파생 타입에만 국한되지 않는 코드를 작성할 수 있기 때문이다[5]. 예를 들어보자.
public void relocate(Bird bird) {
bird.move();
}
문제는 컴파일 시점에서는 그 객체가 베이스 타입(Bird)이라는 것만 알 수 있을 뿐이지 어떤 파생 타입인지 알 수 없다. 따라서 실행하는 시점에 해당 객체의 타입을 알아내야 하므로, 객체지향 언어에서는 late 바인딩(동적 바인딩) 개념을 사용한다. 즉 어떤 파생 타입의 move()를 호출할 것인지 실행할 때 결정하는 것이다. 컴파일러에서는 단지 그러한 함수가 있는지, 그리고 함수의 인자와 반환 타입이 맞는지만을 확인한다[6][7].
_____
1. 고유한 엔티티(entity)다. 예를 들어 모든 계정(객체)는 잔액을 가지며(필드), 모든 금전 출납원은 예금을 받을 수 있다(메소드). 이와 동시에 각 객체는 자신의 상태를 갖는다. 즉, 각 계정(객체)은 서로 다른 잔액을 가진다.
2. 또 다른 방법으로 컴포지션(composition)이 있다. 즉 재사용할 클래스의 객체를 새로운 클래스 내부에 포함시키는 것이다(has-a). 이를 이용하면 코드의 유연성이 굉장히 좋아지는데, 보통 멤버 객체들은 private으로 지정하므로 클라이언트가 접근할 수 없을 뿐 아니라 런타임에 멤버 객체들을 변경할 수 있기 때문이다.
3. 모든 파생 클래스들은 자신들의 베이스 클래스와 같은 인터페이스를 가지므로, 파생 클래스는 베이스 클래스와 같은 타입이 된다. 결과적으로 베이스 클래스의 객체를 파생 클래스의 객체로 대체할 수 있으며, 이것을 대체 원리(substitution principle)라고 한다(is-a).
4. 파생 타입의 객체 참조를 베이스 타입으로 처리하는 것을 업 캐스팅(upcasting)이라고 한다.
5. The main reason you'd do this is decouple your code from a specific implementation of the interface.
6. Overriding vs. Overloading in Java
1) Polymorphism applies to overriding, not to overloading.
2) Overriding is a run-time concept while overloading is a compile-time concept.
3) The real object type in the run-time, not the reference variable's type, determines which overridden method is used at runtime. In contrast, reference type determines which overloaded method will be used at compile time.
7. 이처럼 오버로딩과 오버라이딩은 각각 컴파일 타임과 런타임이라는 차이를 가진다. 하지만 시점으로 설명할 수 있는 것은 이뿐만이 아니다. 흔히 배열은 공변(covariant)이고, 제네릭은 불변 타입이라 하는데 그 이유가 배열의 각 원소 타입은 런타임에 결정되는 반면 제네릭은 컴파일 타임에 타입 검사를 하기[8] 때문이다(출처: 나만의 인덱스).
8. 추가적으로 제네릭의 타입 정보는 런타임에는 없어져(erasure) 알 수 없게 된다. 그 이유는 자바5에 제네릭이 고안될 때 레거시와의 하위 호환성을 위해서라고 한다(출처: 나만의 인덱스).
_____
참고자료
- 나만의 인덱스] 오버로딩 vs. 오버라이딩
'공부 > Java' 카테고리의 다른 글
날짜 유효성 검사 (0) | 2022.01.17 |
---|---|
출력 (0) | 2021.08.13 |
자바 추상화 (0) | 2020.12.14 |
오류 코드보다 예외를 사용하라! (0) | 2020.12.12 |
Java 8 참고자료 (0) | 2020.11.28 |
댓글