본문으로 건너뛰기

7.6 super 키워드와 super() (The 'super' Keyword)

자식 클래스가 부모 클래스로부터 상속을 받게 되면 부모의 멤버 변수와 메서드를 사용할 수 있습니다. 그러나 때로는 자식 클래스와 부모 클래스의 멤버 이름이 완전히 같아서 충돌하거나, 부모의 메서드를 오버라이딩(Overriding)한 뒤에도 원래 부모의 원본 메서드를 호출 해야 할 필요가 있습니다. 이때 super 키워드를 사용합니다.


1. 참조변수 super​

super는 상속받은 자식 클래스 내부에서 부모 클래스의 멤버에 접근할 때 사용하는 참조변수 입니다. 이전 장에서 배운 this가 '자기 자신'을 가리킨다면, super는 '나를 낳아준 부모'를 가리킵니다.

부모 변수와의 충돌 해결​

자식 클래스의 지역 변수와 부모 클래스의 멤버 변수 이름이 같을 때, 혹은 부모에서 물려받은 변수와 자식 클래스의 변수가 같을 때 구분 목적으로 사용됩니다.

class Parent {
int x = 10;
String name = "부모";
}

class Child extends Parent {
int x = 20; // 부모의 x와 이름 충돌
String name = "자식"; // 부모의 name과 이름 충돌

void method() {
System.out.println("x=" + x); // 자식 클래스 변수 (20)
System.out.println("this.x=" + this.x); // 자식 자신의 멤버 변수 (20)
System.out.println("super.x=" + super.x); // 부모로부터 상속받은 변수 (10)

System.out.println("name=" + name); // 자식: "자식"
System.out.println("this.name=" + this.name); // 자식: "자식"
System.out.println("super.name=" + super.name); // 부모: "부모"
}
}

public class SuperFieldDemo {
public static void main(String[] args) {
Child c = new Child();
c.method();
}
}

[실행 결과]

x=20
this.x=20
super.x=10
name=자식
this.name=자식
super.name=부모
필드 숨김(Field Hiding)

자식 클래스에서 부모와 동일한 이름의 필드를 선언하면, 자식 필드가 부모 필드를 숨깁니다(hides). 실제로는 두 필드 모두 메모리에 존재하며, super.x로 부모 필드에, this.x로 자식 필드에 접근합니다. 메서드 오버라이딩과는 다른 개념입니다.


2. super.메서드명(): 부모 메서드 호출​

자식이 부모의 메서드를 오버라이딩하여 내용을 바꿨지만, 그럼에도 불구하고 부모의 원래 로직을 덧붙여서 재사용하고 싶을 때 매우 유용합니다. 이렇게 하면 코드가 중복되는 것을 방지합니다.

오버라이딩 + 확장 패턴​

class Logger {
void log(String message) {
System.out.println("[LOG] " + message);
}
}

class TimestampLogger extends Logger {
@Override
void log(String message) {
// 부모의 log() 기능을 재사용하면서 타임스탬프를 추가
super.log(message); // [LOG] 메시지 출력
System.out.println(" └─ 시각: " + java.time.LocalTime.now());
}
}

class PrefixLogger extends TimestampLogger {
private String prefix;

PrefixLogger(String prefix) {
this.prefix = prefix;
}

@Override
void log(String message) {
// 부모(TimestampLogger)의 log()를 재사용하면서 prefix 추가
super.log("[" + prefix + "] " + message);
}
}
public class LoggerDemo {
public static void main(String[] args) {
Logger basic = new Logger();
basic.log("기본 로그");
System.out.println();

TimestampLogger ts = new TimestampLogger();
ts.log("타임스탬프 로그");
System.out.println();

PrefixLogger pl = new PrefixLogger("ERROR");
pl.log("파일을 찾을 수 없습니다.");
}
}

좌표 클래스 확장 예제​

class Point {
int x, y;

Point(int x, int y) {
this.x = x;
this.y = y;
}

String getLocation() {
return "x:" + x + ", y:" + y;
}

double distanceFromOrigin() {
return Math.sqrt(x * x + y * y);
}
}

class Point3D extends Point {
int z;

Point3D(int x, int y, int z) {
super(x, y); // 부모 생성자 호출
this.z = z;
}

@Override
String getLocation() {
// 부모의 getLocation() 결과에 z축 정보를 이어붙임
return super.getLocation() + ", z:" + z;
}

@Override
double distanceFromOrigin() {
// 3D 거리 공식: sqrt(x^2 + y^2 + z^2)
return Math.sqrt(x * x + y * y + z * z);
}
}
public class PointDemo {
public static void main(String[] args) {
Point p2d = new Point(3, 4);
System.out.println("2D 위치: " + p2d.getLocation());
System.out.printf("원점까지 거리: %.2f%n", p2d.distanceFromOrigin());

Point3D p3d = new Point3D(1, 2, 2);
System.out.println("3D 위치: " + p3d.getLocation());
System.out.printf("원점까지 거리: %.2f%n", p3d.distanceFromOrigin());
}
}

[실행 결과]

2D 위치: x:3, y:4
원점까지 거리: 5.00
3D 위치: x:1, y:2, z:2
원점까지 거리: 3.00

3. super(): 부모 생성자 호출​

자식 클래스의 인스턴스를 생성할 때는, 자식 공간 내부에 항상 부모 클래스의 영역부터 먼저 할당 되어야 부모의 속성들을 온전히 상속받아 사용할 수 있습니다. 이를 보장하기 위해 super()라는 특별한 생성자 호출 명령어를 제공합니다.

규칙​

  • super()는 반드시 자식 생성자의 첫 번째 줄에 위치해야 합니다.
  • 개발자가 super()나 this()를 명시하지 않으면, 컴파일러가 자동으로 super();(빈 부모 생성자 호출)를 첫 줄에 삽입합니다.
  • 부모에 매개변수가 있는 생성자만 존재하고 기본 생성자가 없다면, 자식에서 반드시 super(인자)를 명시해야 합니다.
class Animal {
String name;
int age;

// 매개변수 있는 생성자만 존재 (기본 생성자 없음)
Animal(String name, int age) {
this.name = name;
this.age = age;
System.out.println("Animal 생성자 실행: " + name);
}
}

class Dog extends Animal {
String breed;

Dog(String name, int age, String breed) {
super(name, age); // 반드시 첫 줄! 부모 Animal 생성자 호출
this.breed = breed;
System.out.println("Dog 생성자 실행: " + breed);
}

// super(name, age)가 없으면 컴파일러가 super()를 자동 삽입하려 하지만
// Animal에 기본 생성자가 없으므로 컴파일 에러 발생!
}
public class SuperConstructorDemo {
public static void main(String[] args) {
Dog dog = new Dog("바둑이", 3, "진돗개");
System.out.println(dog.name + " (" + dog.breed + "), " + dog.age + "세");
}
}

[실행 결과]

Animal 생성자 실행: 바둑이
Dog 생성자 실행: 진돗개
바둑이 (진돗개), 3세

4. 생성자 체이닝 (Constructor Chaining)​

여러 단계로 상속된 클래스를 생성할 때 생성자는 자식 → 부모 → 조부모 순서로 체인처럼 호출됩니다.

class Grandparent {
Grandparent() {
System.out.println("1. Grandparent 생성자 실행");
}
}

class Parent extends Grandparent {
Parent() {
super(); // Grandparent() 호출 (생략 시 컴파일러가 자동 삽입)
System.out.println("2. Parent 생성자 실행");
}
}

class Child extends Parent {
Child() {
super(); // Parent() 호출
System.out.println("3. Child 생성자 실행");
}
}

public class ChainDemo {
public static void main(String[] args) {
System.out.println("Child 객체 생성 시작:");
Child c = new Child();
System.out.println("Child 객체 생성 완료.");
}
}

[실행 결과]

Child 객체 생성 시작:
1. Grandparent 생성자 실행
2. Parent 생성자 실행
3. Child 객체 생성자 실행
Child 객체 생성 완료.
생성자 실행 순서

상속 계층에서 객체를 생성할 때, 생성자는 위에서 아래로(조부모 → 부모 → 자식) 순서로 실행됩니다. 즉, new Child()를 호출해도 Grandparent 생성자부터 순서대로 실행됩니다.


5. Object 클래스와 toString()​

자바의 모든 클래스는 명시적 상속이 없으면 자동으로 java.lang.Object를 상속합니다. Object 클래스의 주요 메서드들을 @Override로 재정의하는 것은 모든 클래스 설계의 기본입니다.

class Vehicle {
private String brand;
private int year;

Vehicle(String brand, int year) {
this.brand = brand;
this.year = year;
}

// Object.toString() 재정의
@Override
public String toString() {
return brand + " (" + year + "년식)";
}
}

class Car extends Vehicle {
private int doors;

Car(String brand, int year, int doors) {
super(brand, year);
this.doors = doors;
}

@Override
public String toString() {
// 부모(Vehicle)의 toString()을 재사용하고 자식 정보를 추가
return super.toString() + " [" + doors + "도어 승용차]";
}
}

class ElectricCar extends Car {
private int range; // 주행 가능 거리 (km)

ElectricCar(String brand, int year, int doors, int range) {
super(brand, year, doors);
this.range = range;
}

@Override
public String toString() {
return super.toString() + " [전기차, 주행거리: " + range + "km]";
}
}
public class VehicleDemo {
public static void main(String[] args) {
Vehicle v = new Vehicle("현대", 2020);
Car c = new Car("기아", 2022, 4);
ElectricCar e = new ElectricCar("테슬라", 2024, 4, 600);

// println이 자동으로 toString() 호출
System.out.println(v);
System.out.println(c);
System.out.println(e);
}
}

[실행 결과]

현대 (2020년식)
기아 (2022년식) [4도어 승용차]
테슬라 (2024년식) [4도어 승용차] [전기차, 주행거리: 600km]

6. 실전 예제: 직원 계층 구조 (Employee → Manager → Director)​

실무에서 자주 볼 수 있는 직원 계층 구조를 super를 활용하여 구현해 봅니다.

// 기본 직원 클래스
class Employee {
private String name;
private String employeeId;
private double baseSalary;

Employee(String name, String employeeId, double baseSalary) {
this.name = name;
this.employeeId = employeeId;
this.baseSalary = baseSalary;
}

public String getName() { return name; }
public String getEmployeeId() { return employeeId; }
public double getBaseSalary() { return baseSalary; }

// 급여 계산 (자식에서 오버라이딩 예정)
public double calculateSalary() {
return baseSalary;
}

public String getInfo() {
return String.format("[%s] %s (기본급: %,.0f원)",
employeeId, name, baseSalary);
}

@Override
public String toString() {
return String.format("%s | 실수령: %,.0f원", getInfo(), calculateSalary());
}
}

// 관리자 클래스
class Manager extends Employee {
private double teamBonus; // 팀 성과 보너스
private int teamSize; // 팀원 수

Manager(String name, String employeeId, double baseSalary,
double teamBonus, int teamSize) {
super(name, employeeId, baseSalary); // 부모 생성자 호출
this.teamBonus = teamBonus;
this.teamSize = teamSize;
}

@Override
public double calculateSalary() {
// 부모의 기본급 + 팀 보너스
return super.calculateSalary() + teamBonus;
}

@Override
public String getInfo() {
// 부모 정보에 관리자 추가 정보 덧붙임
return super.getInfo() + String.format(" [팀원: %d명]", teamSize);
}

public double getTeamBonus() { return teamBonus; }
}

// 임원 클래스
class Director extends Manager {
private double stockOptions; // 스톡옵션 금액
private String department; // 담당 부서

Director(String name, String employeeId, double baseSalary,
double teamBonus, int teamSize,
double stockOptions, String department) {
super(name, employeeId, baseSalary, teamBonus, teamSize);
this.stockOptions = stockOptions;
this.department = department;
}

@Override
public double calculateSalary() {
// 부모(Manager)의 급여(기본급 + 팀보너스) + 스톡옵션
return super.calculateSalary() + stockOptions;
}

@Override
public String getInfo() {
return super.getInfo() + String.format(" [부서: %s]", department);
}
}
import java.util.List;

public class CompanyDemo {
public static void main(String[] args) {
Employee emp = new Employee("김일반", "E001", 3_000_000);
Manager mgr = new Manager("이관리", "M001", 4_000_000, 500_000, 5);
Director dir = new Director("박임원", "D001", 6_000_000,
1_000_000, 20, 2_000_000, "개발본부");

List<Employee> staff = List.of(emp, mgr, dir);

System.out.println("=== 전체 직원 급여 명세 ===");
for (Employee e : staff) {
System.out.println(e); // 다형성: 각 클래스의 toString() 호출
}

System.out.println();
double totalPayroll = staff.stream()
.mapToDouble(Employee::calculateSalary)
.sum();
System.out.printf("총 인건비: %,.0f원%n", totalPayroll);

// instanceof 패턴 매칭으로 타입별 처리
System.out.println("\n=== 관리자 이상만 출력 ===");
for (Employee e : staff) {
if (e instanceof Manager m) {
System.out.printf("%s → 팀보너스: %,.0f원, 최종급여: %,.0f원%n",
m.getName(), m.getTeamBonus(), m.calculateSalary());
}
}
}
}

[실행 결과]

=== 전체 직원 급여 명세 ===
[E001] 김일반 (기본급: 3,000,000원) | 실수령: 3,000,000원
[M001] 이관리 (기본급: 4,000,000원) [팀원: 5명] | 실수령: 4,500,000원
[D001] 박임원 (기본급: 6,000,000원) [팀원: 20명] [부서: 개발본부] | 실수령: 9,000,000원

총 인건비: 16,500,000원

=== 관리자 이상만 출력 ===
이관리 → 팀보너스: 500,000원, 최종급여: 4,500,000원
박임원 → 팀보너스: 1,000,000원, 최종급여: 9,000,000원

마치며​

super와 super()를 정리하면 다음과 같습니다.

키워드역할사용 위치
super.필드부모 클래스의 필드에 접근자식 클래스 내부
super.메서드()부모 클래스의 메서드 호출자식 클래스 내부
super()부모 클래스의 생성자 호출자식 생성자의 첫 번째 줄
  • super는 자식이 부모를 '참조'하는 도구로, 코드 재사용과 확장 패턴에 필수입니다.
  • 생성자 체이닝은 상속 계층이 깊을수록 조상 → 자손 순서로 초기화가 보장됩니다.
  • 모든 클래스는 결국 Object를 상속하므로, toString(), equals(), hashCode()를 적절히 오버라이딩하는 것이 중요합니다.