객체지향 프로그래밍 정리

manNomi·2024년 11월 2일

WEB

목록 보기
6/9
post-thumbnail

객체 지향이란 OOP

객체지향 프로그래밍이란

현실세계의 개념을 객체로 표현해 코드의 재사용성과 유지보수성을 높이는 프로그래밍 방식

→ 일상적으로 사용하는 사물을 프로그램 안에서 하나의 객체로 만드는 것

예시로 들자면 자동차가 있다

자동차는 가장 기본적인 개념인데 이를 공통적으로 포함하는 클래스를 선언 가능합니다

class Car {
    String color;
    int speed;

    void drive() {
        // 운전하는 동작
    }

    void stop() {
        // 정지하는 동작
    }
}

이 클래스에는 모든 자동차의 속도 , 색상 , 운전 , 정지 같은 메서드가 들어갈 수 있습니다. 모든 자동차가 기능으로 가지는 기능입니다.

아우디와 벤츠는 자동차 클래스에서 파생된 자동차 모델입니다. 각각의 독특한 디자인과 특징을 추가할 수있습니다.

class Audi extends Car {
    boolean quattro;

    Audi() {
        this.quattro = true;
    }
}

class Benz extends Car {
    boolean mbux;

    Benz() {
        this.mbux = true;
    }
}

Car라는 모든 자동차의 기능을 정의하고 이를 extends를 통해 상속 받고 Car 객체의 메서드를 사용 가능하다

class Car {
    #speed = 0;

    setSpeed(value) {
        this.#speed = value;
    }

    getSpeed() {
        return this.#speed;
    }
}

const myCar = new Car();
myCar.setSpeed(60);
console.log(myCar.getSpeed()); // 60

위 내용 처럼 프라이버시 문자열을 이용해서 하는것도 가능

객체지향의 특징으로는 4가지가 있다

1. 추상화

객체에서 공통된 속성과 행위를 추출하는 기능
즉 아우디 벤츠 르노 → 자동차를 추출

불필요한 정보를 숨기고 중요한 정보만을 표현!!

즉 복잡한 구현 세부사항을 숨기는것에 초점을 둔다

예시 > 결제하기

결제하는 로직은 상당히 복잡하다

  1. 사용자는 결제를 위해 카드 정보 입력 , 결제하기 버튼만 클릭한다
  • 사용자는 결제가 이루어지는 과정을 단순한 인터페이스로 경험 → 이부분이 추상화가 이루어진 부분이다
  1. 내부 구현 세부사항
  • 시스템 내부에서는 카드 번호를 암호화하고 , 결제 게이트 웨이와 통신해 은행을 연결하고 인증프로세스를 거친다
  • 예외상황처리하고 성공/실패 여부를 사용자에게 알려준다
  • 이 모든과정은 은닉되어있으며 내부 구현사항으로 감추어진다.
class PaymentProcessor {
    processPayment(amount, cardInfo) {
        this.#encryptCardInfo(cardInfo); // 카드 정보 암호화
        this.#connectToGateway(); // 결제 게이트웨이 연결
        this.#verifyAndProcess(amount); // 인증 및 결제 진행
        console.log("결제가 완료되었습니다!");
    }

    #encryptCardInfo(cardInfo) {
        // 카드 정보를 암호화하는 내부 로직 (숨겨진 세부사항)
    }

    #connectToGateway() {
        // 결제 게이트웨이에 연결하는 로직 (숨겨진 세부사항)
    }

    #verifyAndProcess(amount) {
        // 결제 인증 및 처리 로직 (숨겨진 세부사항)
    }
}

2. 캡슐화

데이터 구조와 데이터를 다루는 방법들을 결합 시켜 묶는 것 ( 변수와 함수를 하나로 묶는 것을 뜻함) 낮은 결합도를 유지할 수 있도록 설계하는 것

객체의 데이터와 메서드를 하나로 묶고 외부에서 필요한 부분만 접근 가능하도록 제한하는것

이를 통해 데이터의 무결성을 보호하고 객체의 내부 구현을 숨길 수 있다.

목적

  1. 데이터 보호 → 외부에서 객체 내부 데이터를 직접 수정 못하도록 막고 안전하게 보호
  2. 인터페이스 제공 → 외부에서 접근가능한것 들로만 사용해 객체 사용방법을 단순하게 만듬
  3. 유연성 → 내부를 수정해도 외부 인터페이스는 그대로 유지 가능해 유지보수성이 높음

예시 : 은행 계좌 클래스

  • 우리는 잔액을 직접 수정할 수 없습니다.
  • 입금과 출금 메서드를 통해서만 잔액을 변경할 수 있음
class BankAccount {
    #balance = 0; // 잔액은 private으로 설정

    deposit(amount) {
        if (amount > 0) {
            this.#balance += amount;
            console.log(`입금 완료: ${amount}원, 현재 잔액: ${this.#balance}`);
        } else {
            console.log("입금 금액은 양수여야 합니다.");
        }
    }

    withdraw(amount) {
        if (amount > 0 && amount <= this.#balance) {
            this.#balance -= amount;
            console.log(`출금 완료: ${amount}원, 현재 잔액: ${this.#balance}`);
        } else {
            console.log("출금 금액이 잘못되었거나 잔액이 부족합니다.");
        }
    }

    getBalance() {
        return this.#balance;
    }
}

이처럼 캡슐화는 객체 내부 데이터를 보호하고, 외부에는 필요한 메서드만 제공하여 안전하고 효율적인 코드를 작성할 수 있게 도와줍니다

상속

  • 다른 클래스 (부모 , 상위 클래스)의 특성과 행동을 다른 클래스(자식,하위)가 물려받아 재사용하는것을 말한다. 상속을 이용하면 코드의 재사용성을 높이고 객체 간 계층 구조를 만들어 유사한 객체들을 체계적으로 관리가 가능
  1. 코드 재사용 : 부모클래스에 정의된 메서드와 속성을 자식 클래스가 사용해 증복을 피함
  2. 유지보수 용이 : 부모 클래스 수정사항이 자식에게도 수정이 일어남
  3. 계층 구조 형성 : 유사한 객체들을 계층구조로 정리해 객체간 관계 명확하게 정의 가능

예시: 동물 클래스와 상속 관계

class Animal {
    constructor(name) {
        this.name = name;
    }

    speak() {
        console.log(`${this.name}이(가) 소리를 냅니다.`);
    }
}

class Dog extends Animal {
    speak() {
        super.speak(); // 부모 클래스의 speak 메서드 호출
        console.log(`${this.name}이(가) 멍멍! 짖습니다.`);
    }
}

class Cat extends Animal {
    speak() {
        super.speak(); // 부모 클래스의 speak 메서드 호출
        console.log(`${this.name}이(가) 야옹~ 소리를 냅니다.`);
    }
}
class Cow extends Animal {

}
const myDog = new Dog("바둑이");
myDog.speak(); 
// 출력:
// 바둑이이(가) 소리를 냅니다.
// 바둑이이(가) 멍멍! 짖습니다.

const myCat = new Cat("나비");
myCat.speak();
// 출력:
// 나비이(가) 소리를 냅니다.
// 나비이(가) 야옹~ 소리를 냅니다.
const myCow = new Cow("젖소");
myCow.speak(); // 출력: "젖소이(가) 소리를 냅니다."
  1. 메서드 오버라이딩(Overriding): 자식 클래스에서 부모 클래스의 메서드를 재정의하여, 자식 클래스의 특정한 동작을 구현할 수 있습니다.

  2. 다형성(Polymorphism): 상속을 통해 객체는 부모 클래스 타입으로도 사용할 수 있으며, 동적 메서드 호출 시 자식 클래스의 메서드가 우선됩니다. 예를 들어, Animal 타입으로 Dog와 Cat을 관리하면서도 각자의 speak() 메서드를 호출할 수 있습니다.

다형성

  • 같은 인터페이스나 매서드를 여러 형태로 사용할 수 있게 하는것을 의미
  • 다형성 통해 부모 클래스 타입으로 자식 클래스의 인스턴스 다룰 수 있으며 자식 클래스에 따라 서로다른 동작을 수행할 수 있다 .
  1. 유연한 코드작성 : 다양한 자식 클래스 처리가능
  2. 매서드 오버라이딩 ; 부모클래스의 매서드를 재정의(오버라이딩) 해서 구현 기능 가능
  3. 동적 메서드 호출 : 부모 클래스 타입의 변수로 매서드 호출할때 실제 참조 하는 자식 클래스 메서드 실행

예시: 동물 클래스와 다형성

class Animal {
    constructor(name) {
        this.name = name;
    }

    speak() {
        console.log(`${this.name}이(가) 소리를 냅니다.`);
    }
}

class Dog extends Animal {
    speak() {
        console.log(`${this.name}이(가) 멍멍! 짖습니다.`);
    }
}

class Cat extends Animal {
    speak() {
        console.log(`${this.name}이(가) 야옹~ 소리를 냅니다.`);
    }
}

class Cow extends Animal {
    speak() {
        console.log(`${this.name}이(가) 음매~ 소리를 냅니다.`);
    }
}

다형성 활용 예시

const animals = [
    new Dog("바둑이"),
    new Cat("나비"),
    new Cow("젖소")
];

animals.forEach(animal => animal.speak());
바둑이이() 멍멍! 짖습니다.
나비이() 야옹~ 소리를 냅니다.
젖소이() 음매~ 소리를 냅니다.
  1. 동적 메서드 호출
  2. speak는 animal 클래스의 상위 매서드가 아닌 하위 매서드 실행
  3. 상속받아 speak 매서드를 오버라이딩

 같은 메서드 호출로 다양한 객체가 각각 다른 동작을 수행하도록 해주는 유연성을 제공하며, 코드의 확장성을 높이고 객체 간의 상호작용을 효율적으로 관리할 수 있게 도와줍니다

오버라이딩

  • 상위 클래스가 가지고 있는 메소드를 하위 클래스가 재정의해서 사용하는 것

오버로딩

  • 같은 이름의 메서드가 인자의 개수나 자료형에 따라 다른 기능을 하는 것

오버로딩 예시 자바 코드

public class Calculator {
    // 두 개의 정수를 더하는 메서드
    public int add(int a, int b) {
        return a + b;
    }

    // 세 개의 정수를 더하는 메서드
    public int add(int a, int b, int c) {
        return a + b + c;
    }
}
Calculator calculator = new Calculator();
System.out.println(calculator.add(3, 5));      // 출력: 8
System.out.println(calculator.add(3, 5, 2));   // 출력: 10

자바스크립트에서는 오버로딩을 허용하지 않는다


객체 지향 프로그래밍의 장단점

장점

모듈화와 팀 협업: 클래스 단위로 모듈화가 가능하기 때문에, 업무 분담이 편리하고 대규모 소프트웨어 개발에 특히 적합합니다. 각 모듈이 독립적으로 작동할 수 있어 충돌을 최소화할 수 있습니다.

유지보수 용이: 클래스 단위로 수정이 가능해 특정 기능만 변경할 때에도 다른 부분에 영향을 주지 않으므로 유지보수가 편리합니다.

코드 재사용성: 기존 클래스를 재사용하거나 상속을 통해 새로운 기능을 쉽게 추가할 수 있어 코드 재사용이 용이합니다. 이는 개발 시간과 비용을 절감하는 데도 도움이 됩니다.

단점

성능 부담: 처리 속도가 상대적으로 느리며, 객체의 수가 많아지면 메모리 사용량이 증가해 프로그램 용량이 커질 수 있습니다.

설계 복잡성: 설계 단계에서 객체와 클래스의 관계를 명확히 정의해야 하므로, 많은 시간과 노력이 필요할 수 있습니다. 잘못된 설계는 유지보수성을 떨어뜨릴 수 있어 신중한 설계가 필요합니다.

비교 항목객체지향 프로그래밍 (OOP)절차지향 프로그래밍 (POP)함수형 프로그래밍 (FP)
중심 개념객체 (클래스)함수와 명령어의 순서순수 함수, 불변성
주요 특징캡슐화, 상속, 다형성단계적 실행, 함수 중심순수 함수, 고차 함수, 함수 조합
장점유지보수와 확장성 용이, 코드 재사용성직관적인 코드 흐름, 학습이 쉬움가독성 높음, 상태 관리 용이, 병렬 처리 유리
단점설계 복잡, 메모리 사용량 증가 가능코드 중복 발생, 유지보수 어려움학습 난이도 높음, 성능 이슈 가능
적합한 용도대규모 소프트웨어, 복잡한 시스템간단한 작업, 성능 중시 소규모 시스템데이터 처리, 수학적 연산, 병렬 연산

SOLID ( 객체지향 설계 원칙을 나타내는 방법 )

1. 단일 책임 원칙 (Single Responsibility Principle, SRP)

  • 클래스는 하나의 책임만을 가쟈여 한다
  • 특정기능을 수정해야할때 관련 클래스만 수정 가능하도록 하는 방법

예시

User 클래스 → 사용자 정보 관리 클래스

이메일 보내는 기능이 User 클래스에 있을경우 이는 SRP를 위반하는 것이다

EmailService 클래스를 만들어, User 클래스는 사용자 정보 관리에만 집중하도록 분리

2. 개방 - 폐쇠 원칙 (Open-Closed Principle, OCP)

  • 소프트웨어 요소는 확장에 열려있고 수정에는 닫혀있어야 한다
  • 새로운 기능을 추가할때 코드를 수정하지 않아야한다

예시

결제 시스템을 새로운 결제 수단을 추가할때 기존 결제 클래스들을 수정하지 않고 새로운 결제 클래스만 추가하는 방식

3. 리스코프 치환 원칙 (Liskov Substitution Principle, LSP)

  • 자식 클래스는 언제나 부모클래스를 대체할 수 있어야한다
  • 부모 클래스 타입의 객체를 사용하는 코드에서, 자식 클래스의 객체를 부모 클래스처럼 사용할 수 있어야 합니다. 이를 통해 코드의 일관성과 예측 가능성을 유지할 수 있습니다

예시

만약 Bird 클래스를 상속받은 Penguin 클래스가 있다면, fly() 메서드가 없는 Penguin은 LSP를 위반하는 셈입니다. Bird 클래스에서 fly() 메서드를 제거하거나 인터페이스를 나눠야 할 필요가 있습니다.

4. 인터페이스 분리 원칙 (Interface Segregation Principle, ISP)

  • 특정 클라이언트를 위한 인터페이스 여러개 > 범용 인터페이스 한개
  • 클라이언트가 사용하지 않는 매서드에 의존하지 않도록 인터페이스를 더 작고 구체적으로 나누는 것

예시

Workable 인터페이스에 eat()와 work() 메서드가 모두 포함된 경우, eat() 메서드가 불필요한 Robot 클래스는 ISP를 위반하게 됩니다. 대신 Eatable과 Workable 인터페이스로 나누어, 각 클래스가 필요한 기능만 구현하게 합니다.

5. 의존 역전 원칙 (Dependency Inversion Principle, DIP)

  • 고수준 모듈은 저수준 모듈에 의존해서는 안 되며, 둘 다 추상화에 의존해야 합니다.
  • 구체적인 클래스가 아닌 추상화(인터페이스나 추상 클래스)에 의존하게 함으로써, 모듈 간 결합도를 낮춥니다.

예시

자동차 클래스가 구체적인 GasEngine 클래스에 의존하지 않고 Engine 인터페이스에 의존하도록 설계하여, ElectricEngine과 같은 다른 엔진도 쉽게 교체할 수 있게 만듭니다.

profile
weapp개발자

0개의 댓글