
절차지향 프로그래밍(Procedural Programming)은 문제를 순차적인 절차나 함수의 집합으로 해결하는 방식입니다. 데이터와 함수를 분리하여 생각하며, 실행 흐름에 중점을 둡니다. 대표적인 예로는 C 언어가 있습니다.
반면 객체지향 프로그래밍(Object-Oriented Programming, OOP)은 현실 세계의 객체를 모델링하여, 데이터와 그 데이터를 다루는 메서드를 하나의 객체로 묶어 문제를 해결하는 방식입니다. 재사용성, 유지보수성, 확장성을 높이는 데에 유리하며, 대표적으로 Java, C++, Python 등의 언어가 객체지향 언어입니다.
간단히 비교하면,
| 구분 | 절차지향 프로그래밍 | 객체지향 프로그래밍 |
|---|---|---|
| 중심 | 함수와 절차 | 객체와 클래스 |
| 데이터 처리 | 데이터와 기능 분리 | 데이터와 기능 캡슐화 |
| 재사용성 | 낮음 | 높음 (상속, 다형성 등 활용) |
| 유지보수 | 어려움 | 상대적으로 쉬움 |
public class BankAccount {
private int balance; // 외부에서 직접 접근할 수 없음
public BankAccount(int initialBalance) {
this.balance = initialBalance;
}
// 외부에 공개된 인터페이스: 입금
public void deposit(int amount) {
if (amount > 0) {
balance += amount;
}
}
// 외부에 공개된 인터페이스: 출금
public void withdraw(int amount) {
if (amount > 0 && amount <= balance) {
balance -= amount;
}
}
// 외부에 공개된 인터페이스: 잔액 조회
public int getBalance() {
return balance;
}
}
상속 (Inheritance)
// 부모 클래스 (상위 클래스)
public class Animal {
protected String name;
public Animal(String name) {
this.name = name;
}
public void eat() {
System.out.println(name + " is eating.");
}
public void sleep() {
System.out.println(name + " is sleeping.");
}
}
// 자식 클래스 (하위 클래스)
public class Dog extends Animal {
public Dog(String name) {
super(name); // 부모 클래스의 생성자 호출
}
public void bark() {
System.out.println(name + " is barking.");
}
}
// 자식 클래스 (하위 클래스)
public class Cat extends Animal {
public Cat(String name) {
super(name);
}
public void meow() {
System.out.println(name + " is meowing.");
}
}
// 사용 예
public class Main {
public static void main(String[] args) {
Dog dog = new Dog("Max");
dog.eat(); // Animal 클래스에서 상속받은 메서드
dog.bark(); // Dog 클래스 고유 메서드
Cat cat = new Cat("Luna");
cat.sleep(); // Animal 클래스에서 상속받은 메서드
cat.meow(); // Cat 클래스 고유 메서드
}
}
Animal 클래스는 공통 속성(name)과 공통 기능(eat(), sleep())을 정의합니다.Dog, Cat 클래스는 Animal을 상속받아 공통 기능은 재사용하고, 각각의 특화된 기능만 별도로 추가합니다.추상화 (Abstraction)
사용자는 "무엇을 할 수 있는가"만 알면 되고, "어떻게 작동하는가"는 몰라도 되는 구조입니다.
// 추상 클래스 (또는 인터페이스)
public abstract class PaymentProcessor {
public abstract void pay(int amount); // 외부에 공개된 인터페이스
}
// 구체 클래스 1 - 카드 결제
public class CardPayment extends PaymentProcessor {
@Override
public void pay(int amount) {
System.out.println(amount + "원 카드로 결제되었습니다.");
}
}
// 구체 클래스 2 - 현금 결제
public class CashPayment extends PaymentProcessor {
@Override
public void pay(int amount) {
System.out.println(amount + "원 현금으로 결제되었습니다.");
}
}
// 사용 예
public class Main {
public static void main(String[] args) {
PaymentProcessor payment = new CardPayment(); // 또는 new CashPayment();
payment.pay(10000); // 어떤 방식으로 결제되는지 몰라도 사용 가능
}
}
PaymentProcessor는 결제를 수행한다는 기능만을 정의합니다.CardPayment, CashPayment)은 내부적으로 어떻게 결제를 처리하는지는 사용자가 알 필요 없음.pay()만 호출하면 되며, 구현을 몰라도 객체를 사용할 수 있는 것이 추상화의 핵심입니다.즉, "같은 이름의 메서드나 객체가 상황에 따라 다른 방식으로 동작하는 것"
하나의 인터페이스로 여러 동작을 가능하게 하는 것이다.
class Animal {
public void speak() {
System.out.println("동물이 소리를 냅니다.");
}
}
class Dog extends Animal {
@Override
public void speak() {
System.out.println("멍멍!");
}
}
class Cat extends Animal {
@Override
public void speak() {
System.out.println("야옹~");
}
}
Animal a1 = new Dog();
Animal a2 = new Cat();
a1.speak(); // 멍멍!
a2.speak(); // 야옹~
➡️ Animal 타입으로 객체를 다루지만, 실제로는 다형적으로 각 클래스에 맞는 speak()가 실행됨.
➡️ 이것이 런타임 다형성입니다.
class Calculator {
public int add(int a, int b) {
return a + b;
}
public double add(double a, double b) {
return a + b;
}
public int add(int a, int b, int c) {
return a + b + c;
}
}
Calculator calc = new Calculator();
System.out.println(calc.add(1, 2)); // int 버전
System.out.println(calc.add(1.5, 2.5)); // double 버전
System.out.println(calc.add(1, 2, 3)); // 3개 인자 버전
➡️ 동일한 이름의 메서드지만, 다른 방식으로 동작합니다.
➡️ 이것이 정적(컴파일타임) 다형성입니다.
| 구분 | 오버라이딩 | 오버로딩 |
|---|---|---|
| 관련성 | 상속 관계 | 메서드 시그니처 |
| 다형성 | 런타임 다형성 | 컴파일타임 다형성 |
| 의미 | 부모 메서드를 자식이 재정의 | 같은 이름, 인자 다른 메서드들 |
| 목적 | 행동 변경 | 유연한 호출 제공 |
다형성은 코드의 유연성과 확장성을 높이는 데 핵심 역할을 합니다. 오버라이딩과 오버로딩은 그 구현 수단이에요.
public class Report {
public String content;
public void saveToFile(String filename) {
// 파일로 저장
}
public void print() {
// 출력
}
}
📌 이 Report 클래스는 보고서 내용만 다뤄야 하는데,
파일 저장과 출력 기능까지 다 담당하고 있어요.
→ 책임이 3개: 보고서 데이터 관리 / 저장 / 출력
→ SRP 위반
public class Report {
public String content;
// 보고서 내용만 담당
}
public class ReportSaver {
public void saveToFile(Report report, String filename) {
// 파일로 저장
}
}
public class ReportPrinter {
public void print(Report report) {
// 출력
}
}
📌 이 구조에서는 각 클래스가 명확한 한 가지 책임만 가집니다:
Report: 내용 관리ReportSaver: 저장 책임ReportPrinter: 출력 책임이렇게 하면 유지보수나 변경도 훨씬 쉬워진다.
즉, 기존 코드를 수정하지 않고도 새로운 기능을 확장할 수 있어야 한다는 의미입니다.
public class DiscountCalculator {
public int calculateDiscount(String memberType, int price) {
if (memberType.equals("Silver")) {
return price - 1000;
} else if (memberType.equals("Gold")) {
return price - 2000;
} else {
return price;
}
}
}
📌 이 구조는 새로운 회원 유형("VIP")을 추가하려면
→ if 문을 직접 수정해야 함
→ OCP 위반
// 할인 정책 인터페이스
public interface DiscountPolicy {
int getDiscount(int price);
}
// 실버 회원 할인
public class SilverDiscount implements DiscountPolicy {
public int getDiscount(int price) {
return price - 1000;
}
}
// 골드 회원 할인
public class GoldDiscount implements DiscountPolicy {
public int getDiscount(int price) {
return price - 2000;
}
}
// 할인 계산기
public class DiscountCalculator {
public int calculateDiscount(DiscountPolicy policy, int price) {
return policy.getDiscount(price);
}
}
📌 이 구조에서는 새로운 할인 정책을 새 클래스만 추가해서 확장하면 됨
→ 기존 코드(DiscountCalculator)는 수정 필요 없음
→ OCP 준수
class Bird {
public void fly() {
System.out.println("날고 있어요!");
}
}
class Ostrich extends Bird {
@Override
public void fly() {
throw new UnsupportedOperationException("타조는 날 수 없어요!");
}
}
📌 여기서 Ostrich는 Bird를 상속했지만, fly()를 쓸 수 없는 상황
→ 상위 클래스(Bird)를 기대한 코드가 Ostrich를 대입받으면 예외 발생
→ 리스코프 치환 원칙 위반
interface Bird {
void eat();
}
interface Flyable {
void fly();
}
class Sparrow implements Bird, Flyable {
public void eat() {
System.out.println("참새가 모이를 먹어요.");
}
public void fly() {
System.out.println("참새가 날아요.");
}
}
class Ostrich implements Bird {
public void eat() {
System.out.println("타조가 풀을 먹어요.");
}
}
📌 날 수 있는 새와 날 수 없는 새를 분리된 인터페이스로 표현
→ 어떤 코드도 Flyable 인터페이스를 쓰고 싶으면 오직 날 수 있는 새만 대입
→ 대체 가능성 보장 → 리스코프 원칙 만족
즉, 하나의 거대한 인터페이스를 쪼개서 필요한 것만 제공하자는 원칙입니다.
public interface Worker {
void work();
void eat();
void sleep();
}
public class Robot implements Worker {
public void work() {
System.out.println("로봇이 일합니다.");
}
public void eat() {
// 로봇은 밥 안 먹어요
throw new UnsupportedOperationException("로봇은 먹지 않음");
}
public void sleep() {
// 로봇은 자지 않아요
throw new UnsupportedOperationException("로봇은 자지 않음");
}
}
📌 Robot 클래스는 work()만 필요하지만
→ eat(), sleep()도 강제로 구현해야 해서 원칙 위반
public interface Workable {
void work();
}
public interface Eatable {
void eat();
}
public interface Sleepable {
void sleep();
}
public class Human implements Workable, Eatable, Sleepable {
public void work() {
System.out.println("사람이 일합니다.");
}
public void eat() {
System.out.println("사람이 밥을 먹습니다.");
}
public void sleep() {
System.out.println("사람이 잡니다.");
}
}
public class Robot implements Workable {
public void work() {
System.out.println("로봇이 일합니다.");
}
}
📌 Human은 모든 기능을 구현하고, Robot은 필요한 기능만 구현
→ 불필요한 의존 없음 → ISP 만족
말이 좀 어렵지만, 핵심은
“구체적인 클래스에 직접 의존하지 말고, 인터페이스에 의존해라”입니다.
public class EmailService {
public void sendEmail(String message) {
System.out.println("이메일 전송: " + message);
}
}
public class Notification {
private EmailService emailService = new EmailService(); // 직접 의존
public void notifyUser() {
emailService.sendEmail("안녕하세요!");
}
}
📌 Notification 클래스는 EmailService라는 구체 클래스에 직접 의존
→ 나중에 문자 메시지, 카카오톡으로 바꾸려면 코드 수정 필요
→ DIP 위반
// 추상화된 메시지 서비스
public interface MessageService {
void sendMessage(String message);
}
// 이메일 서비스 구현
public class EmailService implements MessageService {
public void sendMessage(String message) {
System.out.println("이메일 전송: " + message);
}
}
// 알림 클래스는 인터페이스에만 의존
public class Notification {
private MessageService messageService;
// 생성자 주입 (외부에서 어떤 구현을 쓸지 결정)
public Notification(MessageService messageService) {
this.messageService = messageService;
}
public void notifyUser() {
messageService.sendMessage("안녕하세요!");
}
}
📌 이제 Notification은 EmailService에 직접 의존하지 않음
→ SMSService, KakaoTalkService 등 다른 구현체로 쉽게 교체 가능
→ DIP 만족
| 항목 | 설명 |
|---|---|
| 핵심 | 구체 클래스가 아니라 인터페이스(추상화)에 의존해라 |
| 이점 | 유연한 구조, 쉽게 확장/변경 가능, 테스트 용이 |
| 구현 방법 | 인터페이스 + 의존성 주입 (생성자 주입 등) |