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=부모
자식 클래스에서 부모와 동일한 이름의 필드를 선언하면, 자식 필드가 부모 필드를 숨깁니다(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()를 적절히 오버라이딩하는 것이 중요합니다.