자바 상속을 한 번 읽고 나면 개념은 이해한 것처럼 느껴집니다. 하지만 직접 클래스를 나누고, 부모 생성자를 호출하고, 메서드를 오버라이딩해보면 헷갈리는 지점이 바로 드러납니다.

이번 글은 상속 개념을 손으로 확인하기 위한 연습문제입니다. 정답을 바로 보기보다 먼저 클래스 구조를 직접 그려보고 코드를 작성해보는 편이 좋습니다.

아직 상속 개념이 익숙하지 않다면 자바 상속 정리 - extends와 오버라이딩 이해하기를 먼저 읽고 다시 돌아와도 좋습니다. 문제를 풀고 나서는 자바 상속 연습문제 풀이 - 음료 클래스 코드로 확인하기에서 코드와 해설을 확인할 수 있습니다.


연습할 내용

이번 문제에서는 아래 내용을 확인합니다.

  • 부모 클래스와 자식 클래스의 관계
  • 공통 기능을 부모 클래스에 두는 방식
  • 자식 클래스에서 필드와 기능을 추가하는 방법
  • 메서드 오버라이딩
  • private, protected 접근 제어자
  • super를 사용한 부모 생성자 호출
  • final 메서드로 오버라이딩을 막는 방법

문제는 총 3개입니다. 난이도는 낮은 단계부터 차례대로 올라가도록 구성했습니다.


문제 1 - 기본 상속과 공통 기능 추가

카페에서 판매하는 음료를 클래스로 표현해보세요. 공통 정보는 부모 클래스에 두고, 음료별로 필요한 정보는 자식 클래스에 추가하는 문제입니다.

요구사항

부모 클래스 Beverage를 만듭니다. Beverage는 아래 필드를 가집니다.

  • name: 음료 이름
  • price: 가격

두 필드는 생성자를 통해 초기화합니다.

Beverage에는 info() 메서드를 만듭니다. 이 메서드는 아래 형식으로 음료 정보를 출력합니다.

음료이름 - 가격원

그다음 CoffeeTea 클래스를 만듭니다. 두 클래스는 모두 Beverage를 상속받습니다.

Coffee에는 아래 필드를 추가합니다.

  • shot: 샷 개수

Tea에는 아래 필드를 추가합니다.

  • teaType: 차 종류

마지막으로 모든 음료가 사용할 수 있는 addIce() 기능을 추가합니다. 이 메서드는 아래 문장을 출력하면 됩니다.

얼음 추가!

이 기능을 어느 클래스에 두어야 CoffeeTea가 함께 사용할 수 있을지 생각해보세요.

실행 조건

main 메서드에서 아래 객체를 만듭니다.

Coffee coffee = new Coffee("아메리카노", 4500, 2);
Tea tea = new Tea("자스민차", 5000, "자스민");

각 객체에서 info()addIce()를 호출합니다.

예상 출력

아메리카노 - 4500원
얼음 추가!
자스민차 - 5000원
얼음 추가!

생각할 부분

이 문제의 핵심은 공통 기능을 어디에 둘 것인가입니다. CoffeeTea가 모두 얼음을 추가할 수 있다면, addIce()는 자식 클래스마다 따로 만들기보다 부모 클래스에 두는 편이 자연스럽습니다.

상속을 사용할 때는 자식 클래스부터 만들기보다, 먼저 공통 부분을 찾아 부모 클래스로 올릴 수 있는지 살펴보면 좋습니다.


문제 2 - 오버라이딩과 접근 제어자

이번에는 문제 1의 구조를 조금 더 다듬어봅니다. 음료마다 출력해야 하는 정보가 다르기 때문에 info()를 각 자식 클래스에서 다시 정의합니다.

또한 price를 외부에서 직접 만지지 못하도록 접근 제어자도 함께 조정합니다.

요구사항

Beverageprice 필드는 private으로 선언합니다. 자식 클래스에서 가격이 필요할 때 직접 필드에 접근하지 않고, 아래 메서드로 값을 가져오게 합니다.

protected int getPrice()

Coffeeinfo()를 오버라이딩합니다. 출력 형식은 아래와 같습니다.

아메리카노 - 4500원 (샷 2개)

Teainfo()를 오버라이딩합니다. 출력 형식은 아래와 같습니다.

자스민차 - 5000원 (자스민)

Beverage에는 할인 금액을 계산하는 discount() 메서드를 추가합니다. 기본 동작은 가격을 그대로 반환하는 것입니다.

protected int discount()

Coffeediscount()를 오버라이딩합니다. 커피는 가격에서 10%를 할인한 값을 반환하도록 만듭니다.

main 메서드에서는 CoffeeTea 객체를 각각 만든 뒤, 아래 내용을 확인합니다.

  • 각 객체의 info() 출력
  • 각 객체의 discount() 결과

작성할 때 지킬 점

오버라이딩하는 메서드 위에는 @Override를 붙입니다. 그리고 Coffee에서 price 필드에 직접 접근하지 않습니다.

priceprivate이면 자식 클래스에서도 직접 사용할 수 없습니다. 그래서 부모 클래스가 제공하는 getPrice()를 통해 값을 가져와야 합니다.

생각할 부분

이 문제의 핵심은 상속을 해도 private 필드는 직접 물려받아 쓰는 느낌으로 접근할 수 없다는 점입니다.

자식 클래스가 부모의 내부 값을 모두 마음대로 만질 수 있다면 캡슐화가 쉽게 깨집니다. 그래서 필요한 값만 protected 메서드로 열어두는 방식이 더 안전합니다.

또 하나의 핵심은 오버라이딩입니다. 같은 info()라는 이름을 사용하더라도, 실제 객체가 Coffee인지 Tea인지에 따라 다른 출력이 나와야 합니다.


문제 3 - super 생성자 체인과 final

이번에는 상속 단계를 3단계로 늘려봅니다. 아래 구조를 만듭니다.

Beverage
  └─ HotBeverage
       └─ Latte

이 문제는 생성자 호출 순서를 확인하는 것이 핵심입니다.

요구사항

Beverage 클래스는 아래 생성자를 가집니다.

Beverage(String name, int price)

이 생성자는 공통 필드를 초기화하고, 아래 문장을 출력합니다.

Beverage 생성

HotBeverageBeverage를 상속받습니다. 추가 필드는 아래와 같습니다.

  • temperature: 음료 온도

생성자는 아래 형태로 만듭니다.

HotBeverage(String name, int price, int temperature)

생성자 첫 줄에서 부모 생성자를 호출합니다. 그다음 아래 문장을 출력합니다.

HotBeverage 생성, 온도: 65도

LatteHotBeverage를 상속받습니다. 추가 필드는 아래와 같습니다.

  • milkType: 우유 종류

생성자는 아래 형태로 만듭니다.

Latte(String name, int price, int temperature, String milkType)

생성자 첫 줄에서 부모 생성자를 호출합니다. 그다음 아래 문장을 출력합니다.

Latte 생성, 우유: 오트밀크

마지막으로 Beverageinfo() 메서드는 final로 선언합니다. 이렇게 하면 자식 클래스에서 info()를 오버라이딩할 수 없습니다.

실행 조건

main 메서드에서 아래 객체를 생성합니다.

Latte latte = new Latte("바닐라라떼", 5500, 65, "오트밀크");

객체를 만들 때 출력 순서를 확인합니다.

예상 출력

Beverage 생성
HotBeverage 생성, 온도: 65도
Latte 생성, 우유: 오트밀크

생각할 부분

Latte 객체를 만들었는데 Beverage 생성이 가장 먼저 출력됩니다. 이유는 자식 객체 안에 부모 객체 부분도 함께 만들어지기 때문입니다.

자식 생성자가 실행되기 전에 부모 생성자가 먼저 실행됩니다. 그래서 생성자 호출 흐름은 아래 순서로 이어집니다.

Latte 생성 요청
→ HotBeverage 생성자 호출
→ Beverage 생성자 호출
→ Beverage 생성자 실행
→ HotBeverage 생성자 실행
→ Latte 생성자 실행

super(...)는 이 흐름을 명확하게 연결해주는 역할을 합니다. 부모 클래스에 기본 생성자가 없고 매개변수가 있는 생성자만 있다면, 자식 생성자에서 직접 super(...)를 호출해야 합니다.


풀면서 확인할 체크리스트

문제를 다 풀었다면 아래 항목을 확인해보세요.

  • 공통 필드와 공통 기능을 부모 클래스에 두었는가
  • 자식 클래스에서 필요한 필드만 추가했는가
  • 오버라이딩한 메서드에 @Override를 붙였는가
  • private 필드에 자식 클래스가 직접 접근하지 않았는가
  • super(...) 호출이 생성자 첫 줄에 있는가
  • 부모 생성자가 자식 생성자보다 먼저 실행되는 흐름을 설명할 수 있는가
  • final 메서드를 오버라이딩하려고 하면 왜 컴파일 오류가 나는지 이해했는가

정리

상속 문제를 풀 때는 먼저 부모와 자식의 관계가 자연스러운지 확인해야 합니다. 공통 기능은 부모 클래스에 두고, 각 자식만 필요한 기능은 자식 클래스에 둡니다.

오버라이딩은 같은 이름의 메서드를 객체에 맞게 다르게 동작시키는 방법입니다. 반대로 final 메서드는 자식 클래스가 다시 정의하지 못하게 막을 때 사용합니다.

생성자에서는 부모가 먼저 준비되고 자식이 나중에 준비됩니다. 그래서 상속 구조가 깊어질수록 super(...) 호출과 생성자 실행 순서를 정확히 이해하는 것이 중요합니다.