Posts Object Oriented Programming
Post
Cancel

Object Oriented Programming

OOP는 너무 거대한 개념이라 다 다루지는 못하지만 간단히만 알아보자


간단한 설명

OOP : 중심적 프로그래밍 패러다임. 현실 세계의 사물들을 객체라고 보고, 그 객체로부터 개발하고자 하는 애플리케이션에 필요한 특징들을 뽑아와 프로그래밍하는 것(추상화)

장점

  • OOP로 작성한 코드는 재사용성이 높다.

    • 자주 사용되는 로직을 라이브러리로 만들어두면 개발로드가 줄어들고, 신뢰성도 확보가 됨

    • 라이브러리를 각종 예외상황에 맞게 잘 구현하면, 개발자가 사소한 실수를 해도 에러를 컴파일 단계에서 잡아낼 수 있음
    • 내부적으로 어떻게 돌아가는지 몰라도 그냥 사용하면 됨.
  • 객체 단위로 코드가 나뉘어져있어 디버깅과 유지보수가 쉽다.

  • 데이터 모델링 할때, 객체와 매핑하는 것이 훨씬 수월하기에 요구사항을 보다 명확하게 파악하여 프로그래밍 가능

단점

  • 객체 간의 정보 교환이 모두 메시지 교환을 통해 일어나므로, overhead가 발생하게 됨.
    • 하지만, 하드웨어의 발전으로 많이 보완됨
  • 객체가 상태를 가짐

    • 객체 안에는 변수가 존재하고, 이 변수를 통해 객체가 예측할 수 없는 상태를 갖게 되어 어플리케이션 내부에서 버그를 발생시킴

      -> 따라서 함수형 프로그래밍을 발전 시켜 해결하고자 함

객체 지향적 설계원칙(SOLID)

  1. SBP(Single Responsibility Principle) : 단일 책임 원칙
    • 클래스는 단 하나의 책임을 가져야하며 클래스를 변경하는 이유는 단 하나의 이유이어야 함
  2. OCP(Open-Closed Principle) : 개방-폐쇄 원칙
    • 확장에는 열려있어야하고, 변경에는 닫혀있어야한다.
    • OCP는 다형성을 통해 지켜질 수 있다.
      • 다형성 : 하나의 객체 혹은 메소드가 여러 타입을 참조할 수 있음.
        • 객체 다형성 : 자식 객체가 부모 객체의 인스턴스로 할당될 수 있음
        • 메소드 다형성 : 오버로딩을 통해 구현 가능
      • IoC 컨테이너에서 DI를 통해 다형성을 이용하여 OCP를 지켜준다. -> 유지보수가 매우 쉬워짐
  3. LSP(Liskov Substitution Principle) : 리스코프 치환원칙
    • 상위 타입의 객체를 하위 타입의 객체로 치환해도 상위 타입을 사용하는 프로그램은 정상적으로 동작해야한다.
  4. ISP(interface Segregation Principle) : 인터페이스 분리 원칙
    • 인터페이스는 그 인터페이스를 사용하는 클라이언트를 기준으로 분리해야한다.
  5. DIP(Dependency Inversion Principle) : 의존 역전 원칙
    • 고수준 모듈은 저수준 모듈의 구현에 의존해서는 안된다.


좀 더 자세히

용어 및 정의

정의

  • 객체 지향의 가장 기본은 객체이며, 객체의 핵심은 기능을 제공하는 것이다.
  • 실제로 객체를 정의할 때 사용하는 것은 객체가 제공해야할 기능이며, 객체 내부의 데이터로 정의되지 않는다.
  • 이러한 기능들을 오퍼레이션(operation)이라고 부른다.

시그니처

  • 객체 지향으로 설계하기 위해서는 오퍼레이션의 사용법을 알아야한다.
  • 오퍼레이션의 사용법은 다음 세가지로 구성되고, 이 세가지를 시그니처라고 부른다.
    • 기능 식별 이름
    • 파라미터 및 파라미터 타입
    • 기능 실행 결과값 및 타입

인터페이스

  • 객체가 제공하는 모든 오퍼레이션의 집합을 객체의 인터페이스라고 부른다.

메시지

  • 오퍼레이션의 실행을 요청하는 것을 메시지를 보낸다라고 표현한다.
  • 자바에서는 메서드를 호출하는 것이 메시지를 보내는 과정에 해당함

설계 원칙

책임

  • 객체마다 자신만이 제공할 수 있는 기능에 대한 책임이 있다.

  • 객체가 갖는 책임의 크기는 작을 수록 좋다.

    • 한 객체가 너무 많은 기능을 포함하면(책임이 크면), 그 기능과 관련된 데이터들도 모두 한 객체에 포함되게 되고, 객체에 정의된 많은 오퍼레이션들이 데이터를 공유하는 방식으로 프로그래밍 되는데, 이는 절차지향과 다를바가 없기 때문이다.
  • 이것을 해결하기 위해, SBP(Single Responsibility Principle) : 단일 책임 원칙이 필요하다.

개방-폐쇄 원칙

  • “기존 코드를 변경하지 않으면서 코드의 수정을 허용하는 것”
  • 즉, 원하는 기능을 구현하였을때, 사용자는 우리가 짠 코드를 변경할 순 없으나, 그 기능은 변경가능하도록 하는 것이다.
  • 예시
    • Car라는 클래스가 있을때, 사용자는 Car라는 클래스를 확장하여 Taxi 라는 클래스를 만들었다고 해보자.
    • 이때 Taxi 에는 Car의 기능을 포함하여 새로운 기능 (요금 받기… 등)이 필요하다. 따라서 이러한 기능들은 Taxi 클래스 안에서 정의하여 구축할 수 있다.
    • 또 Car 클래스에 속한 기본적인 기능(accelate(), break() 등)에 변경이 필요하면, 이 기능들을 오버라이드하여 필요한 기능들을 구축할 수 있다.
    • 위와 같이, Car라는 클래스를 변경하지 않으면서, Car의 확장은 허용하는 것이 OCP 원칙이다.

리스코프 치환 원칙

  • 상위 타입의 객체를 하위 타입의 객체로 치환해도 상위 타입을 사용하는 프로그램은 정상적으로 동작해야한다.

  • 상위 타입인 fruits에 하위타입인 Apple이 있다면, fruits를 사용하는 곳에서는 Apple을 사용해도 문제 없도록 해야한다.

  • 위배한 예시

    • 직사각형은 정사각형이 아니지만, 정사각형은 직사각형이다. 따라서 직사각형을 상속하여 정사각형 객체를 정의했다고 해보자.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
    public class Rectangle {
        private int width;
        private int height;
    
        public void setWidth(final int width) {
            this.width = width;
        }
    
        public void setHeight(final int height) {
            this.height = height;
        }
    
        public int getWidth() {
            return width;
        }
    
        public int getHeight() {
            return height;
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    
    public class Square extends Rectangle {
    
        @Override
        public void setWidth(final int width) {
            super.setWidth(width);
            super.setHeight(width);
        }
    
        @Override
        public void setHeight(final int height) {
            super.setWidth(height);
            super.setHeight(height);
        }
    }
    
    • 이때 직사각형을 인자로 받고, 세로가 가로보다 짧다면, 세로를 가로의 길이에 1을 더한 만큼의 길이를 갖게 만들어 세로를 더 길게만드는 메서드가 있다고 해보자.
    • 이때 정사각형은 항상 가로와 세로의 길이가 같으므로, 위 메서드를 실행하면, 가로와 세로의 길이가 같게 된다. 위 메서드의 수행결과에 문제가 발생한 것이다.
    • 따라서 이는 리스코프 치환원칙에 위배되므로, 정사각형은 직사각형을 상속받으면 안된다.
  • 리스코프 치환원칙은 기능의 명세와 확장에 대한 것이다.

    • Retangle 클래스의 setHeight() 메서드는 아래와 같은 기능을 명세한다.
      • 높이 값을 파라미터로 전달받은 값으로 변경한다.
      • 폭값은 변경되지 않는다.
    • 하지만, Square의 setHeight()는 높이와 폭 모두 변경하게 된다. 이렇게 상위 타입에서 정한 명세를 하위 타입에서도 그대로 지킬 수 없다면 상속을 하면 안된다.

의존성

  • 한 객체가 다른 객체를 이용한다는 것은, 한 객체의 코드에서 다른 객체를 생성하거나 다른 객체의 메서드를 호출한다는 것을 의미한다.
  • 이러한 의존의 영향은 꼬리에 꼬리를 문것처럼 계속 전파되고, 변경한 여파가 다시 자기 자신까지 변화시킬 수 있는데, 이것을 순환의존이라고 한다.
  • 이것을 해결하기 위해, DIP(Dependency Inversion Principle) : 의존 역전 원칙이 필요하다

인터페이스 분리 원칙

  • 인터페이스는 그 인터페이스를 사용하는 클라이언트를 기준으로 분리해야한다.

  • 특정 클라이언트를 위한 인터페이스를 여러개 만드는 것이 범용 인터페이스 하나보다 낫다

  • 예시

    • ISP를 적용하기 전

      1
      2
      3
      4
      5
      6
      
      // ISP를 적용하지 않은 예제
      public interface multifunction {
        void copy();
        void fax(Address from, Address to);
        void print();
      }
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      
      public class CopyMachine implements multifunction {
        @Override
        public void copy() {
          System.out.println("### 복사 ###");
        }
      
        @Override
        public void fax(Address from, Address to) {
          // 사용하지 않는 인터페이스가 변경되어도 함께 수정이 일어난다.
        }
      
        @Override
        public print() {
          // 사용하지 않는 인터페이스가 변경되어도 함께 수정이 일어난다.
        }
      }
      
      • CopyMachine에 필요없는 fax, print 도 모두 구현해줘야한다.
      • 만약 multifunction의 fax() print()의 리턴타입이 변경되면 CopyMachine에서도 모두 변경해줘야한다.
    • ISP를 적용한 이후

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      
      // ISP가 적용된 예제
      public interface Print{
        void print();
      }
          
      public interface Copy {
        void copy();
      }
          
      public interface Fax {
        void fax(Address from, Address to);
      }
      
      1
      2
      3
      4
      5
      6
      
      public class copyMachine implements Copy {
        @Override
        void copy() {
          System.out.println("### 복사 ###");
        }
      }
      

캡슐화

캡슐화

  • 객체가 내부적으로 기능을 어떻게 구현하는지를 숨기는 것

  • 객체지향은 기본적으로 캡슐화를 통해서 한 곳의 변화가 다른 곳에 미치는 영향을 최소화한다.
  • 내부 기능 구현이 변경되더라도, 그 기능을 사용하는 코드는 영향을 받지 않도록 해주어서, 내부 구현 변경의 유연함을 주는 기법이 캡슐화이다.

캡슐화를 위한 두 개의 규칙

  1. Tell, Don’t Ask

    • 데이터를 물어보지 않고, 기능을 실행해 달라고 말하라는 규칙이다.

    • 데이터를 읽는 것은 데이터를 중심으로 코드를 작성하게 만드는 원인이 된다.

    • 데이터를 private로 클래스 내부에 숨기고, 메소드를 통해 데이터에 접근해야한다.

      1
      2
      3
      4
      5
      6
      
      public class Customer {
          private Wallet wallet;
          public Wallet getWallet() {
              return wallet;
          }
      }
      
  2. 데미테르 법칙 : 한 객체 안에서 다른 객체의 메서드를 부를때는 다음 상황일때만이다.

    • 메서드에서 생성한 객체의 메서드만 호출
    • 파라미터로 받은 객체의 메서드만 호출
    • 필드로 참조되는 객체의 메서드만 호출

객체지향 설계과정

  1. 제공해야할 기능을 찾고, 또는 세분화하고 그 기능을 알맞는 객체에 할당한다.
    1. 기능을 구현하는데 필요한 데이터를 객체에 추가한다.
    2. 객체에 데이터를 먼저 추가하고, 그 데이터를 이용하는 기능을 넣는다.
    3. 기능은 최대한 캡슐화하여 구현한다.
  2. 객체간에 어떻게 메시지를 주고 받을 지 결정한다.

상속을 통한 재사용의 단점

  1. 상위 클래스 변경의 어려움
    • 어떤 클래스를 상속 받는 다는 것은 그 클래스에 의존한다는 뜻이고, 따라서 의존하는 클래스의 코드가 변경되면 영향을 받을 수 있다.
  2. 클래스의 불필요한 증가
    • 유사한 기능을 확장하는 과정에서 클래스의 개수가 불필요하게 증가할 수 있다.
  3. 상속의 오용
    • 같은 종류가 아닌 클래스의 구현을 재사용하기 위해 상속을 받게 되면, 문제가 발생한다.
    • 상속을 받는 클래스가 상위 클래스와 IS-A 관계가 아닌 경우에 발생함

객체 조립 : 위 단점의 해소법

  • 객체지향언어에서 객체 조립은 보통 필드에서 다른 객체를 참조하는 방식으로 구현된다.
  • 상속에 비해 조립을 통한 재사용의 단점은 상대적으로 런타임 구조가 복잡해진다는 것이고, 상속보다 구현이 어렵다는 것이다.
  • 하지만, 변경의 유연함을 확보하는데서 오는 장점이 크기 때문에, 상속보다 조립하는 방법을 먼저 고려해야한다.

상속은 언제 사용하는가?

  • 재사용이 아닌, 기능의 확장이라는 관점에서 상속을 적용해야함
  • 명확한 IS-A 관계가 성립 되어야함.

출처

https://asfirstalways.tistory.com/177

This post is licensed under CC BY 4.0 by the author.

React v18 주요 변경점

알고리즘 팁(WIP)

Comments powered by Disqus.