본문 바로가기
JAVA

[JAVA] 상속 - 메소드 오버라이딩, 다형성

by Amy IT 2022. 5. 27.

◆ 메소드 오버라이딩 (method overriding)

 

일반적으로 상속 관계에서 부모 클래스의 메소드를 자식 클래스에서 조건 없이 사용 가능합니다. 그런데 자식 클래스에서 부모 클래스의 메소드를 재정의해서 사용할 수도 있는데, 이를 메소드 오버라이딩(method overriding)이라고 합니다. 메소드 오버로딩(method overloading : 이름은 같으나 매개변수가 다른 함수를 여러번 정의)과는 다른 개념입니다. 

메소드 오버라이딩을 사용하기 위해서 우선 상속이 전제되어야 합니다. 그리고 부모 클래스에서 선언된 형식 그대로 자식 클래스에서 선언되어야 합니다. 즉, 메소드 이름, 리턴타입, 매개변수, 접근지정자 등이 동일해야 합니다. (더 작은 타입의 리턴타입과 더 넓은 범위의 접근지정자로 재정의하는 것은 가능합니다.) 또한, 오버라이딩된 메소드라는 것을 명확히 하기 위해 @Override라는 어노테이션(annotation)을 사용하는 것이 좋습니다. 

 

Employee 클래스와 Employee 클래스를 상속 받는 Manager 클래스를 만들되, getEmployee() 함수와 toString() 함수를 오버라이딩으로 구현해 보겠습니다. 참고로 toString() 함수는 Object 클래스의 함수로서, 객체의 주소값을 반환하도록 구현되어 있습니다. 이를 객체의 주소값이 아닌 객체가 가지고 있는 데이터 값을 반환하도록 자식 클래스에서 재정의할 수 있습니다. 

class Employee {
	String name;
	int salary;
	public Employee() {}
	public Employee(String name, int salary) {
		this.name = name;
		this.salary = salary;
	}
	public String getEmployee() {
		return name + "\t" + salary;
	}
	@Override 
	public String toString() {
		return "Employee [name=" + name + ", salary=" + salary + "]";
	}
}

class Manager extends Employee {
	String depart;
	public Manager() {}
	public Manager(String name, int salary, String depart) {
		super(name, salary);
		this.depart = depart;
	}
	@Override  
	public String getEmployee() {
		return super.getEmployee() + "\t" + depart;
	}
	@Override
	public String toString() {
		return "Manager [depart=" + depart + ", name=" + name + ", salary=" + salary + "]";
	}
}

public class TestMain {

	public static void main(String[] args) {
		Manager man = new Manager("홍길동", 1000, "개발"); 
		System.out.println(man.getEmployee()); 
		System.out.println(man); //man.toString 
	}

}

이전 글에서는 Employee 클래스의 getEmployee() 함수와 Manager 클래스의 getManager() 함수를 각각 정의했었습니다. 그런데 이번에는 getEmployee() 함수를 자식 클래스에서 부모 클래스의 함수와 동일한 형식으로 내용만 다르게 재정의하고 있습니다. toString() 함수 역시 재정의하여 실제 객체가 갖는 데이터 값을 반환하도록 하고 있습니다. 이렇게 하면 각 클래스에서 동일한 이름의 함수를 사용할 수 있게 되기 때문에 재사용성이 향상되는 장점이 있습니다. 메소드 오버라이딩은 특히 다형성이 적용되었을 때 그 효용이 극대화됩니다. 우선 다형성이 무엇인지부터 알아보도록 하겠습니다.

 

 

 

 

◆ 다형성 (Polymorphism)

 

다형성(Polymorphism)이란 상속 관계에서 상위(부모) 타입의 변수로 모든 하위(자식) 타입을 참조할 수 있는 특성입니다. 다형성을 적용하여 부모 타입의 변수에 자식 객체를 저장할 수도 있고, 함수의 매개변수를 부모 타입으로 설정하고 자식 객체를 받거나 또는 함수의 리턴타입을 부모 타입으로 설정하여 자식 객체를 리턴시키는 것도 가능합니다. 예를 들어 위의 예시에서 Manager 클래스는 Employee 클래스를 상속 받고 있기 때문에, Manager 객체를 생성하고 상위 타입인 Employee 타입의 변수에 저장할 수 있습니다. 

 

 

* 다형성을 적용하여 객체 저장 & 오버라이딩 메소드 호출

 

이번에는 Employee 클래스와 Manager 클래스 각각에 유일한 함수를 추가하고, Manager 객체 생성 시 Employee 클래스 타입으로 저장해 보겠습니다.

class Employee {
	String name;
	int salary;
	public Employee() {}
	public Employee(String name, int salary) {
		this.name = name;
		this.salary = salary;
	}
	public String getEmployee() {
		return name + "\t" + salary;
	}
	@Override 
	public String toString() {
		return "Employee [name=" + name + ", salary=" + salary + "]";
	}
	public void testEmployee() {} //부모에 선언된 함수 -> 자식도 사용 가능 
}

class Manager extends Employee {
	String depart;
	public Manager() {}
	public Manager(String name, int salary, String depart) {
		super(name, salary);
		this.depart = depart;
	}
	@Override  
	public String getEmployee() {
		return super.getEmployee() + "\t" + depart;
	}
	@Override
	public String toString() {
		return "Manager [depart=" + depart + ", name=" + name + ", salary=" + salary + "]";
	}
	public void testManager() {} //부모에 없는 자식의 유일한 함수
}

public class TestMain {

	public static void main(String[] args) {
		Manager man = new Manager("홍길동", 1000, "개발"); 
		System.out.println(man.getEmployee()); 
		System.out.println(man); //man.toString 
		//다형성 적용
		Employee emp = new Manager("이순신", 2000, "영업"); //Manager 객체를 Employee 타입으로 저장
		System.out.println(emp.getEmployee()); //실제 객체(자식)의 오버라이딩된 함수 호출
		System.out.println(emp); //실제 객체(자식)의 오버라이딩된 함수 호출
		emp.testEmployee(); //부모에 선언된 함수 호출 가능
//		emp.testManager(); //자식의 유일한 함수 호출 불가
//		emp.depart; //자식의 유일한 변수 사용 불가 
		((Manager)emp).testManager(); //실제 객체 타입으로 형변환 후 사용 가능
		System.out.println("이순신 depart: "+((Manager)emp).depart);
	}

}

자식인 Manager 객체를 생성하면서 부모인 Employee 타입의 emp 변수에 참조시키고 있습니다. 실제 객체는 Manager 객체이지만 Employee 타입으로 저장된 것입니다. 그렇다면 이때 getEmployee() 함수를 호출하면 Employee 클래스의 함수가 호출될까요, Manager 클래스의 함수가 호출될까요? 정답은 Manager 클래스의 getEmployee() 함수입니다.

 

우선 아래에 보시면 Manager 클래스에서만 선언된 testManager() 함수와 depart 변수는 사용이 불가능한 것을 확인할 수 있습니다. 타입이 부모인 Employee 타입이므로 자식인 Manager 클래스의 유일한 함수, 변수는 직접 사용할 수 없게 됩니다. Manager 클래스의 유일한 멤버들을 사용하기 위해선 실제 객체 타입인 Manager 타입으로 형변환을 해야 합니다. 반면, 자식 클래스에 오버라이딩된 함수가 있는 경우 다형성으로 객체 생성을 하여도 실제 생성된 객체의 함수가 호출됩니다. 따라서 testEmployee() 함수 호출 시 Manager 클래스에 있는 함수가 호출되는 것입니다. 

 

 

 

* 다형성을 적용한 메소드 & instanceof 연산자

 

이번에는 다형성을 적용한 함수입니다. 다형성을 적용하여 부모 타입의 매개변수로 자식 객체를 받을 수 있습니다. 그런데 함수 내에서 그것이 정확히 어떤 객체인지 알아야 될 때가 있는데, 이때 instanceof 연산자를 이용할 수 있습니다. instanceof 연산자는 다형성을 적용하여 부모 타입 변수로 자식 객체를 저장했을 때, 실제 저장된 객체의 데이터 타입을 검사하기 위해 사용합니다. instanceof 연산자로 검사한 결과가 true이면, 데이터 타입이 일치하거나 또는 검사한 타입으로 형변환할 수 있다는 것을 의미합니다. 매니저와 엔지니어에 대한 세금을 다르게 구하는 함수를 정의하는데, 자식 클래스에서 각각 오버라이딩하지 않고 부모 클래스에서 instanceof 연산자를 이용해 각 객체에 따라 결과가 다르게 출력되도록 해 보겠습니다.

class Employee {
	public void taxRate(Employee e) {//다형성 매개변수 부모타입 => 자식객체 저장 
		//instanceof 사용시 자식 => 부모 순서로 비교 
		if (e instanceof Manager) {//실제 객체의 타입 비교 
			Manager man = (Manager)e; //형변환
			System.out.println("Manager 세금 구하기");
		} else if(e instanceof Engineer) {
			Engineer eng = (Engineer)e; //형변환
			System.out.println("Engineer 세금 구하기");
		} else if(e instanceof Employee) {
			System.out.println("Employee 세금 구하기");
		}
	}
}
class Manager extends Employee {}
class Engineer extends Employee {}

public class Ex06_8 {

	public static void main(String[] args) {
		Employee e = new Employee();
		e.taxRate(e);
		Manager man = new Manager();
		man.taxRate(man);
		Engineer eng = new Engineer();
		eng.taxRate(eng); 
		Employee emp = new Engineer();
		emp.taxRate(emp);
	}

}

Employee 클래스에 정의된 taxRate() 함수는 자식 클래스인 Manager, Engineer 클래스에 상속되므로, 각 Manager 객체와 Engineer 객체에서 참조해 사용할 수 있습니다. taxRate() 함수의 매개변수는 Employee 타입이므로, 다형성을 적용하여 자식 객체인 Manager 객체와 Engineer 객체를 인자로 받을 수 있습니다. taxRate() 함수 내에서 instanceof 연산자를 이용해 인자로 받는 객체에 따라 다른 결과를 출력하도록 하고 있습니다. 실제 객체의 타입이 Manager이면 첫 번째 조건식을, Engineer이면 두 번째 조건식을 만족합니다. 다형성을 적용하여 Employee 타입으로 저장하더라도 실제 객체의 타입이 Engineer이면 두 번째 조건식을 만족합니다. 따라서 다음과 같은 결과가 출력됩니다. 

 

그런데 주의할 것은 instanceof 연산자 사용 시 반드시 자식 타입에서 부모 타입 순서로 비교해야 한다는 점입니다. 다시 말해, 상속 계층 구조의 하위부터 비교해야 합니다. 부모 타입인 Employee 부터 비교하면 다음과 같이 모두 상위 타입의 조건식을 만족하여 출력문이 나오게 됩니다. 이는 자식이 부모의 멤버들을 모두 상속받아, 자식의 인스턴스가 부모 인스턴스를 포함하고 있다고 할 수 있기 때문입니다. instanceof 연산자를 사용할 때, 데이터 타입이 일치하는 경우 외에 검사한 타입으로 형변환이 가능할 때도 true 값을 반환하게 됩니다. 

 

 

 

 

* 다형성을 적용한 배열

 

다형성을 이용해서 배열을 관리하면 더욱 효율적인 관리가 가능합니다. Pet 클래스와 Pet 클래스를 상속 받는 Cat, Dog 클래스를 만들고 Pet 배열을 사용하여 데이터를 저장해 보겠습니다. 

class Pet {
	String name;
	int age;
	String gender;
	public Pet(String name, int age, String gender) {
		super();
		this.name = name;
		this.age = age;
		this.gender = gender;
	}
	public String getPet() {
		return name + "\t" + age + "\t" + gender;
	}
}
class Cat extends Pet {
	String color;
	public Cat(String name, int age, String gender, String color) {
		super(name, age, gender);
		this.color = color;
	}
	@Override
	public String getPet() {
		return super.getPet() + "\t" + color;
	}
}
class Dog extends Pet {
	String species;
	public Dog(String name, int age, String gender, String species) {
		super(name, age, gender);
		this.species = species;
	}
	@Override
	public String getPet() {
		return super.getPet() + "\t" + species;
	}
}

public class TestPet {

	public static void main(String[] args) {
		Pet [] pets = { new Cat("나비", 2, "암컷", "흰색"),
				new Cat("하늘", 10, "수컷", "회색"),
				new Dog("코코", 3, "수컷", "리트리버"),
				new Dog("망치", 6, "암컷", "불독") };

		System.out.println("1번 for ================");
		for (int i = 0; i < pets.length; i++) {
			System.out.println(pets[i].getPet()); 
		}
		System.out.println("2번 for each ===============");
		for (Pet p : pets) {
			System.out.println(p.getPet());
		}
	}

}

Pet 클래스에 선언된 getPet() 함수를 자식 클래스인 Cat, Dog 클래스에서 오버라이딩하여 재정의하고 있습니다. 이렇게 하면 다형성을 적용하여 부모 타입 객체로 자식 객체를 저장해도, 자식 클래스에 오버라이딩된 함수를 호출할 수 있게 됩니다. 이에 따라 for문을 위와 같이 매우 간단하게 작성하여 각 객체의 데이터를 모두 조회할 수 있습니다. 추가로 다른 코드 작업을 하지 않아도 실제 객체 타입에 맞게 각 클래스에서 오버라이딩된 함수를 호출하게 됩니다. 다음과 같이 결과가 출력됩니다. 

 

 

 

이상으로 상속 관계에서의 메소드 오버라이딩, 다형성에 대해 정리해 보았습니다. 

 

 

댓글