객체지향 프로그래밍, 추상화

Yong Lee·2025년 9월 23일

개체지향 프로그래밍(OOP)
객체지향은 프로그램을 객체들의 모임으로 보는 패러다임이에요. 객체는 데이터(속성)와 기능(메서드)을 가진 단위입니다.

핵심 특징

  • 캡슐화: 데이터와 메서드를 하나로 묶고 외부에서의 접근을 제한
  • 상속: 기존 클래스를 확장해 새로운 클래스 생성
  • 다형성: 같은 인터페이스로 다양한 구현체 접근
  • 추상화: 공통된 특성을 뽑아내 일반화

예시 1

// 동물 추상 클래스

// 클래스 해석

// 1. 추상 클래스는 미완성 설계도이므로 `Animal myPet = Animal("초코")` x 이런 식으로 인스턴스화는 불가능하다.
// 2. 추상 클래스는 여러 클래스에서 공통으로 사용되는 메소드와 타입이 있다면 상위 클래스 형태로 사용된다.
abstract class Animal {
    protected String name;
    
    public Animal(String name) {
        this.name = name;
    }
	// 3. 추상 메서드는 상속받은 클래스 쪽에서 오버라이드화하여서만 사용가능하다.(반드시 override하여 메서드가 상속된 클래스에 있어야한다! 없으면 컴파일러 에러 생김)
    public abstract void makeSound(); // 추상 메서드
    
    public void sleep() {
        System.out.println(name + "이(가) 자고 있습니다.");
    }
}

// 상속을 통한 구체 클래스
class Dog extends Animal {
	// super는 상위 클래스의 오브젝트를 하위 클래스에서 끌어다 쓸 때 쓰는 겁니다.(메서드, 타입 등 다 끌어다 쓸 수 있음)
    public Dog(String name) {
        super(name);
    }
    
    //위에서 말한 오버라이드화한 메서드
    @Override
    public void makeSound() {
        System.out.println(name + ": 멍멍!");
    }
    
    public void fetch() {
        System.out.println(name + "이(가) 공을 가져옵니다.");
    }
}

// 실행 코드
public class Main {
    public static void main(String[] args) {
        Animal myPet = new Dog("초코");
        myPet.makeSound(); // 다형성
        myPet.sleep();
    }
}

예시 1을 본 후에 장점들을 생각해보자

  • 각 클래스에 똑같은 코드를 적을 필요가 없어졌다.(추상 클래스에서 한 곳에만 적으면 끝)
  • 캡슐화를 통해 외부에서 마음대로 데이터가 변하지 않게 되었다(생성자로만 이름을 받아오기 때문)
  • 메서드를 추상화해서 각 클래스에 꼭 필요한 메서드를 만드는 것을 의무화 시켰다.

추상화 방법에는 두 가지가 있다.

추상화는 복잡한 시스템에서 핵심적인 부분만 추출하여 간결하게 표현하는 것이에요.

추상화 방법

  • 추상 클래스: 미완성된 설계도로, 직접 인스턴스 생성 불가
  • 인터페이스: 메서드 시그니처만 정의하고 구현은 각 클래스에 맡김

예시 2

// 인터페이스를 통한 추상화
// 앞서 얘기했듯 인터페이스에는 메서드가 있다고 구현만 해둔다.
// 이 인터페이스를 implement할 경우 그 클래스에서는 반드시 인터페이스에 있던 메서드를 모두 사용해야한다.
interface PaymentProcessor {
    boolean processPayment(double amount);
    void cancelPayment(String transactionId);
}

// 구현체 1
class CreditCardProcessor implements PaymentProcessor {
    @Override
    public boolean processPayment(double amount) {
        System.out.println("신용카드로 " + amount + "원 결제 처리");
        // 실제 결제 로직
        return true;
    }
    
    @Override
    public void cancelPayment(String transactionId) {
        System.out.println("거래 ID: " + transactionId + " 취소됨");
    }
}

// 구현체 2
class PayPalProcessor implements PaymentProcessor {
    @Override
    public boolean processPayment(double amount) {
        System.out.println("PayPal로 " + amount + "원 결제 처리");
        // 실제 결제 로직
        return true;
    }
    
    @Override
    public void cancelPayment(String transactionId) {
        System.out.println("PayPal 거래 ID: " + transactionId + " 취소됨");
    }
}

예시 2를 본 후 인터페이스의 장점은?

  • 표준화 및 구현 강제
    • 예시 1에 있던 추상 메서드와 같다고 보면 된다. 인터페이스에 있는 메서드를 반드시 사용함으로써 오류를 차단하고 프로세스 자체를 표준화할 수 있습니다.
  • 다양화
    • 프로세스 자체는 인터페이스로 메서드 이름이 같더라도 방법에 차이가 있을 수 있습니다. 인터페이스로 선언하고 원하는 방법에 따라 사용할 수 있습니다.
  • 느슨한 결합 (확장성 & 유지보수성)
    • 인터페이스를 사용하면 코드를 짤 때 구현 클래스에 종속되지 않고 인터페이스에만 의존하게 됩니다. 이걸 느슨한 결합(Loose Coupling)이라고 합니다.
    • 예를 들어, 결제 시스템을 만드는데 초반엔 CreditCardProcessor만 썼습니다. 근데 나중에 KakaoPayProcessor도 추가하고 싶으면, 그냥 새로운 클래스가 PaymentProcessor 인터페이스를 구현하게 만들면 끝! 기존 코드(PaymentProcessor 타입으로 결제하는 부분)는 하나도 안 바꿔도 됩니다.
      이렇게 되면 나중에 기능을 바꾸거나 추가할 때 유지보수나 확장이 훨씬 쉬워져요!
  • 다중 상속의 대안
    • extends와 다르게 implements의 특징은 다중 상속이 가능하다는겁니다.
    • class Monkey implements interfaceA, interfaceB...
      이런식으로 여러 인터페이스를 쓸 수 있습니다. 이걸 통해 여러 인터페이스가 제공하는 기능들을 한 클래스에 부여할 수 있어서, 다중 상속의 장점을 어느 정도 가져가면서도 복잡성은 줄이는 효과가 있습니다.
profile
오늘은 어떤 새로운 것이 나를 즐겁게 할까?

0개의 댓글