1. 상속(Inheritance)이란?

상속(Inheritance)은 객체 지향 프로그래밍에서 기존 클래스로부터 필드와 메서드를 물려받아 새로운 클래스를 정의하는 방식입니다. 이를 통해 코드의 재사용성을 극대화할 수 있으며, 자식 클래스는 부모 클래스의 기능을 그대로 사용하거나 필요한 부분만 수정하여 기능을 확장할 수 있습니다. 상속을 활용하면 코드 중복을 줄이고, 유지보수가 쉬운 구조를 만들 수 있습니다.

상속의 주요 개념

  • 부모 클래스 (Super Class): 공통된 속성과 메서드를 정의하는 클래스.
  • 자식 클래스 (Sub Class): 부모 클래스로부터 상속받아 기능을 재사용하거나 확장하는 클래스.


2. 상속을 활용한 예시

Animal 클래스를 부모로, Dog 클래스가 이를 상속받아 기능을 확장해보겠습니다.

// 부모 클래스
public class Animal {
    void makeSound() {
        System.out.println("동물이 운다!");
    }
}
// 자식 클래스
public class Dog extends Animal {
    // 부모 메서드를 오버라이딩
    @Override
    void makeSound() {
        super.makeSound();  // 부모 메서드 호출
        System.out.println("멍멍!");
    }

    // 자식 클래스만의 메서드
    void fetch() {
        System.out.println("공 가져와!");
    }
}
public class DogEx {
    public static void main(String[] args) {
        Dog myDog = new Dog();  // 자식 클래스 인스턴스 생성
        myDog.makeSound();  // 오버라이딩된 메서드 호출
        myDog.fetch();  // 자식 클래스의 추가 메서드 호출
    }
}
  • Animal 클래스는 makeSound() 메서드를 통해 동물의 기본적인 울음소리를 정의합니다.
  • Dog 클래스는 Animal 클래스를 상속받아 makeSound() 메서드를 오버라이딩하고, “멍멍!”이라는 추가적인 행동을 정의합니다. 또한, fetch()라는 자식 클래스만의 기능도 추가됩니다.
  • 실행 결과, 자식 클래스에서 makeSound() 메서드를 호출하면 부모 메서드의 동작에 자식 클래스의 새로운 동작이 더해집니다.


3. 오버라이딩과 부모 메서드 호출

상속을 통해 자식 클래스는 부모 클래스의 메서드를 오버라이딩하여 새로운 동작을 정의할 수 있습니다. 이때 super 키워드를 사용하면 부모 클래스의 메서드를 명시적으로 호출할 수 있습니다. 이렇게 하면 부모 메서드를 그대로 유지하면서 자식 클래스에서만 필요한 동작을 추가할 수 있습니다.

@Override
void makeSound() {
    super.makeSound();  // 부모 클래스의 동작 호출
    System.out.println("멍멍!");  // 추가 동작
}

부모의 기능을 재사용하면서도 자식 클래스에 맞는 동작을 쉽게 추가할 수 있는 방법입니다. 예를 들어, 부모 클래스의 기능을 일부 확장하거나 수정하고 싶을 때 유용하게 사용됩니다.


4. 상속에서의 필드 접근 방식

상속을 사용할 때, 부모 클래스의 필드 접근 수준에 따라 자식 클래스가 그 필드에 어떻게 접근할 수 있는지가 달라집니다. public 필드는 자식 클래스에서 직접 접근할 수 있지만, private 필드는 자식 클래스에서 직접 접근할 수 없으며, getter/setter 메서드를 통해 접근해야 합니다.

1) public 필드 사용

// 부모 클래스의 필드가 public일 때
public class Person {
    public String name;
    public int age;
}
public class Student extends Person {
    public void printDetails() {
        System.out.println("이름: " + name);  // 부모 필드에 직접 접근 가능
        System.out.println("나이: " + age);   // 부모 필드에 직접 접근 가능
    }
}

2) private 필드 사용

// 부모 클래스의 필드가 private일 때
public class Person {
    private String name;
    private int age;

    // getter / setter 메서드
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}
public class Student extends Person {
    public void setDetails(String name, int age) {
        setName(name);  // 부모 클래스의 setter 사용
        setAge(age);    // 부모 클래스의 setter 사용
    }

    public void printDetails() {
        System.out.println("이름: " + getName());  // getter를 통해 접근
        System.out.println("나이: " + getAge());
    }
}
public class Main {
    public static void main(String[] args) {
        Student student = new Student();
        student.setDetails("홍길동", 20);  // 부모 클래스의 필드 초기화
        student.printDetails();  // 부모 클래스의 필드 값 출력
    }
}

출력

이름: 홍길동
나이: 20

필드 접근

  • public 필드는 자식 클래스에서 직접 접근 가능.
  • private 필드는 직접 접근이 불가능하며, 부모 클래스의 getter/setter 메서드를 통해 접근해야 함.


5. 생성자와 상속

상속 관계에서 자식 클래스는 부모 클래스의 생성자를 통해 부모 필드를 초기화할 수 있습니다. 자식 클래스가 인스턴스화될 때 부모 클래스의 생성자가 먼저 호출되며, 자식 클래스에서 부모 필드가 초기화됩니다.

// 부모 클래스
public class Person {
    private String name;
    private int age;

    // 부모 클래스의 생성자
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() { return name; }
    public int getAge() { return age; }
}
// 부모 클래스의 생성자 호출
public class Student extends Person {
    private String campus;

    public Student(String name, int age) {
        super(name, age);  // 부모 클래스의 생성자를 호출하여 name과 age를 초기화
        this.campus = "영등포";
    }
}

부모 클래스의 생성자를 호출하면, 자식 클래스에서 부모 클래스의 필드를 초기화할 수 있으며, 추가적인 자식 클래스만의 필드를 별도로 초기화할 수도 있습니다.


6. 상속을 사용하는 이유

1) 코드 재사용성 증가

상속은 부모 클래스의 필드와 메서드를 자식 클래스가 재사용할 수 있게 해줍니다. 이를 통해 자식 클래스에서 중복된 코드를 작성할 필요 없이 기존의 코드를 활용할 수 있습니다. 이는 개발 시간을 절약하고, 코드의 일관성을 유지하는 데도 큰 도움이 됩니다.

2) 유지보수 용이성

상속을 사용하면 부모 클래스에서 한 번만 수정해도, 이를 상속받은 자식 클래스들에 수정 내용이 자동으로 반영됩니다. 코드가 여러 곳에서 중복되면 유지보수가 어려워지지만, 상속 구조에서는 부모 클래스에서의 수정을 통해 한 번에 전체 코드의 일관성을 유지할 수 있습니다.

3) 확장성 제공

자식 클래스는 부모 클래스를 기반으로 새로운 기능을 추가하거나, 기존 기능을 오버라이딩(재정의)할 수 있습니다. 이를 통해 기존의 구조를 확장할 수 있으며, 필요에 따라 더 복잡하고 고도화된 클래스를 구현할 수 있습니다.

4) 다형성 제공

상속은 다형성(polymorphism)을 가능하게 합니다. 부모 클래스의 타입으로 자식 클래스를 참조할 수 있으며, 이를 통해 동일한 코드로 다양한 자식 클래스를 처리할 수 있습니다. 예를 들어, 부모 클래스 타입의 매개변수로 여러 자식 클래스를 받아서 동일한 인터페이스로 동작하게 할 수 있습니다.

5) 일관된 인터페이스 제공

상속을 사용하면 여러 자식 클래스들이 일관된 인터페이스를 제공하게 됩니다. 즉, 부모 클래스에서 정의된 메서드들이 자식 클래스에서 일관되게 존재하며, 자식 클래스는 이 인터페이스에 맞춰 자신만의 기능을 추가하거나 확장할 수 있습니다.


예시 코드에서는?

  • Person 클래스와 Student 클래스
    • Person 클래스는 nameage라는 공통적인 필드를 정의하고, 이를 자식 클래스인 Student가 재사용할 수 있습니다. 또한 say()와 같은 메서드를 상속받아 추가적인 수정 없이 그대로 사용할 수 있습니다.
  • 코드의 유지보수
    • 만약 Person 클래스에서 nameage를 수정하거나 추가적인 메서드를 정의하게 되면, 이를 상속받는 모든 자식 클래스에서 자동으로 변경 사항이 반영됩니다. 이를 통해 유지보수가 쉬워집니다.
  • 확장성
    • Student 클래스는 상속받은 필드와 메서드에 더해, 자신만의 필드 campus와 메서드를 추가할 수 있습니다. 상속을 통해 기존 클래스를 기반으로 한 확장된 기능을 만들 수 있습니다.


7. 상속에서 주의할 점

  • private 필드 접근: 부모 클래스의 private 필드는 자식 클래스에서 직접 접근할 수 없으므로, getter/setter를 사용해야 합니다.
  • 다중 상속 금지: Java에서는 다중 상속을 허용하지 않으므로, 한 클래스는 하나의 부모 클래스만 상속받을 수 있습니다.
  • 오버라이딩 규칙: 자식 클래스에서 부모 메서드를 오버라이딩할 때는 반드시 부모 메서드의 시그니처(이름, 매개변수 타입, 반환형)를 동일하게 맞춰야 합니다.