Zayson 2024. 6. 17. 21:54

상속 개요


  • 상속(Inheritance): 한 타입을 그대로 사용하면서 구현을 추가할 수 있도록 하는 방법
    • 상속 대상이 되는 클래스 = 상위(super) 클래스, 부모(parent) 클래스
    • 상속을 받는 클래스 = 하위(sub)클래스, 자식(child)클래스
  • 자식 클래스는 부모 클래스에 정의된 구현을 물려 받는다.
private 접근 제어자가 명시된 메소드, 필드를 제외하고 물려받는다.
  • 재정의(Overriding): 하위 클래스에서 상위 클래스에 정의된 메소드를 새로 구현하는 것
    • 메소드를 오버라이딩하면, 메소드 실행 시 상위 타입 메소드가 아닌 재정의한 하위 타입 메소드가 실행된다

다형성과 상속


  • 다형성(Polymorphism): 한 객체가 여러 가지 모습(타입)을 갖는 것, 한 객체가 여러 타입을 가질 수 있는 것

정적 타입 언어(e.g Java)에서는 타입 상속을 통해 다형성을 구현한다.

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

  • 타입 상속은 인터페이스 상속과 구현 상속으로 구분된다.
    • 인터페이스 상속: 타입 정의만 상속 받는 것 (자바의 인터페이스)
    • 구현 상속: 클래스 상속을 통해 이뤄지며, 상위 클래스에 정의된 기능을 재사용하기 위해 사용된다.

추상 타입과 유연함


  • 추상화(abstraction): 데이터나 프로세스 등을 의미가 비슷한 개념이나 표현으로 정의하는 과정
    • 추상화된 타입은 오퍼레이션의 시그니처만 정의하며 실제 구현은 제공하지 못한다.
    • 추상 타입은 구현을 제공하지 않는 타입(인터페이스)을 통해 추상 타입을 정의한다.파일 다운로드, 소켓 로그 읽기, DB 읽기를 로그 수집으로 추상화 할 수 있다.

파일 다운로드, 소켓 로그 읽기, DB 읽기를 로그 수집으로 추상화 할 수 있다.

추상화는 단순 구현 클래스로부터 추상 타입을 이끌어 내는 것만 의미하는 것은 아니다.
e.g) 객체 모델링, 컴퓨터의 수행 처리 과정을 개념적으로 추상화

추상 타입과 실제 구현의 연결

  • 추상 타입과 구현 클래스는 상속을 통해 둘을 연결한다.
      LogCollector collector = createLogCollector(); // 추상 타입을 이용해 코드 사용가능
      collector.collect();
     

  • 콘크리트 클래스: 상위 타입을 실제로 구현한 클래스

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

public class FlowController {
    public void process() {
        FileDataReader reader = new FileDataReader();  
        byte[] data = reader.read();
        ...
    }
}
  • 파일 뿐만 아니라 소켓에서도 데이터를 읽도록 요구사항이 변경되는 경우 구현체를 사용하면 코드는 다음과 같다.
public class FlowController {
    private boolean useFile;
    public FlowController(boolean useFile) {
        this.useFile = useFile;
    }

    public void process() {
        byte[] data = null;

        if(useFile) {
            FileDataReader fileReader = new FileDataReader();  
            data = fileReader.read();
        } else {
            SocketDataReader socketReader = new SocketDataReader();
            data = socketReader.read();
        }
    }
}
  • FlowController의 책임과는 상관없이 데이터 읽기 구현의 변경으로 인해 FlowController 코드 자체가 영향을 받는다.
    • 요구사항이 계속해서 추가되면 계속해서 코드가 영향을 받는 문제가 발생한다.
  • 각 데이터 읽기 구현체는 “데이터 읽기”라는 공통된 기능을 가지며 이를 추상화할 수 있다.
public interface ByteSource {
    public byte[] read();
}

public class FileDataReader implements ByteSource {
    @Override 
    public byte[] read() { ... }
}
public class SocketDataReader implements ByteSource {
    @Override
    public byte[] read() { ... }
}
  • 이제 추상화된 타입을 통해서 가져올 수는 있게 되었지만, 아직 요구사항이 변경되어도 FlowController 클래스 내부에서 구현체를 선택해줘야 하므로 코드 변경이 발생한다.
ByteSource source = null;
if(useFile) 
    source = new FileDataReader();
else 
    source = new SocketDataReader();
  • FlowController가 구현체가 변경되더라도 바뀌지 않게 하는 방법에는 2가지가 존재한다.
    1. 객체를 생성하는 코드를 팩토리로 분리
    2. 생성자를 통해 실제 사용할 구현체를 받기
  • 객체 생성 팩토리 사용
public class ByteSourceFactory {
    public ByteSource create() {
        if(useFile())
            return new FileDataReader();
        else 
            return new SocketDataReader();
    }

    private boolean useFile() {
        String useFileVal = System.getProperty("usefFile");
        return useFileVal != null && Boolean.valueOf(useFileVal)l
    }

    // 싱글톤
    private static ByteSourceFactory instance = new ByteSourceFactory();
    public static ByteSourceFactory getInstance() {
        return instance;
    }

    private ByteSourceFactory() {}
}
ByteSource source = ByteSourceFactory.getInstance().create();
byte[] data = source.read();
  • 생성자를 이용해 구현체를 받는 방법
private final ByteSource byteSource;
public FlowController(ByteSource byteSource) {
    this.byteSource = byteSource;
}

public void process() {
    this.byteSource.read();
    ...
}
  • 추상화를 사용함으로써 크게 2가지 유연성을 갖게되었다.
    1. 추상화 타입을 사용하는 객체가 구현체가 변경되었다 해서 코드 변경이 일어나지 않게 되었다.
    2. 추상화 타입을 사용하는 객체는 자신의 책임에 대한 부분만 코드를 변경할 수 있다.
  • 추상화는 공통된 개념을 도출해 추상 타입을 정의하며, 많은 책임을 가진 객체로부터 책임을 분리할 수 있다.

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


  • “인터페이스에 대고 프로그래밍” 하자.
    • 콘크리트 클래스보단 인터페이스를 사용해 프로그래밍하는 것이 바람직하다.
  • 인터페이스는 추상화를 통해 유연함을 얻는 것이 목적이기 때문에 변화 가능성이 높은 경우에만 사용하자.
  • 변경 가능성이 희박한 클래스에 대해 인터페이스를 만들면 프로그램의 구조만 복잡해진다.

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


  • 인터페이스를 작성할 때는 인터페이스를 사용하는 코드 입장에서 작성하자.
    • 인터페이스를 사용하는 FlowController 입장에서는 FileDataReaderIF가 파일로부터 데이터를 읽어와야 하는 인터페이스라고 생각할 수 있다.

인터페이스와 테스트


  • 인터페이스를 사용하면 Mock 객체를 쉽게 만들어 테스트의 유연함을 가져올 수 있다.
    • 콘크리트 클래스의 구현 없이도 테스트가 가능하다.
    • Mock 객체를 만들어 FlowController 자체에 대한 테스트를 먼저 진행할 수 있다.
public void test() {
    ByteSource mockSource = new MockByteSource();
    FlowController fc = new FlowController(); 
    fc.process();
}
테스트 주도 개발 (Test Driven Development, TDD)

- 테스트 코드를 먼저 작성하고 실제 코드를 작성하는 방법으로 개발하는 기법
- 작성한 테스트를 통과하는 코드를 점진적으로 완성해 나가며, 완성하기 어려운 구현부로 인해 테스트가 어려워지면 해당 부분을 별도의 인터페이스로 구분해 개발하는 방식이다. 
반응형