객체지향 프로그래밍

N’oublie pas de t’aimer·2025년 3월 31일

Java

목록 보기
18/19
post-thumbnail

1. 객체지향 프로그래밍과 절차지향 프로그래밍

절차지향 프로그래밍(Procedural Programming)은 문제를 순차적인 절차나 함수의 집합으로 해결하는 방식입니다. 데이터와 함수를 분리하여 생각하며, 실행 흐름에 중점을 둡니다. 대표적인 예로는 C 언어가 있습니다.

반면 객체지향 프로그래밍(Object-Oriented Programming, OOP)은 현실 세계의 객체를 모델링하여, 데이터와 그 데이터를 다루는 메서드를 하나의 객체로 묶어 문제를 해결하는 방식입니다. 재사용성, 유지보수성, 확장성을 높이는 데에 유리하며, 대표적으로 Java, C++, Python 등의 언어가 객체지향 언어입니다.

간단히 비교하면,

구분절차지향 프로그래밍객체지향 프로그래밍
중심함수와 절차객체와 클래스
데이터 처리데이터와 기능 분리데이터와 기능 캡슐화
재사용성낮음높음 (상속, 다형성 등 활용)
유지보수어려움상대적으로 쉬움

2. **객체지향 프로그래밍의 4가지 특징

  1. 캡슐화 (Encapsulation)
    • 데이터와 그 데이터를 처리하는 메서드를 하나의 객체로 묶는 개념입니다.
    • 외부에서 객체의 내부 구현을 알 필요 없이, 공개된 인터페이스를 통해서만 접근할 수 있습니다.
   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;
      }
}
  • 정보 은닉을 통해 보안성과 안정성을 높일 수 있습니다.
  1. 상속 (Inheritance)

    • 기존 클래스의 속성과 메서드를 새로운 클래스가 물려받는 것입니다.
    • 코드의 재사용성을 높이고, 계층적 관계를 통해 구조를 단순화할 수 있습니다.

    💡 예제: 동물 클래스 (Java)

    // 부모 클래스 (상위 클래스)
    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을 상속받아 공통 기능은 재사용하고, 각각의 특화된 기능만 별도로 추가합니다.
  • 상속을 통해 중복 없이 공통 로직을 한 곳에 정의하고, 하위 클래스는 필요한 기능만 추가 구현하면 됩니다.
  • 이런 계층 구조는 유지보수 시에도 유리하며, 전체 시스템을 논리적으로 분리하여 구조를 단순화합니다.
  1. 추상화 (Abstraction)

    • 복잡한 내부 로직은 숨기고, 중요한 기능만 외부에 공개하는 것입니다.
    • 인터페이스나 추상 클래스를 통해 구현과 사용을 분리하여 유연한 설계가 가능합니다.

    사용자는 "무엇을 할 수 있는가"만 알면 되고, "어떻게 작동하는가"는 몰라도 되는 구조입니다.

💡 예제: 결제 시스템 (Java)

// 추상 클래스 (또는 인터페이스)
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()만 호출하면 되며, 구현을 몰라도 객체를 사용할 수 있는 것이 추상화의 핵심입니다.
  1. 다형성 (Polymorphism)
    • 같은 인터페이스나 부모 클래스를 기반으로 다양한 구현을 사용할 수 있는 특성입니다.
    • 오버라이딩, 오버로딩 등을 통해 객체를 유연하게 사용할 수 있어 확장성과 가독성이 좋아집니다.

즉, "같은 이름의 메서드나 객체가 상황에 따라 다른 방식으로 동작하는 것"

하나의 인터페이스로 여러 동작을 가능하게 하는 것이다.

1. 오버라이딩 (Overriding)

  • 상속 관계에서, 부모 클래스의 메서드를 자식 클래스에서 재정의하는 것
  • 다형성과 직접적으로 연결되어 있음
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()가 실행됨.
➡️ 이것이 런타임 다형성입니다.

2. 오버로딩 (Overloading)

  • 같은 이름의 메서드를 인자의 타입이나 개수에 따라 여러 개 정의하는 것
  • 컴파일러가 호출할 메서드를 구분하므로 컴파일타임 다형성이라고도 합니다.
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개 인자 버전

➡️ 동일한 이름의 메서드지만, 다른 방식으로 동작합니다.
➡️ 이것이 정적(컴파일타임) 다형성입니다.


🔁 정리하면

구분오버라이딩오버로딩
관련성상속 관계메서드 시그니처
다형성런타임 다형성컴파일타임 다형성
의미부모 메서드를 자식이 재정의같은 이름, 인자 다른 메서드들
목적행동 변경유연한 호출 제공

다형성은 코드의 유연성과 확장성을 높이는 데 핵심 역할을 합니다. 오버라이딩과 오버로딩은 그 구현 수단이에요.

3. 객체지향 프로그래밍의 5대 원칙(SOLID)에 대해 설명하세요.

  1. 단일 책임 원칙 (SRP, Single Responsibility Principle)
    • 클래스는 하나의 책임만을 가져야 합니다.
    • 책임이 명확할수록 변경이 적고 유지보수가 쉬운 구조를 만들 수 있습니다.

✅ ❌ 단일 책임 원칙을 어긴 예시

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: 출력 책임

이렇게 하면 유지보수나 변경도 훨씬 쉬워진다.

  1. 개방-폐쇄 원칙 (OCP, Open/Closed Principle)
    • 소프트웨어 구성 요소는 확장에는 열려 있고, 변경에는 닫혀 있어야 합니다.
    • 즉, 기존 코드를 수정하지 않고 기능을 확장할 수 있어야 합니다.

즉, 기존 코드를 수정하지 않고도 새로운 기능을 확장할 수 있어야 한다는 의미입니다.

✅ ❌ 원칙을 어긴 예시

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 준수

🎯 핵심 요약

  • OCP는 변경 없이 확장 가능해야 한다는 원칙
  • 상속, 인터페이스, 다형성을 통해 실현하는 경우가 많음
  1. 리스코프 치환 원칙 (LSP, Liskov Substitution Principle)
    • 자식 클래스는 부모 클래스를 대체할 수 있어야 합니다.
    • 상속 구조에서 자식 클래스가 부모 클래스의 기능을 훼손하지 않고 사용할 수 있어야 합니다.

❌ 위반 예시 (문제 있는 상속)

class Bird {
    public void fly() {
        System.out.println("날고 있어요!");
    }
}

class Ostrich extends Bird {
    @Override
    public void fly() {
        throw new UnsupportedOperationException("타조는 날 수 없어요!");
    }
}

📌 여기서 OstrichBird를 상속했지만, 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 인터페이스를 쓰고 싶으면 오직 날 수 있는 새만 대입
→ 대체 가능성 보장 → 리스코프 원칙 만족

  1. 인터페이스 분리 원칙 (ISP, Interface Segregation Principle)
    • 클라이언트는 자신이 사용하지 않는 메서드에 의존하지 않아야 합니다.
    • 하나의 범용 인터페이스보다는 여러 개의 구체적인 인터페이스로 분리하는 것이 좋습니다.

즉, 하나의 거대한 인터페이스를 쪼개서 필요한 것만 제공하자는 원칙입니다.

❌ 위반 예시 (너무 많은 걸 강요하는 인터페이스)

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 만족

  1. 의존 역전 원칙 (DIP, Dependency Inversion Principle)
    • 구체적인 구현보다 추상(인터페이스)에 의존해야 합니다.
    • 이를 통해 모듈 간 결합도를 낮추고, 유연하고 테스트하기 쉬운 구조를 만들 수 있습니다.

말이 좀 어렵지만, 핵심은
“구체적인 클래스에 직접 의존하지 말고, 인터페이스에 의존해라”입니다.

❌ 위반 예시 (상위 모듈이 하위 구현에 의존함)

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("안녕하세요!");
    }
}

📌 이제 NotificationEmailService에 직접 의존하지 않음
SMSService, KakaoTalkService 등 다른 구현체로 쉽게 교체 가능
→ DIP 만족


✅ 요약

항목설명
핵심구체 클래스가 아니라 인터페이스(추상화)에 의존해라
이점유연한 구조, 쉽게 확장/변경 가능, 테스트 용이
구현 방법인터페이스 + 의존성 주입 (생성자 주입 등)
profile
매일 1퍼센트씩 나아지기 ୧(﹒︠ ̫ ̫̊ ̫﹒︡)୨

0개의 댓글