자바 언어에서 인터페이스와 상속을 통해 객체지향 프로그래밍의 특징 중 하나인 다형성 Polymorphism
을 구현할 수 있다.
상속이란 클래스가 가지고 있는 멤버를 다른 클래스에 계승시키는 것이다. 부모 클래스에 정의된 멤버 변수과 멤버 함수를 자식 클래스에서 재사용하여 중복을 줄일 수 있다.
부모 클래스의 참조 변수로 하위 클래스의 객체를 다룰 수 있어서, 실행 시점에 유연하게 어떤 하위 클래스의 메서드를 호출할지 결정할 수 있다.
예시를 보자.
아래와 같이 부모 클래스 Parent
, 이를 상속받은 자식 클래스 Child
가 있다.
class Parent {
public String name;
public Parent(String name) {
this.name = name;
}
public void say() {
System.out.println("부모 클래스 메서드입니다.");
}
}
class Child extends Parent {
public int age;
public Child(String name, int age) {
super(name);
this.age = age;
}
@Override
public void say() {
System.out.println("자식 클래스 메서드입니다.");
}
}
아래와 같이 실행해본다.
public class Main {
public static void main(String[] args) {
Parent obj1 = new Parent("엘사");
System.out.println(obj1.name);
obj1.say();
System.out.println("=================");
// 업 캐스팅
Parent obj2 = new Child("안나", 10);
System.out.println(obj2.name);
// System.out.println(obj2.age); // 컴파일 에러 발생
obj2.say();
// 다운 캐스팅
if (obj2 instanceof Child) {
Child downObj2 = (Child) obj2;
System.out.println("다운 캐스팅 : " + downObj2.age);
}
System.out.println("=================");
Child obj3 = new Child("올라프", 2);
System.out.println(obj3.name);
System.out.println(obj3.age);
obj3.say();
// Child obj4 = new Parent(); // 컴파일 에러 발생
}
}
엘사
부모 클래스 메서드입니다.
=================
안나
자식 클래스 메서드입니다.
다운 캐스팅 : 10
=================
올라프
2
자식 클래스 메서드입니다.
위 예제에서 4가지 경우로 클래스형 변수를 만들어서 테스트 해보았다.
- Parent obj1 = new Parent();
- Parent obj2 = new Child();
- Child obj3 = new Child();
- Child obj4 = new Parent();
1번 Parent obj1 = new Parent();
의 경우, 당연히 Parent 클래스의 멤버변수와 멤버함수에 접근할 수 있다.
3번 Child obj3 = new Child();
의 경우, 1번과 마찬가지로 Child 클래스의 멤버변수와 멤버함수에 접근할 수 있다.
2번 Parent obj2 = new Child();
의 경우, 업 캐스팅Up-casting
이 발생한다. 업 캐스팅이란 두 클래스가 상속 관계에 있을 때 부모 클래스 타입의 참조 변수로 자식 클래스의 객체를 참조하도록 하는 것이다.
이런 일이 가능한 이유는 JVM의 메모리 구조를 보면 알 수 있다.
자식 클래스 설계도에 따라 생성된 실제 객체는 힙 heap
영역에 있다. 그러나 부모 클래스 설계도에 따라 생성된 부모 클래스형 변수는 스택 stack
영역에 있다. 따라서 부모 클래스형 변수는 자식 클래스에만 있는 멤버변수나 멤버함수를 알 수가 없다. 위 예시에서 obj2.age
에 접근할 수 없다.
하지만 업캐스팅된 객체를 다시 Child
타입으로 다운캐스팅하면 age
에 접근할 수 있다. 이때 instanceof
연산자를 사용하여 객체의 타입을 확인한 후 다운캐스팅을 시도하면 안전하게 다운캐스팅할 수 있다.
자식 클래스에서 메서드 오버라이딩을 한 경우, 메서드 호출시 자식 클래스의 메서드가 호출된다. 이때는 JVM의 동적 바인딩(Dynamic Binding) 매커니즘에 따라 실행시간(runtime)에 동적으로 메서드가 호출된다. JVM이 실제 객체의 메서드를 찾아서 실행한다.
4번 Child obj4 = new Parent();
의 경우, 컴파일 에러가 발생한다. 부모 클래스 설계도에 따라 생성된 실제 객체는 힙 영역에 있고, 자식 클래스 설계도에 따라 생성된 자식 클래스형 변수는 스택 영역에 있다. 따라서 자식 클래스에만 있는 멤버변수, 멤버함수에 접근하여 실제 객체를 참조하려고 할 때 반드시 에러가 발생할 것이다. 자바 컴파일러 입장에서 아예 이런 상황을 허용하지 않는다.
구현이란 클래스에서 인터페이스 안의 추상 메소드를 구체화하는 것이다. 인터페이스는 다른 클래스를 작성할 때 기본이 되는 틀을 제공하면서, 다른 클래스 사이의 중간 매개 역할까지 담당한다.
같은 인터페이스로 동일한 이름으로 여러 클래스가 여러가지 구현을 만들 수 있고, 인터페이스를 구현한 객체를 실행시점에 유연하게 변경할 수 있다.
예시를 보자.
Animal
이라는 인터페이스가 있고 이를 Dog
, Cat
두 개의 클래스가 다르게 구현하였다.
interface Animal {
void makeSound();
}
class Dog implements Animal {
@Override
public void makeSound() {
System.out.println("멍멍!");
}
}
class Cat implements Animal {
@Override
public void makeSound() {
System.out.println("야옹!");
}
}
실행시점에 서로 다르게 실제 객체의 makeSound()
를 찾아서 실행한다.
public class Main {
public static void main(String[] args) {
Animal dog = new Dog();
Animal cat = new Cat();
dog.makeSound();
cat.makeSound();
}
}
멍멍!
야옹!
MemberRepository
라는 인터페이스가 있고, 내부에 save()
라는 새로운 멤버를 저장하는 추상 메소드가 있다고 해보자(*public abstract
는 생략할 수 있음).
public interface MemberRepository {
void save(Member member);
}
정책상 DB 기술을 어떤 걸 쓸지 아직 정해지지 않았다고 해보자. 그래도 데드라인까지 개발은 진행되어야 하기에 MemoryMemberRepository
클래스로 인터페이스를 구현한다.
public class MemoryMemberRepository implements MemberRepository {
private static Map<Long, Member> store = new HashMap<>();
@Override
public void save(Member member) {
store.put(member.getId(), member);
}
}
MemberService
라는 인터페이스를 구현한 MemberServiceImpl
클래스에서 MemberRepository
형 변수에 MemoryMemberRepository
객체를 대입해서 사용할 것이다.
public class MemberServiceImpl implements MemberService {
private final MemberRepository memberRepository = new MemoryMemberRepository();
@Override
public void join(Member member) {
memberRepository.save(member);
}
}
추후에 DB 기술과 정책이 정해지고 나면, 다른 부분은 건드리지 않고, MemoryMemberRepository
를 새로 구현한 JdbcMemberRepository
로 갈아끼우면 된다.
public class MemberServiceImpl implements MemberService {
private final MemberRepository memberRepository = new JdbcMemberRepository();
@Override
public void join(Member member) {
memberRepository.save(member);
}
}
이처럼 상속과 구현을 통해 다형성을 활용하면 코드를 유연하게 변경하고 재사용할 수 있다.
Reference
- 강의 - 스프링 핵심원리 기본편(김영한)
- 도서 - 이재환의 자바 프로그래밍 입문(이재환)
- JVM의 메모리 구조 및 할당과정
개발자로서 배울 점이 많은 글이었습니다. 감사합니다.