extends와 implements의 핵심 개념
Java에서는 클래스 간의 관계를 정의하기 위해 extends와 implements라는 두 가지 키워드를 사용한다. 이 둘은 모두 재사용성을 제공하지만, 그 목적과 사용 방식에는 중요한 차이가 있다.
상속: extends 키워드
extends는 기존 클래스를 상속받아 새로운 하위 클래스를 만드는 데 사용된다. 이를 통해 자식 클래스는 부모 클래스의 필드와 메서드를 물려받으며, 필요 시 오버라이딩을 통해 동작을 변경할 수 있다.
class Vehicle {
void move() {
System.out.println("차량이 이동합니다.");
}
}
class Car extends Vehicle {
@Override
void move() {
System.out.println("자동차가 도로를 따라 달립니다.");
}
}
위 예제에서 Car 클래스는 Vehicle의 move() 메서드를 상속하고 재정의했다. 또한 Car 객체는 부모 클래스의 다른 멤버에도 접근할 수 있다.
Java는 단일 상속만을 허용하므로, 하나의 클래스는 오직 하나의 클래스만을 extends할 수 있다. 다만, final로 선언된 클래스나 추상 클래스도 상속 가능하다 (단, final 클래스는 상속 불가).
인터페이스 구현: implements 키워드
implements는 인터페이스를 구현할 때 사용된다. 인터페이스는 메서드의 시그니처만을 정의하며, 실제 로직은 포함하지 않는다. 따라서 인터페이스를 구현하는 클래스는 해당 메서드들을 반드시 구체적으로 작성해야 한다.
interface Flyable {
void fly();
}
interface Swimmable {
void swim();
}
class Duck implements Flyable, Swimmable {
public void fly() {
System.out.println("오리가 날아갑니다.");
}
public void swim() {
System.out.println("오리가 수영합니다.");
}
}
한 클래스는 여러 인터페이스를 동시에 구현할 수 있다. 위 예제처럼 콤마(,)로 구분하여 다중 인터페이스 구현이 가능하다. 이는 Java에서 다중 상속의 제약을 보완하는 중요한 수단이다.
인터페이스 설계의 특징
- 인터페이스 내 모든 메서드는 암시적으로
public abstract이다. - 인터페이스 내 변수는 자동으로
public static final이 된다. - 추상 클래스가 아닌 구현 클래스라면 인터페이스의 모든 메서드를 구현해야 한다.
- 서로 관련 없는 클래스들이 동일한 인터페이스를 구현함으로써 일관된 행동을 보장할 수 있다.
다형성과 참조 타입 변환
상속 구조에서는 부모 타입 참조 변수로 자식 객체를 가리킬 수 있다. 이를 업캐스팅(upcasting)이라고 하며, 안전한 형변환이다.
Vehicle v = new Car(); // 업캐스팅
v.move(); // 출력: 자동차가 도로를 따라 달립니다.
이 경우 v는 Vehicle 타입이지만, 실제로 호출되는 move() 메서드는 Car 클래스에 오버라이딩된 버전이다. 이처럼 런타임 시점에 실제 객체의 메서드가 호출되는 것을 동적 바인딩 또는 다형성이라 한다.
단, 업캐스팅된 참조 변수는 자식 클래스 고유의 멤버(Car의 새로운 메서드 등)에는 접근할 수 없다. 이를 해결하려면 다운캐스팅(downcasting)이 필요하지만, 잘못 사용하면 ClassCastException이 발생할 수 있으므로 주의가 요구된다.
실제 프로젝트에서의 활용 전략
소프트웨어 아키텍처 설계 시, extends는 "IS-A" 관계를 표현하는 데 적합하다. 예를 들어 Dog extends Animal은 "개는 동물이다"라는 의미를 명확히 전달한다.
반면 implements는 "CAN-DO" 관계를 나타낸다. Robot implements Movable이라면 "로봇은 이동할 수 있다"는 의미가 된다. 인터페이스는 행동 양식을 계약(contract)하는 역할을 하므로, 모듈 간 느슨한 결합(loose coupling)을 유도하는 데 매우 효과적이다.
결론
요약하면:
- extends: 단일 상속, 상태와 기본 동작을 물려받음, 오버라이딩 가능
- implements: 다중 구현 가능, 계약을 통한 행동 강제, 모든 추상 메서드 구현 필수
둘은 서로 배타적이지 않으며, 하나의 클래스가 동시에 부모 클래스를 상속받고 여러 인터페이스를 구현할 수 있다. 예를 들어 다음 코드는 유효하다:
class Android extends Machine implements Runnable, Connectable {
// ...
}