해시맵

해시맵(HashMap) 이란?

키(Key) - 값(Value) 쌍으로 데이터를 저장하는 데이터 구조

해시 함수(Hash Function) 를 사용하여 데이터를 특정 위치(Bucket) 에 저장

 

해시맵 사용 예제

import java.util.HashMap;

public class HashMapExample {
    public static void main(String[] args) {
        HashMap<String, Integer> map = new HashMap<>();
        map.put("apple", 5);
        map.put("banana", 10);
        map.put("cherry", 7);
        
        System.out.println(map.get("banana")); // 출력: 10
    }
}

 

➡︎ O(1) 시간 복잡도로 빠른 데이터 조회 가능

➡︎ 키-값 저장 방식으로 중복 키 허용 안됨


트리

트리(Tree) 란?

계층적 데이터 구조로, 부모-자식 관계를 가짐

검색, 정렬, 계층적 데이터 저장에 유리


이진 탐색 트리

Binary Search Tree, BST

각 노드의 왼쪽 서브트리는 작은 값, 오른쪽 서브트리는 큰 값을 가짐 (왼쪽 자식 노드 < 부모 노드 < 오른쪽 자식 노드)

O(logN) 시간 복잡도로 탐색 가능 (균형이 잡힌 경우)

 

예제

class Node {
    int key;
    Node left, right;

    public Node(int item) {
        key = item;
        left = right = null;
    }
}

class BinarySearchTree {
    Node root;

    void insert(int key) {
        root = insertRec(root, key);
    }

    Node insertRec(Node root, int key) {
        if (root == null) {
            root = new Node(key);
            return root;
        }
        if (key < root.key)
            root.left = insertRec(root.left, key);
        else if (key > root.key)
            root.right = insertRec(root.right);
        return root;
    }
}

 

➡︎ 계층적 데이터 표현 가능, 정렬된 데이터 탐색에 유리: O(logN)

➡︎ 균형이 깨지면 성능이 O(N) 까지 저하될 수 있음


균형 트리

  • AVL 트리: 삽입/삭제 시 균형 유지 ➔ 항상 O(logN) 보장
  • 레드-블랙 트리: 균형을 어느 정도 유지하면서 삽입/삭제 성능 향상

레드-블랙 트리 기반 TreeMap

import java.util.TreeMap;

public class TreeMapExample {
    public static void main(String[] args) {
        TreeMap<Integer, String> tree = new TreeMap<>();
        tree.put(2, "apple");
        tree.put(1, "banana");
        tree.put(3, "cherry");

        System.out.println(tree.firstKey()); // 출력: 1
    }
}

 

➡︎ TreeMap 은 정렬된 순서로 데이터 저장하며, O(logN) 탐색 속도 제공


해시맵 vs 트리

비교 항목 해시맵 (HashMap) 트리 (Tree)
시간 복잡도 O(1) (이론상) O(logN)
데이터 정렬 X (정렬 안됨) O (자동 정렬)
검색 속도 빠름 (충돌 시 성능 저하) 정렬된 데이터 탐색에 유리
메모리 사용 낮음 (기본적으로 효율적) 많음 (추가적인 노드 포인터 필요)
사용 사례 캐싱, 키-값 저장 계층적 데이터, 정렬이 필요한 경우

 

‼️ 빠른 검색이 필요하면 HashMap, 정렬된 데이터가 필요하면 Tree

배열과 리스트

배열(Array) 이란?

메모리 상에서 연속된 공간을 차지하는 데이터 구조

인덱스를 이용하여 O(1) 시간 복잡도로 접근 가능

 

배열 예제

 int[] numbers = {1, 2, 3, 4, 5};
System.out.println(numbers[2]); // O(1) - 즉시 접근 가능

 

➡︎ 빠른 조회 가능

➡︎ 크기가 고정되어 있고, 삽입/삭제가 비효율적


연결 리스트(Linked List) 란?

노드(Node) 와 포인터(Pointer) 로 구성된 데이터 구조

시간 복잡도: 삽입/삭제는 O(1), 검색 속도는 O(N)

 

단일 연결 리스트 예제

class Node {
    int data;
    Node next;
    
    Node(int data) {
        this.data = data;
        this.next = null;
    }
}

class LinkedList {
    Node head;
    
    void append(int data) {
        if (head == null) {
            head = new Node(data);
            return;
        }
        Node temp = head;
        while (temp.next != null) {
            temp = temp.next;
        }
        temp.next = new Node(data);
    }
}

 

➡︎ 동적 크기 조절 가능

➡︎ 연속된 메모리 공간 사용하지 않아도 되지만, 노드 탐색 속도가 느림


배열 vs 연결 리스트 비교

비교 항목 배열 (Array) 연결 리스트 (Linked List)
메모리 구조 연속된 메모리 할당 노드 + 포인터
접근 속도 O(1) (인덱스 접근) O(N) (순차 탐색 필요)
삽입/삭제 속도 O(N) (배열 이동 필요) O(1) (포인터 변경)
메모리 사용 추가적인 공간 필요 없음 포인터 저장을 위한 추가 공간 필요
크기 조절 고정 크기 (미리 할당) 동적 크기 조절 가능

스택과 큐

스택(Stack) 이란?

후입선출(LIFO, Last In First Out) 구조

재귀 함수 호출, 되돌리기(Undo), 괄호 검사 등에 활용

 

스택 예제

import java.util.Stack;

Stack<Integer> stack = new Stack<>();
stack.push(1);
stack.push(2);
stack.push(3);
System.out.println(stack.pop()); // 출력: 3 (후입선출)

 

➡︎ 마지막에 넣은 데이터가 가장 먼저 제거


큐(Queue) 란?

선입선출(FIFO, First In First Out) 구조

작업 대기열, 너비 우선 탐색(BFS) 등에 활용

 

큐 예제

import java.util.LinkedList;
import java.util.Queue;

Queue<Integer> queue = new LinkedList<>();
queue.offer(1);
queue.offer(2);
queue.offer(3);
System.out.println(queue.poll()); // 출력: 1 (선입선출)

 

➡︎ 먼저 들어온 데이터가 먼저 제거


스택 vs 큐

비교 항목 스택 (Stack) 큐 (Queue)
동작 원리 LIFO (후입선출) FIFO (선입선출)
사용 예시 재귀 호출, 실행 취소(Undo) 작업 스케줄링, BFS 탐색
주요 연산 push(), pop() offer(), poll()
시간 복잡도 O(1) (삽입/삭제) O(1) (삽입/삭제)

 

객체지향 프로그래밍의 단점

상속의 남용

  • 코드 복잡도 증가
  • 깊은 상속 계층 구조 ➔ 유지보수 어려움
  • 부모 클래스 수정 시 모든 자식 클래스에 영향 ➔ 강한 결합(Tight Coupling) 발생

객체 간 강한 의존성

  • 재사용성 저하
  • 하나의 클래스를 수정하면 여러 곳에 영향을 받을 수 있음
  • 객체 간 관계가 복잡해지면 확장성과 유연성이 떨어짐

불필요한 메모리 낭비

  • 상속을 사용할 경우, 부모 클래스의 불필요한 필드/메서드도 함께 사용됨
  • 필요하지 않은 기능이 포함될 수 있음

 

상속이 과도하게 사용된 경우

class Animal {
    void eat() { System.out.println("먹는 중..."); }
}

class Bird extends Animal {
    void fly() { System.out.println("나는 중..."); }
}

class Penguin extends Bird {
    void swim() { System.out.println("헤엄치는 중..."); }
}

 

➡︎ Penguin 은 Bird 를 상속받지만, 펭귄은 fly() 메서드 불필요

➡︎ 잘못된 상속 구조로 인해 불필요한 메서드가 포함됨

➡︎ 강한 결합도가 발생하여 유지보수가 어려워짐

 

‼️ 상속을 사용하기 보다 구성(Composition) 으로 변경


구성

구성(Composition) 이란?

하나의 객체가 다른 객체를 멤버 변수로 포함하는 방식

객체 간 결합도를 줄이고, 유연성을 높일 수 있음

 

구성 적용 코드

class Swimmer {
    void swim() { System.out.println("헤엄치는 중..."); }
}

class Penguin {
    private Swimmer swimmer; // 조합(Composition) 사용

    Penguin() {
        this.swimmer = new Swimmer();
    }

    void swim() {
        swimmer.swim();
    }
}

 

➡︎ 필요한 기능만 포함하여 유연한 설계 가능


상속과의 비교

비교 항목 상속(Inheritance) 구성(Composition)
코드 재사용성 높음 높음
유지보수성 낮음 (부모 클래스 변경 시 영향) 녹음 (독립적인 기능 구성 가능)
객체 간 결합도 높음 (Tight Coupling) 낮음 (Loose Coupling)
유연성  낮음 (상속 관계 변경 어려움) 높음 (필요한 기능만 구성 가능)

 

⭐️ 유지보수성과 확장성을 고려할 때, 가능하면 구성(Composition) 을 우선적으로 고려하고, 

     부모-자식 관계가 명확한 경우에만 상속(Inheritance) 을 사용


객체지향 대안

SOLID 원칙 준수

  • SRP(단일 책임 원칙): 하나의 클래스가 너무 많은 역할을 하지 않도록 정리
  • DIP(의존성 역전 원칙): 직접적인 의존성을 줄이고, 인터페이스 활용

함수형 프로그래밍 개념 활용

  • 불변성: 객체 변경이 아닌 새로운 객체 생성
  • 순수 함수: 부작용 최소화

디자인 패턴 활용

  • 전략 패턴: 특정 동작을 외부에서 주입하여 유연성 확보
  • 데코레이터 패턴: 객체 기능을 동적으로 확장

의존성

의존성(Dependency) 이란?

한 클래스가 다른 클래스의 객체를 직접 생성하거나 사용하는 관계

의존성이 높으면 코드 변경이 어렵고, 테스트가 어려움

 

의존성이 높은 코드

class MySQLDatabase {
    void connect() { System.out.println("MySQL 연결"); }
}

class DataManager {
    MySQLDatabase database = new MySQLDatabase(); // 특정 구현체에 직접 의존
}

 

➡︎ DataMangerMySQLDatabase 와 강하게 결합되어 다른 DB 로 변경하려면 기존 코드 수정 필요

➡︎ 의존성 주입(DI) 을 사용하여 DataManager 가 특정 DB 구현체에 직접 의존하지 않도록 변경


의존성 주입(Dependency Injection) 이란?

객체가 직접 다른 객체를 생성하는 것이 아니라, 외부에서 주입받는 방식

객체 간 결합도를 낮추고 유연한 코드를 만들 수 있음

 

의존성 주입 적용 코드

interface Database {
    void connect();
}

class MySQLDatabase implements Database {
    public void connect() { System.out.println("MySQL 연결"); }
}

class PostgreSQLDatabase implements Database {
    public void connect() { System.out.println("PostgreSQL 연결"); }
}

class DataManager {
    private Database database;

    // 생성자를 통한 의존성 주입 (Constructor Injection)
    DataManager(Database database) {
        this.database = database;
    }

    void connectDatabase() {
        database.connect();
    }
}

// DI 적용 후 사용
public class Main {
    public static void main(String[] args) {
        Database db = new MySQLDatabase(); // MySQL 사용
        DataManager manager = new DataManager(db);
        manager.connectDatabase(); // 출력: MySQL 연결
    }
}

 

➡︎ DataManager 가 특정 DB 에 직접 의존하지 않음

➡︎ 인터페이스를 사용하여 유연한 확장 가능

➡︎ Mock 객체를 주입하여 테스트 가능 ➔ 테스트 용이성 향상

 

의존성 주입의 3가지 방식

  1. 생성자 주입 (Constructor Injection): 생성자를 통해 의존 객체를 주입 ex) new DataManager(new MySQLDatabase())
  2. 세터 주입 (Setter Injection): 세터 메서드를 통해 의존 객체 주입 ex) manager.setDatabase(new MySQLDatabase())
  3. 필드 주입 (Field Injection): 필드에 직접 주입 ex) @Autowired private Database db; (Spring 의 경우)

⭐️ 실무에서는 주로 생성자 주입을 사용 - 테스트 용이성 & 불변성 유지 가능


싱글톤 패턴

싱글톤 패턴(Singleton Pattern) 이란?

애플리케이션 전체에서 하나의 객체만 유지하는 디자인 패턴

객체가 불필요하게 여러 개 생성되는 것을 방지하여 메모리 낭비를 줄일 수 있음

 

싱글톤 패턴 적용 코드

class Singleton {
    private static Singleton instance;

    private Singleton() { } // private 생성자 (외부에서 객체 생성 차단)

    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }

    public void showMessage() {
        System.out.println("싱글톤 인스턴스 실행");
    }
}

// 싱글톤 객체 사용
public class Main {
    public static void main(String[] args) {
        Singleton obj1 = Singleton.getInstance();
        Singleton obj2 = Singleton.getInstance();

        obj1.showMessage();  // 출력: 싱글톤 인스턴스 실행

        System.out.println(obj1 == obj2); // true (같은 객체)
    }
}

 

➡︎ getInstance() 를 호출하면 항상 같은 인스턴스 반환

➡︎ new Singleton() 을 직접 호출할 수 없음 (private 생성자)

➡︎ 전역적으로 단 하나의 객체만 유지


싱글톤 패턴의 장단점

장점

  • 객체의 공유: 모든 클래스가 같은 인스턴스를 공유하여 메모리 사용 절약
  • 객체 생성 제한: new 키워드로 객체를 여러 번 생성하는 것을 방지

단점

  • 멀티스레드 환경에서 동기화 문제 발생 가능: 여러 스레드가 동시에 getInstance() 를 호출하면 동시에 객체가 여러 개 생성될 위험
    ➡︎ 이중 체크 락킹(Double-Checked Locking) 기법 사용

멀티스레드 안전한 싱글톤

class ThreadSafeSingleton {
    private static volatile ThreadSafeSingleton instance;
    
    private ThreadSafeSingleton() { }

    public static ThreadSafeSingleton getInstance() {
        if (instance == null) {
            synchronized (ThreadSafeSingleton.class) {
                if (instance == null) {
                    instance = new ThreadSafeSingleton();
                }
            }
        }
        return instance;
    }
}

 

➡︎ synchronized 블록을 사용하여 멀티스레드 환경에서도 안전한 싱글톤 유지

SOLID 원칙

SOLID 원칙이란?

객체지향 설계에서 유지보수성과 확장성을 높이기 위한 5가지 방법

이 원칙을 준수하면, 유연하고 변경에 강한 코드 작성 가능!


단일 책임 원칙 (SRP)

Single Responsibility Principle

 

하나의 클래스는 단 하나의 책임(기능) 만 가져야 함

즉, 하나의 변경 이유(Reason to Change) 만 가져야 함

 

⚠️ 여러 기능이 한 클래스에 섞이면 유지보수가 어려움!

 

SRP 위반

class Report {
    void generateReport() {
        // 리포트 생성 로직
    }

    void saveToFile() {
        // 파일 저장 로직
    }
}

 

➡︎ 리포트 생성파일 저장이라는 두 가지 책임을 가짐

 

SRP 적용

class ReportGenerator {
    void generateReport() {
        // 리포트 생성 로직
    }
}

class FileSaver {
    void saveToFile() {
        // 파일 저장 로직
    }
}

개방-폐쇄 원칙 (OCP)

Open/Closed Principle

 

코드는 확장에는 열려(Open) 있고, 변경에는 닫혀(Closed) 있어야 함

즉, 새로운 기능을 추가할 때 기존 코드를 수정하지 않아야 함

 

OCP 위반

class PaymentService {
    void pay(String paymentType) {
        if (paymentType.equals("CreditCard")) {
            // 신용카드 결제 로직
        } else if (paymentType.equals("PayPal")) {
            // PayPal 결제 로직
        }
    }
}

 

➡︎ 새로운 결제 방식이 추가될 때마다 기존 코드를 수정해야 함

 

OCP 적용

interface Payment {
    void pay();
}

class CreditCardPayment implements Payment {
    public void pay() {
        // 신용카드 결제 로직
    }
}

class PayPalPayment implements Payment {
    public void pay() {
        // PayPal 결제 로직
    }
}

class PaymentService {
    void processPayment(Payment payment) {
        payment.pay();
    }
}

 

➡︎ 다형성을 활용해서 기존 코드를 수정하지 않고, 새로운 결제 방식 추가 가능!


리스코프 치환 원칙(LSP)

Liskov Substitution Principle

 

자식 클래스는 부모 클래스를 대체할 수 있어야 함

즉, 부모 클래스를 상속받은 모든 클래스는  부모의 역할을 온전히 수행할 수 있어야 함

 

LSP 위반

class Rectangle {
    int width, height;

    void setWidth(int width) { this.width = width; }
    void setHeight(int height) { this.height = height; }
}

class Square extends Rectangle {
    void setWidth(int width) { 
        this.width = width;
        this.height = width; // 가로와 세로를 동일하게 설정 (LSP 위반)
    }
}

 

➡︎ Square 클래스는 Rectangle 을 대체할 수 없음

 

LSP 적용

interface Shape {
    int getArea();
}

class Rectangle implements Shape {
    int width, height;
    public int getArea() { return width * height; }
}

class Square implements Shape {
    int side;
    public int getArea() { return side * side; }
}

 

➡︎ 공통 인터페이스를 활용

 


인터페이스 분리 원칙(ISP)

Interface Segregation Principle

 

하나의 큰 인터페이스보다, 여러 개의 작은 인터페이스로 분리하는 게 좋음

즉, 사용하지 않는 메서드에 의존하면 안됨

 

ISP 위반

interface Worker {
    void work();
    void eat();
}

class Robot implements Worker {
    public void work() { System.out.println("일을 합니다."); }
    public void eat() { throw new UnsupportedOperationException(); } // 로봇은 먹을 수 없음 (ISP 위반)
}

 

➡︎ Roboteat() 메서드를 가질 필요가 없음

 

ISP 적용

interface Workable {
    void work();
}

interface Eatable {
    void eat();
}

class Robot implements Workable {
    public void work() { System.out.println("일을 합니다."); }
}

 

➡︎ 인터페이스를 분리하여 불필요한 의존성 제거


의존성 역전 원칙(DIP)

Dependency Inversion Principle

 

상위 모듈이 하위 모듈에 의존하면 안 됨

즉, 세부 구현이 아니라 추상화(Interface)에 의존해야 함

 

DIP 위반

class MySQLDatabase {
    void connect() { System.out.println("MySQL 연결"); }
}

class DataManager {
    MySQLDatabase database = new MySQLDatabase(); // 특정 DB에 강하게 결합 (DIP 위반)
}

 

➡︎ DB 를 변경하려면 DataManager 코드를 수정해야 함

 

DIP 적용

interface Database {  // 추상화 적용
    void connect();
}

class MySQLDatabase implements Database {
    public void connect() { System.out.println("MySQL 연결"); }
}

class PostgreSQLDatabase implements Database {
    public void connect() { System.out.println("PostgreSQL 연결"); }
}

class DataManager {
    Database database;

    DataManager(Database database) { // 의존성 주입 (DIP 적용)
        this.database = database;
    }
}

 

➡︎ 추상화 적용 ➔ 인터페이스를 활용하여 유연한 설계 가능


추상화

추상화(Abstraction) 란?

객체의 핵심점인 특징만 노출하고, 불필요한 세부 사항은 숨기는 개념

인터페이스추상 클래스로 구현 가능

코드의 복잡도/결합도를 낮추고, 확장성을 높임

 

비교 항목 인터페이스 추상 클래스
목적 행동 정의 기본 기능 제공
메서드 구현 전부 구현 X (default 메서드 제외) 일부 구현 가능
다중 상속 가능 - implements 불가능 - extends
사용 예시 다양한 객체가 동일한 동작을 할 때 공통 기능을 제공하면서 일부만 구현이 필요할 때

 

* 자세한 내용은 아래 글의 인터페이스 vs 추상 클래스 내용 참고

 

상속(Inheritance)과 다형성(Polymorphism)

상속상속(Inheritance) 이란?기존 클래스를 확장하여 새로운 클래스를 만드는 개념코드 재사용성 증가 및 객체 간 계층 구조 형성 상속의 기본 구조// 부모 클래스 (Super Class)class Animal { String name; voi

jelliclesu.tistory.com


주요 포인트

  • SOLID 원칙은 단순 암기가 아니라 어떻게 적용되는 지 알아야 함
    • SRP: 하나의 클래스는 하나의 책임만 가져야 함
    • OCP: 코드는 확장에는 열려 있어야 하고, 변경에는 닫혀 있어야 함
    • LSP: 자식 클래스는 부모 클래스를 대체할 수 있어야 함
    • ISP: 하나의 큰 인터페이스보다, 여러 개의 작은 인터페이스로 분리하는 게 좋음
    • DIP: 상위 모듈이 하위 모듈에 의존하면 안 됨
  • 추상화가 왜 필요한지, 인터페이스와 추상 클래스의 차이는 무엇인지 명확하게 알아야 함
    • 추상화: 객체의 핵심적인 특징만 노출하고, 불필요한 세부 사항은 숨김
    • 장점: 코드의 결합도는 낮아지고, 유지보수성 향상
  • SOLID 원칙과 추상화를 연계하여 유연한 설계 방법을 설명할 줄 알아야 함

상속

상속(Inheritance) 이란?

기존 클래스를 확장하여 새로운 클래스를 만드는 개념

코드 재사용성 증가 및 객체 간 계층 구조 형성

 

상속의 기본 구조

// 부모 클래스 (Super Class)
class Animal {
    String name;
    void makeSound() {
        System.out.println("동물이 소리를 냅니다.");
    }
}

// 자식 클래스 (Sub Class)
class Dog extends Animal {
    void makeSound() { // 메서드 오버라이딩 (Overriding)
        System.out.println("멍멍!");
    }
}

public class Main {
    public static void main(String[] args) {
        Dog myDog = new Dog();
        myDog.name = "바둑이";
        myDog.makeSound();  // 출력: 멍멍!
    }
}

 

➡︎ Dog 클래스는 Animal 클래스를 상속

➡︎ makeSound() 메서드를 오버라이딩(Overriding) 하여 고유 기능 구현


상속의 장단점

장점

  • 코드 재사용성 증가: 기본 기능을 부모 클래스에 정의
  • 유지보수 용이: 공통 로직을 부모 클래스에서 관리

단점

  • 상속 구조가 깊어질수록 의존성 증가 ➔ 유지보수 어려움
  • 부모 클래스가 변경되면 자식 클래스에 영향을 미칠 위험

➡︎ 상속보다 "조합(Composition)" 을 사용하는 것이 더 유리한 경우도 많음!


다형성

다형성(Polymorphism) 이란?

하나의 인터페이스로 여러 구현을 처리할 수 있는 개념

오버라이딩(Overriding)오버로딩(Overloading) 이 대표적인 방식

 

다형성 예제

class Animal {
    void makeSound() {
        System.out.println("동물이 소리를 냅니다.");
    }
}

class Cat extends Animal {
    void makeSound() { // 오버라이딩
        System.out.println("야옹~");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal myAnimal = new Cat(); // 부모 타입으로 자식 객체 참조
        myAnimal.makeSound(); // 출력: 야옹~
    }
}

 

➡︎ Animal myAnimal = new Cat(); ➔ 부모 타입을 사용하여 다양한 객체를 다룰 수 있음


오버로딩과 오버라이딩 차이

  • 오버로딩(Overloading): 같은 클래스에서 메서드 이름이 같고 매개변수가 다름 ex) void add(int a), void add(double a)
  • 오버라이딩(Overriding): 부모 클래스의 메서드를 재정의 ex) @Override void makeSound()

인터페이스 vs 추상 클래스

인터페이스(Interface) 란?

메서드의 동작만 정의하고, 구현은 하지 않음

다중 구현 가능 (implements)

 

인터페이스 예제

interface Animal {
    void makeSound(); // 구현 X (추상 메서드)
}

class Dog implements Animal {
    public void makeSound() {
        System.out.println("멍멍!");
    }
}

 

➡︎ 다양한 객체가 동일한 동작을 할 때 사용!


추상 클래스(Abstract Class) 란?

일부 구현이 포함된 클래스: 추상 메서드 + 일반 메서드 포함 가능

단일 상속만 가능 (extends)

 

추상 클래스 예제

abstract class Animal {
    abstract void makeSound(); // 추상 메서드 (구현 X)

    void sleep() { // 일반 메서드 (구현 O)
        System.out.println("동물이 잠을 잡니다.");
    }
}

 

➡︎ 공통 기능을 제공하면서 일부만 구현이 필요할 때 사용!


주요 포인트

  • 상속과 다형성의 차이를 명확히 알아야 함!
    • 상속: 부모 클래스를 기반으로 새로운 클래스를 확장하는 방식, 코드 재사용성 증가, 깊은 상속 구조는 유지보수 어려움
    • 다형성: 부모 타입으로 여러 자식 객체를 다룰 수 있는 개념, 메서드 오버라이딩을 활용하여 객체마다 다른 동작 수행 가능
  • 인터페이스와 추상 클래스의 차이
    • 인터페이스: 행동만 정의하고 구현은 하지 않음, 다중 구현 가능
    • 추상 클래스: 일부 구현이 포함될 수 있음, 단일 상속만 가능

+ Recent posts