7 분 소요

다형성이란?

다형성(Polymorphism)은 “여러 가지 형태”를 의미하며, ==하나의 객체나 메서드가 다양한 형태로 동작==할 수 있는 능력입니다.

실무에서의 다형성

실무에서 다형성은 매일 사용합니다. 예를 들어:

// 다형성 사용
List<String> names = new ArrayList<>();
List<Integer> numbers = new LinkedList<>();
Map<String, Object> data = new HashMap<>();

ArrayList가 아닌 List로 선언할까요?

// 구체 타입으로 선언
ArrayList<String> names = new ArrayList<>();

// 나중에 LinkedList로 변경하려면?
// 모든 코드를 수정해야 함!
ArrayList<String> names = new LinkedList<>();  // 컴파일 에러!
// ✅ 인터페이스로 선언
List<String> names = new ArrayList<>();

// 나중에 구현체를 바꿔도 OK!
List<String> names = new LinkedList<>();  // ✅ 문제없음
List<String> names = new Vector<>();      // ✅ 문제없음

다형성이 필요한 이유

다형성 없이 작성한 코드

class ReportService {
    void sendEmailReport(EmailSender email) {
        email.send("보고서 내용");
    }

    void sendSmsReport(SmsSender sms) {
        sms.send("보고서 내용");
    }

    void sendSlackReport(SlackSender slack) {
        slack.send("보고서 내용");
    }

    // 새로운 발송 수단이 추가될 때마다 메서드를 계속 만들어야 함!
}

다형성을 사용한 코드

// 인터페이스 정의
interface MessageSender {
    void send(String message);
}

// 구현체들
class EmailSender implements MessageSender {
    @Override
    public void send(String message) {
        System.out.println("이메일 발송: " + message);
    }
}

class SmsSender implements MessageSender {
    @Override
    public void send(String message) {
        System.out.println("SMS 발송: " + message);
    }
}

class SlackSender implements MessageSender {
    @Override
    public void send(String message) {
        System.out.println("Slack 발송: " + message);
    }
}

// 하나의 메서드로 모든 발송 수단 처리
class ReportService {
    void sendReport(MessageSender sender, String content) {
        sender.send(content);
    }
}

// 사용
ReportService service = new ReportService();
service.sendReport(new EmailSender(), "보고서 내용");
service.sendReport(new SmsSender(), "보고서 내용");
service.sendReport(new SlackSender(), "보고서 내용");

// 새로운 발송 수단 추가도 쉬움
class KakaoTalkSender implements MessageSender {
    @Override
    public void send(String message) {
        System.out.println("카카오톡 발송: " + message);
    }
}
service.sendReport(new KakaoTalkSender(), "보고서 내용");

다형성의 두 가지 종류

Java의 다형성은 컴파일 타임 다형성런타임 다형성 두 가지로 나뉩니다.

구분 컴파일 타임 다형성 런타임 다형성
다른 이름 정적 바인딩, 조기 바인딩 동적 바인딩, 후기 바인딩
구현 방법 메서드 오버로딩 메서드 오버라이딩
결정 시점 컴파일 시 실행 시
성능 빠름 상대적으로 느림

컴파일 타임 다형성: 오버로딩

오버로딩이란?

메서드 오버로딩(Overloading)은 ==같은 이름의 메서드를 여러 개== 정의하는 것입니다.

예제: String의 valueOf()

public class String {
    // 여러 타입을 String으로 변환
    static String valueOf(int i) {}
    static String valueOf(long l) {}
    static String valueOf(boolean f) {}
}

// 사용
String.valueOf(10);        // int 버전
String.valueOf(10.5);      // double 버전
String.valueOf(true);      // boolean 버전

예제: StringBuilder

StringBuilder sb = new StringBuilder();

// 다양한 타입을 추가할 수 있음
sb.append("Hello");      // String
sb.append(123);          // int
sb.append(true);         // boolean

오버로딩 규칙

반드시 달라야 하는 것:

  • ✅ 매개변수 개수
  • ✅ 매개변수 타입
  • ✅ 매개변수 순서

달라도 오버로딩이 안 되는 것:

  • ❌ 리턴 타입만 다른 경우
  • ❌ 매개변수 이름만 다른 경우
// ❌ 컴파일 에러!
int calculate(int a) {
    return a * 2;
}

double calculate(int a) {  // 리턴 타입만 다름
    return a * 2.0;
}

런타임 다형성: 오버라이딩

오버라이딩이란?

메서드 오버라이딩(Overriding)은 ==부모 클래스의 메서드를 자식 클래스에서 재정의==하는 것입니다.

예제: DAO 계층

// 인터페이스
interface UserRepository {
    User findById(Long id);
    void save(User user);
}

// MySQL 구현
class MySqlUserRepository implements UserRepository {
    @Override
    public User findById(Long id) {
        // MySQL 조회 로직
        return new User();
    }

    @Override
    public void save(User user) {
        // MySQL 저장 로직
    }
}

// MongoDB 구현
class MongoUserRepository implements UserRepository {
    @Override
    public User findById(Long id) {
        // MongoDB 조회 로직
        return new User();
    }

    @Override
    public void save(User user) {
        // MongoDB 저장 로직
    }
}

// Service 계층
class UserService {
    private UserRepository repository;

    // 생성자 주입
    UserService(UserRepository repository) {
        this.repository = repository;
    }

    void processUser(Long id) {
        User user = repository.findById(id);  // 어떤 구현체든 OK!
        // 비즈니스 로직
        repository.save(user);
    }
}

// 사용
UserService mysqlService = new UserService(new MySqlUserRepository());
mysqlService.processUser(1L);  // MySQL 사용

UserService mongoService = new UserService(new MongoUserRepository());
mongoService.processUser(1L);  // MongoDB 사용

예제: List 인터페이스

// List 인터페이스
interface List<E> {
    boolean add(E element);
    E get(int index);
    int size();
}

// ArrayList 구현
class ArrayList<E> implements List<E> {
    @Override
    public boolean add(E element) {
        // 배열 기반 추가
        return true;
    }

    @Override
    public E get(int index) {
        // 배열 인덱스로 빠른 조회
        return null;
    }
}

// LinkedList 구현
class LinkedList<E> implements List<E> {
    @Override
    public boolean add(E element) {
        // 노드 연결로 추가
        return true;
    }

    @Override
    public E get(int index) {
        // 노드 탐색으로 조회
        return null;
    }
}

// 사용
List<String> list1 = new ArrayList<>();
list1.add("Hello");  // ArrayList의 add() 호출

List<String> list2 = new LinkedList<>();
list2.add("Hello");  // LinkedList의 add() 호출

오버로딩 vs 오버라이딩

비교표

특징 오버로딩 오버라이딩
정의 같은 이름, 다른 매개변수 부모 메서드 재정의
위치 같은 클래스 내 부모-자식 클래스 간
시그니처 달라야 함 같아야 함
리턴 타입 상관 없음 같거나 하위 타입
바인딩 컴파일 타임 (정적) 런타임 (동적)
@Override 사용 안 함 사용 권장

코드 비교

// 오버로딩 - StringBuilder
StringBuilder sb = new StringBuilder();
sb.append("text");
sb.append(123);
sb.append(45.6);

// 오버라이딩 - List 구현체
List<String> list1 = new ArrayList<>();  // ArrayList의 add() 호출
List<String> list2 = new LinkedList<>(); // LinkedList의 add() 호출

다형성의 활용

1. Spring Framework의 의존성 주입


@Service
class OrderService {
    private final PaymentService paymentService;

    // 인터페이스로 주입받음
    @Autowired
    OrderService(PaymentService paymentService) {
        this.paymentService = paymentService;
    }

    void createOrder(Order order) {
        // 구체적인 구현체를 몰라도 됨!
        paymentService.processPayment(order);
    }
}

// 실제 구현체는 설정으로 결정
@Service
class CreditCardPaymentService implements PaymentService {
    @Override
    public void processPayment(Order order) {
        // 신용카드 결제 로직
    }
}

2. 전략 패턴 (Strategy Pattern)

// 할인 전략 인터페이스
interface DiscountPolicy {
    int discount(int price);
}

// 정률 할인
class RateDiscountPolicy implements DiscountPolicy {
    @Override
    public int discount(int price) {
        return price * 10 / 100;  // 10% 할인
    }
}

// 정액 할인
class FixDiscountPolicy implements DiscountPolicy {
    @Override
    public int discount(int price) {
        return 1000;  // 1000원 할인
    }
}

// 할인 없음
class NoDiscountPolicy implements DiscountPolicy {
    @Override
    public int discount(int price) {
        return 0;
    }
}

// 주문 서비스
class OrderService {
    private DiscountPolicy discountPolicy;

    // 할인 정책을 자유롭게 변경 가능!
    void setDiscountPolicy(DiscountPolicy policy) {
        this.discountPolicy = policy;
    }

    int calculatePrice(int price) {
        int discountAmount = discountPolicy.discount(price);
        return price - discountAmount;
    }
}

// 사용
OrderService service = new OrderService();
service.setDiscountPolicy(new RateDiscountPolicy());
service.calculatePrice(10000);  // 9000원

service.setDiscountPolicy(new FixDiscountPolicy());
service.calculatePrice(10000);  // 9000원

service.setDiscountPolicy(new NoDiscountPolicy());
service.calculatePrice(10000);  // 10000원

3. 팩토리 패턴 (Factory Pattern)

// 데이터베이스 연결 인터페이스
interface DatabaseConnection {
    void connect();
}

class MySqlConnection implements DatabaseConnection {
    @Override
    public void connect() {
        System.out.println("MySQL 연결");
    }
}

class PostgreSqlConnection implements DatabaseConnection {
    @Override
    public void connect() {
        System.out.println("PostgreSQL 연결");
    }
}

class OracleConnection implements DatabaseConnection {
    @Override
    public void connect() {
        System.out.println("Oracle 연결");
    }
}

// 팩토리
class DatabaseConnectionFactory {
    static DatabaseConnection createConnection(String type) {
        switch (type) {
            case "mysql":
                return new MySqlConnection();
            case "postgresql":
                return new PostgreSqlConnection();
            case "oracle":
                return new OracleConnection();
            default:
                throw new IllegalArgumentException("Unknown type: " + type);
        }
    }
}

// 사용
DatabaseConnection conn = DatabaseConnectionFactory.createConnection("mysql");
conn.connect();  // "MySQL 연결"

타입 캐스팅

업캐스팅 (Upcasting)

업캐스팅은 ==자식 타입을 부모 타입으로 변환==하는 것으로, 자동으로 일어납니다.

기본 예제

Dog dog = new Dog();
Animal animal = dog;  // Dog → Animal (자동 변환)

ArrayList<String> arrayList = new ArrayList<>();
List<String> list = arrayList;  // ArrayList → List (자동 변환)

왜 자동으로 될까?

자식은 항상 부모의 모든 기능을 가지고 있기 때문에 안전합니다.

List<String> list = new ArrayList<>();

// List가 가진 메서드는 ArrayList도 당연히 가지고 있음
list.add("Hello");     // ✅ ArrayList에 add() 있음
list.get(0);           // ✅ ArrayList에 get() 있음
list.size();           // ✅ ArrayList에 size() 있음

업캐스팅을 쓰는 이유

public class UserService {
    // ✅ List 타입으로 받으면 유연함
    public void processUsers(List<String> users) {
        for (String user : users) {
            System.out.println(user);
        }
    }
}

// 어떤 List 구현체든 넘길 수 있음!
UserService service = new UserService();
service.processUsers(new ArrayList<>());     // ✅
service.processUsers(new LinkedList<>());    // ✅
service.processUsers(new Vector<>());        // ✅

다운캐스팅 (Downcasting)

다운캐스팅은 ==부모 타입을 자식 타입으로 변환==하는 것으로, 명시적으로 해야 합니다.

기본 예제

// List로 선언했지만 실제로는 ArrayList
List<String> list = new ArrayList<>();

// ArrayList만 가진 메서드를 쓰고 싶다면?
ArrayList<String> arrayList = (ArrayList<String>) list;  // 명시적 변환
arrayList.trimToSize();  // ArrayList만 가진 메서드

왜 명시적으로 해야 할까?

부모 타입이 자식의 모든 기능을 가지고 있는 건 아니기 때문에 위험합니다.

List<String> list = new LinkedList<>();  // 실제로는 LinkedList

// ArrayList로 캐스팅하려고 하면?
ArrayList<String> arrayList = (ArrayList<String>) list;  
// ❌ 런타임 에러! ClassCastException
// LinkedList를 ArrayList로 바꿀 수 없음!

안전하게 다운캐스팅하기

방법 1: instanceof로 확인

List<String> list = new ArrayList<>();

if (list instanceof ArrayList) {
    ArrayList<String> arrayList = (ArrayList<String>) list;
    arrayList.trimToSize();
    System.out.println("ArrayList 메서드 사용 가능!");
}

방법 2: Java 16+ Pattern Matching

List<String> list = new ArrayList<>();

// instanceof와 동시에 캐스팅까지 됨!
if (list instanceof ArrayList<String> arrayList) {
    arrayList.trimToSize();
    System.out.println("더 간단해진 코드!");
}

업캐스팅 vs 다운캐스팅 정리

구분 업캐스팅 다운캐스팅
방향 자식 → 부모 부모 → 자식
변환 자동 (묵시적) 명시적 필요
안전성 항상 안전 위험 (타입 확인 필요)
사용 시기 일반적인 경우 특정 기능 필요할 때만
// 업캐스팅 (Upcasting)
ArrayList<String> arrayList = new ArrayList<>();
List<String> list = arrayList;  // ✅ 자동 OK

// 다운캐스팅 (Downcasting)
List<String> list2 = new ArrayList<>();
ArrayList<String> arrayList2 = (ArrayList<String>) list2;  // ⚠️ 명시적 필요

정리

다형성이란?

하나의 객체나 메서드가 여러 형태로 동작할 수 있는 능력

가장 많이 보는 다형성

List<String> list = new ArrayList<>();
Map<String, Object> map = new HashMap<>();
UserRepository repository = new MySqlUserRepository();
PaymentService service = new CreditCardPaymentService();

핵심 원칙

“구현이 아닌 인터페이스에 의존하라”

// ❌ 구현에 의존
ArrayList<String> list = new ArrayList<>();

// ✅ 인터페이스에 의존
List<String> list = new ArrayList<>();

댓글남기기