TIL/[TIL]

[TIL]객체 배열 활용, 상속, 오버라이딩, 다형성, 동적 바인딩, 추상클래스와 인터페이스

namerong 2026. 5. 7. 15:43

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 이런 질문을 해보면 된다

  1. 이 클래스들은 본질적으로 같은 종류인가?
  • 같으면 추상클래스 쪽이 더 자연스럽다
  1. 공통 필드/공통 구현을 부모가 많이 제공해야 하는가?
  • 그렇다면 추상클래스가 더 적합하다
  1. 서로 다른 클래스들이 같은 기능만 약속하면 되는가?
  • 그렇다면 인터페이스가 더 적합하다
  1. 여러 기능 규격을 동시에 붙여야 하는가?
  • 그러면 인터페이스가 더 적합하다

14. 한 번에 비교

추상클래스

  • 키워드: abstract class
  • 상속: extends
  • 목적: 공통 기반 클래스 제공
  • 필드: 가능
  • 일반 메서드: 가능
  • 추상 메서드: 가능
  • 생성자: 가능
  • 다중 상속: 불가

인터페이스

  • 키워드: interface
  • 구현: implements
  • 목적: 기능 규격 정의
  • 필드: 상수만 가능
  • 일반 메서드: 원칙적으로 구현 없음, 필요하면 default 가능
  • 추상 메서드: 가능
  • 생성자: 없음
  • 다중 구현: 가능

15. 흐름 정리

15.1 객체 배열과 상속

  • 객체 배열로 여러 객체를 한 번에 관리할 수 있다
  • 상속을 사용하면 부모 기능을 재사용할 수 있다
  • 자식은 필요한 기능만 추가하거나 재정의하면 된다

15.2 다형성과 동적 바인딩

  • 부모 타입 하나로 여러 자식 객체를 다룰 수 있다
  • 메서드 호출 가능 여부는 참조 타입이 결정한다
  • 실제 실행 메서드는 실제 객체 타입이 결정한다
  • 이것이 동적 바인딩이다

15.3 추상클래스와 인터페이스

  • 추상클래스는 공통 부모 설계도
  • 인터페이스는 기능 규격서
  • “같은 종류”를 묶을지, “같은 기능”을 묶을지에 따라 선택이 달라진다

16. 핵심 정리

  1. 객체 배열은 여러 객체를 저장하고 반복 처리하는 데 유용하다.
  2. 상속은 부모의 기능을 재사용하면서 자식 기능을 확장하는 구조이다.
  3. 오버라이딩은 부모 메서드를 자식에 맞게 다시 정의하는 것이다.
  4. 다형성은 부모 타입 하나로 여러 자식 객체를 다룰 수 있는 성질이다.
  5. 업캐스팅은 자동으로 가능하지만, 부모 타입으로 보이기 때문에 자식 고유 기능은 바로 못 쓴다.
  6. 다운캐스팅은 자식 고유 기능을 쓰기 위해 필요하지만 안전 확인이 중요하다.
  7. 동적 바인딩은 실행 시점에 실제 객체의 오버라이딩된 메서드를 호출하는 원리이다.
  8. 추상클래스는 공통 필드와 공통 메서드를 가진 “미완성 부모 클래스”이다.
  9. 인터페이스는 클래스들이 따라야 하는 “기능 규격”이다.
  10. 추상클래스는 “같은 계열”, 인터페이스는 “같은 기능”을 묶는다고 이해하면 훨씬 구분하기 쉽다.