1. 학습 주제
객체 배열을 활용해 회원 데이터를 관리하는 구조를 보고, 상속으로 기존 클래스를 재사용하는 방법을 익혔고, 그 위에서 다형성과 동적 바인딩이 어떻게 동작하는지 확인했다.
또한 추상클래스와 인터페이스가 왜 필요한지, 둘이 비슷해 보이지만 어떤 목적 차이가 있는지도 함께 정리했다.
- 객체 배열 기반 회원 관리 구조 이해하기
- 상속으로 공통 기능 재사용하기
- super 키워드와 생성자 호출 방식 익히기
- 메서드 오버라이딩 규칙 이해하기
- 다형성과 업캐스팅, 다운캐스팅 정리하기
- 동적 바인딩이 실제로 어떤 의미인지 이해하기
- 추상클래스와 인터페이스의 역할 차이 정리하기
2. 객체 배열 활용
회원 관리 예제에서는 Member 객체를 여러 개 저장하기 위해 객체 배열을 사용했다.
private final static Member[] members;
- Member 하나가 회원 1명의 데이터를 담는다.
- Member[] 배열은 여러 회원 객체를 저장하는 저장소 역할을 한다.
- 객체 배열을 이용하면 회원 등록, 전체 조회, 아이디 검색 같은 기능을 구조적으로 만들 수 있다.
구조도 단순하게 나뉘어 있었다.
- Member: 회원 데이터 하나를 표현하는 객체
- MemberRepository: 회원들을 저장하는 저장소
- MemberService: 회원 등록, 조회, 검색 같은 기능 처리
- Application: 사용자 입력과 메뉴 흐름 담당
이 구조를 통해 데이터와 기능, 실행 흐름을 나누는 방식도 함께 익힐 수 있었다.
3. 상속
상속은 기존 클래스의 필드와 메서드를 물려받아 새로운 클래스를 만드는 것이다.
public class FireCar extends Car
public class RacingCar extends Car
- Car는 부모 클래스
- FireCar, RacingCar는 자식 클래스
- 자식은 부모의 기능을 재사용하면서 자신만의 기능을 추가할 수 있다
예를 들어 FireCar는 자동차의 기본 기능을 그대로 물려받으면서 물 뿌리기 기능을 추가했다.
public void sprayWater() {
System.out.println("불난 곳을 발견했습니다, 물을 뿌립니다!");
}
즉 상속의 핵심은 다음과 같다.
- 공통 기능은 부모에 둔다
- 자식은 필요한 기능만 추가한다
- 중복 코드를 줄인다
4. super 키워드
super는 부모 클래스와 관련된 기능에 접근할 때 사용한다.
4.1 부모 생성자 호출
super(code, brand, name, price, manufacturingDate);
- 자식 생성자에서 부모 생성자를 호출할 수 있다
- 부모 필드를 먼저 초기화한 뒤 자식 필드를 초기화한다
- 생성자의 첫 줄에 와야 한다
4.2 부모 메서드 호출
super.run();
- 자식이 메서드를 재정의했더라도, 필요하면 부모 메서드도 함께 실행할 수 있다
예를 들어 RacingCar는 부모의 run()을 먼저 호출한 다음 추가 메시지를 출력한다.
@Override
public void run() {
super.run();
System.out.println("레이싱카가 전속력으로 질주합니다!");
}
5. 오버라이딩
오버라이딩은 부모에게 물려받은 메서드를 자식 클래스에 맞게 다시 정의하는 것이다.
@Override
public void soundHorn() {
if(isRunning()) {
System.out.println("빠아아앙아아아아앙");
} else {
System.out.println("소방차가 앞으로 갈 수 업습니다. 비키세요.");
}
}
FireCar는 부모 Car의 soundHorn()을 자기 역할에 맞게 바꿔서 사용했다.
오버라이딩이 성립하려면:
- 메서드 이름이 같아야 한다
- 반환 타입이 같아야 한다
- 매개변수 목록이 같아야 한다
- 접근 제한자는 부모와 같거나 더 넓어야 한다
오버라이딩이 불가능한 경우도 있다.
- private 메서드
- final 메서드
- 메서드 이름이나 매개변수가 달라진 경우
6. 다형성
다형성은 하나의 부모 타입으로 여러 자식 객체를 다룰 수 있는 성질이다.
Animal a1 = new Cat();
Animal a2 = new Tiger();
이 코드가 다형성의 핵심이다.
- 변수 타입은 Animal
- 실제 객체는 Cat, Tiger
- 같은 부모 타입 하나로 서로 다른 자식 객체를 담을 수 있다
이게 왜 중요하냐면, 코드를 훨씬 유연하게 만들 수 있기 때문이다.
다형성이 없으면:
public void feed(Cat cat) { ... }
public void feed(Tiger tiger) { ... }
동물 종류가 늘어날수록 메서드도 계속 늘어난다.
다형성을 사용하면:
public void feed(Animal animal) {
animal.eat();
}
- Cat도 받을 수 있고
- Tiger도 받을 수 있고
- 나중에 Dog extends Animal이 추가돼도 그대로 받을 수 있다
즉 다형성은 “공통 부모 타입 하나로 여러 자식 객체를 처리하는 능력”이다.
7. 업캐스팅과 다운캐스팅
7.1 업캐스팅
Animal a1 = new Cat();
- 자식 객체를 부모 타입 변수에 담는 것
- 자동으로 된다
- 안전하다
- 다형성의 출발점이다
하지만 업캐스팅을 하면 부모가 가진 것만 보인다.
a1.cry(); // 가능
// a1.jump(); // 불가능
a1 안에 실제로는 Cat이 들어 있어도, 변수 타입이 Animal이기 때문에 Animal이 아는 메서드만 호출할 수 있다.
7.2 다운캐스팅
((Cat)a1).jump();
- 부모 타입 변수를 다시 자식 타입으로 바꾸는 것
- 자식만 가진 기능을 쓰고 싶을 때 필요하다
- 잘못 캐스팅하면 런타임 에러가 난다
예:
// ((Cat)a2).jump();
a2에는 Tiger가 들어 있는데 Cat으로 바꾸려고 하면 오류가 난다.
그래서 보통 instanceof로 확인하고 사용한다.
if(a1 instanceof Cat) {
((Cat)a1).jump();
}
8. 동적 바인딩
이 부분이 가장 중요하다.
동적 바인딩은 “메서드를 호출할 때, 어떤 메서드를 실행할지 실행 시점에 결정하는 것”이다.
예제를 다시 보면:
Animal a1 = new Cat();
Animal a2 = new Tiger();
a1.cry();
a2.cry();
겉으로 보면 둘 다 Animal 타입 변수이다.
그러면 얼핏 보면 Animal의 cry()가 실행될 것 같아 보인다.
하지만 실제 실행은 이렇게 된다.
- a1.cry() -> Cat의 cry()
- a2.cry() -> Tiger의 cry()
왜 그럴까?
변수의 타입은 Animal이지만, 실제 객체는 각각 Cat, Tiger이기 때문이다.
자바는 메서드를 실행할 때 “변수 타입”이 아니라 “실제로 들어 있는 객체 타입”을 보고 오버라이딩된 메서드를 실행한다.
이게 바로 동적 바인딩이다.
8.1 정적으로 보면
컴파일러는 일단 Animal 타입으로 본다.
- Animal에 cry()가 있으니까 호출 가능하다고 판단한다
- 하지만 jump()는 Animal에 없으니 호출 불가라고 판단한다
8.2 실행할 때는
실행 시점에는 실제 객체를 본다.
- 실제 객체가 Cat이면 Cat.cry()
- 실제 객체가 Tiger이면 Tiger.cry()
즉 정리하면:
- “호출 가능 여부”는 참조 변수 타입을 본다
- “실제로 어떤 메서드를 실행할지”는 실제 객체 타입을 본다
이걸 꼭 구분해야 한다.
8.3 한 문장으로 정리
다형성은 “부모 타입으로 자식 객체를 다루는 것”이고,
동적 바인딩은 “그 상태에서 실제 자식의 메서드가 실행되는 원리”이다.
즉
- 다형성 = 형태를 부모로 통일
- 동적 바인딩 = 행동은 실제 자식답게 실행
9. 추상클래스
추상클래스는 “공통점은 이미 어느 정도 정리되어 있지만, 일부 기능은 자식이 반드시 완성해야 하는 클래스”이다.
public abstract class Player {
public abstract void attack();
public void levelUp(){
System.out.println("레벨업!");
}
}
특징
- new로 직접 객체 생성 불가
- 일반 메서드 가질 수 있음
- 필드 가질 수 있음
- 추상 메서드 가질 수 있음
- 상속받은 자식이 추상 메서드를 반드시 구현해야 함
예제에서는
- Player는 공통 부모
- Warrior, Wizard는 자식
players[0] = new Warrior();
players[1] = new Wizard();
attack()은 캐릭터마다 다르기 때문에 추상 메서드로 두고,
levelUp()은 공통 기능이므로 부모에서 구현해 두었다.
즉 추상클래스는 이런 상황에 잘 맞는다.
- 자식들이 공통 속성/공통 기능을 많이 공유함
- 하지만 일부 동작은 자식마다 반드시 다르게 구현해야 함
- “공통 기반 + 구현 강제”가 필요함
10. 인터페이스
인터페이스는 “이 기능을 할 수 있어야 한다”는 규격, 약속, 계약서에 가깝다.
public interface IConnectable {
void connect();
void disconnect();
}
Keyboard, Mouse는 모두 이 인터페이스를 구현한다.
public class Keyboard implements IConnectable
public class Mouse implements IConnectable
그리고 Computer는 구체 클래스가 아니라 인터페이스 타입으로 장치를 받는다.
public void connectDevice(IConnectable device){
device.connect();
}
이 구조의 핵심은
- 컴퓨터는 “마우스인지 키보드인지”에 관심이 없다
- “연결 가능한 장치인지”만 중요하다
- 규격만 맞으면 어떤 장치든 받을 수 있다
즉 인터페이스는 “너가 누구냐”보다 “무슨 기능을 할 수 있냐”를 표현한다.
11. 추상클래스와 인터페이스 차이
이 부분이 가장 헷갈린다.
코드를 보면 둘 다 “공통 타입처럼 쓰이고, 메서드 구현도 강제하는 것 같아 보여서” 비슷하게 느껴진다.
그런데 목적이 다르다.
11.1 추상클래스는 “공통 부모”
- 전사와 마법사는 둘 다 Player다
- 본질적으로 같은 계열이다
- 공통 속성과 공통 동작을 많이 공유한다
즉 추상클래스는 “같은 가족”을 묶는 느낌이다.
11.2 인터페이스는 “기능 규격”
- 키보드와 마우스는 서로 같은 계열이라고 보기 어렵다
- 하지만 둘 다 “컴퓨터에 연결 가능”하다는 공통 기능은 있다
- 그래서 IConnectable이라는 규격으로 묶는다
즉 인터페이스는 “같은 가족”이 아니라 “같은 능력”을 묶는 느낌이다.
12. 둘의 차이를 감각적으로 정리
12.1 추상클래스가 어울리는 경우
Player, Animal, Vehicle 같은 것
이유
- 공통 필드가 있다
- 공통 메서드가 많다
- 자식들이 본질적으로 같은 종류다
- 부모가 어느 정도 구현을 제공해 주고 싶다
예
- Player는 모든 캐릭터가 공통으로 레벨업할 수 있다
- 하지만 공격 방식은 다르다
- 그래서 attack()만 추상으로 남긴다
12.2 인터페이스가 어울리는 경우
Runnable, Connectable, Flyable, Attackable 같은 것
이유
- “무슨 기능을 할 수 있는가”를 정의하고 싶다
- 서로 전혀 다른 클래스들이 같은 기능만 공유할 수 있다
- 다중 구현이 필요할 수 있다
예
- 마우스도 연결 가능
- 키보드도 연결 가능
- 나중에 프린터도 연결 가능
- 셋은 같은 부모 클래스일 필요는 없지만, 같은 규격은 따를 수 있다
13. 왜 둘 다 쓸 수 있어 보이는가
사용자가 느낀 혼란이 아주 자연스럽다.
실제로 간단한 예제에서는 둘 다 비슷하게 보일 수 있다.
예를 들어 connect(), disconnect() 같은 메서드는
추상클래스로도 만들 수 있고 인터페이스로도 만들 수 있다.
그럼 어떻게 구분하냐면
13.1 이런 질문을 해보면 된다
- 이 클래스들은 본질적으로 같은 종류인가?
- 같으면 추상클래스 쪽이 더 자연스럽다
- 공통 필드/공통 구현을 부모가 많이 제공해야 하는가?
- 그렇다면 추상클래스가 더 적합하다
- 서로 다른 클래스들이 같은 기능만 약속하면 되는가?
- 그렇다면 인터페이스가 더 적합하다
- 여러 기능 규격을 동시에 붙여야 하는가?
- 그러면 인터페이스가 더 적합하다
14. 한 번에 비교
추상클래스
- 키워드: abstract class
- 상속: extends
- 목적: 공통 기반 클래스 제공
- 필드: 가능
- 일반 메서드: 가능
- 추상 메서드: 가능
- 생성자: 가능
- 다중 상속: 불가
인터페이스
- 키워드: interface
- 구현: implements
- 목적: 기능 규격 정의
- 필드: 상수만 가능
- 일반 메서드: 원칙적으로 구현 없음, 필요하면 default 가능
- 추상 메서드: 가능
- 생성자: 없음
- 다중 구현: 가능
15. 흐름 정리
15.1 객체 배열과 상속
- 객체 배열로 여러 객체를 한 번에 관리할 수 있다
- 상속을 사용하면 부모 기능을 재사용할 수 있다
- 자식은 필요한 기능만 추가하거나 재정의하면 된다
15.2 다형성과 동적 바인딩
- 부모 타입 하나로 여러 자식 객체를 다룰 수 있다
- 메서드 호출 가능 여부는 참조 타입이 결정한다
- 실제 실행 메서드는 실제 객체 타입이 결정한다
- 이것이 동적 바인딩이다
15.3 추상클래스와 인터페이스
- 추상클래스는 공통 부모 설계도
- 인터페이스는 기능 규격서
- “같은 종류”를 묶을지, “같은 기능”을 묶을지에 따라 선택이 달라진다
16. 핵심 정리
- 객체 배열은 여러 객체를 저장하고 반복 처리하는 데 유용하다.
- 상속은 부모의 기능을 재사용하면서 자식 기능을 확장하는 구조이다.
- 오버라이딩은 부모 메서드를 자식에 맞게 다시 정의하는 것이다.
- 다형성은 부모 타입 하나로 여러 자식 객체를 다룰 수 있는 성질이다.
- 업캐스팅은 자동으로 가능하지만, 부모 타입으로 보이기 때문에 자식 고유 기능은 바로 못 쓴다.
- 다운캐스팅은 자식 고유 기능을 쓰기 위해 필요하지만 안전 확인이 중요하다.
- 동적 바인딩은 실행 시점에 실제 객체의 오버라이딩된 메서드를 호출하는 원리이다.
- 추상클래스는 공통 필드와 공통 메서드를 가진 “미완성 부모 클래스”이다.
- 인터페이스는 클래스들이 따라야 하는 “기능 규격”이다.
- 추상클래스는 “같은 계열”, 인터페이스는 “같은 기능”을 묶는다고 이해하면 훨씬 구분하기 쉽다.
'TIL > [TIL]' 카테고리의 다른 글
| [TIL]Java Collection Framework, List / Set / Map 핵심 정리 (0) | 2026.05.11 |
|---|---|
| [TIL]Java API, Wrapper, 오토박싱/오토언박싱, 제네릭 (1) | 2026.05.08 |
| [TIL]static, final, singleton, 변수의 종류, 초기화 블록, 객체 배열 (0) | 2026.05.06 |
| [TIL]추상화, DTO, 생성자, 메서드 매개변수와 오버로딩 (0) | 2026.05.04 |
| [TIL]2차원 배열, 배열 복사와 정렬, 클래스와 객체, 캡슐화 (0) | 2026.05.01 |