[개발자가 반드시 정복해야 할 객체 지향과 디자인 패턴] 다형성과 추상 타입

·2022년 10월 26일
0
post-thumbnail

[1] 상속 개요

  • 상속(Inheritance): 한 타입을 그대로 사용하면서 구현을 추가할 수 있도록 해주는 방법을 제공
  • 재정의(Overiding): 하위클래스가 필요에 따라 상위 클래스에 정의된 메서드를 새롭게 구현하는 것
    • 메서드를 재정의하면, 해당 메서드를 실행할 때 상위 타입의 메서드가 아닌 하위 타입에서 재정의한 메서드가 실행됨
  • java: extends & @Overide

[2] 다형성과 상속

  • 다형성(Polymorphism): 한 객체가 여러 타입을 가질 수 있다
  • 자바와 같은 정적 타입 언어에서는 타입 상속을 통해 다형성 구현 public class TurboPlane extends Plane implements Turbo ⇒ TurboPlane 타입의 객체는 Plane/Turbo 타입도 될 수 있고, 두 타입에 정의된 모든 기능을 제공함

인터페이스 상속과 구현 상속

  • 인터페이스 상속
    • 순전히 타입 정의만을 상속받는 것
    • ex) java의 인터페이스나 C++의 추상함수만 가진 추상클래스를 상속받는 경우
    • java는 클래스 다중 상속을 자원하지 않기 때문에, 인터페이스를 이용해 객체가 다형을 갖게됨.
  • 구현 상속
    • 클래스 상속을 통해 이루어짐

    • 상위 클래스에 정의된 기능을 재사용하기 위한 목적으로 사용

    • 구현을 재사용하면서 다형성도 함께 제공

      public class TurboPlane extends Plane {
      		public void fly() { // Plane에 정의된 fly()구현을 오버라이딩
      				...
      		}
      }
      Plane p = new TurboPlane();
      p.fly(); // 실제 p의 타입인 TurboPlane의 fly() 실행

추상 타입과 유연함

  • 추상화(abstraction): 데이터나 프로세스 등을 의미가 비슷한 개념이나 표현으로정의하는 과정

  • 타입 추상화
    • 추상화된 타입은 오퍼레이션의 시그니처만 정의할 뿐, 실제 구현은 제공 X
    • 하위 타입들은 모두 상위 타입 인터페이스에 정의된 기능을 실제로 구현 ⇒ 콘크리트 클래스(concrete class)

추상 타입을 이용한 구현 교체의 유연함

  • 예시를 통해 알아보자.

  • FileDataReader와 SocketDataReader의 if-else 블록의 코드 구성이 비슷하다

    ⇒ 추상타입인 ByteSource 타입을 도입하자.

  • ByteSource의 종류가 변경되더라도 FlowController가 바뀌지 않도록 하자

    • ByteSource 타입의 객체를 생성하는 기능을 별도 객체로 분리한 뒤, 그 객체를 사용해 ByeSource 생성
    • 생성자(또는 다른 메서드)를 이용해서 사용할 ByteSource를 전달받기
  • 첫 번째 방법을 활용해보자.

    • ByteSourceFactory 도입 → ByteSource 타입의 객체를 생성하는 과정을 추상화
      훨씬 간결해졌다.
  • 추상화 과정을 통해 얻은 두 가지 유연함

    • ByteSource의 종류가 변경되면, ByteSourceFactory만 변경될 뿐, FlowController 클래스는 변경되지 않는다.
    • FlowController의 제어 흐름을 변경할 때, ByteSource 객체를 생성하는 부분은 영향을 주지 않으면서 FlowController만 변경하면 된다.
  • 정리

    1. 바이트 데이터를 읽기: ByteSource 인터페이스 도출
    2. ByteSource 객체를 생성하기: ByteSourceFactory 인터페이스 도출

변화되는 부분을 추상화하기

  • 추상화 TIP: 변화되는 부분을 추상화하자!
  • 요구사항이 바뀔 때 변화되는 부분은 이후에도 변경될 소지가 많다

인터페이스에 대고 프로그래밍하기

  • 실제 구현을 제공하는 콘크리트 클래스를 사용해 프로그래밍하지 말고, 기능을 정의한 인터페이스를 사용해 프로그래밍하라
  • 인터페이스는 요구사항의 변화와 함께 점진적으로 도출됨
    • ex) FlowController: 파일이 아닌 소켓에서도 데이터를 읽어 와야한다는 새로운 요구사항이 추가되며, ByteSource라는 인터페이스 도출
  • 이 규칙은 바로 추상화를 통한 유연함을 얻기 위한 규칙
    • 기존 코드를 건드리지 않으면서 콘크리트 클래스를 교체할 수 있다.
    • 하지만, 변화 가능성이 높은 경우에 한해서 사용해야한다.

인터페이스는 인터페이스 사용자 입장에서 만들기

  • 인터페이스를 작성할 때에는 그 인터페이스를 사용하는 코드의 입장에서 작성해야함
  • ex) FlowController 입장에서 인터페이스 작성하기. 파일이나 소켓에서 데이터를 읽어 올 수 있는 상황이므로, FileDataReadIF라는 이름보다는 ByteSource라는 이름 사용 (파일이든 소켓이든 데이터를 읽어온다)

인터페이스와 테스트

  • Mock(가짜, 모의) 객체: 콘크리트 클래스 대신에 진짜처럼 행동하는 객체
  • 사용할 대상을 인터페이스로 추상화하면 좀 더 쉽게 Mock 객체를 만들 수 있게됨.
// 흐름 제어 객체
public class FlowController {

    private ByteSource byteSource;

    public FlowController(ByteSource byteSource) {
        this.byteSource = byteSource;
    }

    public void process() {
        byte[] data = byteSource.read();

        // ...
    }

    public void testProcess() {
        ByteSource mockSource = new MockByteSource();
        FlowController fc = new FlowController(mockSource);
        fc.process();

        // 결과가 정상적으로 만들어졌는지 확인하는 코드
    }

    // 보통 Mock객체를 위한 클래스를 별도로 만들기보다는 Mockito나 jmock과 같은 프레임워크를 이용해 Mock 객체 생성함
    class MockByteSource implements ByteSource {
        public byte[] read() {
            byte[] data = new byte[128];
            // data를 테스트 목적의 데이터로 초기화
            return data;
        }
    }
}

0개의 댓글