Java에서의 다형성: 유연한 코드를 위한 “Shape‑Shifter” 비밀

발행: (2026년 3월 7일 PM 04:23 GMT+9)
7 분 소요
원문: Dev.to

Source: Dev.to

커피숍에 있다고 상상해 보세요. 바리스타에게 “음료 하나 주세요”라고 말합니다. 상황에 따라—예를 들어 오전 8시이거나 무더운 여름 오후일 때—그 “음료”는 뜨거운 에스프레소가 될 수도 있고 차가운 콜드브루가 될 수도 있습니다. 하나의 단어를 사용했지만 상황에 따라 다른 결과를 얻는 것이죠.

자바 프로그래밍에서는 이를 polymorphism(다형성)이라고 부릅니다. 그리스어에서 유래된 이 용어는 문자 그대로 “여러 형태”라는 뜻입니다. 하나의 인터페이스나 메서드가 사용 방식에 따라 다르게 동작하도록 하는 마법과도 같습니다. 초보자에게는 컴파일‑타임 다형성과 런타임 다형성을 구분하는 것이 “아하!” 순간이며, 이를 통해 코더에서 설계자로 변신하게 됩니다.

핵심 개념: 정적 vs. 동적

다형성은 단순히 면접용 멋진 용어가 아니라, 코드를 재사용 가능하고 가독성 있게 만드는 것입니다.

컴파일 시점 다형성 (정적 바인딩)

프로그램이 실행되기 전에 Java 컴파일러가 정확히 어떤 메서드를 호출할지 결정할 때 발생합니다. 이는 메서드 오버로드를 통해 구현됩니다.

  • 느낌: 스위스 군용 나이프와 같습니다—하나의 도구에 다양한 작업을 위한 여러 블레이드가 있습니다.
  • 사용 사례: 동일한 연산을 서로 다른 타입이나 입력 개수와 함께 사용해야 할 때 (예: 두 정수를 더하는 경우와 두 실수를 더하는 경우).

런타임 다형성 (동적 바인딩)

여기서는 컴파일러가 어떤 메서드가 실행될지 알 수 없으며, 실행 중에 결정됩니다. 이는 메서드 오버라이드를 통해 구현됩니다.

  • 느낌: 다양한 기계에 있는 “시작” 버튼을 생각해 보세요. 자동차에서는 엔진을 시동하고, 노트북에서는 OS를 부팅합니다. 같은 명령이지만 객체마다 다른 동작을 합니다.
  • 사용 사례: 일반적인 카테고리(Animal 등)가 있지만, 하위 유형(Dog 또는 Cat 등)에 대해 구체적인 동작을 원할 때.

Java 21 코드 예제

예제 1: 컴파일‑타임 (메서드 오버로드)

public class Calculator {

    // Overloaded method: 2 integer parameters
    public int multiply(int a, int b) {
        return a * b;
    }

    // Overloaded method: 3 integer parameters
    public int multiply(int a, int b, int c) {
        return a * b * c;
    }

    public static void main(String[] args) {
        Calculator calc = new Calculator();

        // The compiler knows exactly which one to call based on the arguments
        System.out.println("Product of two: " + calc.multiply(5, 4));          // Output: 20
        System.out.println("Product of three: " + calc.multiply(5, 4, 2));   // Output: 40
    }
}

예제 2: 런타임 (메서드 오버라이드)

// Using a sealed interface – a modern Java feature!
sealed interface PaymentProcessor permits CreditCard, PayPal {}

final class CreditCard implements PaymentProcessor {
    public void process() {
        System.out.println("Processing credit card payment via Stripe API...");
    }
}

final class PayPal implements PaymentProcessor {
    public void process() {
        System.out.println("Redirecting to PayPal checkout...");
    }
}

public class PaymentApp {
    public static void main(String[] args) {
        // Polymorphic reference
        PaymentProcessor payment = (Math.random() > 0.5) ? new CreditCard() : new PayPal();

        // The JVM decides at runtime which process() to call
        payment.process();
    }
}

다형성 모범 사례

  • @Override 어노테이션 사용 – 항상 오버라이드하는 메서드에 어노테이션을 붙이세요. 오타를 방지하고 컴파일러가 실제로 슈퍼클래스 메서드를 오버라이드하고 있는지 검증하도록 합니다.
  • 상속보다 인터페이스 선호PaymentProcessor와 같은 인터페이스는 코드를 유연하게 만들고, 테스트하기 쉬우며, 나중에 변경하기도 간단합니다.
  • 과도한 오버로드 금지 – 같은 이름의 메서드가 너무 많으면 개발자를 혼란스럽게 할 수 있습니다. 오버로드 집합을 직관적으로 유지하세요.
  • “뚱뚱한” 인터페이스 피하기 – 인터페이스를 만족시키기 위해 클래스가 필요 없는 메서드를 구현하도록 강요하지 마세요.

요약

  • Compile‑time polymorphism = overloading: 같은 메서드 이름, 다른 시그니처, 컴파일러가 해결합니다.
  • Runtime polymorphism = overriding: 같은 메서드 이름과 시그니처, 실행 시 실제 객체 타입에 따라 JVM이 해결합니다.

이 개념들을 마스터하면 깔끔하고 확장 가능한 Java 애플리케이션을 만들 수 있습니다. 더 깊이 알아보려면 Official Oracle Java Tutorials 를 확인하세요.

0 조회
Back to Blog

관련 글

더 보기 »

Spring Boot에서 Liquibase – 개발자 가이드

데이터베이스 스키마 변경을 관리하는 것은 처음엔 간단해 보일 수 있지만, 그렇지 않을 때도 있습니다. 여기저기서 빠른 ALTER TABLE을 사용하는 것이 한 명의 개발자에게는 통할 수 있지만, 여러 사람이 참여하게 되면…