Java의 객체 지향 프로그래밍의 4가지 특징은 추상화, 상속, 다형성, 캡슐화 이다.
여기서 추상화, 상속, 다형성의 개념은 항상 헷갈리는 개념이다. 본문에선 개념을 자세하게 설명하기보단 인터페이스와 추상 클래스의 차이, 그리고 추상화와 다형성의 차이를 중점적으로 설명한다.
우선 상속에 대해 알고 넘어가야 추상화와 다형성을 이해할 수 있다. 상속은 기존 클래스의 속성과 메서드를 새로운 클래스에 물려주는 것을 뜻한다. 이때 기존 클래스를 상위 클래스, 또는 부모 클래스라 부르며, 새로운 클래스를 하위 클래스, 또는 자식 클래스라 부른다. 상속은 extends 키워드를 통해 이루어진다.
public Class Parent{ .... }; // 부모 클래스
public Class Child extends parent { .... }; // 자식 클래스
여기서 주의할 점이 있다. 부모 클래스의 접근 제한자는 private이 되면 안된다. private 타입은 해당 클래스 내부에서만 접근이 가능한 접근제한자이므로 자식 클래스가 접근할 수 없게 된다. 같은 맥락으로 부모 클래스 내부의 private 필드나 메서드는 자식 클래스가 상속할 수 없다.
또 extends 키워드를 통해 상속을 한다 했지만 implements 키워드도 상속에 사용된다. 이는 클래스가 인터페이스를 상속 할 때 사용된다. 클래스끼리 상속하거나 인터페이스끼리 상속할 때는 extends를 쓰고, 클래스가 인터페이스를 상속할 때는 implements를 쓴다.

추상화의 사전적 의미는 공통성과 본질을 모아 추출 하는 것이다.
즉, 공통적인 기능과 속성들은 따로 코드를 분리해 관리하는 것이 유지보수에 유리하다는 점에서 착안한 특징이다. 자동차와 오토바이가 있다 치자. 둘의 공통점은 시동, 운전 등이 있을 것이고, 차이점은 외형, 속도, 연비 등이 있을 것이다. 우리는 추상화를 통해 시동, 운전을 하나의 코드로 관리할 것이다.
추상화에는 두 가지 개념이 사용된다. 바로 추상 클래스와 인터페이스이다.
public abstract class 클래스명{
//필드
//생성자
//메서드
//추상메서드
}
추상 클래스는 abstract 키워드를 통해 구현된다. class 앞에 abstract를 붙이기만 하면 추상 클래스다! 속성, 생성자, 일반 메서드, 추상 메서드.. 모두 가질 수 있다. 추상 메서드 란 메서드의 선언부만 존재하고, 구현부가 없는 메서드이다. 추상 클래스와 마찬가지로 abstract 키워드를 사용한다. 여기서 중요한 점은, 추상 클래스는 추상 메서드를 가질 수도 있고 안 가질 수도 있지만, 추상 메서드는 반드시 추상 클래스 내에 있어야 한다. 추상 메서드가 일반 클래스 내에서 선언될 수 없다는 것이다.
여기서 아주아주아주 중요한 추상화의 개념이 등장한다.
- 추상 클래스는 객체를 만들 수 없다. (인스턴스화가 불가하다.)
- 추상 클래스의 추상 메서드는 반드시 재정의(Override) 돼야 한다.
- 추상화도 결국 상속이다.
사실상 이 3가지만 알아도 추상화에 대한 설명은 끝이다.
조금 더 알아듣기 쉽게 코드를 통해 설명하겠다.
public abstract class Animal {
protected String name;
protected int age; // 속성을 가질 수 있다. 이때 자식 클래스는 모두 해당 속성을 가진다.
public Animal(String name, int age) { // 생성자를 가질 수 있다.
this.name = name;
this.age = age;
}
public abstract void makeSound(); // 추상 메서드를 가질 수 있다.
public void eat() { // 일반 메서드도 가질 수 있다.
System.out.println("The animal is eating.");
}
}
public class Dog extends Animal { // 추상 클래스의 상속은 extends 키워드를 사용한다.
@Override
public void makeSound() {
System.out.println("Woof!");
}
}
// 보이는가? makeSound는 오버라이드 되었고 eat는 오버라이드 되지 않았다.
// 추상 메서드는 재정의가 강제되지만 일반 메서드는 강제 되지 않기 때문이다.
// 물론 재정의 의무가 없을 뿐 eat 메서드도 상속 대상이므로 자식 클래스에서 사용 가능하다.
여기까지 이해가 갔다면 이런 의문점이 들 수 있다.
아니, 객체 못 만든다며? 생성자는 어따 쓰는데?
이는 상속과 아주아주 밀접한 관련이 있다.
우선, 상속 관계에서 자식 클래스는 부모 클래스의 메서드를 자동으로 갖게 된다.
위 코드에서 eat() 메서드를 자식 클래스에서 재정의하거나 선언하지 않았지만, 자식 클래스는 부모 클래스의 메서드인 eat()을 당연히 쓸 수 있다.
이는 생성자도 마찬가지다. Dog 클래스에선 기본 생성자가 생략되어있고, 그 생략된 기본 생성자 안에는 부모 생성자, 즉, super(); 가 생략되어있다.
추상 클래스의 생성자는 해당 클래스의 인스턴스를 찍어내는 목적이 아닌, 자식 클래스의 객체를 생성할 때 호출되어 해당 객체의 인스턴스 변수를 초기화 하는 목적이다.
공통된 메서드를 따로 추상화 해놓은 것처럼, 생성자도 공통된 초기화 로직이 있다면 추상 클래스로 빼서 관리하는 것이다!
추상화는 interface를 통해서도 이루어진다. 앞서 추상 클래스가 abstract 키워드로 생성된다면 인터페이스는 interface 키워드로 생성된다. 인터페이스의 등장 배경에는 Java의 단일상속이 있었다. Java는 기본적으로 단일상속을 원칙으로 하기 때문에 하나의 클래스는 하나의 클래스만 상속할 수 있었다. 즉, 추상 클래스를 아~무리 많이 구현해도 한 자식 클래스가 상속 받을 수 있는 클래스는 단 하나였던 것이다. 이러한 점을 개선하고자 다중 상속이 가능한 인터페이스가 등장하였다.
public interface 인터페이스명 {
//상수
//추상 메소드
//디폴트 메소드
//정적 메소드
}
사실 기존 인터페이스는 이렇게 다양한 구현과 선언이 이루어지는 구조가 아니었다.
원래는 추상 메서드만 선언할 수 있었고, 따라서 인터페이스 내에서는 구현이 이루어지지 않았다.
그런데 Java8부터 dafault 메서드와 static 메서드, Java9부턴 private 메서드까지 선언할 수 있게 되었다. (근본 없어짐) 이거에 대해서도 할 말이 많은데 글이 너무 길어질 것 같으므로 다음에 기회가 된다면 정리해보겠다.
인터페이스를 사용해 다음과 같이 다중 상속을 구현할 수 있다.
class Animal implements Cat, Dog, Duck{
...
}
그럼 다중 상속 말고 추상 클래스와 인터페이스의 차이점은 뭘까? 엄청 많다. 나도 알아보면서 놀랐다. 이걸 모두 자세히 외울 필욘 없겠지만, 인지만 해두고 필요할 때 꺼내보면 유용할 거다.
| 구분 | 추상 클래스 | 인터페이스 |
|---|---|---|
| 사용 키워드 | abstract | interface |
| 사용 가능 변수 | 제한 없음 | static final (상수) |
| 사용 가능 접근 제어자 | 제한 없음 (public, private, protected, default) | public |
| 사용 가능 메소드 | 제한 없음 | 추상 메서드, 디폴트 메서드, static 메서드, private 메서드 |
| 상속 키워드 | extends | implements |
| 다중 상속 가능 여부 | 불가능 | 가능 (클래스에 다중 구현, 인터페이스 끼리 다중 상속) |

다형성이란 객체가 상황에 따라 여러 가지 형태를 가질 수 있는 성질을 의미한다.
'나' 는 학교에 가면 학생이 되고, 집에 가면 자식이 되고, 회사에 가면 직원이 되는 것처럼 말이다. 그렇다면 여기까지 설명만 들었을 때 왜 인터페이스가 다형성을 구현하는데 유리한지 감이 오지 않는가? 추상 클래스를 통한 다형성 구현은 한 가지 형태만 상속할 수 있지만, 인터페이스를 통해 구현하면 여러 가지 형태를 상속할 수 있으니 다형성 구현에 유리한 것이다.
또 다형성의 가장 큰 특징은 느슨한 결합도다.
Driver 클래스가 MotorBike와 Car 클래스에 의존한다 해보자.

이렇게 클래스가 직접적인 관계로 의존하는 것을 객체간의 결합도가 높다고 말한다.
지금은 탈 것이 2 종류여서 그렇지, 10개, 1000개가 된다면 유지보수성이 어마어마하게 떨어질 것이다.

따라서 이것을 Vehicle이라는 인터페이스로 묶어 클래스간의 직접적인 연결을 끊어주고, 이 인터페이스만 관리한다면 유지보수성이 올라가게 된다. 이것이 느슨한 결합도이다.
결론적으로 추상화는 인스턴스의 공통된 코드를 뽑아 추상 클래스나 인터페이스로 관리해 유지보수성을 높이고자 하는 특징이고, 다형성은 추상화의 하위 개념으로 다중상속을 지원하는 인터페이스로 구현해 객체간의 결합도를 낮추는 것을 목적으로 한다.
또 추상 클래스와 인터페이스는 객체를 생성 못하거나 추상 메서드를 갖는 점에서 공통점을 지니지만, 인터페이스는 상수만 가질 수 있는 점, 생성자를 쓰지 못하는 점 등 제한점이 더 많다.