추상화
일반 클래스와 동일한데 추상 메서드를 하나라도 포함하면 추상 클래스
- Service.java→ interface
- ServiceImpl.java→ class (클래스가 인터페이스를 상속받으면 구현부 = implement)
특징
- 객체 생성 불가
- extends 사용 가능
- abstract 키워드를 사용하여 추상 클래스 생성
- 인터페이스나 추상클래스를 상속 받는 서브클래스는 반드시 그들의 모든 추상 메서드를 오버라이딩 해야 한다.
이 특징을 기억하면서 추상화 과정을 살펴보자.
날 수 있는 새와 슈퍼맨이 있다고 하자.
둘은 ‘날 수 있는 동물’에 포함된다. 즉, ‘새와 슈퍼맨은 동물이다’(is-a)가 성립된다.
우리는 Animal이라는 추상 클래스를 만들어 볼 것이다.
public abstract class Animal { // fly라는 추상 메소드를 가지고 있으므로 추상 클래스가 된다
    private String name; // 부모로서 공통적으로 물려주는 요소
    public void eating(String food) {
        System.out.println(name + " is eating " + food);
    } 
    // 추상 메서드
    public abstract void fly(); // 추상 메서드는 구현부가 존재하지 않음
}public class Bird extends Animal {
    @Override
    public void fly() {
        System.out.println("Bird is flying");
    }
}public class Superman extends Animal {
    @Override
    public void fly() {
        System.out.println("Superman is flying");
    }
}공통적으로 모든 동물은 새든 슈퍼맨이든 호랑이든 음식을 먹기 때문에 eating이라는 메소드를 정의해줬다.
난다는 것은 새와 슈퍼맨 등 날 수 있는 동물만이 가진 특징이므로 추상 메소드로 작성해줬다. 물론, 추상 메소드를 하나만 가지고 있어도 해당 클래스는 추상 클래스가 된다.
Bird와 Superman 클래스는 extends 키워드를 붙여 Animal을 상속 받고, 각각 fly 메소드의 구현부를 작성해준다.
=> 추상 클래스를 상속 받은 서브 클래스는 반드시 그들이 가진 모든 추상 메소드를 오버라이딩해야 한다는 특징
동물은 일반화한 것일 뿐, 구체화하면 새가 될 수도, 슈퍼맨이 될 수도 있다.
따라서 이들을 담을 배열을 먼저 선언해본다.
Animal [] animalAry = new Animal[10];
😮 잠깐, 추상 클래스는 객체로 생성할 수 없다면서요!
Animal의 객체를 만드는 것이 아니라 배열 객체를 만들 뿐 그 요소들의 타입이 Animal인 것이기 때문에 이 경우는 문제가 없다.
animalArr[0] = new Bird(); // 캐스팅 발생 (Animal 타입의 배열에 Bird 객체를 할당)
animalArr[1] = new Superman(); // 캐스팅 발생 (Animal 타입의 배열에 Superman 객체를 할당)
for (Animal animal : animalArr) { 
    if (animal != null) {
        animal.fly(); // 오버라이딩된 메소드는 부모 타입의 메서드를 자식 타입의 메서드로 호출
    }
}그리고 이 배열에 Bird와 Superman 객체를 담고, 반복문을 통해 두 객체의 공통 특징인 '날기'를 위해 fly 메소드를 호출해본다. 오버라이딩 된 fly 메소드는 각각 자식의 것으로 호출되어 실행될 것이다.
따라서Bird is flyingSuperman is flying
이 출력 된다.
인터페이스(interface)의 필요성
혹시 동물로 Tiger를 추가하게 된다면 어떨까?
그렇게 되면 호랑이도 날아야 한다. (..)
그래서 이런 경우 때문에 우리는 추상클래스와 함께 인터페이스를 같이 자주 쓰게 되는 것이다.
동물 중 나는 동물을 Flyer라는 인터페이스를 만들어준다.
public interface Flyer {
    public final int a = 10; // 상수 필드
    public void fly(); // 추상 메서드
    public void takeOff(); // 추상 메서드
    public void landing(); // 추상 메서드
}public class Bird extends Animal implements Flyer { // implements 키워드 추가이제 Animal에서 fly라는 추상메서드는 삭제 → 추상 메서드가 0개가 되므로 Animal은 일반 클래스가 됨.
대신 fly 메소드는 Flyer 인터페이스로부터 각각의 Bird, Superman 클래스에서 구체화되어 아래와 같이 사용 가능하다.
// AbstractApp.java
for (Animal element : animalArr) { 
    if (element instanceof Bird) {
        ((Bird)element).fly(); // 다운 캐스팅 (Animal은 부모이기 때문에 Bird가 Animal을 상속 받아 fly를 오버라이딩 했다고 해도, Bird의 fly 메소드를 호출하기 위해서는 직접 명시를 통해 캐스팅 필요)
    }
    if (element instanceof Superman) {
        ((Superman)element).fly(); // 다운 캐스팅 -> 이전 포스팅 참고
    }
}
😮 잠시 여기서 또 비슷한 흐름으로 질문 :
Flyer [] animalArr = new Flyer[2];는 가능할까? Flyer는 인터페이스이다.(객체 생성 불가)
→ yes. 인터페이스도 추상 클래스와 같이 직접 객체를 생성할 수는 없지만, 이 경우는 배열 객체를 생성하는 경우이므로 배열 요소의 타입으로 지정할 수 있다.
그렇다면 우리는 위의 코드를 아래와 같이 표현할 수도 있을 것이다.
for (Flyer element : animalArr) { 
     element.fly();
}동물들 중 나는 동물도, 날지 않는 동물도 있기 떄문에 '난다'는 행위에 대해서는 인터페이스를 활용하여
이렇게하면 배열의 요소들을 Bird인지 Superman인지 확인하는 조건을 써주지 않아도 된다.
인터페이스가 필요한 이유는 이뿐만이 아니다❗️
삼성의 tv와 엘지의 tv가 있다고 하자. (이 또 다른 티비 예시는 '은닉화'에 대해서 다룰 다음 포스팅에서 다룰 것이기 때문에 참고하자!)
public class LgTV {
    public void powerOn() {
        System.out.println("LgTV is turning on");
    }
}public class SamsungTV {
    public void turnOn() {
        System.out.println("SamsungTV is turning on");
    }
}
public static void main(String[] args) {
        // SamsungTV samsungTV = new SamsungTV();
        // samsungTV.powerOn();
        LgTV lgTV = new LgTV();
        lgTV.powerOn();
    }삼성 TV 객체를 생성해주고, 전원을 키는 코드를 작성했다가, 브랜드가 삼성에서 엘지로 바뀌게 된다면 생성 타입부터 메소드까지 다 바꿔줘야하는 문제가 있다. (= 결합도가 높다)
이럴 때 인터페이스를 활용해보자.
public static void main(String[] args) {
        TV tv = new SamsungTV();
        tv.powerOn();
    }public static void main(String[] args) {
        TV tv = new LgTV();
        tv.powerOn();
    }브랜드가 바뀌게 돼도 번거롭게 그에 따라 메소드를 수정할 필요 없어졌음을 알 수 있다.

⭐️ 추상 클래스 vs 인터페이스
여기까지 읽어보고 '인터페이스랑 추상 클래스랑 보면 비슷한거같은데, 그래서 뭐가 다른거지?'라는 생각이 아직까지는 들 수도 있다.
- 객체 생성이 불가하고,
- 추상 메소드에 대해 자식 클래스에서 그 구현부를 작성해줘야 하고(오버라이딩),
- 다형성을 지원한다
는 점에서는 공통적이지만, 다음과 같은 차이점이 있다.
| 추상 클래스 | 인터페이스 | |
|---|---|---|
| 목적 | 공통된 기본 구조 + 일부 구현 제공 | 규격(기능 목록)만 강제 | 
| 상속/구현 | extends (단일 상속만 가능) | implements (다중 구현 가능) | 
| 메서드 | - 추상 메서드(구현 없음) + 일반 메서드(구현 있음) 모두 가능 | - 원래는 전부 추상 메서드(구현 없음) - Java 8부터 default 메서드(구현 있음) 가능 | 
| 필드 | 인스턴스 변수, static 변수 모두 가능 | 기본적으로 public static final 상수만 가능 | 
| 생성자 | 가질 수 있음 → 자식 생성자에서 호출됨 | 생성자 불가 | 
| 사용 의도 | - 부모가 기본 동작 일부 구현 제공 - 공통 상태(필드) 공유 - “is-a” 관계 | - 구현 객체가 반드시 가져야 하는 기능 명세 - 서로 다른 클래스들에 동일한 기능 보장 | 
비유하자면
• 추상 클래스 : “같은 집안” — 공통된 성과 특징을 가지고, 기본 유전자(기본 구현)를 물려줌
• 인터페이스 : “자격증” — 서로 다른 집안이라도, 같은 자격증을 땄으면 동일한 기능을 수행할 수 있음
이렇게 이해하면 조금 이해가 쉬울 것 같다.
'백엔드 > JAVA' 카테고리의 다른 글
| [JAVA] 자바 OOP의 특징 1, 2 - 상속과 다형성 (+캐스팅, 오버라이딩, final의 개념) (3) | 2025.08.11 | 
|---|---|
| [JAVA] Java Package 선언의 의미와 역할 (3) | 2025.08.11 |