is-a 관계와 has-a 관계
Is-a 관계 (상속)
- 클래스를 상속 받거나 인터페이스를 구현함을 나타낸다.
- 하나의 클래스가 다른 클래스의 구체화된 버전이라는 의미다.
- 커플링이 강하여 부모가 자식에게 영향을 준다.
- 컴파일 타임에 위계 구조가 고정된다.
- 런타임에 상속받을 부모 클래스를 변경하거나, 조건부적으로 상속받을 수 없다.
- 다형성이 필요하거나 클래스들 간 관계가 변경될 일이 없을 때 사용한다.
-
코드 예시
// Base class class Vehicle { void start() { System.out.println("Vehicle starting"); } } // Derived class - Car IS-A Vehicle class Car extends Vehicle { void drive() { System.out.println("Car driving"); } } // Interface example interface Flyable { void fly(); } // Bird IS-A Flyable class Bird implements Flyable { public void fly() { System.out.println("Bird flying"); } }
Has-a 관계 (구성/집합)
- 구성(composition) 또는 집합(aggregation) 관계를 나타낸다.
- Composition: 강한 전체와 부품의 관계. 부품은 전체가 없이는 존재할 수 없다.
- Aggregation: 약한 전체와 부품의 관계. 부품은 전체가 없어도 존재할 수 있다.
- 하나의 클래스가 다른 클래스의 요소를 포함하거나 사용한다는 의미이다.
- 커플링이 약하여 포함된 객체가 변경될 수 있다.
- 런타임에 행동이 변경 가능하다.
- 클래스들 간의 관계가 추후에 변경될 수 있거나 상속의 관계 없이 다른 클래스의 기능을 사용하고 싶을 때 따르는 구조이다.
-
코드 예시
// Engine class class Engine { void ignite() { System.out.println("Engine ignited"); } } // Car HAS-A Engine (Composition) class Car { private Engine engine; // Composition public Car() { this.engine = new Engine(); // Engine created with Car } void start() { engine.ignite(); } } // Department class class Department { private String name; public Department(String name) { this.name = name; } } // University HAS-A Department (Aggregation) class University { private List<Department> departments; // Aggregation public University(List<Department> departments) { this.departments = departments; // Departments exist independently } }
➕상속보다 구성 원칙(Composition Over Inheritance principle)
- 객체들을 상속보다 구성의 방식으로 구축하는 방식을 제안하는 OOP 디자인 원칙 중 하나다.
- 상속의 강한 커플링, 클래스의 경우 하나밖에 상속할 수 없는 점, 부모 클래스가 자식 클래스에게 끼치는 영향 등상속의 단점을 해소하기 위한 방식이다.
-
상속 대신에 인터페이스 또는 컴포지션 방식을 택한다.
// Instead of inheritance, use interfaces and composition interface FlyBehavior { void fly(); } interface QuackBehavior { void quack(); } class FlyWithWings implements FlyBehavior { public void fly() { System.out.println("Flying with wings"); } } class FlyNoWay implements FlyBehavior { public void fly() { System.out.println("Can't fly"); } } class Duck { private FlyBehavior flyBehavior; private QuackBehavior quackBehavior; public Duck(FlyBehavior flyBehavior, QuackBehavior quackBehavior) { this.flyBehavior = flyBehavior; this.quackBehavior = quackBehavior; } void performFly() { flyBehavior.fly(); } void performQuack() { quackBehavior.quack(); } // Dynamically change behavior at runtime void setFlyBehavior(FlyBehavior fb) { this.flyBehavior = fb; } } // Usage Duck mallard = new Duck(new FlyWithWings(), new LoudQuack()); Duck rubberDuck = new Duck(new FlyNoWay(), new Squeak()); - 장점
-
유연성 및 런타임 행동의 변화 가능
class Character { private WeaponBehavior weapon; // Composition void setWeapon(WeaponBehavior weapon) { this.weapon = weapon; // Change behavior at runtime } void fight() { weapon.useWeapon(); } } // Character can switch weapons dynamically character.setWeapon(new Sword()); character.fight(); // Uses sword character.setWeapon(new Bow()); character.fight(); // Now uses bow -
상속보다 나은 캡슐화
// Inheritance exposes protected members class Base { protected String internalData; // Exposed to subclasses } // Composition keeps implementation hidden class Container { private Base base; // Internal implementation hidden public void doSomething() { // Use base internally } } -
클래스의 과도한 추가 방지
// Without composition: combinatorial explosion // class FlyingSwordWieldingElf extends Elf {} // class FlyingAxeWieldingElf extends Elf {} // class WalkingSwordWieldingElf extends Elf {} // ... and so on // With composition: combine behaviors class GameCharacter { private MovementBehavior movement; private WeaponBehavior weapon; private Race race; // Combine independent behaviors GameCharacter elfArcher = new GameCharacter( new FlyingMovement(), new BowWeapon(), new ElfRace() ); }
-
- 실무에서의 구현 패턴
-
Strategy 패턴 (행동적인 구성)
interface PaymentStrategy { void pay(double amount); } class CreditCardPayment implements PaymentStrategy { private String cardNumber; public void pay(double amount) { System.out.println("Paid " + amount + " using credit card"); } } class PayPalPayment implements PaymentStrategy { private String email; public void pay(double amount) { System.out.println("Paid " + amount + " using PayPal"); } } class ShoppingCart { private PaymentStrategy paymentStrategy; public void setPaymentStrategy(PaymentStrategy strategy) { this.paymentStrategy = strategy; } public void checkout(double amount) { paymentStrategy.pay(amount); } } -
Decorator 패턴 (구조적인 구성)
interface Coffee { double getCost(); String getDescription(); } class BasicCoffee implements Coffee { public double getCost() { return 5.0; } public String getDescription() { return "Basic coffee"; } } abstract class CoffeeDecorator implements Coffee { protected Coffee decoratedCoffee; public CoffeeDecorator(Coffee coffee) { this.decoratedCoffee = coffee; } } class MilkDecorator extends CoffeeDecorator { public MilkDecorator(Coffee coffee) { super(coffee); } public double getCost() { return decoratedCoffee.getCost() + 1.5; } public String getDescription() { return decoratedCoffee.getDescription() + ", Milk"; } } // Usage: Compose features dynamically Coffee myCoffee = new MilkDecorator( new SugarDecorator( new BasicCoffee())); -
의존성 주입 (생성자 구성)
class DataProcessor { private final Repository repository; private final Validator validator; // Dependencies injected via constructor public DataProcessor(Repository repo, Validator validator) { this.repository = repo; this.validator = validator; } public void process(Data data) { if (validator.isValid(data)) { repository.save(data); } } }
-
- 그럼에도 상속을 써야 하는 경우
-
공유하는 정체성이 있는 실제 “is-a” 관계
// Good inheritance: Shape hierarchy abstract class Shape { abstract double area(); abstract double perimeter(); } class Circle extends Shape { // Circle IS-A Shape private double radius; double area() { return Math.PI * radius * radius; } } -
위계 구조가 필요한 프레임워크 / API 디자인
// Java Collections Framework public abstract class AbstractList<E> implements List<E> { // Provides skeletal implementation // Subclasses fill in specifics } -
Template Method 패턴
abstract class DataParser { // Template method with fixed algorithm public final void parse() { // final to prevent overriding readData(); processData(); // Abstract - subclass implements saveData(); } protected abstract void processData(); }
-
- Composition을 지원하는 Java의 특징
-
인터페이스의 default 메서드
interface Logger { void log(String message); // Default implementation - like inheritance but composable default void logError(String error) { log("ERROR: " + error); } } // Multiple interfaces can be composed class FileLogger implements Logger, AutoCloseable { public void log(String message) { /* write to file */ } public void close() { /* close file */ } } -
데이터 구성을 위한 레코드
// Records automatically compose equals(), hashCode(), toString() record Point(int x, int y) {} record Line(Point start, Point end) { // Composition double length() { return Math.hypot(end.x() - start.x(), end.y() - start.y()); } }
-