객체(Object)란?
📌 정의
"객체(Object)란 상태(속성)와 동작(행위)을 가지는 독립적인 존재"
객체는 현실 세계의 개념을 프로그래밍에서 표현하는 단위입니다.
✅ 객체의 특징
특징설명
| 속성(Attributes, 상태) | 객체가 가지고 있는 데이터 (예: 자동차의 색상, 브랜드) |
| 행위(Behavior, 동작, 메서드) | 객체가 수행하는 동작 (예: 자동차의 가속, 정지) |
| 고유성(Identity) | 각 객체는 고유한 존재로 인식됨 |
2️⃣ 객체지향 프로그래밍(OOP)란?
📌 정의
"객체를 중심으로 프로그램을 설계하고 개발하는 프로그래밍 패러다임"
현실 세계의 개념을 **클래스(Class)와 객체(Object)**로 모델링하여 코드의 재사용성과 유지보수성을 높이는 방식입니다.
✅ OOP의 4가지 핵심 원칙 (객체지향 4대 원칙)
원칙설명
| 1. 캡슐화(Encapsulation) | 객체의 내부 상태를 보호하고, 외부에서 직접 접근을 제한 |
| 2. 상속(Inheritance) | 기존 클래스의 기능을 확장하여 새로운 클래스를 생성 |
| 3. 다형성(Polymorphism) | 같은 메서드를 다양한 방식으로 동작하게 구현 |
| 4. 추상화(Abstraction) | 중요한 속성과 동작만 추출하여 클래스로 표현 |
3️⃣ OOP의 핵심 원칙 설명 및 예제
✅ 1. 캡슐화(Encapsulation)
"객체의 속성을 보호하고, 외부에서 접근을 제한"
데이터를 직접 변경하지 않고, 메서드를 통해서만 접근 가능하도록 설계합니다.
📌 예제: 캡슐화 적용 (getter/setter 활용)
class BankAccount {
private int balance; // 외부에서 직접 접근 불가
// 생성자
public BankAccount(int initialBalance) {
this.balance = initialBalance;
}
// 예금
public void deposit(int amount) {
if (amount > 0) {
balance += amount;
System.out.println(amount + "원 입금. 현재 잔액: " + balance + "원");
}
}
// 출금
public void withdraw(int amount) {
if (amount > 0 && balance >= amount) {
balance -= amount;
System.out.println(amount + "원 출금. 현재 잔액: " + balance + "원");
} else {
System.out.println("잔액 부족!");
}
}
// 현재 잔액 확인 (getter)
public int getBalance() {
return balance;
}
}
public class Main {
public static void main(String[] args) {
BankAccount account = new BankAccount(5000);
account.deposit(2000); // 출력: 2000원 입금. 현재 잔액: 7000원
account.withdraw(3000); // 출력: 3000원 출금. 현재 잔액: 4000원
}
}
📌 캡슐화 적용: balance 변수를 private로 선언하여 외부에서 직접 수정할 수 없도록 보호함 ✅
✅ 2. 상속(Inheritance)
"기존 클래스를 확장하여 새로운 클래스를 생성하는 개념"
코드 재사용성을 높이고, 중복을 줄이는 역할을 합니다.
📌 예제: 상속 적용
// 부모 클래스 (Vehicle)
class Vehicle {
String brand;
void honk() {
System.out.println("빵빵! 경적을 울립니다.");
}
}
// 자식 클래스 (Car)
class Car extends Vehicle {
int wheels;
Car(String brand, int wheels) {
this.brand = brand;
this.wheels = wheels;
}
void drive() {
System.out.println(brand + " 자동차가 " + wheels + "개의 바퀴로 달립니다.");
}
}
// 사용 예제
public class Main {
public static void main(String[] args) {
Car myCar = new Car("Hyundai", 4);
myCar.honk(); // 부모 클래스 메서드 호출
myCar.drive(); // 자식 클래스 메서드 호출
}
}
📌 상속 적용: Car 클래스가 Vehicle 클래스를 상속받아 honk() 메서드를 재사용 ✅
✅ 3. 다형성(Polymorphism)
"같은 메서드를 여러 방식으로 동작하도록 구현하는 개념"
오버로딩(Overloading)과 오버라이딩(Overriding) 방식이 있음.
📌 예제: 오버라이딩 적용
// 부모 클래스
class Animal {
void sound() {
System.out.println("동물이 소리를 냅니다.");
}
}
// 자식 클래스
class Dog extends Animal {
@Override
void sound() {
System.out.println("멍멍!");
}
}
// 사용 예제
public class Main {
public static void main(String[] args) {
Animal myDog = new Dog();
myDog.sound(); // 출력: 멍멍!
}
}
📌 예제: 오버로딩 적용
class MathOperations {
// 1. 두 개의 정수를 더하는 메서드
int add(int a, int b) {
return a + b;
}
// 2. 세 개의 정수를 더하는 메서드 (오버로딩)
int add(int a, int b, int c) {
return a + b + c;
}
// 3. 실수(double)를 더하는 메서드 (오버로딩)
double add(double a, double b) {
return a + b;
}
}
public class Main {
public static void main(String[] args) {
MathOperations math = new MathOperations();
System.out.println(math.add(2, 3)); // 출력: 5
System.out.println(math.add(2, 3, 4)); // 출력: 9
System.out.println(math.add(2.5, 3.2)); // 출력: 5.7
}
}
✅ 4. 추상화(Abstraction)
"중요한 속성과 동작만 추출하여 클래스로 표현하는 개념"
인터페이스(Interface)나 추상 클래스(Abstract Class)를 활용하여 구현.
📌 예제: 인터페이스를 이용한 추상화
// 인터페이스 정의
interface Animal {
void makeSound();
}
// 구현 클래스
class Cat implements Animal {
@Override
public void makeSound() {
System.out.println("야옹!");
}
}
public class Main {
public static void main(String[] args) {
Animal myCat = new Cat();
myCat.makeSound(); // 출력: 야옹!
}
}
📌 예제: 추상클래스
// 1. 추상 클래스 정의
abstract class Animal {
String name;
// 생성자
Animal(String name) {
this.name = name;
}
// 추상 메서드 (구현 없음 → 반드시 하위 클래스에서 구현해야 함)
abstract void makeSound();
// 일반 메서드 (공통 기능 제공 가능)
void sleep() {
System.out.println(name + "가 잠을 잡니다.");
}
}
// 2. 구체적인 동물 클래스들 (추상 클래스 상속 및 구현)
class Dog extends Animal {
Dog(String name) {
super(name);
}
@Override
void makeSound() {
System.out.println(name + "가 멍멍 짖습니다.");
}
}
class Cat extends Animal {
Cat(String name) {
super(name);
}
@Override
void makeSound() {
System.out.println(name + "가 야옹 울습니다.");
}
}
// 3. 메인 실행
public class Main {
public static void main(String[] args) {
Animal myDog = new Dog("바둑이");
Animal myCat = new Cat("나비");
myDog.makeSound(); // 출력: 바둑이가 멍멍 짖습니다.
myCat.makeSound(); // 출력: 나비가 야옹 울습니다.
myDog.sleep(); // 출력: 바둑이가 잠을 잡니다.
myCat.sleep(); // 출력: 나비가 잠을 잡니다.
}
}
📌 추상화 적용: Animal 인터페이스를 통해 Cat이 공통 기능을 구현
추상클래스 : "공통적인 속성과 동작을 정의하는데 사용되며, 일부 메서드는 구현되지 않은 상태로 남겨둘 수 있음"
일반 클래스와 다르게 객체를 직접 생성할 수 없으며, 반드시 상속받아 사용해야 함
SOLID 원칙이란?
SOLID 원칙은 5가지 객체 지향 설계 원칙을 나타내는 약어입니다.
이 원칙을 따르면 코드가 더 이해하기 쉽고, 유지보수가 용이하며, 확장성이 뛰어난 구조가 됩니다.
원칙원칙의 개념
| SRP (단일 책임 원칙) | 클래스는 하나의 책임만 가져야 한다. |
| OCP (개방-폐쇄 원칙) | 확장에는 열려 있고, 변경에는 닫혀 있어야 한다. |
| LSP (리스코프 치환 원칙) | 상위 타입 객체를 하위 타입 객체로 치환해도 정상적으로 동작해야 한다. |
| ISP (인터페이스 분리 원칙) | 클라이언트가 사용하지 않는 기능에 의존하지 않도록 인터페이스를 분리해야 한다. |
| DIP (의존 역전 원칙) | 고수준 모듈이 저수준 모듈에 의존하지 않아야 한다. |
1️⃣ SRP (Single Responsibility Principle) - 단일 책임 원칙
📌 정의
"클래스는 단 하나의 책임을 가져야 하며, 변경하는 이유도 단 하나여야 한다."
즉, 하나의 클래스는 단일한 기능(책임)만을 수행해야 한다.
🔹 단일 책임 원칙을 지켜야 하는 이유
✅ 하나의 클래스가 여러 기능을 담당하면 변경이 어려워짐
✅ 수정할 때, 여러 기능이 한꺼번에 영향을 받을 가능성이 있음
✅ 기능별로 클래스를 나누면 코드의 재사용성과 유지보수성이 향상됨
🔹 잘못된 예제 (SRP 위반)
class User {
void saveToDatabase() { /* DB 저장 로직 */ }
void sendEmail() { /* 이메일 전송 로직 */ }
}
- User 클래스가 데이터 저장과 이메일 전송이라는 두 가지 책임을 가짐
- 데이터 저장이 변경되면 이메일 전송도 영향을 받을 가능성이 있음 → 단일 책임 원칙 위반
🔹 올바른 예제 (SRP 준수)
class UserRepository {
void saveToDatabase() { /* DB 저장 로직 */ }
}
class EmailService {
void sendEmail() { /* 이메일 전송 로직 */ }
}
- 데이터 저장은 UserRepository가 담당
- 이메일 전송은 EmailService가 담당
- 역할을 분리하여 유지보수가 쉬워짐 ✅
2️⃣ OCP (Open-Closed Principle) - 개방-폐쇄 원칙
📌 정의
"클래스는 확장에는 열려 있어야 하고, 변경에는 닫혀 있어야 한다."
즉, 기존 코드를 수정하지 않고도 새로운 기능을 추가할 수 있어야 한다.
🔹 OCP를 지키지 않으면?
❌ 기존 클래스를 직접 수정해야 하므로 변경이 어렵고, 버그 발생 가능성이 높아짐
✅ 새로운 기능 추가 시 기존 코드 수정 없이 확장할 수 있도록 설계해야 함
🔹 잘못된 예제 (OCP 위반)
class NotificationService {
void sendNotification(String type, String message) {
if (type.equals("EMAIL")) {
System.out.println("Send email: " + message);
} else if (type.equals("SMS")) {
System.out.println("Send SMS: " + message);
}
}
}
- 새로운 알림 방식 (예: 푸시 알림) 추가 시 기존 코드를 수정해야 함 → OCP 위반
🔹 올바른 예제 (OCP 준수)
interface Notification {
void send(String message);
}
class EmailNotification implements Notification {
public void send(String message) {
System.out.println("Send email: " + message);
}
}
class SMSNotification implements Notification {
public void send(String message) {
System.out.println("Send SMS: " + message);
}
}
class NotificationService {
private Notification notification;
public NotificationService(Notification notification) {
this.notification = notification;
}
void sendNotification(String message) {
notification.send(message);
}
}
- 기존 코드를 변경하지 않고 새로운 Notification 구현체만 추가하면 됨 ✅
- 확장성 증가, 유지보수성 향상
3️⃣ LSP (Liskov Substitution Principle) - 리스코프 치환 원칙
📌 정의
"상위 타입 객체를 하위 타입 객체로 치환해도 프로그램이 정상적으로 동작해야 한다."
즉, 자식 클래스는 부모 클래스의 기능을 깨지 않도록 확장해야 한다.
즉, 부모 클래스가 예상하는 동작을 모든 자식 클래스가 동일하게 유지해야 한다.
자식 클래스가 부모 클래스의 규칙을 깨지 않고 확장해야 한다.
🔹 잘못된 예제 (LSP 위반)
class Bird {
void fly() {
System.out.println("날아간다!");
}
}
class Penguin extends Bird {
@Override
void fly() {
throw new UnsupportedOperationException("펭귄은 날 수 없습니다!");
}
}
- 펭귄(Penguin) 객체를 Bird 타입으로 치환하면 예외 발생 → LSP 위반 ❌
🔹 올바른 예제 (LSP 준수)
abstract class Bird { }
class FlyingBird extends Bird {
void fly() { System.out.println("날아간다!"); }
}
class Penguin extends Bird { }
- Bird를 날 수 있는 새(FlyingBird)와 날 수 없는 새(Penguin)로 분리
- 하위 클래스가 부모 클래스의 행위를 깨뜨리지 않음 ✅
4️⃣ 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("로봇은 먹을 수 없음!"); }
}
- 로봇(Robot)은 eat() 메서드가 필요 없지만 강제로 구현해야 함 → ISP 위반 ❌
🔹 올바른 예제 (ISP 준수)
interface Workable {
void work();
}
interface Eatable {
void eat();
}
class Robot implements Workable {
public void work() { System.out.println("일하는 중"); }
}
class Human implements Workable, Eatable {
public void work() { System.out.println("일하는 중"); }
public void eat() { System.out.println("밥 먹는 중"); }
}
- 인터페이스를 기능별로 분리하여 불필요한 구현을 방지 ✅
5️⃣ DIP (Dependency Inversion Principle) - 의존 역전 원칙
📌 정의
"고수준 모듈(추상적인 개념)은 저수준 모듈(구체적인 구현)에 의존해서는 안 된다."
즉, 인터페이스를 활용하여 의존성을 역전해야 한다.
🔹 잘못된 예제 (DIP 위반)
class Keyboard {
void type() {
System.out.println("키보드를 사용합니다.");
}
}
class Computer {
private Keyboard keyboard = new Keyboard(); // 직접 의존 ❌
void useKeyboard() {
keyboard.type();
}
}
- Computer가 Keyboard의 구현체에 직접 의존 → DIP 위반
🔹 올바른 예제 (DIP 준수)
// 1. 인터페이스(추상화) 정의
interface Keyboard {
void type();
}
// 2. 구체적인 구현체들 (다양한 키보드 타입)
class MechanicalKeyboard implements Keyboard {
@Override
public void type() {
System.out.println("기계식 키보드를 사용합니다.");
}
}
class WirelessKeyboard implements Keyboard {
@Override
public void type() {
System.out.println("무선 키보드를 사용합니다.");
}
}
// 3. Computer 클래스는 Keyboard 인터페이스에 의존
class Computer {
private Keyboard keyboard;
// 생성자를 통한 의존성 주입 (Dependency Injection)
public Computer(Keyboard keyboard) {
this.keyboard = keyboard;
}
void useKeyboard() {
keyboard.type();
}
}
- 인터페이스(Keyboard)를 사용하여 구현체를 분리 ✅