본문 바로가기
JAVA

[JAVA] 상속 - 추상 클래스와 인터페이스

by Amy IT 2022. 5. 28.

상속을 적용하면 코드를 재사용하고 다형성, 오버라이딩 메소드 등을 이용하여 재사용성 및 유지보수를 향상시킬 수 있습니다. 하지만 상속은 강제성이 없습니다. 하위 클래스에서 상위 클래스의 메소드를 상속받아 사용하지 않고 자신만의 메소드를 사용하게 되면 상속의 장점을 얻을 수 없게 됩니다. 이 때문에 상위 클래스의 메소드를 반드시 사용하게끔 강제할 필요성이 생기는데, 추상 클래스와 인터페이스를 사용하면 상위 클래스의 메소드를 반드시 사용하도록 강제할 수 있어 재사용성 및 유지보수를 더욱 향상시킬 수 있게 됩니다. 

 

 

◆ 추상 클래스 (abstract class)

 

클래스가 설계도라면, 추상 클래스는 미완성 설계도라고 할 수 있습니다. 블록({ }) 이 없는 추상 함수(abstract method)를 포함할 수 있는 미완성 클래스입니다. abstract 키워드를 사용하여 표현하고, 다음의 구성요소들을 포함할 수 있습니다. 

public abstract class 클래스명 {
      //인스턴스 변수
      //일반 메소드
      //생성자
      //추상 메소드
}

 

추상 함수는 구현이 되어 있지 않은 함수입니다. 따라서 추상 클래스를 상속받는 하위 클래스에서는 반드시 추상 함수를 재정의해야 합니다. 특정 함수를 통일하여 사용하도록 강제할 수 있게 되는 것입니다. 또한, 추상 클래스는 구현이 되어 있지 않은 추상 함수를 포함할 수 있기 때문에 객체 생성이 불가능합니다. 

 

예시를 통해 살펴보겠습니다.

abstract class AbstractClass {
	private int num;
	public abstract void a(); //추상함수
	public abstract void b(); //추상함수
	public AbstractClass() {}
	public AbstractClass(int num) { 
		this.num = num;
	}
	public int getNum() {
		return num;
	}
}
class ConcreteClass extends AbstractClass {
	public ConcreteClass() {}
	public ConcreteClass(int num) {
		super(num);
	}
	@Override
	public void a() {
		System.out.println("a() 함수 호출");
	}
	@Override
	public void b() {}
}

public class TestMain {

	public static void main(String[] args) {
//		AbstractClass x = new AbstractClass(); //객체 생성 불가
		ConcreteClass y = new ConcreteClass(100); 
		y.a(); 
		y.b(); 
		System.out.println(y.getNum());
		AbstractClass z = new ConcreteClass(); //다형성 적용 
		z.a(); //오버라이딩된 a() 함수 호출
	}
    
}

AbstractClass 클래스에서 a(), b() 라는 추상 함수를 정의하고 있습니다. 이에 따라 AbstractClass 클래스를 상속 받는 ConcreteClass에서는 a(), b() 함수를 반드시 오버라이딩해야 합니다. 오버라이딩하지 않으면 오류가 발생하기 때문에 통일성을 유지하기 쉬워집니다. 추상 클래스인 AbstractClass는 객체 생성이 불가능한 것을 확인할 수 있습니다. ConcreteClass 생성자를 호출하여 100 값을 넘겨주면서 객체 생성을 한 후, a(), b() 함수와 getNum() 함수를 호출하고 있습니다. 다형성을 적용하여 AbstractClass 타입으로 저장할 수도 있습니다. 마찬가지로 a() 함수를 호출하면 실제 저장된 객체의 오버라이딩된 함수가 호출됩니다. 따라서 다음과 같은 결과가 출력됩니다. 

 

 

 

지난 글에서 부모 클래스인 Pet 클래스의 getPet() 함수를 Cat, Dog 클래스에 오버라이딩함으로써 재사용성을 높여 데이터의 효율적 관리가 가능해짐을 살펴보았습니다. 이번에는 Pet 클래스에서 cry() 함수를 추상 함수로 정의함으로써 Cat, Dog 클래스에서 동일한 형식의 cry() 함수를 반드시 재정의하도록 만들어 보겠습니다. 

abstract class Pet {
	private String name;
	private int age;
	public abstract void cry(); //추상함수
	public Pet(String name, int age) {
		this.name = name;
		this.age = age;
	}
	public String getName() {
		return name;
	}
}
class Cat extends Pet {
	@Override
	public void cry() {
		System.out.println("Cat cry() 야옹~");
	}
	public Cat(String name, int age) {
		super(name, age);
	}
}
class Dog extends Pet {
	@Override
	public void cry() {
		System.out.println("Dog cry() 멍멍!");
	}
	public Dog(String name, int age) {
		super(name, age);
	}
}

public class TestMain {

	public static void main(String[] args) {
		
		Pet [] pets = { new Cat("나비", 2),
					new Dog("코코", 5),
					new Cat("율무", 7) };
		for (Pet pet : pets) {
			System.out.print(pet.getName()+" ");
			pet.cry();
		}
	}

}

Pet 클래스를 추상 클래스로 만들어 Cat과 Dog이 가지고 있는 공통 속성을 인스턴스 변수로 선언하는 동시에 cry() 함수를 추상 함수로 정의해 하위 클래스들에서 반드시 재정의하도록 강제하고 있습니다. Cat 객체와 Dog 객체를 Pet 타입의 배열로 저장하고 모든 객체의 정보를 for문으로 꺼내오고 있습니다. cry() 함수는 각 클래스에 재정의되어 있기 때문에, 다형성으로 객체를 부모 타입에 저장해도 실제 저장된 객체의 오버라이딩된 함수를 호출하게 됩니다. 따라서 다음과 같은 결과가 출력됩니다. 

 

 

 

 

 

◆ 인터페이스 (interface)

 

인터페이스도 일종의 추상 클래스입니다. 하지만 추상 클래스보다 추상화 정도가 높아서 일반 메소드나 멤버변수를 가질 수 없고, 추상 메소드와 상수만을 멤버로 가질 수 있습니다. 추상 클래스가 부분적으로 완성된 미완성 설계도라면, 인터페이스는 밑그림만 그려진 기본 설계도라고 할 수 있습니다. interface 키워드를 사용하고 다음의 구성요소를 포함할 수 있습니다. (static 메소드와 default 메소드 예외적으로 가능)

public interface 인터페이스명 {
      //public static final로 지정한 상수
      //public abstract 지정자를 이용한 추상 메소드
}

인터페이스도 추상 클래스와 마찬가지로 구현되지 않은 추상 함수를 갖기 때문에 하위 클래스에서 메소드를 오버라이딩하도록 강제하며, 객체 생성이 불가능합니다. 인터페이스를 구현할 때는 implements 키워드를 사용하고 클래스와 다르게 다중 구현이 가능하며, 인터페이스 간에도 extends 키워드를 사용해서 다중 상속이 가능합니다. 

 

예시를 통해 알아보겠습니다. 

interface A {
	public abstract void a();
}
interface B {
	public abstract void b();
	public abstract void bb();
}
interface C extends A, B { //인터페이스 다중 상속
	public abstract void c(); 
	//상속받은 a(), b(), bb() 추상함수 포함됨 
}

class ConcreteClass implements A, B { //다중 구현
	@Override
	public void a() {}
	@Override
	public void b() {}
	@Override
	public void bb() {}
}
class ConcreteClass2 implements C {
	@Override
	public void a() {}
	@Override
	public void b() {}
	@Override
	public void bb() {}
	@Override
	public void c() {}
	//인터페이스 C가 상속받고 있는 것까지 모두 구현 필요
}
class ConcreteClass3 extends ConcreteClass implements C {
	@Override
	public void c() {}
	//ConcreteClass의 오버라이딩된 a(), b(), bb() 함수 포함됨
	//C에서 구현안된 c() 함수만 여기서 구현
}

인터페이스 A와 B가 있고, A, B를 다중 상속받고 있는 인터페이스 C가 있습니다. C에는 추상 함수인 a(), b(), bb() 함수도 포함되어 있는 것입니다. ConcreteClass 에서 인터페이스 A, B를 다중 구현하고 있습니다. A, B가 가지는 모든 추상 함수를 구현해야 합니다. ConcreteClass2는 인터페이스 C를 구현하고 있는데, C는 A, B를 상속받고 있으므로 A, B, C에 있는 모든 추상 함수를 구현해야 합니다. ConcreteClass3은 ConcreteClass를 상속받는 동시에 인터페이스 C를 구현하고 있습니다. ConcreteClass를 상속받기 때문에 오버라이딩되어 있는 a(), b(), bb() 함수를 포함하고 있고, C에서 구현되지 않은 c() 함수만을 구현하고 있습니다. 

 

 

이번에는 "날다"라는 기능을 공통적으로 구현하기 위해 Flyer 인터페이스와 Flyer 인터페이스를 구현하는 Airplane, Bird, SuperMan 클래스를 만들어 보겠습니다. 

interface Flyer {
	public abstract void fly();
}
class Airplane implements Flyer {
	@Override
	public void fly() {
		System.out.println("Airplane.fly()");
	}
}
class Bird implements Flyer {
	@Override
	public void fly() {
		System.out.println("Bird.fly()");
	}
}
class SuperMan implements Flyer {
	@Override
	public void fly() {
		System.out.println("SuperMan.fly()");
	}
}

public class TestFlyer {

	public static void main(String[] args) {
		Flyer [] flyers = {new Airplane(), new Bird(), new SuperMan()};
		for (Flyer flyer : flyers) {
			flyer.fly();
		}
	}

}

Airplane, Bird, SuperMan 클래스 모두 Flyer 인터페이스를 구현하도록 만들어 fly() 함수를 반드시 오버라이딩하도록 하고 있습니다. 인터페이스를 이용해 함수명을 통일하도록 강제하니, "날다"라는 공통된 기능을 동일하게 fly() 함수로 표현할 수 있게 되어 코드가 간결해졌습니다. Airplane, Bird, SuperMan 객체를 생성하여 Flyer 타입으로 저장하고 있습니다. 계층 구조적으로 인터페이스가 구현 클래스보다 큰 타입이기 때문에 다형성 적용이 가능합니다. Flyer 배열에 저장된 각 객체들에는 오버라이딩된 fly() 함수가 존재하므로 for문에서 각 객체가 갖는 fly() 함수를 호출하게 됩니다. 따라서 다음과 같은 결과가 출력됩니다. 

 

 

 

이상으로 추상 클래스와 인터페이스의 개념과 사용 방법에 대해 정리해 보았습니다.

 

 

댓글