[4일차] Object Oriented 프로그래밍

sani·2023년 6월 10일

CS스터디

목록 보기
4/7
post-thumbnail

사전 지식

🧐 parameter vs argument in Java

  • parameter : method 선언부에 명시된 variables
  • argument : method 호출시 method의 입력으로 전달되는 real value

🧐 가상 함수 (virtual function/method)

  • 상속하는 class 내에서 overriding 될 수 있는 메소드. OOP의 다형성에서 중요한 부분.
  • C++에서는 기초 class에 virtual 키워드를 선언하여 가상 함수를 정의한다.

🧐 address binding

  • process의 logical address를 실제 메모리 위치인 physical address로 maaping 하는 작업을 address binding이라 한다. 이 binding이 일어나는 시기에 따라 다음 3가지로 분류된다.
  1. compile time : 소스 코드를 compiler를 통해 object module로 변환하는 compile time에 binding을 진행한다. 따라서 실행 파일에 물리 주소를 포함시킨다. 그러나 이때 생성된 주소 공간이 다른 process에 의해 침범되어 충돌할 수 있으므로 다시 컴파일해야하는 경우가 발생할 수 있다.
  2. load time : object들을 linking하는 시기에 loader에 의해 relocatable address를 abslute address로 binding한다.
  3. run time : 현재 대부분의 OS들이 사용하는 방식으로서 해당 객체가 실행될때 binding을 진행한다. 이떄 MMU(Memory Management Unit)에 의해 physical address로의 변환이 이루어진다.

객체 지향 프로그래밍이란

  • 프로그램 구현에 필요한 객체를 정의하고 그들간의 상호작용을 통해 프로그램을 작성하는 것을 의미한다. 자료구조와 알고리즘이 객체 안에 들어있으며 그들간의 연결로 구성된다.
  • 절차 지향과의 비교 : 대형 application의 경우, 많은 기능을 수반하기에 이들을 묶을 수 있는 객체 지향이 적합. 소형 application의 경우는 작은 기능을 객체로 나누는 것이 오히려 복잡도만 키울 수 있음.

특징

1. 추상화 (abstraction)

  • 객체들의 공통적 속성을 도출하는 것. class를 정의하는 것을 추상화라 할 수 있다. 물론 JS와 같이 class 선언부가 없는 객체 지향 언어도 존재한다(모던 JS의 경우, class 지원).
abstract class Car {

	public void CurrentState () {
		System.out.println(" 움직이고 있습니다.");
	}
}

class Bus extends Car {
	String color;
	public void Color(String color) {
		System.out.println("버스는 "+color+" 입니다.");
	}
}

public class MyBus {
	public static void main(String[] args) {
		Bus bus = new Bus();

		bus.Color("초록색");
		bus.CurrentState();
	}
}

2. 캡슐화 (encapsulation)

  • 외부로의 실제 구현부 노출을 최소화하여 module간의 결합도를 낮춤으로써 유연함과 유지보수성을 확보. Java에서는 4가지 접근 제한을 제공.
  • 데이터 뿐만 아니라 기능까지 하나로 묶어 객체가 독립적으로 역할할 수 있도록 관리.
  • 외부와 상호작용시에는 메소드를 이용.
class Area {

  // fields to calculate area
  int length;
  int breadth;

  // constructor to initialize values
  Area(int length, int breadth) {
    this.length = length;
    this.breadth = breadth;
  }

  // method to calculate area
  public void getArea() {
    int area = length * breadth;
    System.out.println("Area: " + area);
  }
}

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

    // create object of Area
    // pass value of length and breadth
    Area rectangle = new Area(5, 6);
    rectangle.getArea();
  }
}

아래와 같이 접근 제한자를 이용해 엄밀한 data hiding을 지원할 수 있다.

class Person {

  // private field
  private int age;

  // getter method
  public int getAge() {
    return age;
  }

  // setter method
  public void setAge(int age) {
    this.age = age;
  }
}

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

    // create an object of Person
    Person p1 = new Person();

    // change age using setter
    p1.setAge(24);

    // access age using getter
    System.out.println("My age is " + p1.getAge());
  }
}

3. 상속성 (inheritance)

  • 한 class의 속성(메소드, 데이터)을 다른 class가 물려받는 것. 즉, 이미 작성된 class를 그대로 상속받아 새로운 class를 생성하는 것.

4. 다형성 (polymorphism)

  • 약간 다른 방식으로 동작하는 함수를 동일한 이름으로 호출하는 것.
  • Overriding : 부모 class의 메소드와 같은 이름, 같은 parameter를 사용하되, 내부 로직을 재정의
public abstract class Figure {
    protected int dot;
    protected int area;
    
    public Figure(int dot, int area) {
        this.dot = dot;
        this.area = area;
    }
    
    public abstract void display();  // 하위 클래스에서 오버라이딩 해야 할 추상 메소드
    
    // getter
    public int getDot() {
        return this.dot;
    }
}
public class Triangle extends Figure {
    public Triangle(int dot, int area) {
        super(dot, area);
    }
    
    @Override
    public void display() {
        System.out.println("넓이가 %d인 삼각형입니다.", area);
    }
}

public class Rectangle extends Figure {
    public Rectangle(int dot, int area) {
        super(dot, area);
    }
    
    @Override
    public void display() {
        System.out.println("넓이가 %d인 사각형입니다.", area);
    }
}
  • Overloading : 같은 이름의 메소드를 여러개 정의하되, parameter들을 다르게 하여, 경우에 따라 호출해 사용하도록 하는 것.
public class Employee {
    ...
    
    public Employee() {  // 기본 생성자
    }
    
    public Employee(String email) {  // email로 직원 객체 생성하기
        this.email = email;
    }
    
    public Employee(String email, String empNum) {  // email과 사번으로 직원 객체 생성하기
        this.email = email;
        this.empNum = empNum;
    }
}

5. 동적바인딩 (dynamic binding)

  • 가상 함수를 호출하는 코드를 컴파일할 때, binding을 compile time이 아닌, run time에 진행하는 것.
  • 파생 class에서 overriding한 함수의 호출을 보장할 수 있다.
/**
 * created by victory_woo on 2020/07/06
 */
class SuperClass {
    void methodA() {
        System.out.println("SuperClass A ");
    }

    static void methodB() {
        System.out.println("SuperClass B");
    }
}

class SubClass extends SuperClass {
    @Override
    void methodA() {
        System.out.println("SubClass A");
    }

    static void methodB() {
        System.out.println("SubClass B");
    }
}


public class PolymorphismTest {
    public static void main(String[] args) {
        SuperClass superClass = new SuperClass();
        superClass.methodA();
        superClass.methodB();


        SuperClass subClass = new SubClass();
        subClass.methodA();
        subClass.methodB();
    }
}

// 예상 결과
SuperClass A 
SuperClass B
SubClass A
SubClass B

// 실제 결과
SuperClass A 
SuperClass B
SubClass A
SuperClass B
  • SubClass는 SuperClass를 상속받고, methodA()를 Overriding했다. 따라서 methodA()가 어떤 class의 method인지는 class 파일이 실행되는 Runtime에 결정된다. 즉, 동적 바인딩은 Runtime 시점에 해당 method를 구현하고 있는 실제 객체을 찾아가 실행될 함수를 호출한다.
  • 정리
    • 모든 instance의 method는 Runtime에 호출된다.
    • class(static) method와 instance variable은 Compile time에 결정된다.
    • static method는 overriding 할 수 없다. 이미 Compile time에 결정되었기에 Runtime에 다시 결정될 수 없다.

장점

  • 생산성 향상 : 다형성, 객체, 캡슐화를 통해 재사용을 지향.
  • 실설계에 대한 쉬운 모델링 : 산업 전반의 process들은 절차로 모델링하기 어렵다. 모든 것을 객체들의 상호작용으로 생각한다면, 실제 process를 모델링하기에 적합하다.
  • 보안성 향상 : 캡슐화를 통해 접근을 제한함으로써 정보 은닉이 가능하다.

단점

  • 복잡성 : 설계에 많은 시간 소요된다.
  • 느린 처리 속도 : 캡슐화와 격리구조 때문에 다른 패러다임에 비해 상대적으로 느리다.
  • 추가적인 메모리 : 모든 것을 객체로 다루기에 추가적인 pointer 크기 및 연산에 대한 cost가 발생한다.

SOLID 원칙

객체 지향 프로그래밍을 설계할 때, 지켜야할 5가지 원칙으로 다음과 같다.

1. Single Responsibility Principle(단일 책임 원칙)
class는 하나의 기능만 가지며 하나의 책임(목적, 역할)만을 수행해야한다. 비슷한 책임을 갖는다면 부모 class로 추출해야하며 class 이름 또한 책임에 맞춰 지어야 한다.

public class Book {

    private String name;
    private String author;
    private String text;

    //constructor, getters and setters

    // methods that directly relate to the book properties
    public String replaceWordInText(String word, String replacementWord){
        return text.replaceAll(word, replacementWord);
    }

    public boolean isWordInText(String word){
        return text.contains(word);
    }
}

public class BadBook {
    //...

    void printTextToConsole(){
        // our code for formatting and printing the text
    }
}

위의 코드에서 BadBook class의 경우 책의 속성들에 대한 기능뿐만 아니라, 그들을 출력하는 책임 또한 맡고 있으므로 SRP를 위반했다. 따라서 아래와 같이 print에 대한 책임은 다른 class를 선언하여 부여한다.

public class BookPrinter {

    // methods for outputting text
    void printTextToConsole(String text){
        //our code for formatting and printing the text
    }

    void printTextToAnotherMedium(String text){
        // code for writing to any other location..
    }
}

2. Open Close Principle (개방 폐쇄 원칙)

  • SW의 구성요소(component, class, module, method)는 확장에는 열려있고 변경에는 닫혀있어야한다. 따라서 요구사항 반영시, 기존 구성요소의 변경은 최소화하면서 쉽게 확장할 수 있어야 한다.
public class Guitar {

    private String make;
    private String model;
    private int volume;

    //Constructors, getters & setters
}

public class SuperCoolGuitarWithFlames extends Guitar {

    private String flameColor;

    //constructor, getters + setters
}

위처럼 기존의 Guitar class와 다른 기능을 제공하기위해 기존 class를 상속받아 새롭게 class를 정의함으로써 기존 class의 변경은 없애고, 새로운 기능으로 확장시킬수 있다.


3. Liskov Substitution Principle (리스코브 치환 원칙)
객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다. 즉, 부모 class에 자식 class를 넣어도 문제없이 돌아갈 수 있도록 만들어야한다.

여기Rectangle, Square 예시 코드를 통해 LSP를 위반하는 코드와 준수하는 코드를 확인할 수 있다.


4. Interface Segregation Principle (인터페이스 분리 원칙)
한 class는 자신이 사용하지 않는 interface는 구현하지 말아야 한다. 따라서 client가 사용하는 구체적인 여러개의 interface를 구현해야한다. 즉, interface의 단일 책임을 강조하는 개념이다.

public interface BearKeeper {
    void washTheBear();
    void feedTheBear();
    void petTheBear();
}

위와 같이 구현할 경우, 구현체의 역할에 따라 구현하지 않는 함수들이 발생할 수 있다. 따라서 아래와 같이 interface들을 나누게 되면, 필요한 역할에 맞게 자유로운 메서드 구성이 가능해진다.

public interface BearCleaner {
    void washTheBear();
}

public interface BearFeeder {
    void feedTheBear();
}

public interface BearPetter {
    void petTheBear();
}

public class BearCarer implements BearCleaner, BearFeeder {

    public void washTheBear() {
        //I think we missed a spot...
    }

    public void feedTheBear() {
        //Tuna Tuesdays...
    }
}

public class CrazyPerson implements BearPetter {

    public void petTheBear() {
        //Good luck with that!
    }
}

5. Dependency Inversion Principle (의존성 역전 원칙)
상위 계층은 하위 계층의 변화에 대한 구현으로부터 독립해야한다. 즉, 추상화된 interface나 상위 class를 두어 변하기 쉬운 것의 변화에 영향받지 않게 하는 원칙을 뜻한다.

Spring에서의 DIP, IoC, DI에 대한 설명은 여기를 참고하길 바란다.


출처

http://www.incodom.kr/객체_지향
https://gamedevlog.tistory.com/83
https://jin8371.tistory.com/64
https://developerbee.tistory.com/153
https://www.programiz.com/java-programming/encapsulation
https://woovictory.github.io/2020/07/05/Java-binding/
https://www.javatpoint.com/solid-principles-java
https://www.baeldung.com/solid-principles
https://inpa.tistory.com/entry/OOP-💠-아주-쉽게-이해하는-ISP-인터페이스-분리-원칙
https://mangkyu.tistory.com/125

profile
블로그 이전했습니다. https://devsan.tistory.com/

0개의 댓글