PHpullh
언어 비교/인터페이스와 상속

LANGUAGE COMPARISON

인터페이스와 상속를 언어별로 비교하기

상속으로 확장하는 언어와 합성·트레이트로 확장하는 언어를 나란히 놓고 봅니다.

같은 모양이면 같은 타입인가

인터페이스를 구현한다고 선언해야 하는 언어와, 모양만 맞으면 자동으로 만족하는 언어가 있습니다. Java, C#, Kotlin, PHP는 implements나 :로 관계를 명시합니다. 반면 Go는 메서드 집합만 맞으면 인터페이스를 만족하고, TypeScript도 구조가 같으면 호환됩니다.

이 차이는 인터페이스를 누가 정의하는가를 바꿉니다. 명시적 언어에서는 타입을 만들 때 어떤 인터페이스를 따를지 미리 정해야 하지만, Go나 TypeScript에서는 쓰는 쪽이 필요한 모양을 나중에 선언할 수 있습니다. 남이 만든 타입을 고치지 않고도 내 인터페이스에 끼워 넣을 수 있다는 뜻입니다.

Rust는 세 번째 길입니다. 트레이트를 나중에 구현할 수 있어 확장성은 Go에 가깝지만, 구현을 명시해야 해서 의도는 분명히 드러납니다. C++의 템플릿도 컴파일 시점에 모양만 맞으면 통과하므로 구조적 타이핑에 가깝게 동작합니다.

Kotlin

인터페이스 & 추상 클래스

인터페이스의 기본 구현(default method)과 추상 클래스의 차이를 이해합니다.

// 인터페이스 — 기본 구현 가능
interface Drawable {
    val color: String       // 추상 프로퍼티
    fun draw()              // 추상 메서드
    fun describe() = "나는 $color 색 도형"  // 기본 구현
}

interface Resizable {
    fun resize(factor: Double)
}

// 다중 인터페이스 구현
class Circle(
    override val color: String,
    var radius: Double
) : Drawable, Resizable {
    override fun draw() = println("⭕ 반지름 $radius 원")
    override fun resize(factor: Double) { radius *= factor }
}

// 추상 클래스 — 상태(필드)를 가질 수 있음
abstract class Shape(val name: String) {
    var x = 0.0; var y = 0.0

    abstract fun area(): Double

    fun move(dx: Double, dy: Double) {
        x += dx; y += dy
    }
}

class Rectangle(val w: Double, val h: Double)
    : Shape("사각형") {
    override fun area() = w * h
}

fun main() {
    val c = Circle("red", 5.0)
    c.draw()
    println(c.describe())
    c.resize(2.0)
    c.draw()

    val r = Rectangle(3.0, 4.0)
    r.move(1.0, 2.0)
    println("넓이: ${r.area()}, 위치: (${r.x}, ${r.y})")
}

Python

상속 & ABC & Protocol

다형성을 구현하는 세 가지 방법: 상속, 추상 기반 클래스(ABC), 구조적 서브타이핑(Protocol).

from abc import ABC, abstractmethod
from typing import Protocol, runtime_checkable

# 추상 기반 클래스 (ABC)
class Animal(ABC):
    def __init__(self, name: str):
        self.name = name

    @abstractmethod
    def sound(self) -> str: ...

    def describe(self):   # 공통 구현
        print(f"{self.name}는 {self.sound()} 합니다")

class Dog(Animal):
    def sound(self) -> str: return "멍멍"

class Cat(Animal):
    def sound(self) -> str: return "야옹"

# Protocol — 구조적 서브타이핑 (duck typing 공식화)
@runtime_checkable
class Drawable(Protocol):
    def draw(self) -> None: ...

class Circle:   # Drawable을 명시적으로 상속하지 않음
    def draw(self): print("⭕ 원 그리기")

class Square:
    def draw(self): print("⬜ 사각형 그리기")

def render(shape: Drawable):
    shape.draw()

# super() 활용
class Vehicle:
    def __init__(self, brand: str):
        self.brand = brand

class ElectricCar(Vehicle):
    def __init__(self, brand: str, battery: int):
        super().__init__(brand)
        self.battery = battery

Dog("Rex").describe()          # Rex는 멍멍 합니다
render(Circle())               # ⭕ 원 그리기
print(isinstance(Circle(), Drawable))  # True

Go

인터페이스 — 덕 타이핑

Go 인터페이스는 암묵적으로 구현됩니다. 선언 없이 메서드만 맞으면 됩니다.

package main

import (
	"fmt"
	"math"
)

// 인터페이스 정의
type Shape interface {
	Area() float64
	Perimeter() float64
}

// Stringer 인터페이스 (fmt.Stringer)
type Stringer interface {
	String() string
}

// 구현체 — 명시적 선언 없음
type Rectangle struct{ W, H float64 }
func (r Rectangle) Area() float64      { return r.W * r.H }
func (r Rectangle) Perimeter() float64 { return 2*(r.W+r.H) }
func (r Rectangle) String() string {
	return fmt.Sprintf("Rect(%.1f×%.1f)", r.W, r.H)
}

type Circle struct{ R float64 }
func (c Circle) Area() float64      { return math.Pi * c.R * c.R }
func (c Circle) Perimeter() float64 { return 2 * math.Pi * c.R }

// 인터페이스를 파라미터로
func printShape(s Shape) {
	fmt.Printf("넓이: %.2f, 둘레: %.2f
", s.Area(), s.Perimeter())
}

// 인터페이스 슬라이스
func totalArea(shapes []Shape) float64 {
	var total float64
	for _, s := range shapes { total += s.Area() }
	return total
}

// 빈 인터페이스 — 모든 타입 허용
func describe(i interface{}) {
	fmt.Printf("타입: %T, 값: %v
", i, i)
}

// 타입 단언
func main() {
	shapes := []Shape{
		Rectangle{3, 4},
		Circle{5},
	}
	for _, s := range shapes { printShape(s) }
	fmt.Printf("총 넓이: %.2f
", totalArea(shapes))

	describe(42)
	describe("hello")
	describe(Rectangle{1, 2})

	// 타입 단언
	var s Shape = Rectangle{3, 4}
	if r, ok := s.(Rectangle); ok {
		fmt.Println("Rectangle:", r.W, r.H)
	}
}

Java

클래스 & 상속 & @Override

Java OOP의 핵심. 캡슐화, 상속, 다형성의 실전 적용.

// 부모 클래스
public abstract class Animal {
    private final String name;   // 캡슐화 (private)
    protected int age;

    public Animal(String name, int age) {
        this.name = name;
        this.age  = age;
    }

    // abstract — 하위 클래스에서 반드시 구현
    public abstract String sound();

    // 공통 구현
    public void introduce() {
        System.out.printf("%s: %s (나이 %d)%n",
            name, sound(), age);
    }

    public String getName() { return name; }
}

// 자식 클래스
public class Dog extends Animal {
    private final String breed;

    public Dog(String name, int age, String breed) {
        super(name, age);  // 부모 생성자 호출
        this.breed = breed;
    }

    @Override
    public String sound() { return "멍멍"; }

    @Override
    public String toString() {
        return String.format("Dog{name=%s, breed=%s}",
            getName(), breed);
    }
}

// final class — 상속 불가
public final class Cat extends Animal {
    public Cat(String name, int age) { super(name, age); }

    @Override public String sound() { return "야옹"; }
}

class Main {
    public static void main(String[] args) {
        Animal[] animals = {
            new Dog("Rex", 3, "진돗개"),
            new Cat("Kitty", 2),
        };

        for (Animal a : animals) {
            a.introduce();  // 다형성

            // instanceof + Pattern Matching (Java 16+)
            if (a instanceof Dog d) {
                System.out.println("견종: " + d.breed);
            }
        }
    }
}

PHP

상속 기본

기본 상속과 메서드 오버라이드를 가장 작은 예제로 보여줍니다.

<?php
class Animal {
    public function speak(): string {
        return '...';
    }
}

class Dog extends Animal {
    public function speak(): string {
        return 'woof';
    }
}

echo (new Dog())->speak();

JavaScript

상속 또는 확장

제목과 달리 코드에는 extends가 없고, name과 level을 갖는 User 클래스 하나가 summary()로 요약 문자열을 만들 뿐입니다. 계층이 한 단계면 이렇게 단일 클래스로 두는 편이 읽기 쉽습니다. 실제로 상속을 넣는 순간 파생 클래스 생성자는 this를 건드리기 전에 반드시 super()를 먼저 호출해야 합니다.

// 상속 또는 확장
class User {
  constructor(name, level) { this.name = name; this.level = level; }
  summary() { return `${this.name}:${this.level}`; }
}
console.log(new User("Ada", 5).summary());

TypeScript

상속 또는 확장

제목과 달리 코드에는 extends가 없고, name과 level을 갖는 User 클래스 하나가 summary()로 요약 문자열을 만들 뿐입니다. 계층이 한 단계면 이렇게 단일 클래스로 두는 편이 읽기 쉽습니다. 실제로 상속을 넣는 순간 파생 클래스 생성자는 this를 건드리기 전에 반드시 super()를 먼저 호출해야 합니다.

// 상속 또는 확장
class User {
  constructor(public name: string, public level: number) {}
  summary(): string { return `${this.name}:${this.level}`; }
}
console.log(new User("Ada", 5).summary());

C#

상속 또는 확장

제목과 달리 코드에는 extends가 없고, name과 level을 갖는 User 클래스 하나가 summary()로 요약 문자열을 만들 뿐입니다. 계층이 한 단계면 이렇게 단일 클래스로 두는 편이 읽기 쉽습니다. 실제로 상속을 넣는 순간 파생 클래스 생성자는 this를 건드리기 전에 반드시 super()를 먼저 호출해야 합니다.

// 상속 또는 확장
using System;

public class User { public User(string name, int level) { Name = name; Level = level; } public string Name { get; } public int Level { get; } }
Console.WriteLine(new User("Ada", 5).Name);

C++

상속 또는 확장

제목과 달리 코드에는 extends가 없고, name과 level을 갖는 User 클래스 하나가 summary()로 요약 문자열을 만들 뿐입니다. 계층이 한 단계면 이렇게 단일 클래스로 두는 편이 읽기 쉽습니다. 실제로 상속을 넣는 순간 파생 클래스 생성자는 this를 건드리기 전에 반드시 super()를 먼저 호출해야 합니다.

// 상속 또는 확장
#include <iostream>
#include <string>

struct User { std::string name; int level; };
int main() {
    User user{"Ada", 5};
    std::cout << user.name << "\n";
}

Rust

상속 또는 확장

제목과 달리 코드에는 extends가 없고, name과 level을 갖는 User 클래스 하나가 summary()로 요약 문자열을 만들 뿐입니다. 계층이 한 단계면 이렇게 단일 클래스로 두는 편이 읽기 쉽습니다. 실제로 상속을 넣는 순간 파생 클래스 생성자는 this를 건드리기 전에 반드시 super()를 먼저 호출해야 합니다.

// 상속 또는 확장
struct User { name: String, level: i32 }
fn main() {
    let user = User { name: "Ada".to_string(), level: 5 };
    println!("{}", user.name);
}

상속은 기본값이 아닙니다

클래스 상속은 코드 재사용 수단으로 배우지만, 실무에서는 부모의 변경이 자식 전부에게 번지는 결합을 만듭니다. 그래서 대부분의 최신 언어가 상속을 조심스럽게 다룹니다. Kotlin과 C#은 클래스가 기본적으로 상속 불가이고 열려면 open이나 virtual을 명시해야 합니다. Go와 Rust에는 클래스 상속 자체가 없습니다.

대안은 합성입니다. 기능을 가진 객체를 필드로 들고 필요한 메서드만 위임하면, 부모의 나머지 동작까지 물려받지 않고 원하는 부분만 조립할 수 있습니다. 상속은 "A는 B다"가 모든 맥락에서 참일 때만 쓰고, 그 외에는 합성을 기본으로 두는 편이 나중에 덜 아픕니다.

다중 상속을 어떻게 피했는가

둘 이상의 부모에게서 같은 이름의 구현을 물려받으면 어느 쪽을 쓸지 모호해집니다. C++은 다중 상속을 허용하되 가상 상속이라는 별도 장치로 이 문제를 다루고, Java·C#·Kotlin은 클래스는 하나만 상속하되 인터페이스는 여러 개 구현하도록 제한했습니다.

인터페이스에 기본 구현을 넣을 수 있게 되면서(Java의 default 메서드, Kotlin·PHP의 인터페이스 기본 구현) 이 경계가 다시 흐려졌습니다. 두 인터페이스가 같은 시그니처의 기본 구현을 주면 구현 클래스가 직접 골라 재정의해야 합니다. 편리한 기능이지만 여러 개를 겹칠수록 추적이 어려워집니다.

동적 디스패치의 비용

인터페이스 타입으로 받아 두면 실제 구현이 실행 시점에 정해집니다. 유연한 대신 호출 비용이 조금 늘고, 컴파일러의 인라인 최적화가 어려워집니다. Rust의 dyn Trait, Go의 인터페이스 값, Java의 인터페이스 호출이 모두 여기 해당합니다.

대부분의 코드에서 이 비용은 무시해도 되지만, 좁은 루프 안에서 수백만 번 호출된다면 제네릭으로 컴파일 시점에 타입을 고정하는 편이 낫습니다. 선택 기준은 제네릭 비교에서 이어집니다.