[Interview] 프로그래밍 패러다임

박세윤·2023년 10월 21일

Interview

목록 보기
3/3

📋 프로그래밍 패러다임


📌 프로그래밍 패러다임이란?


  • 프로그래머가 코드를 어떻게 작성해야 할 지 결정하는 역할

  • 프로그래밍 패러다임을 통해 여러 방식으로 코드를 바라보게 되고, 이를 바탕으로 구현하게 된다.


  • 명령형 프로그래밍 : 무엇(What)보다 어떻게(How)를 설명하는 패러다임
    • 절차지향 프로그래밍 : 수행되어야 할 기능을 순차적인 처리 과정으로 진행하는 방식
    • 객체지향 프로그래밍 : 객체들의 집합으로 프로그램의 상호작용으로 진행하는 방식
  • 선언형 프로그래밍 : 어떻게(How)보다 무엇(What)을 설명하는 패러다임
    • 함수형 프로그래밍 : 순수 함수를 조합하여 SW를 만드는 방식



📌 명령형 프로그래밍


✅ 절차지향 프로그래밍 (Procedural Programming)

  • 순차적 처리를 중요하게 생각
  • 프로그램 전체가 유기적으로 연결되도록 만드는 기법

  • 장점

    • 가독성이 좋다
    • 코드의 단위화
    • 컴퓨터의 실제 처리 구조와 비슷하여 실행 속도가 빠르다
  • 단점

    • 각 코드가 순서에 따라 실행되어 변경/유지보수/분석이 어렵다.
    • 변수, 상수 등 값들을 관리하는 자료형과 해당 자료형을 사용하는 함수가 분리되어 사용된다.

  • 대표적인 절차지향 프로그래밍 언어 : C, Pascal



✅ 객체지향 프로그래밍 (Object Oriented Programming)

  • 모든 데이터를 객체(Object)로 취급하여 객체가 처리 요청을 받았을 때 객체 내부 기능을 활용하여 해당 요청을 처리하는 방식
  • 모든 객체가 내부 자료형(Field)와 함수(Method)로 구성된 구조

객체(Object) : 객체지향 프로그래밍의 가장 기본적인 단위이자 시작점

  • 모든 실재하는 대상
  • 우리가 보고 느끼고 인지할 수 있는 모든 것
  • 모든 사건은 객체들 간의 상호작용으로 발생한다.
  • 속성 / 기능 / 변수 / 함수

  • 장점
    • 프로그램을 보다 유연하고 변경이 용이하게 만든다.
    • 코드의 재사용 (유지보수에 좋다)
    • 분석과 설계의 전환이 쉽다.

  • 단점
    • 처리 속도가 상대적으로 빠르다
    • 설계에 많은 시간이 소요된다.

  • 대표적인 객체지향 프로그래밍 언어 : Java, Python, C++



✅ 객체지향 프로그래밍의 4가지 특징

  1. 추상화 (Abstraction) : 사물이나 표상을 어떤 성질, 공통성, 본질에 착안하여 그것을 추출하여 파악하는 것
    • 불필요한 세부 사항은 제거하고 가장 본질적이고 공통적인 부분만을 추출하여 표현
    • 객체의 공통적인 속성과 기능을 추출하여 정의하는 것
    • 대표적인 객체 지향 프로그래밍, Java에서 추상화를 구현할 수 있는 무법 요소 : 추상 클래스(abstract class), 인터페이스(interface)
// Vehicle 인터페이스
public interface Vehicle {
    
    public abstract void start();
      
    void moveForward(); // public abstract 키워드는 생략이 가능하다.
    
    void moveBackward();
}
// Car 클래스
// 이동 수단을 구체화한 자동차 클래스
public class Car implements Vehicle { 
    
    @Override
    public void moveForward() {
        System.out.println("자동차가 앞으로 전진합니다!");
    }
    
    @Override
    public void moveBackward() {
        System.out.println("자동차가 뒤로 후진합니다!");
    }
}
// MotorBike 클래스
public class MotorBike implements Vehicle {

  @Override
  public void moveForward() {
    System.out.println("오토바이가 앞으로 전진합니다!");
  }

  @Override
  public void moveBackward() {
    System.out.println("오토바이가 뒤로 후진합니다!");
  }
}
  • 위처럼 인터페이스에 정의한 역할을 각 클래스의 맥락에 맞게 구현한다.
  • 이것을 역할과 구현의 분리라고 한다.

  • 객체지향 프로그래밍에서는 보다 유연하고 변경에 열려있는 프로그램을 설계하기 위해 역할과 구현을 분리한다.



  1. 캡슐화 (Encapsulation) : 서로 연관있는 속성과 기능들을 하나의 캡슐로 만들어 데이터를 외부로부터 보호하는 것
  • 데이터 보호 (Data Protection) : 외부로부터 클래스에 정의된 속성과 기능들을 보호
  • 데이터 은닉 (Data Hiding) : 내부의 동작을 감추고 외부에는 필요한 부분만 노출

  • 외부로부터 클래스에 정의된 속성과 기능들을 보호하고, 필요한 부분만 외부로 노출될 수 있도록 하여 각 객체 고유의 독립성과 책임 영역을 안전하게 지키고자 하는 목적
  • 자바에서 캡슐화를 구현하기 위한 두 기능 : 접근제어자, getter/setter 메서드
접근 제어자클래스 내패키지 내다른 패키지의 하위 클래스패키지 외설명
privateOXXX동일 클래스 내에서만 접근 가능
defaultOOXX동일 패키지 내에서만 접근 가능
protectedOOOX동일 패키지 + 다른 패키지의 하위 클래스에서 접근 가능
publicOOOO접근 제한 없음



  1. 상속성 (Inheritance) : 기존의 클래스를 재활용하여 새로운 클래스를 작성하는 문법 요소
  • 상위 클래스로부터 확장된 여러 하위 클래스들이 모두 상위 클래스의 속성과 기능을 사용할 수 있다.
  • 딱 한번 정의하고 간편하게 재사용이 가능하여 반복적인 코드를 최소화하고 공유하는 속성과 기능에 간편하게 접근가능하다.

public class Car {
    
    String model;
    String color;
    
    int wheels;
    
    // Car 클래스의 고유 속성
    boolean isConvertible;
    
    void moveForward() {
        System.out.println("앞으로 전진!");
    }
    
    void moveBackward() {
        System.out.println("뒤로 후진!");
    }
    
    // Car 클래스의 고유 기능
    void openWindow() {
        System.out.println("모든 창문을 연다!");
    }
}
public class MotorBike {
    
    String model;
    String color;
    
    int wheels;
    
    // MotorBike 클래스 고유 속성
    boolean isRaceable;
    
    void moveForward() {
        System.out.println("앞으로 전진!");
    }
    
    void moveBackward() {
        System.out.println("뒤로 후진!");
    }
    
    // MotorBike 클래스 고유 기능
    public void stunt() {
        System.out.println("묘기 부리기!");
    }
}
  • 위 코드에서는 다른 두 클래스가 반복적으로 사용되는 속성과 기능들이 있다.
  • 또한 하나의 코드에서 변경이 일어나면 다른 곳에서도 수정이 필요하다.



// 추상화를 통해 상위 클래스 정의
public class Vehicle {
    
    String model;
    String color;
  
    int wheels;

    void moveForward() {
      System.out.println("앞으로 전진!");
    }
  
    void moveBackward() {
      System.out.println("뒤로 후진!");
    }
}
// Car 클래스
public class Car extends Vehicle {
    
    boolean isConvertible;
    
    void openWindow() {
        System.out.println("모든 창문을 열다!");
    }
}
// MotorBike 클래스
public class MotorBike extends Vehicle {
    
    boolean isRaceable;
    
    // 메서드 오버라이딩 -> 기능 재정의
    @Override
    void moveForward() {
        System.out.println("오토바이가 앞으로 전진합니다!");
    }
    
    public void stunt() {
        System.out.println("묘기를 부립니다!");
    }
}
  • 위 코드에서 Car과 MotorBike의 공통적인 속성과 기능을 추상화하여 Vehicle 이라는 상위 클래스에 정의하고, extends 키워드를 통해 하위 클래스로 확장하였다.
  • MotorBike에서는 클래스의 맥락에 맞게 메서드 오버라이딩 기능도 사용하여 내용을 재정의할 수 있다.

  • 상속
    • extends 키워드
    • 상위 클래스의 속성과 기능들을 하위 클래스에서 그대로 받아 사용하거나 오버라이딩을 통해 선택적으로 재정의하여 사용
  • 구현
    • implements 키워드
    • 반드시 인터페이스에 정의된 추상 메서드의 내용이 하위 클래스에서 정의되어야 한다.



  1. 다형성 (Polymorphism) : 어떤 객체의 속성이나 기능이 상황에 따라 여러 형태를 가질 수 있는 성질
  • 어떤 객체의 속성이나 기능이 그 맥락에 따라 다른 역할을 수행할 수 있는 객체지향의 특성을 의미
  • ex> 메서드 오버라이딩, 메서드 오버로딩

  • 한 타입의 참조변수를 통해 여러 타입의 객체를 참조할 수 있도록 만든 것
    • 상위 클래스 타입의 참조변수로 하위 클래스의 객체를 참조할 수 있도록 하는 것
// Main 클래스
public class Main {
    public static void main(String[] args) {
        
        // 원래 사용했던 객체 생성 방식
        Car car = new Car();
        MotorBike motorBike = new MotorBike();
        
        // 다형성을 활용한 객체 생성 방식
        Vehicle car2 = new Car();
    }
}
  • 상위클래스 타입의 참조변수로 하위클래스 객체를 참조

public class Main {
    public static void main(String[] args) {
        
        // 상위 클래스 타입의 객체 배열 생성
        Vehicle vehicles[] = new Vehicle[2];
        
        vehicles[0] = new Car();
        vehicles[1] = new MotorBike();
        
        for(Vehicle vehicle : vehicles)
            System.out.println(vehicle.getClass());
    }
}

------------- 출력 ------------
class Car
class MotorBike
  • 하나의 타입만으로 여러 가지 타입의 객체를 참조했다.

// Driver 클래스
public class Driver {
    
    void drive(Car car) {
        car.moveForward();
        car.moveBackward();
    }
    
    void drive(MotorBike motorBike) {
        motorBike.moveForward();
        motorBike.moveBackward();
    }
}
// main 클래스
public class Main {
    public static void main(String[] args) {
        
        Car car = new Car();
        MotorBike motorBike = new MotorBike();
        Driver driver = new Driver();
        
        driver.drive(car);
        driver.drive(motorBike);
    }
}
  • Driver 클래스는 Car 클래스와 MotorBike 클래스에 의존한다.
  • 객체간 결합도가 높다.
    • 이 상태는 객체 지향적 설계에 불리하다.
      • 이러한 문제를 해결하기 위해 등장한 개념이 의존관계 주입(dependency injection)이다.
      • 이는 스프링 프레임워크의 핵심적인 개념이다.



✅ 객체지향 프로그래밍의 5 가지 설계 원칙 : SOLID

  • SOLID : 객체 지향 프로그래밍을 설계하면서 지켜야 할 다섯 가지 설계 원칙이다.
  • SRP, OCP, LSP, DIP, ISP



  1. 단일 책임 원칙 (SRP, Single Responsibility Principle)
  • 모듈이 변경되는 이유가 한가지이어야 한다.
  • 해당 모듈이 여러 대상 또는 액터들에 대해 책임을 가져서는 안되고, 오직 하나의 액터에 대해서면 책임을 져야 한다.

  • 단일 책임 원칙을 지키면 변경이 필요할 때 수정할 대상이 명확해진다.
  • 단일 책임 원칙을 적용하여 적절하게 책임과 관심이 다른 코드를 분리하고 서로 영향을 주지 않도록 추상화하여 애플리케이션 변화에 손쉽게 대응할 수 있다.



  1. 개방 폐쇄 원칙 (OCP, Open-Closed Principle)
  • 확장에 열려있고 수정에 닫혀있어야 한다.
  • 확장에 열려있다 : 요구사항이 변경될 때 새 동작을 추가하여 애플리케이션의 기능을 확장할 수 있다.
  • 수정에 닫혀있다 : 기존 코드를 수정하지 않고 애플리케이션의 동작을 추가하거나 변경할 수 있다.

  • 개방-폐쇄 원칙을 지키기 위해서는 추상화에 의존해야 한다.
    • 추상화를 통해 변하지 않는 부분만 남김으로써 기능을 구체화하고 확장할 수 있다.
    • 변하지 않는 부분은 고정하고 변하는 부분을 생략하여 추상화함으로써 변경이 필요한 경우에 생략된 부분을 수정하여 개방-폐쇄 원칙을 지킬 수 있다.

  • 개방-폐쇄 원칙이 본질적으로 말하는 것은 추상화이고, 이는 결국 런타임 의존성과 컴파일타임 의존성에 대한 이야기이다.
  • 런타임 의존성 : 애플리케이션 실행 시점에서의 객체들의 관계
  • 컴파일타임 의존성 : 코드에 표현된 클래스들의 관계를 의미한다.

    관련 설명 : https://mangkyu.tistory.com/226


  • 추상화를 통해 변하는 것을 숨기고 변하지 않는것에 의존한다면, 기존 코드 및 클래스를 수정하지 않고 애플리케이션의 확장이 가능하다.
    • 이 것이 개방-폐쇄 원칙의 의미이다.



  1. 인터페이스 분리 원칙 (ISP, Interface Segregation Principle)
  • 목적과 관심이 각자 다른 클라이언트가 있다면 인터페이스를 통해 적절히 분리한다.
  • 클라이언트의 목적과 용도에 적합한 인터페이스만을 제공하는 것
  • 인터페이스 분리 원칙을 준수함으로써 모든 클라이언트가 자신의 관심에 맞는 퍼블릭 인터페이스만 접근하여 불필요한 간섭을 최소화할 수 있다.



  1. 리스코프 치환 원칙 (LSP, Liskov Subsitution Principle)
  • 하위 타입은 상위 타입을 대체할 수 있어야 한다.
  • 해당 객체를 사용하는 클라이언트는 상위 타입이 하위 타입으로 변경되어도 차이점을 인식하지 못한 채 상위 타입의 퍼블릭 인터페이스를 통해 서브 클래스를 사용할 수 있어야 한다.

  • 자식 클래스가 부모 클래스를 대체하기 위해 부모 클래스에 대한 클라이언트의 가정을 준수해야 한다는 것을 강조한다.
  • 대체 가능성을 결정해야 하는 것은 해당 객체를 이용하는 클라이언트라는 것을 반드시 잊지 말아야 한다.



  1. 의존 역전 원칙 (DIP, Dependency Inversion Principle)
  • 고수준 모듈은 저수준 모듈의 구현에 의존해서는 안되며, 저수준 모듈이 고수준 모듈에 의존해야 한다는 것.
  • 고수준 모듈 : 입력과 출력으로부터 먼, 비즈니스와 관련된 추상화된 모듈
  • 저수준 모듈 : 입력과 출력으로부터 가까운, HTTP/DB/Cache와 관련된 구현 모듈

  • 결국 비즈니스와 관련된 부분이 세부 사항에는 의존하지 않는 설계 원칙을 말한다.



  • 결국 위 다섯가지 객체지향 설계원칙인 SOLID는 추상화과 다형성이다.
    • 구체 클래스에 의존하지 않고 추상 클래스 또는 인터페이스에 의존함으로써 유연하고 확장가능한 애플리케이션을 만들 수 있는 것이다.



📌 선언형 프로그래밍


✅ 함수형 프로그래밍 (Functional Programming)

  • 순수 함수를 사용하여 상태를 제어하기 보다 빠르게 처리하는 것에 초점을 둔 방법론
  • 실행 순서를 지정할 필요 없다. => 비절차형 프로그래밍

  • 장점
    • 코드에서 프로그래밍에 영향을 미치는 영역과 순수한 영역을 최대한 분리
    • 코드의 가독성이 높고 유지보수가 좋다
    • 테스트가 쉽다

  • 단점
    • 외부 데이터 또는 내부 데이터의 상태를 조작할 수 없다.

  • 대표적인 함수형 프로그래밍 언어 : Haskell, OCamal, Lisp, Clojure, Erlang



📌 각 패러다임의 비교


✅ 절차지향 프로그래밍 vs 객체지향 프로그래밍

  • 절차지향 프로그래밍의 부족한 점을 객체지향 프로그래밍이 보완한다.
  • 두 방식 모두 함수가 있지만, 객체지향 프로그래밍에만 '객체' 라는 개념이 존재한다.
  • 절차지향 프로그래밍은 데이터 중심, 객체지향 프로그래밍은 기능 중심의 방법론이다.
  • 둘은 서로 반대의 방식이 아니다. 다른 방식이다.



✅ 객체지향 프로그래밍 vs 함수형 프로그래밍

  • 객체지향 프로그래밍에서는 클래스가 일급 객체가 되지만, 함수형 프로그래밍에서는 함수가 일급 객체가 된다.
  • 객체지향 프로그래밍에서는 프로그램을 상호작용하는 객체들의 집합으로 보지만, 함수형 프로그래밍에서는 프로그램을 상태값을 지니지 않는 함수들의 연속으로 볼 수 있다.

일급 객체 (First-Class Object) : 프로그래밍 언어에서 특별한 권한 없이 아래 조건을 충족하는 객체
1. 변수나 데이터 구조 안에 담을 수 있어야 한다.
2. 함수의 인자로 전달할 수 있어야 한다.
3. 함수의 반환 값으로 사용할 수 있어야 한다.

일급 함수 (First-Class Function) : 함수형 프로그래밍에서 함수를 변수에 저장하고 함수를 함수의 인자로 전달하거나 함수를 함수의 반환값으로 사용할 수 있는 것.


  • 객체지향 프로그래밍
    • 클래스 디자인과 객체들의 관계를 중심으로 코드 작성
    • 상태, 멤버변수, 메서드 등이 긴밀한 관계를 가짐
    • 멤버변수가 어떤 상태를 가지고 있는지에 따라 결과가 달라진다.

  • 함수형 프로그래밍
    • 값의 연산 및 결과 도출 중심으로 코드 작성
    • 함수는 인자로 받은 값을 별도로 저장하지 않고, 간결한 과정으로 처리하고 매핑하는데 목적을 가진다.



📚 출처



profile
개발 공부!

0개의 댓글