객체 지향의 4가지 특징
🔒 캡슐화란?
- 객체의 데이터(속성)을 외부에서 직접 접근하지 못하게 감추고,
정해진 방법(메서드)으로만 접근하게 만드는 것
🛡️ 접근 제어자란?
- 클래스, 변수, 메서드 등이 어디에서 접근 가능한지를 지정하는 키워드

| 접근 제어자 | 접근 범위 설명 | 사용 예시 |
|---|---|---|
private | 같은 클래스 내에서만 접근 가능 | 데이터 보호, 캡슐화 핵심 |
protected | 같은 패키지 + 상속받은 클래스에서 접근 가능 | 상속 구조에서 자식에게 열어 줄 때 |
default (아무것도 안 쓰면) | 같은 패키지 내에서만 접근 가능 | 패키지 단위로 관리할 때 |
public | 어디서든 접근 가능 | 외부에서도 자유롭게 써야 할 때 |
public class Account {
private int balance; // 외부에서 직접 접근 불가
public void deposit(int amount) {
balance += amount;
}
public int getBalance() {
return balance;
}
}
-> balance 는 private 라 외부에서 account.balance = 1000; 사용 불가능
-> 대신 deposit() 이나 getBalance() 처럼 정해진 방식만 허용
🔍 게터(getter)란?
- 객체의 private 변수 값을 가져오는 메서드
- 보통 get변수명() 형태로 이름을 지음
public String getName() { return name; }➡️ 이 메서드를 호출하면 name 변수의 값을 돌려줌
🔍 세터(setter)란?
- 객체의 private 변수 값을 설정하는 메서드
- 보통 set변수명(값) 형태로 이름을 지음
public void setName(String newName) { name = newName; }➡️ 이 메서드를 호출하면 name 변수에 새로운 값이 저장됨
📦 왜 필요할까?
1. 캡슐화 유지
→ private 변수는 외부에서 직접 접근할 수 없기 때문에 getter/setter 를 통해 안전하게 접근
2. 데이터 유효성 검사
→ 세터 안에 조건을 넣어서 잘못된 값이 들어오는 것을 방지
public void setAge(int age) {
if (age >= 0) {
this.age = age;
}
}
3. 유지보수에 유리
→ 직접 변수에 접근하는 것보다 메소드를 통해 조작하면 기능 수정과 추가에 용이
🧾 전체 예시:
public class Person {
private String name;
public String getName() {
return name; // getter
}
public void setName(String name) {
this.name = name; // setter
}
}
→ Person 객체의 name 변수는 외부에서 getName() 과 setName() 을 통해서만 접근 가능
→ 캡슐화 + 안전한 데이터 처리의 대표적인 형태
⚠️ 무분별한 세터 사용이란?
❌ 왜 문제일까?
1. 캡슐화 파괴
→ 세터를 열어 두면 객체의 상태를 외부가 마음대로 변경할 수 있어서 보호가 안 됨
→ 결국 캡슐화의 의미가 사라짐
2. 객체의 무결성 훼손
→ setAge(-5) 와 같은 잘못된 값이 들어와도 조건 없이 반영돼 버릴 수 있음
3. 설계가 취약해짐
→ 외부 코드가 객체의 내부 상태를 자주 바꾸면 코드 흐름 예측이 불가능해지고, 디버깅도 힘들어짐
🧾 예시 코드:
public class User {
private String role;
public void setRole(String role) {
this.role = role; // 위험! 아무 역할이나 막 설정 가능
}
}
User user = new User();
user.setRole("admin");
user.setRole("guest");
❗문제점 요약
🔧 안전한 데이터 설정 로직 추가
public class User {
private String role;
// 허용된 역할만 저장
public void setRole(String role) {
if (role.equals("admin") || role.equals("user") || role.equals("guest")) {
this.role = role;
} else {
throw new IllegalArgumentException("허용되지 않은 역할입니다: " + role);
}
}
public String getRole() {
return role;
}
}
🧬 상속이란?
- 기존 클래스(부모, 상위 클래스)의 속성과 동작을 새로운 클래스(자식, 하위 클래스)가 물려받는 것
→ 즉, 코드를 재사용하면서 확장할 수 있는 기능
🧾 예시 코드:
// 부모 클래스
public class Animal {
public void eat() {
System.out.println("먹는다.");
}
}
// 자식 클래스
public class Dog extends Animal {
public void bark() {
System.out.println("멍멍!");
}
}
public class Main {
public static void main(String[] args) {
Dog myDog = new Dog();
myDog.eat(); // 부모 클래스 메서드
myDog.bark(); // 자식 클래스 메서드
}
}
➡️ Dog 클래스는 Animal 클래스를 상속받았기 때문에 eat() 이라는 메소드 그대로 사용 가능
🔍 상속의 장점
| 장점 | 설명 |
|---|---|
| 코드 재사용 | 이미 만든 클래스를 다시 써서 새로운 클래스를 쉽게 만들 수 있음 |
| 확장성 | 기존 코드를 바꾸지 않고 기능을 덧붙일 수 있음 |
| 유지 보수 편리 | 공통 기능을 부모에 모아 두면 변경할 때 한 곳만 수정하면 됨 |
🔍 super - 부모 인스턴스란?
- 자식 클래스에서 부모 클래스의 필드(변수), 메서드, 생성자에 접근할 때 사용하는 키워드
- 즉, "부모 클래스의 것"을 가리키는 참조
✅ super의 주요 용도
1. 부모의 메서드 호출
class Animal {
public void sound() {
System.out.println("동물이 소리를 낸다.");
}
}
class Dog extends Animal {
@Override
public void sound() {
super.sound(); // 부모 메서드 호출
System.out.println("멍멍!");
}
}
출력 결과
동물이 소리를 낸다
멍멍!
2. 부모의 생성자 호출
class Animal {
public Animal(String name) {
System.out.println("동물 이름: " + name);
}
}
class Dog extends Animal {
public Dog() {
super("댕댕이"); // 부모 생성자 호출
System.out.println("강아지 생성됨.");
}
}
출력 결과
동물 이름: 댕댕이
강아지 생성됨
3. 부모 클래스의 필드 참조
class Animal {
String type = "동물";
}
class Dog extends Animal {
String type = "강아지";
public void printType() {
System.out.println(type); // 강아지
System.out.println(super.type); // 동물
}
}
⚠️ 주의할 점
🔁 메서드 오버라이딩(Overriding)이란?
- 부모 클래스에서 정의한 메서드를 자식 클래스에서 똑같은 이름, 매개변수, 반환형으로 다시 정의하는 것
- 즉, 부모의 행동을 자식이 자신에 맞게 "덮어쓰는" 것
⚙️ 오버라이딩의 조건
| 조건 | 설명 |
|---|---|
| 메서드 이름 | 부모와 완전히 같아야 함 |
| 매개변수 | 개수, 순서, 타입이 같아야 함 |
| 반환형 | 부모와 같거나 부모보다 더 구체적인 타입(covariant return type) |
| 접근 제어자 | 부모보다 같거나 더 넓은 범위(public > protected > default > private) |
| 예외 | 부모가 던지는 예외보다 더 넓거나 같아야 함 |
🧾 예시 코드:
class Animal {
public void sound() {
System.out.println("동물이 소리를 낸다.");
}
}
class Dog extends Animal {
@Override
public void sound() {
System.out.println("멍멍!");
}
}
public class Main {
public static void main(String[] args) {
Animal a = new Dog();
a.sound(); // "멍멍!" 출력됨
}
}
➡️ sound()는 Animal 에도 있지만, Dog 에서 오버라이딩 되었기 때문에 Dog 버전이 실행됨
✅ 오버라이딩의 목적
🚫 오버로딩과 차이점
| 개념 | 설명 |
|---|---|
| 오버로딩(Overloading) | 같은 이름, 다른 매개변수 (→ 메서드 중복 정의) |
| 오버라이딩(Overriding) | 같은 이름, 같은 매개변수 (→ 부모 메서드 재정의) |
🎯 추상화란?
- 복잡한 시스템에서 핵심적인 특징만 추려 내어 표현하고, 불필요한 세부 정보는 감추는 것
- 즉, 필요한 것만 보여 주고, 나머지는 숨긴다는 개념
✅ 왜 추상화가 중요한가?
| 이유 | 설명 |
|---|---|
| 복잡성 감소 | 사용자는 내부 구현을 몰라도 객체를 사용할 수 있음 |
| 유연한 설계 | 전체 시스템을 더 쉽게 설계하고 구조화할 수 있음 |
| 재사용성 증가 | 공통된 구조를 기반으로 다양한 객체를 만들 수 있음 |
| 캡슐화와 연결 | 내부 구현을 감추기 때문에 보호도 자연스럽게 따라옴 |
🧩 객체 지향에서의 추상화
🐾 예시: 고양이 → 동물 → 생명체

1. 고양이(Cat)
2. 동물(Animal)
3. 생명체(LivingThing)
🧾 예시 코드:
abstract class LivingThing {
public abstract void grow();
}
abstract class Animal extends LivingThing {
public abstract void makeSound();
public void eat() {
System.out.println("먹는다.");
}
}
class Cat extends Animal {
@Override
public void makeSound() {
System.out.println("야옹");
}
@Override
public void grow() {
System.out.println("고양이가 자란다.");
}
}
🎯 추상 클래스란?
- 일부만 구현되어 있고 나머지는 자식 클래스가 반드시 완성해야 하는 클래스
- 즉, 공통적인 구조와 기능은 정의해 두고, 구체적인 동작은 자식 클래스에게 맡기는 클래스
📌 특징 정리
✅ 추상 클래스를 사용하는 이유
| 이유 | 설명 |
|---|---|
| 공통 구조 제공 | 여러 자식 클래스가 공유하는 속성과 기능을 한곳에 모아 둠 |
| 틀(template) 제공 | 구현해야 하는 메서드를 명시적으로 강제 가능 |
| 코드 재사용 | 기본 동작은 추상 클래스에, 변화되는 부분은 자식 클래스에 분리 가능 |
| 캡슐화 + 추상화 | 내부 구현을 감추고 필요한 구조만 제공 가능 |
🧾 예시 코드:
abstract class Animal {
public void eat() {
System.out.println("먹는다.");
}
public abstract void sound(); // 추상 메서드
}
class Dog extends Animal {
@Override
public void sound() {
System.out.println("멍멍!");
}
}
public class Main {
public static void main(String[] args) {
// Animal a = new Animal(); // ❌ 오류: 추상 클래스는 인스턴스화 불가
Animal a = new Dog(); // ✅ 자식 클래스는 인스턴스화 가능
a.eat(); // "먹는다."
a.sound(); // "멍멍!"
}
}
🚫 추상 클래스와 인터페이스의 차이점
| 구분 | 추상 클래스 | 인터페이스 |
|---|---|---|
| 키워드 | abstract class | interface |
| 다중 상속 | 불가 | 가능(자바 8 이후 디폴트 메서드도 허용) |
| 생성자 | 있음 | 없음 |
| 필드 | 일반 변수 가능 | 상수(public static final)만 가능 |
| 메서드 | 구현된 메서드 포함 가능 | (자바 8 이전엔) 모든 메서드는 추상 메서드만 가능 |
🔍 다형성이란?
- 하나의 이름(메서드, 클래스, 인터페이스 등)으로 여러 가지 형태의 동작을 수행할 수 있는 성질
- 즉, 같은 메서드 이름을 써도 객체에 따라 다르게 동작하는 것

🧾 예시 코드:
class Animal {
public void sound() {
System.out.println("동물이 소리를 낸다");
}
}
class Dog extends Animal {
@Override
public void sound() {
System.out.println("멍멍!");
}
}
class Cat extends Animal {
@Override
public void sound() {
System.out.println("야옹~");
}
}
public class Main {
public static void main(String[] args) {
Animal a1 = new Dog();
Animal a2 = new Cat();
a1.sound(); // 멍멍!
a2.sound(); // 야옹~
}
}
➡️ sound() 라는 하나의 메서드 이름으로 실제 객체에 따라 다르게 실행되는 것
✅ 왜 중요한가?
| 장점 | 설명 |
|---|---|
| 코드 유연성 | 같은 메서드를 다양한 객체에 사용할 수 있음 |
| 유지 보수 쉬움 | 코드 변경 없이 기능 확장 가능 |
| 모듈화 & 재사용 | 공통 인터페이스로 다양한 구현체를 만들 수 있음 |
🔁 형변환이란?
- 객체의 자료형(참조형)을 다른 자료형으로 바꾸는 것
→ 주로 상속/다형성과 관련해서 부모 ↔ 자식 타입 간에 변환할 때 사용됨
🔼 업캐스팅(Upcasting)
- 자식 타입 → 부모 타입으로 형변환
- 자동 형변환(명시적 캐스팅 불필요)
🧾 예시 코드:
class Animal {
public void sound() {
System.out.println("동물이 소리를 낸다.");
}
}
class Dog extends Animal {
public void sound() {
System.out.println("멍멍!");
}
public void wagTail() {
System.out.println("꼬리를 흔든다.");
}
}
public class Main {
public static void main(String[] args) {
Dog dog = new Dog();
Animal a = dog; // 업캐스팅(자동)
a.sound(); // 멍멍! ← 다형성
// a.wagTail(); // ❌ 사용 불가(Animal 타입엔 없음)
}
}
→ 자동으로 변환됨
→ 자식 객체를 부모 타입으로 다룰 수 있음 ⇢ 다형성 구현 가능
→ 부모가 가진 기능만 사용할 수 있음
🔽 다운캐스팅(Downcasting)
- 부모 타입 → 자식 타입으로 형변환
- 명시적 형변환 필요
🧾 예시 코드:
Animal a = new Dog(); // 업캐스팅
Dog d = (Dog) a; // 다운캐스팅(명시적)
d.wagTail(); // 가능해짐
⚠️ 주의:
Animal a = new Animal();
Dog d = (Dog) a; // ❌ 오류: 실제로는 Dog가 아님
➡ 이럴 땐 런타임 에러(ClassCastException) 발생
💡 instanceof 연산자
if (a instanceof Dog) {
Dog d = (Dog) a;
d.wagTail();
}
📌 요약 정리
| 구분 | 업캐스팅 | 다운캐스팅 |
|---|---|---|
| 방향 | 자식 → 부모 | 부모 → 자식 |
| 명시적 여부 | 필요 없음 | 필요함 |
| 안전성 | 안전함 | 위험함(런타임 오류 가능) |
| 사용 목적 | 다형성 구현, 공통 인터페이스 사용 | 자식 고유 기능 사용 시 필요 |