connecting dots

React & TS 9회차 (7/31) | OOP (객체 지향 프로그래밍), OOP의 5대 원칙 본문

Live Class/DevCamp

React & TS 9회차 (7/31) | OOP (객체 지향 프로그래밍), OOP의 5대 원칙

dearsuhyun 2024. 8. 1. 12:33
코드를 작성한다는 것은 ?
기계에 명령을 주는 것 --> 목적을 달성하기 위해서
명령 --> 순서가 중요 !
어떤 목적을 달성하기 위해 명령을 순서대로 나열하여 기계에 전달하는 것 = 알고리즘
프로그래밍 = 알고리즘을 여러 개 짜는 것

명령어 + 데이터 --> 무언가를 하겠죠 ?
예제: 월급을 전송하는 알고리즘을 작성 (은행, 전송 방식, 금액)
* ftp = 큰 파일 전송에 특화된 프로토콜

 

 

 

같은 목적을 수행하는 중복된 코드를 함수 (또는 프로시저)로 묶어서 재활용
cf. 프로시저: 함수와 비슷하지만 값을 반환하지 않는 특징을 지니고 특정 작업을 수행함

 

 

조금 더 맥락을 가지고 복잡도가 높을 때 이걸 낮출 수 있는 방법은 없을까 ?

 

--> 객체지향프로그래밍(OOP)

클래스와 인스턴스

 

속성만 가지고 있음

메소드도 가짐

 

속성은 취약한 구조임 (외부에서 이름 바꾸면 의도하지 않았지만 이름이 바뀌게 됨)

모든 속성을 보호하기 위한 '접근제어자' 필요 !

public (기본값)

constructor(생성자)는 무조건 public

 

private

 

클래스의 크기가 영원히 커지지 않도록 해야 함

--> 작은 클래스를 만들고 '상속' 개념을 이용해서 복잡성을 높이지 않고 더 큰 소프트웨어를 만들어보자 !

 

크기는 커지지만 복잡도는 유지하는 방법 !

 

자식 클래스에서 접근하지 못하게 부모 클래스를 만들고 싶다면 ?

--> private ! 해당 클래스 안에서만 접근 가능

name은 접근 가능하지만 datdOfjoining에는 접근 불가

 

Getter와 Setter

객체 지향 프로그래밍에서 클래스의 속성을 읽거나 설정할 때 사용하는 메서드

Getter:  객체의 속성 값을 읽어오는 메서드. 속성의 값을 외부에서 읽을 수 있도록 함

Setter: 객체의 속성 값을 설정하는 메서드. 속성의 값을 외부에서 설정할 수 있도록 함

 

 

다형성

객체지향 프로그래밍의 중요한 개념 중 하나로,

같은 인터페이스나 상위 클래스에 대해 여러 다른 하위 클래스가 존재할 수 있으며, 각 하위 클래스가 자신만의 방식으로 그 인터페이스나 상위 클래스의 메서드를 구현할 수 있게 하는 것. 이는 코드를 더 유연하고 확장 가능하게 만들어 줌

대표적인 문법: generic(제네릭)

상위 클래스에 대해서 어떤 상위 클래스를 사용하는 코드가 있을 때, 그 코드에 하위 클래스를 넣어도 문제가 없어야 한다 !

 

interface Animal {
  makeSound(): void
}

class Dog implements Animal {
  makeSound(): void {
   console.log('멍멍')
}
}

class cat implements Animal {
  makeSound(): void {
    console.log('야옹')
  }
}

function playWithAnimal(animal: Animal): void {
  animal.makeSound()
}
const myDog = new Dog()
const myCat = new cat()

playWithAnimal(myDog)
playWithAnimal(myCat)

 

--> 같은 Animal 인터페이스를 구현한 객체들(dog, cat)이 동일한 메소드(makeSound)를 서로 다른 방식으로 구현

--> playWithAnimal 함수는 Animal 타입의 객체를 인자로 받아 동작하므로, Dog 객체나 Cat 객체 모두를 인자로 받아 처리할 수 있음

이는 코드를 더 유연하게 만들어줌 !

 

캡슐화

메소드 오버라이딩(method overriding)

객체 지향 프로그래밍(OOP)에서 하위 클래스가 상위 클래스의 메서드를 재정의하여 자신만의 기능을 제공하는 것을 의미.

이는 상속과 다형성의 중요한 부분 중 하나 ! 오버라이딩된 메서드는 상위 클래스의 메서드와 이름, 매개변수, 반환 타입이 동일해야 함.

자식이 부모를 상속받은 후, 부모가 가진 메소드를 같은 이름으로 재구현하는 것 (부모의 메소드는 사용되지 않음)

 

extends VS implements

  extneds implement
사용 클래스가 다른 클래스를 상속받을 때 사용 클래스가 인터페이스 구현할 때 사용
특징 부모 클래스의 속성과 메소드를
자식 클래스가 상속받아 사용 가능

한 클래스는 하나의 부모 클래스만 상속 가능
인터페이스에 정의된 모든 메소드를
클래스가 반드시 구현해야 함

한 클래스는 여러 인터페이스 구현 가능

 

OOP의 5대 원칙

어떤 소프트웨어를 만들더라도 5가지 원칙을 위배하지 마라 !

** 면접 때 많이 물어봐요 ,, ~

 

 

1. SRP: 단일책임원칙 single responsibility principle **

하나의 클래스는 하나의 기능만 가져야 한다는 원칙

--> 하나의 클래스가 너무 많은 일을 하지 않도록 하여 유지보수와 이해가 쉽도록 함 !

 

잘못된 예제

UserService 클래스에 3가지 기능이나 정의되어 있음

public class UserService {
    public void saveUser(User user) {
        // Save user to database
        System.out.println("User saved to database: " + user.getName());
    }

    public void sendWelcomeEmail(User user) {
        // Send welcome email to user
        System.out.println("Welcome email sent to: " + user.getEmail());
    }

    public void logUserActivity(User user) {
        // Log user activity
        System.out.println("Logging activity for user: " + user.getName());
    }
}

class User {
    private String name;
    private String email;

    public User(String name, String email) {
        this.name = name;
        this.email = email;
    }

    public String getName() {
        return name;
    }

    public String getEmail() {
        return email;
    }
}

 

올바른 예제

위 코드에서 뭉쳐있던 기능이 각 클래스로 나뉨

각 클래스는 단일책임원칙을 따르고 userService 클래스가 이를 조합하여 사용자를 등록함

public class UserRepository {
    public void saveUser(User user) {
        // Save user to database
        System.out.println("User saved to database: " + user.getName());
    }
}

public class EmailService {
    public void sendWelcomeEmail(User user) {
        // Send welcome email to user
        System.out.println("Welcome email sent to: " + user.getEmail());
    }
}

public class UserActivityLogger {
    public void logUserActivity(User user) {
        // Log user activity
        System.out.println("Logging activity for user: " + user.getName());
    }
}

class User {
    private String name;
    private String email;

    public User(String name, String email) {
        this.name = name;
        this.email = email;
    }

    public String getName() {
        return name;
    }

    public String getEmail() {
        return email;
    }
}

public class UserService {
    private UserRepository userRepository = new UserRepository();
    private EmailService emailService = new EmailService();
    private UserActivityLogger userActivityLogger = new UserActivityLogger();

    public void registerUser(User user) {
        userRepository.saveUser(user);
        emailService.sendWelcomeEmail(user);
        userActivityLogger.logUserActivity(user);
    }
}

 

2. OCP: 개방 폐쇄 원칙 open/close principle **

소프트웨어 모듈이 확장에는 열려 있고, 수정에는 닫혀 있어야 한다는 원칙

--> 새로운 기능을 추가할 수 있지만 기존 코드를 변경하지 말아야 한다는 것

 

잘못된 예제

새로운 형식을 추가할 때 마다 generateReport 메소드를 수정해야 함

public class ReportGenerator {
    public void generateReport(String type) {
        if (type.equals("PDF")) {
            System.out.println("Generating PDF report...");
        } else if (type.equals("HTML")) {
            System.out.println("Generating HTML report...");
        }
        // If we need to add another format, we have to modify this method.
    }
}

 

올바른 예제

각 보고서 형식(PDF, HTML, XML)에 대해 별도의 클래스를 정의하고, 공통 인터페이스(Report)를 구현하여 새로운 보고서 형식을 추가할 때 기존 코드를 수정할 필요 없이 확장할 수 있게 함.

public interface Report {
    void generate();
}

public class PDFReport implements Report {
    @Override
    public void generate() {
        System.out.println("Generating PDF report...");
    }
}

public class HTMLReport implements Report {
    @Override
    public void generate() {
        System.out.println("Generating HTML report...");
    }
}

public class XMLReport implements Report {
    @Override
    public void generate() {
        System.out.println("Generating XML report...");
    }
}

public class Main {
    public static void main(String[] args) {
        Report pdfReport = new PDFReport();
        pdfReport.generate();  // Generating PDF report...

        Report htmlReport = new HTMLReport();
        htmlReport.generate();  // Generating HTML report...

        Report xmlReport = new XMLReport();
        xmlReport.generate();  // Generating XML report...
    }
}

 

 

3. LSP: 리스코프 치환 원칙 liskov substitution principle

자식 클래스는 언제나 부모 클래스를 대체할 수 있어야 한다는 원칙

즉, 부모 클래스의 인스턴스를 사용하는 곳에서는 자식 클래스의 인스턴스를 대신 사용해도 프로그램이 정상적으로 작동해야 한다는 뜻

올바른 상속을 위해, 자식 객체의 확장이 부모 객체의 방향을 온전히 따르도록 권고하는 원칙

 

잘못된 예제

Penguin 클래스는 Bird 클래스를 상속받았지만, Penguinfly 메서드를 지원하지 않기 때문에, 부모 클래스의 인스턴스로 대체될 수 없음

// Parent class Bird
public class Bird {
    public void fly() {
        System.out.println("Bird is flying");
    }
}

// Child class Penguin that violates LSP
public class Penguin extends Bird {
    @Override
    public void fly() {
        // Penguins cannot fly
        throw new UnsupportedOperationException("Penguins cannot fly");
    }
}

public class Main {
    public static void main(String[] args) {
        Bird bird = new Bird();
        bird.fly(); // Bird is flying

        Bird penguin = new Penguin();
        penguin.fly(); // Throws UnsupportedOperationException
    }
}

 

올바른 예제

 Flyable 인터페이스: 날 수 있는 새를 위한 인터페이스. fly 메서드를 정의

Bird 클래스: 모든 새의 기본 클래스. eat 메서드를 구현

Sparrow 클래스: Bird 클래스를 상속받고, Flyable 인터페이스를 구현하여 날 수 있는 새

Penguin 클래스: Bird 클래스를 상속받지만, Flyable 인터페이스를 구현하지 않아서 날 수 없는 새

// Interface for birds that can fly
public interface Flyable {
    void fly();
}

// Base class Bird
public class Bird {
    public void eat() {
        System.out.println("Bird is eating");
    }
}

// Class for a bird that can fly
public class Sparrow extends Bird implements Flyable {
    @Override
    public void fly() {
        System.out.println("Sparrow is flying");
    }
}

// Class for a bird that cannot fly
public class Penguin extends Bird {
    // Penguins do not implement Flyable
}

public class Main {
    public static void main(String[] args) {
        Bird sparrow = new Sparrow();
        sparrow.eat(); // Bird is eating
        ((Flyable) sparrow).fly(); // Sparrow is flying

        Bird penguin = new Penguin();
        penguin.eat(); // Bird is eating
        // ((Flyable) penguin).fly(); // Compilation error, Penguin is not Flyable
    }
}

 

4. ISP: 인터페이스 분리 원칙 interface segregation principle

클라이언트가 자신이 사용하지 않는 메서드에 의존하지 않도록 인터페이스를 작고 구체적으로 분리해야 한다는 원칙

하나의 큰 인터페이스보다 여러 개의 작은 인터페이스를 사용하는 것이 더 낫다는 것을 의미

 

올바른 예제

interface Workable {
  work(): void;
}

interface Eatable {
  eat(): void;
}
// Workable 인터페이스는 work 메서드만을 정의하고, Eatable 인터페이스는 eat 메서드만을 정의합니다. 
// 이렇게 하면 각 인터페이스가 하나의 책임만을 가지게 됩니다.

class HumanWorker implements Workable, Eatable {
  work(): void {
    console.log('Human working');
  }
  
  eat(): void {
    console.log('Human eating');
  }
}
// HumanWorker 클래스는 Workable과 Eatable 인터페이스를 둘 다 구현합니다. 
// 사람은 일을 할 수도 있고, 먹을 수도 있기 때문에 두 인터페이스를 모두 구현하는 것이 맞습니다.

class RobotWorker implements Workable {
  work(): void {
    console.log('Robot working');
  }
}
// RobotWorker 클래스는 Workable 인터페이스만 구현합니다. 로봇은 일만 할 수 있고 먹을 수 없기 때문에 Eatable 인터페이스를 구현할 필요가 없습니다. 
// 이는 ISP를 잘 준수하는 예입니다. 로봇은 자신에게 필요 없는 eat 메서드에 의존하지 않습니다.

// 만약 Workable과 Eatable을 하나의 큰 인터페이스로 만들었다면, RobotWorker 클래스는 필요하지 않은 eat 메서드를 구현해야 했을 것입니다. 
// 이는 불필요한 코드와 복잡성을 초래하게 됩니다. 따라서 인터페이스를 작은 단위로 분리하는 것이 더 효율적입니다.

 

 

5. DIP: 의존성 역전 원칙 dependency inversion principle

고수준 모듈이 저수준 모듈에 의존해서는 안 되고, 둘 다 추상화에 의존해야 한다는 원칙

 

잘못된 예제

Switch 클래스는 Fan 클래스에 직접 의존하고 있음.

이는 고수준 모듈이 저수준 모듈에 의존하는 형태로, 의존성 역전 원칙을 위반하고 있음

// Low-level class
public class Fan {
    public void spin() {
        System.out.println("Fan is spinning");
    }

    public void stop() {
        System.out.println("Fan is stopping");
    }
}

// High-level class
public class Switch {
    private Fan fan;

    public Switch(Fan fan) {
        this.fan = fan;
    }

    public void turnOn() {
        fan.spin();
    }

    public void turnOff() {
        fan.stop();
    }
}

 

올바른 예제

// Interface for switchable devices
public interface Switchable {
    void turnOn();
    void turnOff();
}

// Low-level class implementing the interface
public class Fan implements Switchable {
    @Override
    public void turnOn() {
        System.out.println("Fan is spinning");
    }

    @Override
    public void turnOff() {
        System.out.println("Fan is stopping");
    }
}

// High-level class
public class Switch {
    private Switchable device;

    public Switch(Switchable device) {
        this.device = device;
    }

    public void turnOn() {
        device.turnOn();
    }

    public void turnOff() {
        device.turnOff();
    }
}

 

cf. 애노테이션(annotation)

 코드 사이에 주석처럼 쓰이며 특별한 의미, 기능을 수행하도록 하는 기술

 프로그램에게 추가적인 정보를 제공해주는 메타데이터(데이터를 위한 데이터)

 

@Override

메서드가 부모 클래스나 인터페이스의 메서드를 오버라이드하고 있음을 명시

class Animal {
    public void makeSound() {
        System.out.println("Animal sound");
    }
}

class Dog extends Animal {
    @Override
    public void makeSound() {
        System.out.println("Woof");
    }
}

https://developer.mozilla.org/ko/docs/Learn/JavaScript/Objects/Object-oriented_programming

 

객체 지향 프로그래밍 - Web 개발 학습하기 | MDN

객체 지향 프로그래밍(OOP)은 Java 및 C++를 비롯한 많은 프로그래밍 언어의 기본이 되는 프로그래밍 패러다임입니다. 이 기사에서는 OOP의 기본 개념에 대한 개요를 제공합니다. 클래스와 인스턴스

developer.mozilla.org

https://www.youtube.com/watch?v=4O6k9GN8FPo

 

 

반응형