다형성은 객체지향 프로그래밍(OOP)의 중요한 개념으로, 같은 타입의 변수나 메서드가 여러 형태로 동작할 수 있는 특성을 말한다. 자바에서는 주로 상속, 인터페이스, 그리고 메서드 오버로딩/오버라이딩을 통해 다형성을 구현한다.
코드의 유연성 :
상위 타입 변수로 다양한 하위 클래스 객체를 다룰 수 있어 코드가 간결해진다.
확장성 :
새로운 하위 클래스를 추가해도 기존 코드를 수정하지 않아도 된다.
재사용성 :
상위 클래스에서 정의한 메서드를 하위 클래스가 재사용하거나 재정의하여 효율적으로 활용 가능하다.
오버로딩(Overloading)
1. 개념
특징
add(int a, int b)와 add(double a, double b)처럼 매개변수 목록이 달라야 한다. 예시
class Calculator {
int add(int a, int b) {
return a + b;
}
double add(double a, double b) {
return a + b;
}
int add(int a, int b, int c) {
return a + b + c;
}
}
add지만 매개변수의 타입, 개수 등이 달라서 서로 다른 메서드로 구분된다.오버라이딩(Overriding)
1. 개념
특징
@Override 애노테이션을 사용하면 컴파일 시점에 재정의 여부를 체크할 수 있다.예시
class Parent {
void greet() {
System.out.println("Hello from Parent");
}
}
class Child extends Parent {
@Override
void greet() {
System.out.println("Hello from Child");
}
}
public class Main {
public static void main(String[] args) {
Parent p = new Parent();
p.greet(); // "Hello from Parent"
Parent c = new Child();
c.greet(); // "Hello from Child" (오버라이딩 메서드 호출)
}
}
Child 클래스는 Parent의 greet() 메서드를 재정의하여, 객체 타입(런타임 타입)에 따라 다른 구현이 실행된다.정의
객체 지향 프로그래밍(OOP)에서 상위(부모) 클래스의 속성과 메서드를 하위(자식) 클래스가 물려받아 재사용하는 개념이다.
코드 재사용성을 높이고, 계층 구조로 클래스를 조직화할 수 있게 해 준다.
결합도 증가
하위 클래스가 상위 클래스에 강하게 의존한다.
상위 클래스 변경이 하위 클래스에 연쇄적으로 영향을 미칠 수 있다.
설계 유연성 저하
상속 구조가 깊어질수록, 객체 구조(클래스 계층)가 복잡해진다.
부모 클래스에 대한 종속이 커서 코드 수정 시 주의가 필요하다.
클래스 폭발(Class Explosion)
기능별로 세분화하다 보면 불필요하게 많은 하위 클래스를 만들 가능성이 있다.
관리가 어려워지고, 잘못된 설계로 이어질 수 있다.
자바에서 상속은 extends 키워드를 사용하여 구현한다.
// 상위(부모) 클래스
class Animal {
String name;
void eat() {
System.out.println(name + " is eating.");
}
}
// 하위(자식) 클래스
class Dog extends Animal {
void bark() {
System.out.println(name + " says: Woof!");
}
}
public class Main {
public static void main(String[] args) {
Dog dog = new Dog(); // Dog 객체 생성
dog.name = "Rex"; // Animal 클래스에서 물려받은 name 필드 사용
dog.eat(); // Animal 클래스의 eat() 메서드 사용
dog.bark(); // Dog 클래스에서 추가로 정의한 bark() 메서드 사용
}
}
Rex is eating.
Rex says: Woof!
Dog 클래스는 Animal 클래스를 상속받는다(class Dog extends Animal).
Dog 객체는 상위 클래스(Animal)의 필드와 메서드(name, eat)를 그대로 사용할 수 있고, bark() 같은 고유 기능을 추가로 가질 수 있다.
super 키워드상속받은 클래스(하위 클래스)에서 부모 클래스의 멤버에 접근하거나 부모 클래스 생성자를 호출할 때는 super 키워드를 사용할 수 있다.
class Animal {
String name;
Animal(String name) {
this.name = name;
}
void eat() {
System.out.println(name + " is eating.");
}
}
class Dog extends Animal {
// Dog 생성자가 호출될 때, 부모 클래스(Animal)의 생성자도 호출
Dog(String name) {
super(name); // 부모 생성자 호출
}
void bark() {
System.out.println(name + " says: Woof!");
}
void parentEat() {
super.eat(); // 부모 클래스의 eat() 메서드 직접 호출
}
}
public class Main {
public static void main(String[] args) {
Dog dog = new Dog("Buddy");
dog.eat(); // Buddy is eating.
dog.bark(); // Buddy says: Woof!
dog.parentEat(); // 내부적으로도 super.eat() 사용
}
}
super(name);는 하위 클래스가 상위 클래스 생성자에 name을 전달하여 초기화하는 부분이다.
super.eat();는 자식 메서드에서 부모의 메서드를 명시적으로 호출할 때 사용한다
기본 생성자가 있으면, 자바 컴파일러가 자동으로 super() 호출을 삽입해 준다.
매개변수 있는 생성자만 존재한다면, 자식 쪽에서 반드시 super(…)를 써서 적절한 부모 생성자를 호출해야 한다.
메서드 호출(super.method()) 부분은 필수는 아니지만, 부모 메서드의 실행 로직을 유지·확장하거나, 특정 부모 로직을 이용할 때 유용하다.
단일 상속
자바는 단일 상속만 지원한다. 즉, 한 클래스가 여러 부모 클래스를 동시에 상속받을 수 없다.
다중 상속이 필요한 경우, 인터페이스(interface)를 통해 구현 다형성을 확장한다.
final 클래스
final 키워드가 붙은 클래스는 상속할 수 없다.
예: public final class String은 자식 클래스를 둘 수 없다.
접근 제한자
private 멤버는 하위 클래스에서 직접 접근할 수 없고, protected 멤버는 같은 패키지나 하위 클래스에서 접근 가능하다.메서드 오버라이딩
| 구분 | 상속 | 조합 |
|---|---|---|
| 개념 | 부모 클래스의 속성과 메소드를 자식 클래스가 물려받아 사용하는 방식. | 객체가 다른 객체를 포함하여 기능을 확장하거나 동작을 조합하는 방식. |
| 결합도 | 부모 클래스와 자식 클래스 간의 강한 결합을 초래. 부모 클래스의 변경이 자식 클래스에 영향을 미침. | 객체 간의 관계를 느슨하게 유지하여 변경에 대한 영향을 최소화함. |
| 재사용성 | 코드의 재사용성을 높이고 계층 구조를 통해 논리적인 관계를 표현 가능. | 객체의 기능을 재사용할 수 있으며, 더 유연한 설계를 가능하게 함. |
| 유연성 | 부모-자식 간의 고정된 계층 구조로 인해 상대적으로 유연성이 낮음. | 객체를 포함하여 동작을 확장하므로 더 유연하며 설계 변경이 쉬움. |
| 적용 상황 | - "is-a" 관계를 표현할 때 적합. 예: Dog is-a Animal. | - "has-a" 관계를 표현할 때 적합. 예: Car has-a Engine. |
| 단점 | - 부모 클래스의 변경이 자식 클래스에 미치는 영향이 큼. - 계층 구조가 깊어지면 관리가 어려워질 수 있음. | - 설계가 복잡할 수 있음. - 포함된 객체와의 관계를 명확히 이해하고 설계해야 함. |
| 장점 | - 코드의 중복을 줄임. - 계층 구조를 통해 논리적인 관계를 직관적으로 표현 가능. | - 결합도를 낮추어 독립성을 높임. - 변경 및 확장이 용이함. |
class Animal {
void eat() {
System.out.println("Eating...");
}
}
class Dog extends Animal {
void bark() {
System.out.println("Barking...");
}
}
// 사용
Dog dog = new Dog();
dog.eat(); // 부모 클래스의 메소드 사용
dog.bark();
class Engine {
void start() {
System.out.println("Engine starting...");
}
}
class Car {
private Engine engine;
Car() {
this.engine = new Engine();
}
void start() {
engine.start();
System.out.println("Car starting...");
}
}
// 사용
Car car = new Car();
car.start();
이러한 차이를 명확히 이해하고 상황에 맞는 방식을 선택하는 것이 중요합니다.
참고 : 상속과 조합의 차이점 이해하기
정의:
자바에서 객체의 실제 타입(클래스 또는 인터페이스)을 확인하기 위한 이항 연산자이다.
예) if (obj instanceof String) { ... }
왼쪽 피연산자는 객체, 오른쪽 피연산자는 클래스나 인터페이스가 온다.
결과는 true 또는 false를 반환한다.
용도:
런타임 시 객체가 특정 클래스나 인터페이스 타입에 속하는지 확인할 때 사용한다.
캐스팅((Type) obj) 전 안전 검사 용도로 많이 쓴다.
다형성을 구현한 상황에서, 특정 하위 클래스만의 메서드가 필요할 때(다운캐스팅) 타입 검증용으로 사용한다.
예시:
Object obj = "Hello";
if (obj instanceof String) {
String str = (String) obj; // 안전하게 캐스팅
System.out.println(str.toUpperCase());
}
OOP 원칙 위배 가능성
객체의 실제 타입을 직접 구분하는 로직이 많아지면, 다형성을 통한 확장성·유연성을 해칠 수 있다.
자식 클래스가 늘어날 때마다 if (obj instanceof SubClass) 같은 조건 분기가 증가하여 코드가 복잡해진다.
유지보수성 저하
여러 곳에서 instanceof 검사 후 다운캐스팅하는 코드를 자주 쓰면, 새로운 하위 클래스를 추가할 때마다 이 검사를 모두 수정해야 할 수 있다.
“수정해야 할 범위”가 커져, 에러 발생 위험도 증가한다.
코드 가독성 저하
instanceof와 다운캐스팅이 반복되는 로직은 if-else 블록이 여러 개로 늘어나 코드 읽기가 어렵다. instanceof로 분기하면, 메서드 호출만으로 해결될 상황을 복잡하게 만든다.대안
가능하다면 다형성을 적극 활용하여, 하위 클래스에서 오버라이딩 메서드를 제공하게 하고 상위 타입에서 동적 바인딩으로 호출하게 하는 것이 권장된다.
그래도 instanceof가 필요한 상황(예: 특정 인터페이스 구현 여부 검사, 조건부 다운캐스팅 등)에서는 최소한으로 사용하는 것이 바람직하다.
interface는 자바에서 추상적(추상 메서드의 모음)인 규격을 정의하는 한 형태의 참조 타입이다. 주된 목적은 메서드 시그니처(메서드 이름, 매개변수, 반환 타입)만을 선언하고, 해당 메서드들의 구현체(실제 로직)는 이를 구현하는 클래스에서 작성하도록 하는 것이다. 자바 8 이후로는 default 메서드(구현부 포함), 정적 메서드, private 메서드를 추가할 수도 있지만, 본질적으로는 “기능을 정의해 놓고 구현은 없는” 역할을 맡는다.
추상 메서드의 집합
public interface MyInterface {
void doSomething();
int calculate(int x, int y);
}default 메서드(구현부 포함)나 static 메서드, private 메서드도 선언 가능하다.다중 구현
public class MyClass implements InterfaceA, InterfaceB {
// InterfaceA, InterfaceB의 모든 추상 메서드 구현
}계약(Contract) 역할
느슨한 결합(Loose Coupling)
인터페이스를 통해 실제 구현체가 바뀌어도 상위 로직(인터페이스를 사용하는 코드)을 수정할 필요가 줄어든다.
예)
public interface Logger {
void log(String message);
}
public class FileLogger implements Logger {
@Override
public void log(String message) {
// 파일에 로그 남기기
}
}
public class ConsoleLogger implements Logger {
@Override
public void log(String message) {
// 콘솔에 로그 남기기
}
}
// 의존성 주입으로 원하는 구현체 사용 가능
public class MyService {
private Logger logger;
public MyService(Logger logger) {
this.logger = logger;
}
// ...
}
MyService는 Logger 인터페이스를 사용하므로, 파일로 찍든 콘솔로 찍든 구현체 교체가 자유롭다.
상수 선언 가능
public static final로 인식된다. static final 상수를 선언할 때 사용한다.추상 클래스(abstract class)는 자바에서 공통된 특징과 일부 구체적인 구현을 함께 제공하면서, 완성되지 않은(추상적인) 메서드를 포함해 자식 클래스에서 구체화(구현)해야 하는 부분을 남겨두는 클래스다. 즉, 상속받는 클래스가 반드시 재정의해야 할 메서드와 이미 구현된 메서드를 함께 가질 수 있는 일종의 ‘설계도’ 역할을 한다.
abstract 키워드 사용
abstract 키워드를 붙여 선언한다. public abstract class Animal {
// ...
}추상 메서드
abstract 키워드를 붙인다. public abstract void sound();구체 메서드와 멤버
인스턴스화 불가
추상 클래스 자체로는 객체(인스턴스)를 생성할 수 없다.
반드시 추상 클래스를 상속받은 자식 클래스에서 추상 메서드를 구현한 뒤, 그 자식 클래스를 통해 인스턴스를 생성한다.
예:
// 불가능
Animal a = new Animal(); // 컴파일 에러
// 자식 클래스를 통해 가능
Animal d = new Dog(); // OK
상속과 다형성
// 추상 클래스
public abstract class Animal {
String name;
// 추상 메서드 (구현부 없음)
public abstract void sound();
// 구체(완성) 메서드
public void eat() {
System.out.println(name + " is eating.");
}
}
// 구체 클래스(추상 클래스 Animal을 상속받아 구현)
public class Dog extends Animal {
// 추상 메서드 구현(오버라이딩)
@Override
public void sound() {
System.out.println("Bark!");
}
}
public class Main {
public static void main(String[] args) {
// 불가능: 추상 클래스로는 직접 객체 생성 X
// Animal animal = new Animal(); // 컴파일 에러
// 가능: 추상 클래스를 상속받아 만든 구체 클래스의 인스턴스
Dog dog = new Dog();
dog.name = "Rex";
dog.eat(); // Rex is eating.
dog.sound(); // Bark!
}
}
Animal은 추상 클래스이며, sound() 메서드를 추상 메서드로 선언했다. Dog 클래스는 Animal을 상속받아, 반드시 sound() 메서드를 구현해야 한다. Animal 자체로는 인스턴스를 만들 수 없지만, Dog 객체를 생성해 사용할 수 있다.a. 구현(메서드 몸체) 유무와 멤버 구성
default, static 메서드를 통해 일부 구현이 가능하지만 상태(필드)를 가질 수는 없다(상수 제외). b. 상속(implements / extends) 방식과 다중 상속 여부
implements 키워드를 통해 구현하며, 다중 구현 가능 (클래스 하나가 여러 인터페이스를 동시에 구현할 수 있음). extends 키워드로 상속하며, 자바에서는 단일 상속만 허용 (이미 다른 클래스를 상속 중이라면 추상 클래스 상속 불가).| 구분 | 인터페이스 | 추상 클래스 |
|---|---|---|
| 주요 목적 | - 기능(메서드 시그니처)만 정의하고, 구현은 전적으로 클래스가 맡도록 - 서로 다른 계층/타입에도 동일한 기능(규약)을 강제하고 싶을 때 | - 공통된 필드, 메서드를 일부 구현해 두고, 일부는 추상 메서드로 남겨두어 자식 클래스에서 재정의하도록 - 같은 계층(유사한 도메인)에서만 공유할 수 있는 기본 로직을 제공하고 싶을 때 |
| 상속 여부 | - 다중 구현 가능 (implements)- 클래스가 여러 인터페이스를 동시에 구현할 수 있음 | - 단일 상속 (extends)- 이미 다른 클래스를 상속 중이면 추상 클래스 상속 불가능 |
| 상태(필드) 보유 | - 원칙적으로 불가능 (상수 public static final만 허용)- 자바 8 이후 default 메서드로 일부 구현 가능하지만 필드는 없음 | - 일반 클래스처럼 필드와 구현 메서드를 가질 수 있음 - abstract 메서드는 자식이 반드시 재정의 |
| 사용 예 | - 여러 클래스에 통일된 기능(메서드 시그니처)이 필요하지만, 내부 구현이 달라야 할 때 - 예: Comparable, Runnable | - 공통된 로직 + 필수적으로 구현해야 할 추상 메서드를 함께 제공할 때 - 예: HttpServlet(서블릿 API) |
인터페이스
주로 여러 클래스에 같은 기능(메서드 시그니처)을 강제하고 싶을 때 사용한다.
다양한 계층의 클래스가 동일한 행위를 구현하도록 할 때, 다중 구현이 가능하므로 유연하다.
추상 클래스
같은 계층(유사한 도메인) 내에서 공통된 로직이나 상태를 일부 제공하면서, 구체적인 구현은 하위 클래스에 맡길 때 사용한다.
단일 상속 구조에서 일부는 완성된 메서드로, 일부는 추상 메서드로 둬서 재정의하도록 강제한다.
final 키워드는 “한 번 결정된 뒤 더는 변경할 수 없다”는 의미를 나타낸다.
한 번만 값 할당 가능
final이 붙은 변수는 한 번 값이 설정되면 더는 변경할 수 없다. final int MAX_SPEED = 100;
// MAX_SPEED = 150; // 컴파일 에러상수 용도로 활용
static과 함께 final을 사용해 상수로 선언한다(주로 대문자로 표기). public static final int MIN_AGE = 18;생성자에서 초기화 가능
final은 생성자를 통해 초기화가 가능하다. final 메서드는 자식 클래스에서 재정의(override)할 수 없다.
예)
class Parent {
final void cannotOverride() {
System.out.println("This is final method");
}
}
class Child extends Parent {
// void cannotOverride() { ... } // 컴파일 에러 (오버라이딩 불가능)
}
상속 불가
final이 붙은 클래스는 상속할 수 없다. public final class String {
// ...
}String 클래스가 그 대표적인 예이며, 다른 개발자가 이 클래스를 상속해서 내부 구조를 바꾸지 못하도록 막는다.의도