먼저 상속과 다형성의 개념과 필요성을 이해하고, 자바에서 이를 어떻게 구현하는지 코드 예제로 확인한 후, 실무에서 어떻게 활용할 수 있는지 예제와 함께 정리하고자 한다.
1. 상속(Inheritance)이란?
- 기존 클래스를 기반으로 새로운 클래스를 만들고 코드를 재사용하는 기법.
- 자식(하위) 클래스가 부모(상위) 클래스의 속성과 메서드를 물려받는다.
1-1. 상속이 필요한 이유
- 코드의 재사용성 증가
- 중복 코드를 줄이고 유지보수성을 높일 수 있음.
- 확장성
- 새로운 기능을 추가할 때 기존 클래스를 수정하지 않고 확장 가능.
- 객체 간의 계층 구조 형성
- 현실 세계를 더 직관적으로 모델링 가능
- 예: 동물 -> 개, 고양이
1-2. 상속 구현 방법(예제 코드)
// 부모 클래스 (상위 클래스)
public class Animal {
public void eat() {
System.out.println("This animal eats food.");
}
}
// 자식 클래스 (하위 클래스)
public class Dog extends Animal {
public void bark() {
System.out.println("The dog barks.");
}
}
// 실행 코드
public class Main {
public static void main(String[] args) {
Dog dog = new Dog();
dog.eat(); // 부모 클래스의 메서드 사용
dog.bark(); // 자식 클래스의 메서드 사용
}
}
결과
This animal eats food.
The dog barks.
- Dog 클래스는 Animal 클래스를 상속받아 eat() 메서드를 사용 가능.
- Dog 클래스에서 bark() 메서드를 추가하여 새로운 기능을 확장.
1-4. 상속의 한계(오버라이딩과 오버로딩)
- 단순 상속만으로는 부모 클래스의 기능을 수정할 수 없다.
- 부모 클래스의 기능을 변경하고 싶다면? 오버라이딩(Overriding)
- 동일한 메서드 이름을 유지하면서 다양한 입력을 처리하고 싶다면? 오버로딩(Overloading)
(1) 메서드 오버라이딩(Overriding)
- 부모클래스의 메서드를 자식 클래스에서 재정의하는 기법.
@Override
어노테이션을 사용하여 가독성 높일 수 있음
public class Animal {
public void makeSound() {
System.out.println("Animal makes a sound");
}
}
public class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("Dog barks");
}
}
public class Main {
public static void main(String[] args) {
Animal myDog = new Dog();
myDog.makeSound(); // Dog barks
}
}
- 부모 클래스의 makeSound()를 자식 클래스에서 재정의함.
- 부모 타입으로 선언했지만 오버라이딩된 메서드가 실행됨.(다형성 적용)
(2) 메서드 오버로딩(Overloading)
- 같은 이름의 메서드를 다른 매개변수로 정의하여 유연성을 제공.
public class MathUtil {
public int add(int a, int b) {
return a + b;
}
public double add(double a, double b) {
return a + b;
}
}
public class Main {
public static void main(String[] args) {
MathUtil util = new MathUtil();
System.out.println(util.add(3, 5)); // 8
System.out.println(util.add(2.5, 3.5)); // 6.0
}
}
- 메서드 이름은 같지만 매개벼수 타입이 다르면 서로 다른 메서드로 인식됨.
1-5. 상속의 단점
- 부모 클래스가 변경되면, 모든 자식 클래스가 영향을 받음(강한 결합 문제)
- 다중 상속을 지원하지 않아 유연성이 떨어짐
- 해결책: 인터페이스를 사용하여 유연성 확보 가능
// 상속 대신 인터페이스 활용
public interface SoundMaker {
void makeSound();
}
public class Dog implements SoundMaker {
@Override
public void makeSound() {
System.out.println("Dog barks");
}
}
public class Main {
public static void main(String[] args) {
SoundMaker dog = new Dog();
dog.makeSound();
}
}
- 인터페이스를 활용하면 클래스 간 결합도 낮출 수 있음
2. 다형성(Polymorphism)이란?
- 같은 메서드를 다른 객체에서 다르게 동작하도록 하는 원리.
- 부모 클래스 타입으로 여러 자식 클래스 객체를 다룰 수 있음.
- 유연성과 확장성이 증가하여 유지보수가 쉬워짐.
2-1. 다형성 구현 방법
public class Main {
public static void main(String[] args) {
Animal myAnimal = new Dog(); // 부모 타입으로 자식 객체 참조
myAnimal.makeSound(); // "Dog barks"
}
}
- Animal 타입 변수에 Dog 객체를 대입 -> 업캐스팅(Upcasting)
- makeSound()를 호출했을 때, 오버라이딩된 자식 클래스의 메서드가 실행됨.
2-2. 다형성의 장점
- 유연한 코드 작성 가능
- 같은 메서드를 호출하더라도 다른 객체에서 다르게 동작
- 확장성이 뛰어남
- 새로운 객체(클래스)를 추가해도 기존 코드 수정이 거의 필요 없음
- 코드 유지보수성 증가
- 부모 타입을 사용하면 새로운 기능을 추가해도 기존 코드 수정 최소화.
2-3. 다형성 활용 예제
public abstract class Animal {
public abstract void makeSound();
}
public class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("Dog barks");
}
}
public class Cat extends Animal {
@Override
public void makeSound() {
System.out.println("Cat meows");
}
}
public class Main {
public static void main(String[] args) {
Animal[] animals = {new Dog(), new Cat()};
for (Animal animal : animals) {
animal.makeSound(); // 다형성 적용
}
}
}
- Animal 배열을 통해 다양한 객체를 한 번에 처리할 수 있음.
- 출력 결과
Dog barks Cat meows
2-4. 다형성의 실무 활용 사례
다형성은 유연한 코드 작성과 유지보수성을 높이는 핵심 원리이다. 실무에서는 다형성을 활용하여 다양한 객체를 동일한 방식으로 처리할 수 있으며, 특히 프레임워크(Spring)에서 의존성 주입(DI)과 함께 사용된다.
다형성을 활용한 알림 서비스 예제
- 이메일과 SNS 알림을 전송하는 시스템을 만들 때, NotificationService 인터페이스를 사용하면 새로운 알림 유형을 쉽게 추가할 수 있음.
// 1. 공통 인터페이스 정의
public interface NotificationService {
void sendNotification(String message);
}
// 2. 이메일 알림 서비스 구현
public class EmailNotification implements NotificationService {
@Override
public void sendNotification(String message) {
System.out.println("Email sent: " + message);
}
}
// 3. SMS 알림 서비스 구현
public class SMSNotification implements NotificationService {
@Override
public void sendNotification(String message) {
System.out.println("SMS sent: " + message);
}
}
// 4. 다형성을 활용한 알림 전송 클래스
public class NotificationSender {
private NotificationService notificationService;
public NotificationSender(NotificationService notificationService) {
this.notificationService = notificationService;
}
public void send(String message) {
notificationService.sendNotification(message);
}
}
// 5. 실행 예제
public class Main {
public static void main(String[] args) {
NotificationSender emailSender = new NotificationSender(new EmailNotification());
emailSender.send("Hello via Email!");
NotificationSender smsSender = new NotificationSender(new SMSNotification());
smsSender.send("Hello via SMS!");
}
}
예시 정리
- 확장성 증가: 새로운 알림 방식(예: 카카오톡 알림) 추가 시 기존 코드 수정 없이 확장 가능.
- 유지보수 용이: 특정 알림 방식의 내부 구현을 변경해도 NotificationSender 클래스에는 영향을 주지 않음.
- 스프링(Spring)과 같은 프레임워크에서 활용 가능:
- NotificationService를 Spring Bean으로 등록하고 의존성 주입(DI)을 통해 동적으로 객체를 주입받을 수 있음.
2-5. 다형성과 제네릭(Generic) 활용
다형성과 제네릭을 함께 사용하면 더 유연한 코드를 작성할 수 있다.
제네릭을 활용하면, 타입을 유연하게 유지하면서 코드의 중복을 줄이고 다양한 객체에 대해 동일한 로직을 적용할 수 있음.
제네릭을 활용한 프린터 클래스 예제
// 제네릭을 활용한 다형성 구현
public class Printer<T> {
private T item;
public Printer(T item) {
this.item = item;
}
public void print() {
System.out.println(item);
}
}
public class Main {
public static void main(String[] args) {
Printer<Integer> intPrinter = new Printer<>(123);
Printer<String> stringPrinter = new Printer<>("Hello OOP!");
intPrinter.print(); // 123 출력
stringPrinter.print(); // Hello OOP! 출력
}
}
예제 정리
- 코드 중복 제거: Printer
클래스 하나만 만들어두면, 다양한 타입의 데이터를 처리 가능. - 유지보수성 증가: 데이터 타입을 바꿀 필요 없이 제네릭을 활용해 타입을 유연하게 설정 가능.
- 컬렉션과 함께 활용 가능: List
, Map<K, V> 등의 컬렉션 API와 함께 사용하면 더욱 강력함.
제네릭과 다형성을 활용한 리스트 처리
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Animal> animals = new ArrayList<>();
animals.add(new Dog());
animals.add(new Cat());
for (Animal animal : animals) {
animal.makeSound(); // 다형성 적용
}
}
}
- List
을 사용하여 다양한 동물을 한 번에 처리할 수 있음. - 새로운 동물 클래스를 추가해도 기존 코드를 수정할 필요가 없음.
정리
✔︎ 상속(Inheritance)
- 코드 재사용성과 유지보수성을 높인다.
- 부모 클래스의 기능을 물려받아 새로운 클래스 쉽게 생성 가능
✔︎ 다형성(Polymorphism)
- 같은 메서드 호출이 객체에 따라 다르게 동작.
- 유지보수와 확장성이 뛰어나고, 다양한 객체를 쉽게 다룰 수 있음.
- 제네릭과 함께 사용하면 코드 중복을 줄이고 다양한 타입을 처리할 수 있음.
✔︎ 실무 적용
- 스프링(Spring) 같은 프레임워크에서 의존성 주입(DI) 및 인터페이스 기반 개발에 활용됨.
- List
animals = new ArrayList<>(); → 다양한 구현체를 다룰 수 있음.
'개발 > Basics' 카테고리의 다른 글
[Java/Basics] 자바 기초 스터디 4주차: 예외처리와 스트림 (0) | 2025.02.12 |
---|---|
[Java/Basics] 자바 기초 스터디 2주차: 객체지향 프로그래밍(OOP) 기초 (1) | 2025.01.28 |
[Java/Basics] 자바 기초 스터디 1주차: 자바의 실행 구조와 메모리 모델(동작 원리) (0) | 2025.01.18 |
[Java/Basics] 자바 기초 스터디 (0) | 2025.01.18 |