포스트

스프링입문을위한 자바 객체지향의 원리와 이해(3)

스프링입문을위한 자바 객체지향의 원리와 이해(3)

3-4. ⭐ 추상화 자세히 알아보기 (가장 중요)

우리가 사용하는 프로그래밍 언어는 기계어부터 Java, Python에 이르기까지 점점 인간이 이해하기 쉽게 진화해 왔다.
어셈블리어는 010001 같은 이진수 대신 add, jmp 같은 명령어를 사용했고,
C언어는 for, if, struct 등을 통해 보다 자연스럽고 읽기 쉬운 코드를 작성할 수 있도록 해주었다.

프로그래밍의 패러다임도 마찬가지다.
초기에는 코드를 순서대로 실행하는 순차적 프로그래밍이 있었고,
이후에는 함수 단위로 논리를 분할한 절차지향 프로그래밍(Procedural Programming)이 등장했다.
하지만 여전히 C언어 같은 절차지향 언어는 포인터, 메모리 주소 등 기계 중심적인 개념을 많이 포함하고 있어,
사람이 이해하고 관리하기엔 어려움이 있었다.

그래서 나온 접근 방식이 바로 Object-Oriented Programming (객체지향 프로그래밍) 이다.
우리가 현실 세계를 인식하듯, “사물”을 중심으로 프로그래밍을 해보자는 발상이다.


🍪 클래스(Class) vs 객체(Object) – 쿠키틀 비유의 함정

많은 사람이 클래스와 객체를 설명할 때 “쿠키와 쿠키틀”이라는 비유를 쓴다.
하지만 다음과 같은 코드를 보면 이 비유가 이상하게 느껴진다:

1
쿠키틀 쿠키1 = new 쿠키틀();  // 이상함

쿠키틀을 new로 만든다고 쿠키가 되는가?
이상한 이유는, 클래스(Class)는 사실 ‘틀’이 아니라 분류(Classification)이고
객체(Object)는 그 분류에 속한 구체적인 실체이기 때문이다.

이미지 없음

클래스 : 객체 = 사람 : 홍길동
클래스 : 객체 = 콜라 : 펩시
클래스 : 객체 = Shape : Circle


📦 추상화란?

추상화(Abstraction)는 복잡한 현실 세계에서 공통적이고 핵심적인 특징만 뽑아내어 표현하는 것
= “구체적인 것을 단순화하여 분류(class)로 만드는 과정”

예를 들어, 다양한 도형이 존재할 때 Shape라는 상위 개념으로 추상화할 수 있다.

1
2
3
abstract class Shape {
    abstract double area();
}

이때, Shape는 “면적을 가진 무언가”를 표현하며,
구체적인 동그라미(Circle), 직사각형(Rectangle)은 그 구체적인 실체, 즉 객체이다.


🔍 추상화가 중요한 이유

  1. 복잡한 현실을 단순화하여 표현할 수 있다.
  2. 코드를 재사용하고, 유지보수가 쉬운 구조로 만들 수 있다.
  3. 여러 객체를 하나의 상위 타입으로 다룰 수 있는 기반이 된다. (다형성과 연결됨)

예를 들어 다음과 같이 다양한 도형을 같은 타입으로 처리할 수 있다.

1
2
3
4
5
Shape s1 = new Circle(3.0);
Shape s2 = new Rectangle(4.0, 5.0);

System.out.println(s1.area());  // 원 면적
System.out.println(s2.area());  // 사각형 면적

이처럼 클래스는 단순한 ‘틀’이 아니라 “공통된 속성과 행위를 가진 분류”,
객체는 그 분류의 실제 인스턴스이며,
이 모든 기반에는 바로 추상화가 존재한다.

3-5. 🎭 다형성 자세히 알아보기

📌 다형성이란?

다형성(Polymorphism)

%같은 메시지를 보냈을 때, 서로 다른 방식으로 응답할 수 있는 능력%을 의미한다.

즉, 하나의 상위 타입(추상 클래스, 인터페이스)을 통해
여러 하위 클래스의 인스턴스를 동일한 방식으로 다룰 수 있다.

이 개념은 유연하고 확장성 있는 객체지향 설계의 핵심이다.


☕ 예시: 커피머신

1
2
3
4
커피머신이 '커피를 내려라(brew)' 라는 명령을 내렸을 때,
- 아메리카노는 뜨거운 물을 붓고
- 라떼는 우유와 커피를 섞고
- 카페모카는 초콜릿 시럽도 추가한다.

→ 커피머신은 “커피” 라는 공통 타입만 알면 되고,
어떤 종류의 커피인지에 따라 내부 동작은 달라진다.


🧑‍💻 자바 코드로 보기

먼저, 상위 타입(추상 클래스)을 정의한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
abstract class Coffee {
    abstract void brew();
}
``

하위 클래스들이 각자 고유한 방식으로 %brew()%  구현한다.

```java
class Americano extends Coffee {
    @Override
    void brew() {
        System.out.println("뜨거운 물에 커피를 내립니다.");
    }
}

class Latte extends Coffee {
    @Override
    void brew() {
        System.out.println("우유와 커피를 섞습니다.");
    }
}

다형성을 활용해보자.

1
2
3
4
5
6
7
8
9
public class Cafe {
    public static void main(String[] args) {
        Coffee c1 = new Americano();
        Coffee c2 = new Latte();

        c1.brew();  // Americano 방식
        c2.brew();  // Latte 방식
    }
}

→ “Coffee” 라는 상위 타입으로 다루지만, 결과는 각 하위 클래스에 따라 달라진다.


🔄 다형성의 이점

  1. 코드의 유연성 증가 – 조건문 없이 다양한 객체를 다룰 수 있다.
  2. 확장성 향상 – 새로운 타입을 추가해도 기존 코드를 거의 수정하지 않는다.
  3. 코드 중복 감소 – 공통 코드는 상위 타입에, 개별 구현은 하위 타입에 분리.

🧩 오버라이딩 vs 오버로딩

다형성을 이야기할 때 가장 자주 헷갈리는 개념
바로 “오버라이딩(Overriding)” 과 “오버로딩(Overloading)” 이다.

구분오버라이딩 (Overriding)오버로딩 (Overloading)
정의부모의 메서드를 하위 클래스에서 재정의같은 이름의 메서드를 매개변수 다르게 여러 개 정의
목적다형성 구현다양한 입력에 유연하게 대응
클래스 관계상속 관계 필요같은 클래스 내 가능
메서드 이름같아야 함같아야 함
매개변수동일해야 함달라야 함
반환 타입부모와 동일하거나 더 구체적자유

✅ 오버라이딩 예제

1
2
3
4
5
6
7
8
9
10
11
12
class Animal {
    void sound() {
        System.out.println("동물이 소리를 낸다");
    }
}

class Dog extends Animal {
    @Override
    void sound() {
        System.out.println("멍멍");
    }
}

→ 부모 클래스의 “sound()” 메서드를
→ 자식 클래스에서 “같은 시그니처”로 재정의한 경우


✅ 오버로딩 예제

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Printer {
    void print(String text) {
        System.out.println(text);
    }

    void print(int number) {
        System.out.println("숫자: " + number);
    }

    void print(String text, int count) {
        for (int i = 0; i < count; i++) {
            System.out.println(text);
        }
    }
}

→ 같은 클래스 안에서 “print()” 라는 이름을
→ 인자 수나 자료형을 다르게 하여 여러 번 정의한 것 (다른 시그니쳐)

🧾 함수 시그니처(Signature)란?

함수 시그니처란 함수의 이름과 매개변수 목록(자료형과 개수)을 말한다.
즉, 다음 두 요소가 같으면 같은 시그니처로 간주된다:

  • 함수 이름
  • 매개변수의 자료형과 순서

반환 타입은 시그니처에 포함되지 않는다.

1
2
3
void print(String text)         // 시그니처: print(String)
void print(int number)          // 시그니처: print(int)
int print(String text)          // ← 컴파일 오류 ❌ (위와 시그니처 같음)

→ 따라서 오버로딩은 반환 타입이 다르다고 해서 성립하지 않는다.

🎯 정리

항목오버라이딩오버로딩
핵심 키워드재정의중복 정의
목적부모 메서드의 동작 변경다양한 입력 처리
관계상속 필요상속 불필요
시점런타임(실행 중) 결정컴파일 타임 결정

💡 오버라이딩, 오버로딩
“오버라이딩은 부모 메서드를 덮어쓴다”
“오버로딩은 같은 이름을 여러 개 만든다”


이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.