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("숨쉬기");
}
}
abstractpublic abstract class Digimon { }
이 클래스는 인스턴스를 직접 만들 수 없다.
Digimon d = new Digimon(); // 컴파일 에러
추상 클래스는 미완성 설계도다. 직접 찍어낼 수 없고, 반드시 자식 클래스를 통해서만 사용한다.
abstractpublic abstract void attack();
이 메서드는 구현이 없다. 자식 클래스가 반드시 오버라이딩해야 한다.
public class Agumon extends Digimon {
@Override
public void attack() { // 구현 안 하면 컴파일 에러
System.out.println("불꽃 공격");
}
}
추상 메서드가 하나라도 있으면 그 클래스는 반드시 abstract여야 한다.
반대로 abstract 클래스라도 추상 메서드가 없어도 된다 — 인스턴스 생성만 막고 싶을 때.
추상 클래스는 "공통된 특성을 가진 것들의 불완전한 부모" 다.
인터페이스는 "특정 기능을 할 수 있다는 약속" 이다.
public interface Attackable {
void attack(); // 무조건 public abstract — 생략 가능
}
public interface Flyable {
void fly();
}
인터페이스의 모든 메서드는 기본적으로 public abstract다. 명시적으로 안 써도 컴파일러가 자동으로 붙인다.
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("날기"); }
}
클래스는 단일 상속만 되지만, 인터페이스는 다중 구현이 가능하다.
public interface Attackable {
void attack();
default void log() {
System.out.println("공격 로그 기록");
}
}
default 메서드는 인터페이스에 구현을 제공할 수 있다. 구현 클래스에서 오버라이딩 안 해도 된다. 인터페이스에 메서드를 추가할 때 기존 구현 클래스들을 전부 수정하지 않아도 되도록 나온 기능이다.
| 추상 클래스 | 인터페이스 | |
|---|---|---|
| 키워드 | abstract class | interface |
| 상속/구현 | extends | implements |
| 다중 적용 | 불가 (단일 상속) | 가능 (다중 구현) |
| 필드 | 일반 필드 가질 수 있음 | 상수만 가능 (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(); // 동적 바인딩
}
인터페이스 기반이 더 유연하다. Agumon이 Digimon을 상속받지 않더라도 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()를 실행한다.
// 인터페이스로 약속 정의
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());
DigimonService는 DigimonRepositoryImpl을 전혀 모른다. 외부에서 뭘 넣어주든 DigimonRepository 인터페이스만 맞으면 동작한다. 구현체를 바꾸고 싶으면 DigimonService 코드는 건드리지 않고 주입하는 쪽만 바꾸면 된다.
// DigimonService 코드 수정 없이 구현체만 교체
DigimonService service = new DigimonService(new DigimonRepositoryV2());
확장에는 열려있고, 수정에는 닫혀있다.
이것이 Spring에서 @Autowired가 자동으로 해주는 일이다.
| 개념 | 핵심 |
|---|---|
| 추상 클래스 | 미완성 설계도, 인스턴스 생성 불가, 공통 상태/동작 공유 |
abstract 클래스 | 인스턴스 생성 불가 |
abstract 메서드 | 구현 없음, 자식이 반드시 오버라이딩 |
| 인터페이스 | 기능의 약속/계약, 다중 구현 가능 |
| 다운캐스팅 불필요 | 인터페이스에 선언된 메서드는 동적 바인딩으로 구현체 실행 |
| 다운캐스팅 필요 | 구현체 고유 메서드에 접근할 때 |
| DI | 외부에서 구현체를 주입 → 구현체 교체 시 서비스 코드 수정 불필요 |