[TIL] 추상 클래스 vs 인터페이스

revo·2026년 2월 8일

자바

목록 보기
5/30
post-thumbnail

추상 클래스 (Abstract Class)

왜 필요한가

public class Digimon {
    public void attack() {
        System.out.println("???"); // 뭘 출력해야 하지?
    }
}

Digimon은 그 자체로 실체가 없다. 아구몬은 불꽃 공격, 가루루몬은 얼음 공격인데 Digimon의 공격은 정의할 수가 없다.

attack()은 반드시 있어야 하고, 자식 클래스가 반드시 구현하도록 강제하고 싶을 때 추상 클래스를 쓴다.

public abstract class Digimon {
    public abstract void attack(); // 구현 없음 — 자식이 반드시 구현해야 함

    public void breathe() {        // 일반 메서드도 가질 수 있음
        System.out.println("숨쉬기");
    }
}

abstract가 붙는 곳마다의 의미

클래스에 abstract

public abstract class Digimon { }

이 클래스는 인스턴스를 직접 만들 수 없다.

Digimon d = new Digimon(); // 컴파일 에러

추상 클래스는 미완성 설계도다. 직접 찍어낼 수 없고, 반드시 자식 클래스를 통해서만 사용한다.

메서드에 abstract

public abstract void attack();

이 메서드는 구현이 없다. 자식 클래스가 반드시 오버라이딩해야 한다.

public class Agumon extends Digimon {
    @Override
    public void attack() {         // 구현 안 하면 컴파일 에러
        System.out.println("불꽃 공격");
    }
}

추상 메서드가 하나라도 있으면 그 클래스는 반드시 abstract여야 한다.
반대로 abstract 클래스라도 추상 메서드가 없어도 된다 — 인스턴스 생성만 막고 싶을 때.


인터페이스 (Interface)

추상 클래스와 뭐가 다른가

추상 클래스는 "공통된 특성을 가진 것들의 불완전한 부모" 다.
인터페이스는 "특정 기능을 할 수 있다는 약속" 이다.

public interface Attackable {
    void attack(); // 무조건 public abstract — 생략 가능
}

public interface Flyable {
    void fly();
}

인터페이스의 모든 메서드는 기본적으로 public abstract다. 명시적으로 안 써도 컴파일러가 자동으로 붙인다.

클래스는 인터페이스를 구현(implements)한다

public class Agumon extends Digimon implements Attackable {
    @Override
    public void attack() {
        System.out.println("불꽃 공격");
    }
}

public class Angelmon extends Digimon implements Attackable, Flyable {
    @Override
    public void attack() { System.out.println("천사 공격"); }

    @Override
    public void fly() { System.out.println("날기"); }
}

클래스는 단일 상속만 되지만, 인터페이스는 다중 구현이 가능하다.

인터페이스의 default 메서드 (Java 8+)

public interface Attackable {
    void attack();

    default void log() {
        System.out.println("공격 로그 기록");
    }
}

default 메서드는 인터페이스에 구현을 제공할 수 있다. 구현 클래스에서 오버라이딩 안 해도 된다. 인터페이스에 메서드를 추가할 때 기존 구현 클래스들을 전부 수정하지 않아도 되도록 나온 기능이다.


추상 클래스 vs 인터페이스

추상 클래스인터페이스
키워드abstract classinterface
상속/구현extendsimplements
다중 적용불가 (단일 상속)가능 (다중 구현)
필드일반 필드 가질 수 있음상수만 가능 (public static final)
생성자있음없음
메서드일반 + 추상 메서드추상 + default + static
용도공통 상태/동작을 공유하는 계층기능의 약속/계약

언제 뭘 쓰나

추상 클래스 → "~이다(is-a)" 관계. 공통된 상태(필드)나 동작을 공유해야 할 때.

public abstract class Digimon {
    protected int level;  // 공통 상태
    protected int hp;

    public void levelUp() { level++; } // 공통 동작
    public abstract void attack();     // 각자 구현
}

인터페이스 → "~할 수 있다(can-do)" 관계. 특정 기능을 보장해야 할 때.

public interface Flyable {
    void fly();
}

다형성과의 연결관계

추상 클래스와 인터페이스는 다형성의 설계 수단이다.
동작 원리는 동일하다 — 업캐스팅 + 오버라이딩 + 동적 바인딩.

// 추상 클래스 기반 다형성
Digimon[] team = { new Agumon(), new Garurumon() };
for (Digimon d : team) {
    d.attack(); // 동적 바인딩
}

// 인터페이스 기반 다형성
Attackable[] attackers = { new Agumon(), new Angelmon() };
for (Attackable a : attackers) {
    a.attack(); // 동적 바인딩
}

인터페이스 기반이 더 유연하다. AgumonDigimon을 상속받지 않더라도 Attackable만 구현하면 같이 다룰 수 있다.


핵심 — 다운캐스팅이 필요한 시점

인터페이스 타입으로 업캐스팅된 상태에서:

  • 인터페이스에 선언된 메서드 → 다운캐스팅 없이 동적 바인딩으로 구현체 메서드 실행
  • 구현체 고유 메서드 → 다운캐스팅 필요
public interface DigimonRepository {
    Digimon findById(Long id); // 인터페이스에 선언
}

public class DigimonRepositoryImpl implements DigimonRepository {
    @Override
    public Digimon findById(Long id) { ... } // 오버라이딩

    public void customMethod() { ... }       // 구현체 고유 메서드
}
DigimonRepository repository = new DigimonRepositoryImpl(); // 업캐스팅

repository.findById(1L);    // 가능 — 인터페이스에 선언된 메서드, 동적 바인딩으로 구현체 실행
repository.customMethod();  // 컴파일 에러 — 구현체 고유 메서드, 다운캐스팅 필요

((DigimonRepositoryImpl) repository).customMethod(); // 다운캐스팅 후 가능

findById()가 다운캐스팅 없이 구현체 코드까지 실행되는 이유는 구현체가 인터페이스 메서드를 오버라이딩했기 때문이다. 컴파일 타임엔 인터페이스의 findById()로 접근 허용하고, 런타임엔 JVM이 실제 객체를 보고 오버라이딩된 구현체의 findById()를 실행한다.


DI(의존성 주입)와의 연결

// 인터페이스로 약속 정의
public interface DigimonRepository {
    Digimon findById(Long id);
}

// 구현체
public class DigimonRepositoryImpl implements DigimonRepository {
    @Override
    public Digimon findById(Long id) { /* DB 조회 */ }
}

// 생성자로 외부에서 주입받음
public class DigimonService {
    private DigimonRepository repository;

    public DigimonService(DigimonRepository repository) {
        this.repository = repository;
    }

    public Digimon getDigimon(Long id) {
        return repository.findById(id);
    }
}

// 사용하는 쪽에서 뭘 넣을지 결정
DigimonService service = new DigimonService(new DigimonRepositoryImpl());

DigimonServiceDigimonRepositoryImpl을 전혀 모른다. 외부에서 뭘 넣어주든 DigimonRepository 인터페이스만 맞으면 동작한다. 구현체를 바꾸고 싶으면 DigimonService 코드는 건드리지 않고 주입하는 쪽만 바꾸면 된다.

// DigimonService 코드 수정 없이 구현체만 교체
DigimonService service = new DigimonService(new DigimonRepositoryV2());

확장에는 열려있고, 수정에는 닫혀있다.

이것이 Spring에서 @Autowired가 자동으로 해주는 일이다.


정리

개념핵심
추상 클래스미완성 설계도, 인스턴스 생성 불가, 공통 상태/동작 공유
abstract 클래스인스턴스 생성 불가
abstract 메서드구현 없음, 자식이 반드시 오버라이딩
인터페이스기능의 약속/계약, 다중 구현 가능
다운캐스팅 불필요인터페이스에 선언된 메서드는 동적 바인딩으로 구현체 실행
다운캐스팅 필요구현체 고유 메서드에 접근할 때
DI외부에서 구현체를 주입 → 구현체 교체 시 서비스 코드 수정 불필요

0개의 댓글