Java

상속과 다형성 2(추상 타입을 이용한 구현 교체의 유연함)

태인킴 2020. 11. 5. 23:41
반응형


 

상속과 다형성 1

객체 지향이 주는 장점은 구현 변경의 유연함 입니다. 객체는 캡슐화를 통해서 객체를 사용하는 다른 코드에 영향을 최소화 하면서 객체의 내부 구현을 변경할 수 있는 유연함을 얻을 수 있습니

coding-food-court.tistory.com

 

 

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

public class FlowController {
    
    public void process() {
        FileDataReader reader = new FileDataReader();
        byte[] data = reader.read();
        
        Encryptor encryptor = new Encryptor();
        byte[] encryptedData = encryptor.encrypt(data);
        
        FileDataWriter writer = new FileDataWriter();
        writer.write(encryptedData);
    }
}

위와 같이 '파일을 읽어와' -> '파일을 암호화 하고' -> '파일을 write' 하는 코드가 있습니다. 위와 같은 상황에서 '파일 뿐만 아니라 소켓을 통해서 데이터를 읽어 와 암호화할 수 있도록 해 달라는 기능'이 요청 들어 옵니다. 

 

 

2. 기능 추가로 인한 FlowController의 변경 발생

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 socketDataReader = new SocketDataReader();
            data = socketDataReader.read();
        }

        Encryptor encryptor = new Encryptor();
        byte[] encryptedData = encryptor.encrypt(data);

        FileDataWriter writer = new FileDataWriter();
        writer.write(encryptedData);
    }
}

위와 같이 if / else 구문을 추가하여, SocketDataReader를 사용하여 로직을 수정 할 수 있습니다. 그렇다면, 만약 HTTP로 데이터를 읽어와야 한다면, 어떻게 바뀔까요? if / else 문도 추가 되고, useFile 이라는 boolean 타입으로 FileDataReader, SocketDataReader, HttpDataReader를 구분 할 수 없으므로, userFile 생성자 파라미터 또한 바뀌어야 합니다. '데이터를 읽어 오는 요구 사항의 변화'가 생길 때마다 FlowController는 계속 영향을 받습니다. FlowController 자체는 파일이건 소켓이건 상관 없이 데이터를 읽어 오고 이를 암호화해서 특정 파일에 기록하는 책임을 가집니다. FlowController의 본연의 책임(흐름 제어)와 상관없는, 데이터 읽기 구현의 변경 때문에 FlowController도 함께 바뀌어야 합니다.

 

- 기존 요구 사항 : 파일에서 바이트 데이터를 읽어 온다.

- 추가 요구 사항 : 소켓에서 바이트 데이터를 읽어 온다.

- 공통점 : 바이트 데이터를 읽어 온다.

 

이 두 구현을 추상화 하면 다음과 같이 표현할 수 있습니다.

- 추상화 : 어떤 곳에서 바이트 데이터 읽기

 

 

3. '데이터 읽기' 추상화

public interface ByteSource {
    public byte[] read();
}

public class FileDataReader implements ByteSource {
    @Override
    public byte[] read() {
    }
}

public class SocketDataReader implements ByteSource {
    @Override
    public byte[] read() {
    }
}
ByteSource source = null;

if (useFile) {
    source = new FileDataReader();
} else {
    source = new SocketDataReader();
}

byte[] data = source.read();

위와 같이 ByteSource를 만들어 추상화를 시켰습니다. 하지만, 여전히 if / else 문과 useFile 파라미터는 FlowController의 영향을 미치고 있습니다.

 

따라서, ByteSource의 종류가 변경되더라도 FlowController가 바뀌지 않도록 하는 방법에는 다음의 2가지 방법이 있습니다.

1. ByteSource 타입의 객체를 생성하는 기능을 별도 객체로 분리한 뒤, 그 객체를 사용해서 ByteSource 생성

2. 생성자(또는 다른 메서드)를 이용해서 ByteSource를 주입 받기

 

 

4. 객체 생성 책임 분리를 통한 의존도 줄이기

1. ByteSource 타입의 객체를 생성하는 기능을 별도 객체로 분리

public class ByteSourceFactory {
    
    // 객체 생성 기능을 위한 오퍼레이션 정의
    public ByteSource create() {
        if (useFile())
            return new FileDataReader();
        else 
            return new SocketDataReader();
    }
    
    private boolean useFile() {
        String useFileVal = System.getProperty("useFile");
        return useFileVal != null && Boolean.valueOf(useFileVal);
    }
    
    // 싱글톤 패턴 적용
    private ByteSourceFactory() {}
    private static ByteSourceFactory instance = new ByteSourceFactory();
    private static ByteSourceFactory getInstance() {
        return instance;
    }
}

ByteSource 타입의 객체를 생성해 주는 책임을 갖는 ByteSourceFactory 클래스를 구현 하였습니다. create() 메서드는 ByteSource 타입의 객체를 생성하는 기능을 제공 합니다. 따라서, ByteSourceFactory 클래스는 ByteSource 타입의 객체 생성하는 과정을 '추상화' 하였습니다.

 

public class FlowController {

    public void process() {
        ByteSource source = ByteSourceFactory.getInstance().create();
        byte[] data = source.read();

        Encryptor encryptor = new Encryptor();
        byte[] encryptedData = encryptor.encrypt(data);

        FileDataWriter writer = new FileDataWriter();
        writer.write(encryptedData);
    }
}

이제, HTTP를 이용하여 암호화 할 데이터를 읽어 와야 하는 새로운 요구사항이 생긴다면, ByteSourceFactory 클래스만 변경 된다. FlowController 클래스의 코드는 영향을 받지 않습니다. 따라서, ByteSource라는 추상 타입을 얻어내어, FlowController의 코드는 변경하지 않으면서, ByteSource를 변경할 수 있는 유연함을 얻을수 있었습니다.

 

 

5. 다형성/추상화를 통한 책임 분리

객체책임을 작게 가질수록 변경에 대한 유연함을 가질 수 있습니다.(단일 책임 원칙) 기존의 FlowController는 크게 2가지 책임을 가지고 있었습니다. 

1. 데이터를 읽어오는 객체 생성 책임

2. 흐름을 제어 하는 책임

    public void process() {
        // 책임1 - 데이터 읽기 객체 직접 생성
        FileDataReader reader = new FileDataReader();
        // 책임2 - 흐름제어 1 : 데이터 읽기
        byte[] data = reader.read();

        Encryptor encryptor = new Encryptor();
        // 책임2 - 흐름제어 2 : 암호화
        byte[] encryptedData = encryptor.encrypt(data);

        FileDataWriter writer = new FileDataWriter();
        // 책임2 - 흐름제어 3 : 쓰기
        writer.write(encryptedData);
    }

따라서, '데이터를 읽어오는 객체 생성''흐름을 제어' 하는 부분의 변화가 생길 때, 모두, FlowController 코드를 수정해 주어야 했습니다. 

 

FlowController에서 책임을 분리 할수 있었던 것은 추상화를 한 덕분 이었습니다. 

- 바이트 데이터 읽기 : ByteSource 인터페이스 도출

- ByteSource 객체 생성 : ByteSourceFactory 도출

 

저는 위와 같이 2번의 추상화를 진행 하였습니다. 따라서, 추상화는 공통된 개념을 도출해서 추상 타입을 정의해 주기도 하지만, 또한, 많은 책임을 가진 객체로 부터 책임을 분리하는 역할을 하기도 합니다.

 

반응형

'Java' 카테고리의 다른 글

가비지 콜렉션 (Java 가상 머신의 메모리 영역)  (0) 2022.11.09
Gson Expose 어노테이션  (2) 2021.01.05
상속과 다형성 3(Program to interface)  (0) 2020.11.07
상속과 다형성 1  (0) 2020.11.04