스프링입문을위한 자바 객체지향의 원리와 이해(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
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” 라는 상위 타입으로 다루지만, 결과는 각 하위 클래스에 따라 달라진다.
🔄 다형성의 이점
- 코드의 유연성 증가 – 조건문 없이 다양한 객체를 다룰 수 있다.
- 확장성 향상 – 새로운 타입을 추가해도 기존 코드를 거의 수정하지 않는다.
- 코드 중복 감소 – 공통 코드는 상위 타입에, 개별 구현은 하위 타입에 분리.
🧩 오버라이딩 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) // ← 컴파일 오류 ❌ (위와 시그니처 같음)
→ 따라서 오버로딩은 반환 타입이 다르다고 해서 성립하지 않는다.
🎯 정리
항목 | 오버라이딩 | 오버로딩 |
---|---|---|
핵심 키워드 | 재정의 | 중복 정의 |
목적 | 부모 메서드의 동작 변경 | 다양한 입력 처리 |
관계 | 상속 필요 | 상속 불필요 |
시점 | 런타임(실행 중) 결정 | 컴파일 타임 결정 |
💡 오버라이딩, 오버로딩
“오버라이딩은 부모 메서드를 덮어쓴다”
“오버로딩은 같은 이름을 여러 개 만든다”