Java 상속과 다형성 핵심 정리

상속(Inheritance) 기본 개념

여러 클래스에 공통으로 존재하는 속성이나 기능이 있다면, 이를 별도의 클래스로 분리하여 다른 클래스에서 재사용할 수 있다. 이것이 상속의 핵심 목적이다.

접근 제한자 활용

멤버 변수는 일반적으로 private으로, 메서드는 public으로 선언하는 것이 관례이다. 패키지 내에서만 접근 가능한 default와 상속 관계에서만 접근 가능한 protected는 상황에 따라 선택적으로 사용한다.

상속의 특징

  • 단일 상속: 하나의 클래스는 하나의 직접적인 부모 클래스만 가질 수 있다
  • 계층적 상속: 부모의 부모 클래스가 존재할 수 있어 다층 구조 형성 가능
  • 최상위 클래스: 모든 클래스는 암묵적으로 Object 클래스를 상속받는다
  • 우선순위 규칙: 동일한 이름의 멤버가 있을 때, 현재 클래스의 멤버가 부모 클래스의 멤버보다 우선한다
public class Demo {
    String nickname = "현재클래스";
    
    public void display() {
        String nickname = "지역변수";
        System.out.println(nickname);      // 지역변수
        System.out.println(this.nickname); // 현재 클래스의 멤버 변수
        System.out.println(super.nickname); // 부모 클래스의 멤버 변수
    }
}

메서드 오버라이딩

부모 클래스에서 정의된 메서드를 자식 클래스에서 같은 이름, 같은 매개변수, 같은 반환 타입으로 재정의하는 것을 의미한다. 이 때 @Override 어노테이션을 명시하면 컴파일러가 올바른 오버라이딩인지 검증해준다.

대표적인 활용 사례는 Object 클래스의 toString() 메서드를 재정의하여 객체의 실제 데이터를 출력하도록 만드는 것이다.

생성자의 상속 규칙

자식 클래스의 생성자가 호출되면, 항상 부모 클래스의 생성자가 먼저 실행된 후 자식 클래스의 생성자가 실행된다. 이는 super()를 통해 명시적으로 제어할 수 있다.

public class Offspring extends Ancestor {
    public Offspring() {
        // super(); // 생략 가능, 부모의 기본 생성자 호출
        // super(값); // 부모의 특정 생성자 명시 호출
        System.out.println("자식 생성자 실행");
    }
}

public class Ancestor {
    public Ancestor() {
        System.out.println("부모 생성자 실행");
    }
}

this를 이용한 생성자 위임

같은 클래스 내의 다른 생성자를 호출하여 코드 중복을 줄일 수 있다.

public class Member {
    private String username;
    private int years;
    private char gender;
    private String team;
    
    public Member(String username, int years, char gender) {
        // this()와 super()는 생성자의 첫 줄에만 작성 가능하며 동시 사용 불가
        this(username, years, gender, "기본팀");
    }
    
    public Member(String username, int years, char gender, String team) {
        this.username = username;
        this.years = years;
        this.gender = gender;
        this.team = team;
    }
}

다형성(Polymorphism)

다형성의 본질

부모 클래스 타입의 참조 변수가 자식 클래스의 인스턴스를 가리킬 수 있는 특성이다. 메서드 호출 시 실제 객체의 타입에 따라 해당 메서드가 실행된다.

class Vehicle {
    public void move() {
        System.out.println("이동합니다");
    }
}

class Sedan extends Vehicle {
    @Override
    public void move() {
        System.out.println("승용차가 부드럽게 달립니다");
    }
}

class Truck extends Vehicle {
    @Override
    public void move() {
        System.out.println("트럭이 힘차게 달립니다");
    }
}

// 실행
Vehicle v1 = new Sedan();
Vehicle v2 = new Truck();
v1.move(); // 승용차가 부드럽게 달립니다
v2.move(); // 트럭이 힘차게 달립니다

핵심 원리는 "컴파일 시점에는 참조 타입을, 실행 시점에는 실제 객체 타입을 따른다"는 것이다.

다형성의 장점

  1. 결합도 감소: 구현체를 쉽게 교체할 수 있어 유지보수가 용이하다
  2. 확장성 향상: 부모 타입 매개변수로 다양한 자식 객체를 받을 수 있다
public static void startRace(Vehicle competitor) {
    System.out.println("경기 시작!");
    competitor.move();
}

// Sedan, Truck 등 어떤 객체든 전달 가능
startRace(new Sedan());
startRace(new Truck());

다형성의 한계와 타입 변환

부모 타입 참조로는 자식 클래스만의 고유 기능을 직접 사용할 수 없다. 이를 해결하려면 강제 타입 변환(다운캐스팅)이 필요하다.

Vehicle v = new Sedan();
// v.openSunroof(); // 컴파일 에러!

// 안전한 다운캐스팅
if (v instanceof Sedan) {
    Sedan s = (Sedan) v;
    s.openSunroof();
}

실전 예제: 주유소 멤버십 시스템

다형성을 활용한 결제 시스템을 구현해본다.

공통 카드 클래스

import lombok.*;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class MembershipCard {
    private String plateNumber;
    private String ownerName;
    private String contact;
    private double balance;
    
    public void charge(double amount) {
        this.balance += amount;
    }
    
    public void pay(double amount) {
        this.balance -= amount;
    }
}

VIP 카드 (20% 할인)

public class VipCard extends MembershipCard {
    
    public VipCard(String plateNumber, String ownerName, 
                   String contact, double balance) {
        super(plateNumber, ownerName, contact, balance);
    }
    
    @Override
    public void pay(double amount) {
        double discounted = amount * 0.8;
        System.out.println("VIP 할인 적용: " + discounted + "원");
        
        if (getBalance() < discounted) {
            System.out.println("잔액이 부족합니다");
            return;
        }
        
        setBalance(getBalance() - discounted);
        
        if (discounted > 200000) {
            issueWashCoupon();
        }
    }
    
    private void issueWashCoupon() {
        System.out.println("세차 폰이 발급되었습니다!");
    }
}

일반 카드 (10% 할인)

public class StandardCard extends MembershipCard {
    
    public StandardCard(String plateNumber, String ownerName, 
                        String contact, double balance) {
        super(plateNumber, ownerName, contact, balance);
    }
    
    @Override
    public void pay(double amount) {
        double discounted = amount * 0.9;
        System.out.println("회원 할인 적용: " + discounted + "원");
        
        if (getBalance() < discounted) {
            System.out.println("잔액이 부족합니다");
            return;
        }
        
        setBalance(getBalance() - discounted);
    }
}

결제 처리 메서드

public class PaymentKiosk {
    
    public static void processPayment(MembershipCard card) {
        Scanner scanner = new Scanner(System.in);
        System.out.print("결제 금액을 입력하세요: ");
        double price = scanner.nextDouble();
        
        card.pay(price);
        System.out.println("현재 잔액: " + card.getBalance());
    }
    
    public static void main(String[] args) {
        MembershipCard vip = new VipCard("12가3456", "김철수", 
                                          "010-1234-5678", 500000);
        MembershipCard standard = new StandardCard("34나7890", "이영", 
                                                     "010-8765-4321", 300000);
        
        processPayment(vip);      // VIP 할인 로직 실행
        processPayment(standard);  // 일반 할인 로직 실행
    }
}

이 예제에서 MembershipCard 타입 하나로 VipCardStandardCard 모두 처리할 수 있는 것이 다형성의 핵심이다. 객체의 실제 타입에 따라 각기 다른 할인 로직이 실행되며, 새로운 카드 종류가 추가되어도 processPayment 메서드는 변경할 필요가 없다.

태그: java 상속 다형성 오버라이딩 다운캐스팅

7월 23일 02:57에 게시됨