코딩관계론

객체지향이란 무엇인가 본문

개발

객체지향이란 무엇인가

개발자_티모 2024. 6. 1. 14:45
반응형

객체지향이란

객체지향 프로그래밍은 프로그램을 구성하는 기본 단위인 '객체'를 중심으로 하는 프로그래밍 패러다임입니다.

객체는 데이터와 그 데이터를 처리하기 위한 메서드를 함께 묶은 것으로, 이러한 객체들은 메시지를 주고받고, 상호작용하여 프로그램의 기능을 수행합니다.

객체의 예시

 

특징

객체지향에는 아래의 기능들을 제공하여 코드를 구조화하고, 관리할 수 있으며, 유연하고 확장 가능한 소프트웨어를 개발할 수 있습니다.

  1. 캡슐화 (Encapsulation): 객체는 데이터와 해당 데이터를 처리하는 메서드를 하나의 단위로 묶어서 외부로부터의 접근을 제한합니다. 이는 객체의 내부 상태를 숨기고, 외부에서의 직접적인 접근을 제한하여 데이터의 무결성과 보안을 보장합니다.
  2. 상속 (Inheritance): 상속은 기존 클래스(부모 클래스)의 속성과 메서드를 새로운 클래스(자식 클래스)가 상속받아 재사용할 수 있게 합니다. 이를 통해 코드의 중복을 줄이고, 확장성을 높이며, 코드의 유지보수를 용이하게 합니다.
  3. 다형성 (Polymorphism):같은 인터페이스나 상위 클래스의 메서드를 사용하여 여러 종류의 객체를 처리할 수 있게 합니다. 이는 코드의 유연성을 높이고, 확장성을 강화합니다.
  4. 추상화 (Abstraction): 추상화는 복잡한 시스템을 단순화하여 필요한 부분에만 집중할 수 있도록 합니다. 객체지향에서는 클래스와 인터페이스를 통해 추상화를 구현하여 객체의 핵심적인 특징을 강조하고, 불필요한 세부 사항을 숨깁니다.

캡슐화

캡슐화는 객체지향 프로그래밍(OOP)의 핵심 개념 중 하나로, 데이터와 해당 데이터를 조작하는 메서드를 함께 묶음으로써 객체의 상태를 보호하고 안전하게 유지하는 것을 의미합니다. 이는 객체의 내부 구현을 외부로부터 숨기고, 오직 정의된 인터페이스를 통해서만 객체에 접근할 수 있도록 하는 것을 의미합니다.

캡슐화의 목적

  1. 데이터 은닉(Data Hiding): 캡슐화를 통해 객체의 상태를 외부로부터 은닉함으로써 데이터의 무결성과 안전을 보장할 수 있습니다. 객체의 내부 상태를 직접 접근할 수 없게 되므로, 데이터를 변경할 때 의도치 않은 오류를 방지할 수 있습니다.
  2. 모듈화(Modularity): 캡슐화를 통해 관련된 데이터와 기능을 하나의 단위로 묶음으로써 모듈화를 실현할 수 있습니다. 이는 객체를 독립적으로 설계하고 구현할 수 있도록 하며, 유연하고 재사용 가능한 코드를 작성할 수 있게 합니다.
  3. 유지보수성(Maintainability): 객체의 내부 구현을 감춤으로써 외부에서 객체의 동작 방식에 영향을 미치지 않고 내부 구현을 변경할 수 있습니다. 이는 코드의 유지보수성을 향상시키고, 시스템의 확장성을 높일 수 있습니다.
  4. 추상화(Abstraction): 캡슐화는 객체의 핵심적인 특성을 강조하고 불필요한 세부 사항을 숨김으로써 추상화를 실현합니다. 이는 객체의 복잡성을 숨기고 필수적인 부분에만 집중할 수 있도록 합니다.

절차지향에서는 완벽한 캡슐화를 달성할 수 없습니다. 왜냐하면 접근제한자라는 기능이 제공되지 않기 때문에 데이터의 은닉 및 추상화는 불가능하기 때문입니다.

 

아래의 절차지향 코드를 보시면 데이터와 데이터를 처리하는 메소드를 하나로 묶는 것까지는 성공했지만, 구조체 변수의 값을 main에서 언제든지 수정할 수 있습니다

#include <stdio.h>

// 구조체 정의
struct Rectangle {
    int width;
    int height;
    // 멤버 함수 포인터
    int (*calculateArea)(struct Rectangle rect);
};

// 멤버 함수 정의
int calculateAreaFunc(struct Rectangle rect) {
    return rect.width * rect.height;
}

int main() {
    // 구조체 변수 생성
    struct Rectangle myRect = {5, 10, calculateAreaFunc};
    myRect.width = 3; //직접접근으로 변경 가능함
    // 멤버 함수 호출
    int area = myRect.calculateArea(myRect);
    
    printf("사각형의 넓이: %d\n", area);
    
    return 0;
}

 

 

하지만 객체지향 언얼를 사용하면 접근제어자라는 기능을 제공하여, 외부에서 클래스 멤버변수에 접근할 수 없도록 명시할 수 있습니다. 따라서 데이터의 무결성을 보장할 수 있습니다.

public class Rectangle {
    private int width;
    private int height;
    
    // 생성자
    public Rectangle(int width, int height) {
        this.width = width;
        this.height = height;
    }
    
    // 메서드
    public int calculateArea() {
        return this.width * this.height;
    }
    
    public static void main(String[] args) {
        // 객체 생성
        Rectangle myRect = new Rectangle(5, 10);
        
        // 메서드 호출
        int area = myRect.calculateArea();
        
        System.out.println("사각형의 넓이: " + area);
    }
}

 

상속

상속은 기존 클래스(부모 클래스)의 속성과 메서드를 새로운 클래스(자식 클래스)가 상속받아 재사용할 수 있게 합니다.

이를 통해 코드의 중복을 줄이고, 확장성을 높이며, 코드의 유지보수를 용이하게 합니다.

 

특징:

  • 재사용성: 상속을 통해 자식 클래스는 부모 클래스의 모든 속성과 메서드를 상속받아 사용할 수 있습니다.
  • 계층 구조: 상속은 클래스 간의 계층 구조를 형성합니다. 이는 "is-a" 관계를 나타내며, 자식 클래스는 부모 클래스의 일종으로 간주됩니다.
  • 확장: 자식 클래스는 부모 클래스의 기능을 확장하거나 수정할 수 있습니다.
// 도형을 나타내는 부모 클래스
class Shape {
    void draw() {
        System.out.println("Drawing a shape");
    }
}

// 사각형 클래스 (부모 클래스를 상속받음)
class Rectangle extends Shape {
    private double width;
    private double height;

    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }

    // 부모 클래스의 draw() 메서드를 오버라이드하지 않음
    // 대신에 새로운 drawRectangle() 메서드를 추가함
    void drawRectangle() {
        System.out.println("Drawing a rectangle");
    }
}

// 원 클래스 (부모 클래스를 상속받음)
class Circle extends Shape {
    private double radius;

    public Circle(double radius) {
        this.radius = radius;
    }

    // 부모 클래스의 draw() 메서드를 오버라이드하지 않음
    // 대신에 새로운 drawCircle() 메서드를 추가함
    void drawCircle() {
        System.out.println("Drawing a circle");
    }
}

public class Main {
    public static void main(String[] args) {
        Shape rectangle = new Rectangle(5, 10);
        Shape circle = new Circle(3);

        ((Rectangle) rectangle).drawRectangle(); // 출력: "Drawing a rectangle"
        ((Circle) circle).drawCircle(); // 출력: "Drawing a circle"
    }
}

 

다형성

같은 인터페이스나 상위 클래스의 메서드를 사용하여 여러 종류의 객체를 처리할 수 있게 합니다. 이는 코드의 유연성을 높이고, 확장성을 강화합니다.

 

다형성의 목적

  • 유연성과 확장성: 새로운 클래스 추가 시 기존 코드 수정 없이 확장이 용이합니다.
  • 재사용성: 동일한 코드로 다양한 객체를 처리할 수 있습니다.
  • 가독성과 유지보수성: 코드 구조가 단순해지고 일관성이 유지됩니다.
  • 런타임 다형성: 프로그램의 유연성과 동적 행동을 가능하게 합니다.

유연성과 확장성

인터페이스를 구현했기 때문에 PaymentProcessor는 PaymentMethod의 동작 방식을 몰라도 되고, 신용카드 페이팔 방식이 아니라 수표 방식이 나와도 새로 구현해서 주입해주면 되기 때문에 유연성이 굉장히 높다

public interface PaymentMethod {
    void pay(double amount);
}

public class CreditCardPayment implements PaymentMethod {
    @Override
    public void pay(double amount) {
        System.out.println("Paid " + amount + " with credit card.");
    }
}

public class PayPalPayment implements PaymentMethod {
    @Override
    public void pay(double amount) {
        System.out.println("Paid " + amount + " with PayPal.");
    }
}

public class PaymentProcessor {
    private PaymentMethod paymentMethod;

    public PaymentProcessor(PaymentMethod paymentMethod) {
        this.paymentMethod = paymentMethod;
    }

    public void processPayment(double amount) {
        paymentMethod.pay(amount);
    }
}

 

 

런타임 다형성

notifiaction의 구현체가 런타임 시점에 어느 함수를 호출할지 결정됩니다.

public interface Notification {
    void notifyUser();
}

public class EmailNotification implements Notification {
    @Override
    public void notifyUser() {
        System.out.println("Sending an email notification");
    }
}

public class SMSNotification implements Notification {
    @Override
    public void notifyUser() {
        System.out.println("Sending an SMS notification");
    }
}

public class Main {
    public static void main(String[] args) {
        Notification notification;

        // 런타임 시점에 결정
        if (/* some condition */) {
            notification = new EmailNotification();
        } else {
            notification = new SMSNotification();
        }

        notification.notifyUser();
    }
}

 

절자지향에서는 다형성이 달성되기 어려운 이유

  1. 인터페이스와 상속의 부재: 절차지향 프로그래밍에서는 클래스, 인터페이스, 상속 등의 개념이 없기 때문에 동일한 인터페이스를 여러 형태로 구현하는 것이 어렵습니다. 객체지향 프로그래밍에서는 인터페이스와 상속을 통해 다형성을 쉽게 구현할 수 있습니다.
  2. 동적 바인딩의 부재: 절차지향 프로그래밍에서는 함수 호출이 컴파일 시점에 결정됩니다. 반면에 객체지향 프로그래밍에서는 동적 바인딩을 통해 런타임 시점에 메서드 호출을 결정할 수 있습니다

추상화 

추상화는 복잡한 시스템을 단순화하여 필요한 부분에만 집중할 수 있도록 합니다. 객체지향에서는 클래스와 인터페이스를 통해 추상화를 구현하여 객체의 핵심적인 특징을 강조하고, 불필요한 세부 사항을 숨깁니다.

 

 즉, 추상화는 객체들의 공통된 특징을 파악하여 정의해놓은 설계 방법이라고 볼 수 있다. 이때 추상(abstract) 클래스를 상속(inheritance)하거나 interface를 구현(implements)하는 방식으로 추상화를 실현하는 경우가 많다.

 

아래 예시에서, Car 클래스는 추상 클래스로 정의되어 있으며, start와 stop 같은 추상 메서드를 가지고 있습니다. 이 클래스를 상속받은 Sedan 및 SUV 클래스에서는 start와 stop 메서드를 구현해야 합니다. 이렇게 하면 공통된 특성을 가진 차량을 만들 수 있습니다. 이것이 추상화를 통한 간단한 예시입니다.

// 추상 클래스인 Car 정의
abstract class Car {
    // 추상 메서드로서 start 정의
    abstract void start();

    // 추상 메서드로서 stop 정의
    abstract void stop();
}

// Sedan 클래스, Car를 상속받음
class Sedan extends Car {
    // start 메서드를 오버라이드하여 Sedan 차량이 시동을 걸 때 실행되는 동작을 정의
    @Override
    void start() {
        System.out.println("Sedan is starting");
    }

    // stop 메서드를 오버라이드하여 Sedan 차량이 정지할 때 실행되는 동작을 정의
    @Override
    void stop() {
        System.out.println("Sedan is stopping");
    }
}

// SUV 클래스, Car를 상속받음
class SUV extends Car {
    // start 메서드를 오버라이드하여 SUV 차량이 시동을 걸 때 실행되는 동작을 정의
    @Override
    void start() {
        System.out.println("SUV is starting");
    }

    // stop 메서드를 오버라이드하여 SUV 차량이 정지할 때 실행되는 동작을 정의
    @Override
    void stop() {
        System.out.println("SUV is stopping");
    }
}

// 추상 클래스와 추상 메서드를 사용한 예시
public class Main {
    public static void main(String[] args) {
        // Car를 상속받은 클래스의 인스턴스 생성
        Car sedan = new Sedan();
        Car suv = new SUV();

        // 각 인스턴스의 start 메서드 호출
        sedan.start(); // 출력: "Sedan is starting"
        suv.start(); // 출력: "SUV is starting"
    }
}
 

 

결국 장점은?

 

1. 모듈화

객체가 데이터와 해당 데이터를 처리하는 함수로 묶임으로써 가능해집니다. 이로써 객체는 자체적으로 데이터와 관련된 작업을 처리할 수 있으며, 다른 객체와의 상호작용을 최소화할 수 있습니다.

 

2. 재사용성

상속과 다형성을 통해 기존의 코드를 재사용할 수 있습니다. 상속을 통해 기존 클래스의 특성을 새로운 클래스에서 확장하고, 다형성을 통해 동일한 인터페이스를 가진 객체들을 서로 다른 방식으로 사용할 수 있습니다.

 

3. 유지보수성

객체지향 프로그래밍은 캡슐화와 추상화를 통해 코드의 유지보수성을 높입니다. 캡슐화는 객체의 내부 구현을 숨기고 외부에 인터페이스를 노출함으로써 객체의 변경에 대한 영향을 최소화합니다. 추상화는 객체의 중요한 특징을 강조하고 세부 사항을 숨김으로써 코드의 가독성을 높이고 유지보수를 쉽게 만듭니다.

 

4. 확장성

상속을 통해 기존 클래스를 확장하여 새로운 기능을 추가할 수 있습니다. 이는 기존 코드를 수정하지 않고도 새로운 기능을 추가할 수 있는 유연성을 제공합니다.

 

5. 생산성 향상

객체 단위로 클래스가 나뉘고, 인터페이스를 통해 의존성을 역전시킴으로써 대규모 프로젝트의 개발과 유지보수가 용이해집니다. 객체 지향의 구조화된 접근 방식은 팀원 간의 협업을 원활하게 만들고, 재사용 가능한 모듈을 쉽게 구성할 수 있도록 돕습니다.

 
 

장점만 있을까?

1. 복잡성

객체 지향 시스템은 복잡성을 증가시킬 수 있습니다. 클래스와 객체 간의 관계, 상속과 다형성 등의 개념을 이해하고 구현하는 데 시간이 많이 소요될 수 있습니다.

 

2. 성능 저하

객체 지향 프로그래밍은 추상화와 다형성을 통해 유연성을 얻지만, 이는 실행 시간에 추가적인 오버헤드를 초래할 수 있습니다. 특히 대규모 시스템에서는 이러한 오버헤드가 성능에 영향을 미칠 수 있습니다.

 

3. 상태 관리의 복잡성

객체는 상태를 가지고 있기 때문에 객체 간의 상태 변화를 관리해야 합니다. 이로 인해 복잡한 상태 관리 문제가 발생할 수 있으며, 예기치 않은 버그의 원인이 될 수 있습니다.

 

4. 잘못된 설계로 인한 복잡성

객체 지향 설계가 잘못되면 코드의 응집도가 낮아지고 결합도가 높아질 수 있습니다. 이는 코드의 이해와 유지보수를 어렵게 만들 수 있습니다.

 

5. 유연성 부족

상속을 잘못 사용하거나 클래스 간의 관계를 잘못 설계하면 유연성이 떨어지고, 코드의 재사용이 어려워질 수 있습니다. 부모 클래스의 변경이 자식 클래스에 영향을 미치는 문제가 발생할 수도 있습니다.

 

 

그럼에도 사용하는 이유는 객체를 단위로 기능을 나누면서 대규모 협업에 용이하기 때문인 것 같다.

실제 회사에소는 모든 기능을 혼자서만 개발하지 않는다.

 

반응형

'개발' 카테고리의 다른 글

타임딜 서비스 개발기  (2) 2024.07.25
최적의 PK를 찾아서  (0) 2024.07.25
Git Branch 관리 전략  (0) 2024.07.17
ProtoBuf (Protocol Buffer, 프로토콜 버퍼)  (0) 2023.07.27
gRPC 개념  (0) 2023.07.25