오버로딩 오버라이딩 차이 – Java 다형성 핵심 개념과 실무 활용 가이드


오버로딩 오버라이딩 차이를 정확히 설명하지 못하면, Java 면접에서 “둘 다 메서드 이름이 같은 거 아닌가요?”라는 애매한 답변을 하거나, @Override 어노테이션을 왜 붙이는지 모른 채 복사붙여넣기로 개발하다가 부모 메서드가 조용히 그냥 실행되는 버그를 만들게 됩니다. 오버로딩과 오버라이딩은 이름이 비슷하지만 동작하는 시점도, 목적도, 규칙도 근본적으로 다릅니다. 오버로딩은 컴파일 타임에 결정되는 정적 다형성, 오버라이딩은 런타임에 결정되는 동적 다형성입니다. 이 글에서는 두 개념의 정의와 조건, 컴파일러가 어떻게 메서드를 결정하는지의 바인딩 원리, 상속과 다형성에서의 역할, @Override의 중요성, 그리고 실무에서 자주 등장하는 패턴과 함정까지 예제 코드와 함께 완벽하게 정리합니다.


목차

  1. 다형성이란? 오버로딩·오버라이딩의 공통 뿌리
  2. 오버로딩(Overloading) – 정적 다형성의 원리와 규칙
  3. 오버라이딩(Overriding) – 동적 다형성의 원리와 규칙
  4. 바인딩 원리 – 컴파일 타임 vs 런타임 결정
  5. 핵심 차이 비교와 실무 선택 기준
  6. 전문가 관점 – 설계 패턴·면접 함정·안티패턴 정리

1. 다형성이란? 오버로딩·오버라이딩의 공통 뿌리

다형성(Polymorphism)의 의미

다형성은 “여러 형태를 가질 수 있는 성질”입니다. Java에서는 같은 이름의 메서드가 다른 상황에서 다르게 동작하는 것을 의미합니다. 오버로딩과 오버라이딩은 다형성을 구현하는 두 가지 서로 다른 방식입니다.

[다형성의 두 가지 형태]

다형성 (Polymorphism)
   │
   ├── 정적 다형성 (Static Polymorphism)
   │       구현 방법: 오버로딩(Overloading)
   │       결정 시점: 컴파일 타임
   │       기반 조건: 메서드 시그니처 (이름 + 파라미터)
   │
   └── 동적 다형성 (Dynamic Polymorphism)
           구현 방법: 오버라이딩(Overriding)
           결정 시점: 런타임
           기반 조건: 실제 객체 타입 (상속 + 실제 인스턴스)

두 개념을 한 문장으로 구분

오버로딩 (Overloading):
  "같은 클래스 안에서 메서드 이름은 같지만 파라미터가 다른 메서드를 여러 개 정의하는 것"
  → 컴파일러가 파라미터를 보고 어떤 메서드를 호출할지 결정

오버라이딩 (Overriding):
  "자식 클래스가 부모 클래스의 메서드를 같은 시그니처로 재정의하는 것"
  → JVM이 실행 시점에 실제 객체 타입을 보고 어떤 메서드를 호출할지 결정

일상 비유로 이해하기

[오버로딩 비유 – 커피숍 주문]

바리스타(메서드 이름): "make coffee"
  make(Espresso espresso)        → 에스프레소 제조법
  make(Latte latte)              → 라테 제조법
  make(Americano americano)      → 아메리카노 제조법

→ "make coffee"라는 동작 하나지만 재료(파라미터)에 따라 다른 레시피 실행
→ 손님이 무엇을 주문하는지(컴파일 타임)에 이미 결정됨

[오버라이딩 비유 – 직원 역할]

부모: Employee.work()        → 기본 업무: "사무실 일반 업무"
자식: Developer.work()       → 재정의: "코드 작성"
자식: Designer.work()        → 재정의: "디자인 작업"
자식: Manager.work()         → 재정의: "팀 관리"

Employee emp = new Developer();
emp.work();   → "코드 작성" 출력 (컴파일 타입: Employee, 런타임 타입: Developer)
→ 실제 어떤 직원인지(런타임)에 따라 다른 업무 실행

2. 오버로딩(Overloading) – 정적 다형성의 원리와 규칙

오버로딩이란

오버로딩은 하나의 클래스 안에서 같은 이름의 메서드를 여러 개 정의하되, 각 메서드는 파라미터 타입, 개수, 순서 중 하나 이상이 달라야 합니다. 컴파일러가 호출 시 전달된 인자를 분석해 어떤 메서드를 실행할지 결정합니다.

오버로딩 성립 조건

[오버로딩 성립 조건 – 파라미터가 달라야 함]

✅ 파라미터 타입이 다름
✅ 파라미터 개수가 다름
✅ 파라미터 순서가 다름 (타입이 다를 때)

❌ 반환 타입만 다름 → 오버로딩 아님 (컴파일 오류)
❌ 파라미터 이름만 다름 → 오버로딩 아님 (컴파일 오류)
❌ 접근제어자만 다름 → 오버로딩 아님 (컴파일 오류)
❌ 예외 선언만 다름 → 오버로딩 아님 (컴파일 오류)

java

public class Calculator {

    // ✅ 오버로딩 성립 – 파라미터 타입이 다름
    public int add(int a, int b) {
        System.out.println("int + int");
        return a + b;
    }

    public double add(double a, double b) {
        System.out.println("double + double");
        return a + b;
    }

    public long add(long a, long b) {
        System.out.println("long + long");
        return a + b;
    }

    // ✅ 오버로딩 성립 – 파라미터 개수가 다름
    public int add(int a, int b, int c) {
        System.out.println("int + int + int");
        return a + b + c;
    }

    // ✅ 오버로딩 성립 – 파라미터 순서가 다름 (타입이 달라야 의미 있음)
    public String format(String prefix, int number) {
        return prefix + number;
    }

    public String format(int number, String suffix) {
        return number + suffix;
    }

    // ❌ 오버로딩 불가 – 반환 타입만 다름 (컴파일 오류!)
    // public long add(int a, int b) {   // ← int add(int, int)와 충돌
    //     return (long)(a + b);
    // }

    // ❌ 오버로딩 불가 – 파라미터 이름만 다름 (컴파일 오류!)
    // public int add(int x, int y) {    // ← int add(int, int)와 동일
    //     return x + y;
    // }
}

// 호출 예시
Calculator calc = new Calculator();
calc.add(1, 2);         // → "int + int" 출력 (컴파일러가 타입 보고 결정)
calc.add(1.0, 2.0);     // → "double + double" 출력
calc.add(1, 2, 3);      // → "int + int + int" 출력
calc.add(1L, 2L);       // → "long + long" 출력

오버로딩과 타입 자동 변환 – 주의해야 할 함정

java

public class TypePromotionExample {

    public void print(int x) {
        System.out.println("int: " + x);
    }

    public void print(long x) {
        System.out.println("long: " + x);
    }

    public void print(double x) {
        System.out.println("double: " + x);
    }

    public void print(Integer x) {
        System.out.println("Integer(박싱): " + x);
    }
}

// 타입 자동 승급(Promotion) 발생 순서
TypePromotionExample ex = new TypePromotionExample();

ex.print(42);       // int → "int: 42"
ex.print(42L);      // long → "long: 42"
ex.print(42.0);     // double → "double: 42.0"

// ⚠️ 자동 승급 함정
// print(Integer)가 없다면?
// byte → short → int → long → float → double 순으로 자동 승급

public class PromotionTrap {
    public void show(long x) {
        System.out.println("long: " + x);
    }
    public void show(double x) {
        System.out.println("double: " + x);
    }
}

PromotionTrap trap = new PromotionTrap();
trap.show(42);       // int → long으로 자동 승급 → "long: 42"
trap.show(42.0f);    // float → double로 자동 승급 → "double: 42.0"

// ⚠️ 박싱 vs 오토박싱 우선순위
// Java는 오토박싱보다 타입 승급을 먼저 시도함
public class BoxingTrap {
    public void test(long x)    { System.out.println("long"); }
    public void test(Integer x) { System.out.println("Integer"); }
}

BoxingTrap bt = new BoxingTrap();
bt.test(42);    // int → long(승급) vs Integer(오토박싱)
                // → "long" 출력! 타입 승급이 오토박싱보다 우선

실무에서 자주 사용하는 오버로딩 패턴

java

// 패턴 1: 편의 메서드 제공 (선택적 파라미터 효과)
public class EmailService {

    // 모든 파라미터를 받는 핵심 메서드
    public void send(String to, String subject, String body,
                     List<String> cc, boolean isHtml) {
        // 실제 발송 로직
    }

    // 간편 버전들 (선택적 파라미터 시뮬레이션)
    public void send(String to, String subject, String body) {
        send(to, subject, body, Collections.emptyList(), false);
    }

    public void send(String to, String subject, String body, boolean isHtml) {
        send(to, subject, body, Collections.emptyList(), isHtml);
    }

    public void send(String to, String subject, String body, List<String> cc) {
        send(to, subject, body, cc, false);
    }
}

// 패턴 2: 다양한 타입 입력 지원
public class MoneyConverter {

    public Money from(int amount) {
        return Money.ofKrw(amount);
    }

    public Money from(long amount) {
        return Money.ofKrw(amount);
    }

    public Money from(BigDecimal amount) {
        return Money.of(amount, "KRW");
    }

    public Money from(String amount) {
        return Money.of(new BigDecimal(amount), "KRW");
    }
}

// 패턴 3: 로깅·출력 메서드
public class Logger {

    public void log(String message)             { write("[INFO] " + message); }
    public void log(String message, Throwable t){ write("[ERROR] " + message, t); }
    public void log(Level level, String message){ write("[" + level + "] " + message); }
}

3. 오버라이딩(Overriding) – 동적 다형성의 원리와 규칙

오버라이딩이란

오버라이딩은 자식 클래스가 부모 클래스로부터 상속받은 메서드를 같은 시그니처로 재정의하는 것입니다. 자식 클래스의 인스턴스로 메서드를 호출하면 부모 메서드가 아닌 자식이 재정의한 메서드가 실행됩니다.

오버라이딩 성립 조건 – 5가지 규칙

[오버라이딩 성립 조건]

✅ 1. 메서드 이름이 동일해야 함
✅ 2. 파라미터 타입·개수·순서가 완전히 동일해야 함
✅ 3. 반환 타입이 같거나 공변 반환 타입(Covariant Return Type)이어야 함
✅ 4. 접근제어자를 좁힐 수 없음 (같거나 더 넓어야 함)
✅ 5. 더 넓은 예외를 checked exception으로 선언할 수 없음

❌ static 메서드는 오버라이딩 불가 (hiding으로 처리됨)
❌ final 메서드는 오버라이딩 불가
❌ private 메서드는 오버라이딩 불가 (상속 안 됨)
❌ 생성자는 오버라이딩 불가

java

public class Parent {

    public Object getInfo() {
        return "부모 정보";
    }

    protected void process() throws Exception {
        System.out.println("부모 처리");
    }

    public static void staticMethod() {
        System.out.println("부모 정적 메서드");
    }

    public final void finalMethod() {
        System.out.println("변경 불가");
    }
}

public class Child extends Parent {

    // ✅ 규칙 3: 공변 반환 타입 (Object → String, 더 구체적 타입 가능)
    @Override
    public String getInfo() {       // Object → String (하위 타입) OK
        return "자식 정보";
    }

    // ✅ 규칙 4: 접근제어자 확장 (protected → public) OK
    @Override
    public void process() throws IOException {
        // ✅ 규칙 5: Exception → IOException (더 좁은 예외) OK
        System.out.println("자식 처리");
    }

    // ❌ 규칙 4 위반: public → protected (접근 축소) → 컴파일 오류!
    // @Override
    // protected String getInfo() { return "자식"; } // 컴파일 오류!

    // ❌ 규칙 5 위반: IOException → Exception (더 넓은 checked 예외) → 컴파일 오류!
    // @Override
    // public void process() throws Exception { }  // 컴파일 오류!

    // ⚠️ static 메서드는 오버라이딩이 아닌 hiding (메서드 숨기기)
    public static void staticMethod() {
        System.out.println("자식 정적 메서드");
        // → @Override 붙이면 컴파일 오류! static은 오버라이딩 불가
    }

    // ❌ final 메서드 오버라이딩 불가
    // @Override
    // public void finalMethod() { } // 컴파일 오류!
}

@Override 어노테이션 – 왜 반드시 붙여야 하는가

java

// @Override 없이 오버라이딩 시도 – 조용한 버그 발생
public class Animal {
    public void makeSound() {
        System.out.println("...");
    }
}

public class Dog extends Animal {

    // ❌ @Override 없이 오타 발생 → 오버라이딩이 아닌 새 메서드로 인식!
    public void makesound() {    // 소문자 s → 오타!
        System.out.println("멍멍");
    }
    // → 컴파일 오류 없음
    // → Dog dog = new Dog(); dog.makeSound() → "..." 출력 (부모 실행!)
    // → 버그 찾기 매우 어려움
}

// ✅ @Override 사용 → 오타 즉시 컴파일 오류로 감지
public class Cat extends Animal {

    @Override
    public void makesound() {    // ← 컴파일 오류! "makesound"는 부모에 없음
        System.out.println("야옹");
    }
    // → 즉시 오류 감지 → 수정 가능

    @Override
    public void makeSound() {    // ← 정확히 일치 → 컴파일 OK
        System.out.println("야옹");
    }
}

super 키워드 – 부모 메서드 재활용

java

public class Vehicle {

    protected int speed;
    protected String fuel;

    public String describe() {
        return String.format("속도: %dkm/h, 연료: %s", speed, fuel);
    }

    public void start() {
        System.out.println("엔진 시동");
    }
}

public class ElectricVehicle extends Vehicle {

    private int batteryLevel;
    private boolean regenerativeBraking;

    public ElectricVehicle(int speed, int batteryLevel) {
        this.speed         = speed;
        this.fuel          = "전기";
        this.batteryLevel  = batteryLevel;
        this.regenerativeBraking = true;
    }

    // 부모 메서드를 완전히 대체
    @Override
    public void start() {
        super.start();  // 부모 로직 먼저 실행: "엔진 시동"
        System.out.println("배터리 확인: " + batteryLevel + "%");
        System.out.println("전기 모터 활성화");
        // 결과: "엔진 시동" → "배터리 확인: 85%" → "전기 모터 활성화"
    }

    // 부모 결과에 추가 정보를 덧붙임
    @Override
    public String describe() {
        String baseDescription = super.describe();  // 부모 describe() 결과 재사용
        return baseDescription + String.format(
            ", 배터리: %d%%, 회생제동: %s",
            batteryLevel, regenerativeBraking ? "활성" : "비활성"
        );
        // "속도: 120km/h, 연료: 전기, 배터리: 85%, 회생제동: 활성"
    }
}

// ── super 사용 시 주의사항 ────────────────────────────────────
public class PremiumElectricVehicle extends ElectricVehicle {

    @Override
    public void start() {
        super.start();              // ElectricVehicle.start() 호출
        // → 내부에서 super.start() → Vehicle.start() 순서로 호출
        System.out.println("프리미엄 기능 활성화");
    }
    // 출력 순서:
    // "엔진 시동" (Vehicle)
    // "배터리 확인: ..." (ElectricVehicle)
    // "전기 모터 활성화" (ElectricVehicle)
    // "프리미엄 기능 활성화" (PremiumElectricVehicle)
}

4. 바인딩 원리 – 컴파일 타임 vs 런타임 결정

이 섹션이 오버로딩과 오버라이딩의 본질적 차이를 이해하는 핵심입니다.

정적 바인딩 (Static Binding) – 오버로딩

[오버로딩의 정적 바인딩 과정]

소스 코드:
  calculator.add(1, 2);

컴파일 타임에 컴파일러가 판단:
  → 1: int 타입, 2: int 타입
  → int add(int, int) 시그니처와 일치
  → 바이트코드에 add(int, int) 호출 코드 생성

런타임:
  → 이미 결정된 메서드 그냥 실행
  → 추가 판단 없음

→ "정적 바인딩" = 컴파일 타임에 이미 고정

java

public class StaticBindingDemo {

    public void show(Parent p) {
        System.out.println("Parent 파라미터");
    }

    public void show(Child c) {
        System.out.println("Child 파라미터");
    }
}

// ⚠️ 오버로딩은 컴파일 타임 타입 기준
StaticBindingDemo demo = new StaticBindingDemo();

Parent obj = new Child();   // 컴파일 타입: Parent, 런타임 타입: Child

demo.show(obj);
// → 컴파일러는 obj의 컴파일 타임 타입(Parent)을 봄
// → show(Parent p) 호출
// → "Parent 파라미터" 출력!
// → 실제 객체가 Child여도 오버로딩은 컴파일 타입 기준

동적 바인딩 (Dynamic Binding) – 오버라이딩

[오버라이딩의 동적 바인딩 과정]

소스 코드:
  Animal animal = new Dog();
  animal.makeSound();

컴파일 타임에 컴파일러가 판단:
  → animal의 타입: Animal
  → Animal.makeSound() 존재 확인
  → 바이트코드: "Animal 타입에서 makeSound 호출" 생성
  (어떤 구현을 실행할지는 아직 결정 안 함)

런타임에 JVM이 판단:
  → 실제 객체: Dog 인스턴스
  → Dog 클래스에 makeSound() 오버라이딩 존재
  → Dog.makeSound() 실행
  → "멍멍" 출력

→ "동적 바인딩" = 런타임에 실제 객체 타입 기준으로 결정

java

// 동적 바인딩 완전 이해 예제
public class Animal {
    public void makeSound() {
        System.out.println("...");
    }
    public void breathe() {
        System.out.println("숨을 쉽니다");
    }
}

public class Dog extends Animal {
    @Override
    public void makeSound() {
        System.out.println("멍멍");
    }
    // breathe()는 오버라이딩 없음 → 부모 메서드 그대로 상속
}

public class Cat extends Animal {
    @Override
    public void makeSound() {
        System.out.println("야옹");
    }
}

public class Bird extends Animal {
    @Override
    public void makeSound() {
        System.out.println("짹짹");
    }
}

// 동적 바인딩의 강력함 – 다형성 활용
public class AnimalSound {

    // Animal 타입으로 받지만 실제 어떤 동물인지는 런타임에 결정
    public static void makeAllSounds(List<Animal> animals) {
        for (Animal animal : animals) {
            animal.makeSound();  // ← 런타임에 실제 타입 기준으로 실행
        }
    }

    public static void main(String[] args) {
        List<Animal> animals = List.of(
            new Dog(),
            new Cat(),
            new Bird(),
            new Dog(),
            new Cat()
        );

        makeAllSounds(animals);
        // 출력:
        // 멍멍 (Dog.makeSound)
        // 야옹 (Cat.makeSound)
        // 짹짹 (Bird.makeSound)
        // 멍멍 (Dog.makeSound)
        // 야옹 (Cat.makeSound)

        // → makeAllSounds 메서드 코드를 전혀 변경하지 않고
        //   새로운 동물 클래스 추가 가능 → 개방-폐쇄 원칙(OCP)
    }
}

static 메서드의 hiding – 오버라이딩과 혼동 주의

java

public class Parent {
    public static void staticMethod() {
        System.out.println("Parent static");
    }
    public void instanceMethod() {
        System.out.println("Parent instance");
    }
}

public class Child extends Parent {
    // static 메서드는 오버라이딩이 아닌 "hiding(숨기기)"
    public static void staticMethod() {
        System.out.println("Child static");
    }

    @Override
    public void instanceMethod() {
        System.out.println("Child instance");
    }
}

// 핵심 차이 확인
Parent obj = new Child();

// 오버라이딩 (인스턴스 메서드): 동적 바인딩 → 런타임 타입 기준
obj.instanceMethod();    // → "Child instance" (런타임 타입 Child 기준)

// Hiding (static 메서드): 정적 바인딩 → 컴파일 타입 기준
obj.staticMethod();      // → "Parent static" (컴파일 타입 Parent 기준!)
Parent.staticMethod();   // → "Parent static"
Child.staticMethod();    // → "Child static"
// → static은 클래스 레벨 메서드 → 다형성 적용 안 됨

5. 핵심 차이 비교와 실무 선택 기준

최종 비교표

구분오버로딩 (Overloading)오버라이딩 (Overriding)
정의같은 이름, 다른 파라미터 메서드 여러 개 정의부모 메서드를 자식이 같은 시그니처로 재정의
발생 위치같은 클래스 (또는 상속 계층) 내상속 관계의 부모-자식 클래스 간
메서드 이름동일동일
파라미터반드시 달라야 함반드시 동일해야 함
반환 타입달라도 됨 (파라미터 다를 때)같거나 공변 반환 타입
접근제어자무관같거나 더 넓어야 함
바인딩정적 바인딩 (컴파일 타임)동적 바인딩 (런타임)
다형성 종류정적 다형성동적 다형성
@Override사용 불가 (해당 없음)반드시 사용 권장
static 메서드가능불가 (hiding으로 처리)
private 메서드가능불가 (상속 안 됨)
final 메서드가능불가
목적같은 기능의 다양한 입력 타입 지원부모 기능을 자식이 특화·확장
OOP 원칙코드 편의성다형성·OCP·LSP

두 가지를 동시에 사용하는 패턴

java

// 오버로딩 + 오버라이딩 동시 활용 예시
public abstract class Formatter {

    // 오버로딩: 다양한 타입 입력 지원
    public String format(int value) {
        return format(String.valueOf(value));  // String 버전으로 위임
    }

    public String format(double value) {
        return format(String.format("%.2f", value));
    }

    public String format(LocalDate date) {
        return format(date.toString());
    }

    // 오버라이딩 대상: 핵심 포맷 로직은 자식이 구현
    public abstract String format(String value);
}

public class HtmlFormatter extends Formatter {

    @Override  // 오버라이딩
    public String format(String value) {
        return "<span>" + value + "</span>";
    }
    // format(42) → format("42") → "<span>42</span>"
}

public class JsonFormatter extends Formatter {

    @Override  // 오버라이딩
    public String format(String value) {
        return "\"" + value + "\"";
    }
    // format(42) → format("42") → "\"42\""
}

// 사용
Formatter html = new HtmlFormatter();
html.format(42);          // 오버로딩: int → String → 오버라이딩: HTML
html.format(3.14);        // 오버로딩: double → String → 오버라이딩: HTML
html.format("hello");     // 오버라이딩만: HTML

6. 전문가 관점 – 설계 패턴·면접 함정·안티패턴 정리

오버라이딩과 리스코프 치환 원칙(LSP)

오버라이딩을 잘못 사용하면 **리스코프 치환 원칙(LSP)**을 위반할 수 있습니다. LSP는 “자식 클래스는 부모 클래스를 대체할 수 있어야 한다”는 원칙입니다.

java

// ❌ LSP 위반 – 오버라이딩으로 부모 계약 파괴
public class Rectangle {
    protected int width;
    protected int height;

    public void setWidth(int width)   { this.width  = width; }
    public void setHeight(int height) { this.height = height; }

    public int area() { return width * height; }
}

public class Square extends Rectangle {

    // 정사각형이므로 가로·세로 같아야 함 → 오버라이딩으로 강제
    @Override
    public void setWidth(int width) {
        this.width  = width;
        this.height = width;   // 부모 계약 위반! setWidth는 width만 바꿔야 함
    }

    @Override
    public void setHeight(int height) {
        this.width  = height;  // 부모 계약 위반!
        this.height = height;
    }
}

// LSP 위반 결과
Rectangle rect = new Square();  // Square를 Rectangle로 사용
rect.setWidth(5);
rect.setHeight(4);
System.out.println(rect.area()); // 20을 기대하지만 → 16 출력! (Square가 height를 4로 덮어씀)

// ✅ LSP 준수: 상속 대신 별도 클래스 또는 인터페이스로 설계
public interface Shape {
    int area();
}

public class Rectangle implements Shape {
    private final int width;
    private final int height;

    public Rectangle(int width, int height) {
        this.width  = width;
        this.height = height;
    }

    @Override
    public int area() { return width * height; }
}

public class Square implements Shape {
    private final int side;

    public Square(int side) { this.side = side; }

    @Override
    public int area() { return side * side; }
}

면접 단골 함정 문제

java

// 함정 1: 오버로딩 + 상속 조합
public class Parent {
    public void print(Object obj) {
        System.out.println("Parent.print(Object): " + obj);
    }
}

public class Child extends Parent {
    public void print(String str) {      // 오버로딩 (파라미터 타입 다름)
        System.out.println("Child.print(String): " + str);
    }
}

// Q: 출력 결과는?
Parent p = new Child();
p.print("hello");
// → "Parent.print(Object): hello" 출력!
// 이유: 오버로딩은 컴파일 타임 타입 기준
//       p의 컴파일 타입은 Parent
//       Parent.print(Object)만 있으므로 그것이 선택됨
//       Child.print(String)는 오버로딩이지만 Parent 타입에서 보이지 않음

Child c = new Child();
c.print("hello");
// → "Child.print(String): hello" 출력!
// 이유: c의 컴파일 타입은 Child
//       Child.print(String)과 Parent.print(Object) 모두 보임
//       String이 더 구체적 타입이므로 Child.print(String) 선택

// ─────────────────────────────────────────────────
// 함정 2: 오버라이딩 + 필드 숨기기
public class Base {
    public String name = "Base";

    public String getName() { return name; }
}

public class Derived extends Base {
    public String name = "Derived";  // 필드 숨기기 (오버라이딩 아님!)

    @Override
    public String getName() { return name; }  // Derived.name 반환
}

Base obj = new Derived();
System.out.println(obj.name);       // → "Base" (필드는 컴파일 타입 기준)
System.out.println(obj.getName());  // → "Derived" (메서드는 런타임 타입 기준)
// → 필드는 오버라이딩되지 않음! 항상 컴파일 타임 타입의 필드

// ─────────────────────────────────────────────────
// 함정 3: 생성자에서 오버라이딩 메서드 호출
public class Parent {
    public Parent() {
        init();  // 오버라이딩된 메서드 호출!
    }
    public void init() {
        System.out.println("Parent.init()");
    }
}

public class Child extends Parent {
    private final String message;

    public Child(String message) {
        super();  // Parent() 호출 → init() 호출 → Child.init() 실행
        this.message = message;
    }

    @Override
    public void init() {
        System.out.println("Child.init(): " + message);
        // ⚠️ message는 아직 null! (super() 실행 중 this.message 초기화 전)
    }
}

new Child("안녕");
// 출력: "Child.init(): null" ← message가 null
// → 생성자에서 오버라이딩될 수 있는 메서드 호출은 위험!

안티패턴 정리

java

// ❌ 안티패턴 1: 의미론적으로 관련 없는 메서드를 오버로딩
public class BadExample {
    public void process(String text)   { /* 텍스트 처리 */ }
    public void process(int number)    { /* 숫자 처리 */ }
    public void process(File file)     { /* 파일 처리 */ }
    // → 전혀 다른 동작인데 같은 이름 → 혼란 유발
    // → processText(), processNumber(), processFile()로 명확하게 분리

// ❌ 안티패턴 2: 오버라이딩으로 부모 기능 완전 무효화
public class Parent {
    public void saveWithTransaction() {
        beginTransaction();
        doSave();
        commitTransaction();
    }
}

public class Child extends Parent {
    @Override
    public void saveWithTransaction() {
        doSave(); // 트랜잭션 없이 그냥 저장 → 부모 계약 파괴
    }
}

// ❌ 안티패턴 3: @Override 없이 오버라이딩
public class MyClass extends ParentClass {
    public void doSomething() {  // @Override 없음
        // 오타나 시그니처 변경 시 조용한 버그 발생
    }
}

// ✅ 올바른 설계 원칙 요약
// 오버로딩: 의미적으로 동일한 동작, 다른 입력 타입/개수
// 오버라이딩: 부모 계약 유지하면서 자식 특화 동작 추가
// @Override 어노테이션 항상 사용
// 생성자에서 오버라이딩될 메서드 호출 금지
// LSP 준수 확인 후 오버라이딩

오버로딩·오버라이딩 설계 체크리스트

[오버로딩 사용 전 체크리스트]
□ 같은 이름의 메서드들이 의미론적으로 동일한 동작을 하는가?
□ 파라미터 타입/개수/순서 중 하나 이상이 실제로 다른가?
□ 타입 자동 승급으로 의도치 않은 메서드가 호출되지 않는가?
□ @Override가 아닌 실제 오버로딩인지 확인했는가?

[오버라이딩 사용 전 체크리스트]
□ @Override 어노테이션을 붙였는가?
□ 접근제어자를 좁히지 않았는가?
□ 부모 클래스의 계약(Javadoc, 사전조건·사후조건)을 유지하는가?
□ 리스코프 치환 원칙(LSP)을 위반하지 않는가?
□ 생성자에서 이 메서드를 호출하는 부모 생성자가 없는가?
□ 필요 시 super를 호출해 부모 로직을 재사용하는가?

결론

오버로딩 오버라이딩 차이의 핵심은 결정 시점입니다. 오버로딩은 컴파일 타임에 메서드 시그니처(이름+파라미터)를 보고 결정되는 정적 다형성으로, 같은 클래스 안에서 다양한 입력 타입을 우아하게 처리할 때 사용합니다. 오버라이딩은 런타임에 실제 객체 타입을 보고 결정되는 동적 다형성으로, 상속 계층에서 자식 클래스가 부모의 동작을 특화·확장할 때 사용하며, @Override 어노테이션을 반드시 붙여 컴파일 타임에 오류를 잡아야 합니다. 두 개념을 구분하는 가장 빠른 방법은 “파라미터가 다른가(오버로딩)”와 “부모-자식 관계에서 같은 시그니처로 재정의하는가(오버라이딩)”를 물어보는 것입니다.

지금 바로 현재 프로젝트에서 @Override 없이 오버라이딩한 메서드가 있는지 검색하고, static 메서드를 오버라이딩하려다 실제로 hiding이 된 코드가 없는지 점검해 보세요.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다