상속

상속이란 기존의 클래스를 토대로 새로운 클래스를 정의하는 것이다. 말 그대로 기존 클래스의 모든 멤버를 물려받는다. 사용 방법은 새로 작성할 클래스 이름 뒤에 'extends' 키워드와 상속받고자 하는 클래스의 이름을 작성하면 된다. 상속해 주는 클래스를 상위 클래스, 상속받는 클래스를 하위 클래스라 하며 이 두 클래스는 서로 상속 관계에 있다고 한다. 

상속을 통해 하위 클래스를 정의하면 상위 클래스의 모든 멤버를 사용할 수 있다. 단, 생성자와 초기화 블럭은 상속되지 않는다. 상위 클래스의 멤버를 직접 정의하지 않고도 하위 클래스에서 사용할 수 있다는 의미이다. 그래서 하위 클래스가 상위 클래스보다 멤버(필드와 메서드)가 항상 같거나 더 많다.

상속을 통해 클래스를 정의하면 클래스 간의 공통된 코드를 상위 클래스 하나로 관리할 수 있기 때문에 생산성과 유지보수에 유리하다. 하지만 이 글에서는 다형성에 대한 이해를 돕기 위해 오버라이딩에 초점을 맞춘다. 오버라이딩은 상위 클래스의 메서드를 하위 클래스의 메서드에서 '재정의'하는 것이다. 메서드의 선언부는 그대로 두고 구현부만 수정한다.

 

참조변수의 형변환

기본형 변수처럼 참조변수도 형변환이 가능하다. 단, 상속 관계에 있는 클래스 타입이어야 한다. 하위 클래스 타입의 참조변수를 상위 클래스 타입으로 변환할 수 있고 그 반대도 가능하다. 전자를 업 캐스팅(Up-casting), 후자를 다운 캐스팅(Down-casting)이라고 한다. 그리고 형변환은 참조변수의 타입을 변환할 뿐이지 참조하는 인스턴스를 변환하는 것이 아니라는 점을 기억해야 한다.

그런데 다운 캐스팅은 형변환을 생략할 수 없고 업 캐스팅은 생략이 가능하다. 왜 그런 것일까? 참조변수의 타입에 따라 인스턴스에서 사용할 수 있는 멤버의 개수가 달라지기 때문이다. 하위 클래스가 상위 클래스보다 더 '구체적인' 클래스이므로 하위 클래스의 멤버 개수가 같거나 더 많다. 즉, 다운 캐스팅을 하면 참조변수가 다룰 수 있는 멤버 개수보다 실제 인스턴스의 멤버 개수가 더 적은 상황이 발생할 수 있다. 반면, 업 캐스팅은 실제 인스턴스의 멤버 개수가 참조변수가 다룰 수 있는 멤버 개수보다 항상 같거나 더 많다. 업 캐스팅이 더 안전하므로 형변환을 생략할 수 있다. 기본 자료형에서 int 타입을 double 타입으로 변환할 때 소수점 부분의 데이터 손실이 없으므로 자동 형변환이 되는 것과 같은 맥락이다.

예를 들어, Vehicle 클래스의 하위 클래스가 Car 클래스라고 한다면 업 캐스팅이 주로 사용되는 예는 다음과 같다.

// 형변환이 생략된 업 캐스팅
Vehicle vehicle = new Car();

// 형변환을 생략하지 않은 업 캐스팅
Car car = new Car();
Vehicle vehicle = (Vehicle) car;

 

다형성(Polymorphism)

다형성이란 프로그래밍 언어의 각 요소들(상수, 변수, 객체, 메서드, 함수 등)이 다양한 자료형에 속하는 것이 허가되는 성질을 가리킨다. 


보다 구체적으로 말하면, 한 타입의 참조변수로 여러 타입의 객체를 참조할 수 있도록 하는 것이다. 이로 인해 같은 모양의 코드가 상황에 따라 다른 기능을 수행한다. Java에서는 주로 상속 또는 구현, 참조변수 형변환, 오버라이딩을 통해 다형성을 구현한다. 이 세 요소의 흐름을 이해하는 것이 중요하다. 다형성이 필요한 이유는 코드 간 결합도를 낮춰 유지보수를 쉽게 하기 위함이다. 글로 된 설명보다는 아래 예시코드를 통해 이해하는 것이 훨씬 편할 것이다.

필드 다형성

public class Device {
	public void run() {
		System.out.println("기기를 실행합니다.");
	}
}

public class Mobile extends Device {
	public void run() {
		System.out.println("[모바일] 기기를 실행합니다.");
	}
}

public class Desktop extends Device {
	public void run() {
		System.out.println("[데스크탑] 기기를 실행합니다.");
	}
}

public class User {
	Device device = new Device();

	public void controlDevice() {
		device.run();
	}
}
public class App {
	public static void main(String[] args) {
		User user = new User();
		user.controlDevice();
		
		user.device = new Mobile();
		user.controlDevice();
		
		user.device = new Desktop();
		user.controlDevice();
	}
}
/* 실행결과
    기기를 실행합니다.
    [모바일] 기기를 실행합니다.
    [데스크탑] 기기를 실행합니다.
*/

 

Mobile과 Desktop 클래스는 Device 클래스를 상속받았으며 run() 메서드를 오버라이딩했다. User 클래스의 device 필드는 Device 타입이며 controlDevice() 메서드는 device의 run() 메서드를 호출한다. device 필드는 Device 타입이기 때문에 Device 클래스를 상속받은 어떤 인스턴스든지 device 필드에 저장할 수 있다. 그리고 run() 메서드를 오버라이딩 했기 때문에 해당 클래스에서 재정의된 run() 메서드가 실행될 것이다.

User 객체를 사용하는 측에서는 Device 객체가 아닌, Device 클래스를 상속받은 다른 객체를 사용하고 싶을 때 device 필드에 저장된 인스턴스만 변경하면 된다. 즉, User 클래스가 어떻게 수정되든 상관없이 main() 메서드의 user.controlDevice; 코드는 수정할 필요가 없다. 만약 Mobile과 Desktop 클래스가 Device 클래스와 상속 관계가 아니라면 User 클래스는 다음과 같을 것이다.

public class User {
	Device device = new Device();
	Mobile mobile = new Mobile();
	Desktop desktop = new Desktop();
	
	public void controlDevice() {
		device.run();
	}
	public void controlMobileDevie() {
		mobile.run();
	}
	public void controlDesktopDevice() {
		desktop.run();
	}
}


이에 따라 main() 메서드에서도 Device 인스턴스가 아닌 다른 인스턴스의 run() 메서드를 사용하고 싶다면 user.controlDevice; 코드를 수정해야 할 것이다.

매개변수 다형성

public class Device {
	public void run() {
		System.out.println("기기를 실행합니다.");
	}
}

public class Mobile extends Device {
	public void run() {
		System.out.println("[모바일] 기기를 실행합니다.");
	}
}

public class Desktop extends Device {
	public void run() {
		System.out.println("[데스크탑] 기기를 실행합니다.");
	}
}

public class User {
	public void controlDevice(Device device) {
		device.run();
	}
}
public class App {
	public static void main(String[] args) {
		User user = new User();
		
		user.controlDevice(new Device());
		user.controlDevice(new Mobile());
		user.controlDevice(new Desktop());
	}
}
/* 실행결과
    기기를 실행합니다.
    [모바일] 기기를 실행합니다.
    [데스크탑] 기기를 실행합니다.
*/


필드 다형성에서 설명한 코드와 대부분 비슷하지만 차이점은 User 클래스의 device 필드가 없고 run() 메서드를 호출하는 메서드가 controlDevice(Device device)로 수정되었다는 점이다. 이 메서드는 Device 타입뿐만 아니라 Device 타입을 상속받은 어떤 인스턴스든지 매개변수로 받는다. 그리고 오버라이딩된 run() 메서드를 호출한다.

만약 Mobile과 Desktop 클래스가 Device와 상속 관계가 아니라면 User 클래스는 다음과 같을 것이다.

public class User {
	public void controlDevice(Device device) {
		device.run();
	}
 
	public void controlDevice(Mobile mobile) {
		mobile.run();
	}

	public void controlDevice(Desktop desktop) {
		desktop.run();
	}
}

 

정리

프로그래밍에서 다형성을 구현하면 이처럼 클래스 간 결합도를 낮출 수 있고 중복 코드를 줄일 수 있다. 결합도가 낮다는 말은 한 클래스가 수정되었을 때 그것과 연관되어 있는 다른 클래스에서 수정해야 할 코드가 줄어든다는 얘기다. 다형성을 이해하기 위해서는 클래스 간 상속 - 메서드 오버라이딩 - 참조변수 형변환의 흐름을 파악하는 것이 중요하다.

+ Recent posts