본문 바로가기
책 내용 정리/[책] 개발자가 반드시 정복해야 할 객체지향과 디자인 패턴

[3] 다형성, 추상타입

by 문자메일 2023. 2. 27.

https://link.coupang.com/a/QoNwV

 

개발자가 반드시 정복해야 할 객체 지향과 디자인 패턴

COUPANG

www.coupang.com

 

본문의 내용은 위 책에서 정리한 내용 일부이며, 자세히 알고 싶은 분들은 위 책을 구매해서 보시는 것을 추천드립니다.

씹고 뜯고 맛보고 즐겨도 내용이 지루하지 않고, 정말 돈이 아깝지 않은 책!

 

다형성과 상속

자바와 같은 정적 타입 언어에서는 타입 상속을 통해서 다형성을 구현한다.

타입 상속의 종류

  • 인터페이스 상속 : 자바의 인터페이스 처럼 순전히 타입 정의만 상속받는 경우
  • 구현 상속 : 보통 상위 클래스에 정의된 기능을 재사용 하기 위한 목적으로 사용

 

추상화

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

 

아래와 같은 3개의 기능이 있다고 가정

  • FTP에서 파일을 다운로드
  • 소켓에서 데이터 읽기
  • DB 테이블의 데이터를 조회

위 3개 기능은 알고보니 모두 로그를 수집하기 위한 목적의 기능이였다.

그러면, 위 3개 기능을 추상화하면 '로그 수집' 이라는 개념으로 정의가 가능하다.

 

 

 

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

public class FlowController {
	
	public void process() {
		FileDataReader reader = new FileDataReader(fileName); // 객체 생성
		byte[] plainBytes = reader.read(); // 메서드 호출
		
		ByteEncryptor encryptor = new ByteEncryptor(); // 객체 생성
		byte[] encryptedBytes = encryptor.encrypt(plainBytes); // 메서드 호출
		
		FileDataWriter writer = new FileDataWriter(); // 객체 생성
		writer.write(encryptedBytes); // 메서드 호출
	}

}

추상 타입을 사용하는 이유는?

위 코드는 파일을 암호화하는 프로그램의 흐름 제어 역할을 하는 FlowController 클래스이다.

 

그런데 어느 날 파일 뿐만 아니라 소켓을 통해서도 데이터를 읽어 와 암호화할 수 있도록 해달라는 요구사항이 들어왔다면 아래와 같은 방법으로 구현할 수 있을 것이다.

public class FlowController2 {
	
	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();
		}
		
		Encryptor encryptor = new Encryptor();
		byte[] encryptedData = encryptor.encrypt(data);
		
		FileDataWriter writer = new FileDataWriter();
		writer.write(encryptedData);
	}

}

 

그런데, 위 코드를 보면 if - else 블록의 코드 구성이 비슷하다. 만약 HTTP로 데이터를 읽어 오는 요구사항이 추가된다면, 또 다른 if -else 블록을 prcoess() 메서드 안에 추가해야 할 것이고, 또한 조건문의 useFile 플래그도 수정이 필요할 것이다.

=> 뒤에 나오는 내용이지만, 위 내용은 개방-폐쇄의 원칙(OCP) 이 위배된 사례라고 볼 수 있다.

 

기존 기능과 새로운 기능 비교하면

기존 요구사항 : 파일에서 바이트 데이터 읽어서

추가 요구사항 : 소켓에서 바이트 데이터 읽어서

-> 바이트 데이터를 읽는 다는 부분에서 공통점이 있고, 바이트 데이터 읽기라는 기능을 추상화하여 인터페이스로 정의 가능하다

 

package chapter3;

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


package chapter3;

public class FileDataReader implements ByteSource{

	@Override
	public byte[] read() {
		// TODO Auto-generated method stub
		return null;
	}

}

package chapter3;

public class SocketDataReader implements ByteSource{

	@Override
	public byte[] read() {
		// TODO Auto-generated method stub
		return null;
	}

}

package chapter3;

public class FlowController3 {
	
	private boolean useFile;
	
	public FlowController(boolean useFile) {
		this.useFile = useFile;
	}
	
	public void process() {
		ByteSource byteSource = null;
		if (useFile) {
			byteSource = new FileDataReader();
		} else {
			byteSource = new SocketDataReader();
		}
		byte[] data = byteSource.read();
		
		Encryptor encryptor = new Encryptor();
		byte[] encryptedData = encryptor.encrypt(data);
		
		FileDataWriter writer = new FileDataWriter();
		writer.write(encryptedData);
	}

}

 

ByteSource 인터페이스를 만들고, 인터페이스를 구현한 concreate 클래스를 만들어서 사용함으로써, FlowController의 prcoess 메서드가 이전보다 간결해 지긴 했지만 여전히 if-else 구문이 남아있다.

 

ByteSource의 Concreate 클래스를 process 메서드 안에서 생성하고 있기 때문이다.

concreate 클래스를 메서드 안에서 생성하는 한, FlowController 클래스는 흐름제어의 책임 이외에 다른 요인(객체 생성)에 의하여 지속 수정이 필요하게 된다.

 

위 FlowController 클래스에서 ByteSource의 종류가 변경되더라도 코드가 변경되지 않도록 하는 방법에는 아래 2가지가 있다.

  • ByteSource 타입의 객체를 생성하는 기능을 별도 객체로 분리한 뒤(팩토리 클래스), 그 객체를 사용해서 ByteSource 생성
  • 생성자( 또는 set 메서드)를 이용해서 사용할 ByteSource 전달 받는 것

 

 

위 방법에서 객체를 생성하는 기능을 별도로 분리하는 방법을 적용하여, ByteSource 타입의 객체를 생성해 주는 책임을 갖는 ByteSourceFactory 클래스를 구현하면 아래와 같이 코드가 변하게 된다.

 

 

package chapter3;

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 static ByteSourceFactory instance = new ByteSourceFactory();
	public static ByteSourceFactory getInstance() {
		return instance;
	}
	
	private ByteSourceFactory() {}
}



package chapter3;

public class FlowController4 {
	
	public void process() {
		ByteSource byteSource = ByteSourceFactory.getInstance().create();
		byte[] data = byteSource.read();
		
		Encryptor encryptor = new Encryptor();
		byte[] encryptedData = encryptor.encrypt(data);
		
		FileDataWriter writer = new FileDataWriter();
		writer.write(encryptedData);
	}

}

 

위 방식으로 ByteSource 객체를 생성하는 클래스를 분리한 결과, 새로운 ByteSource 구현 클래스가 추가되어도 FlowController 클래스의 코드는 영향을 받지 않게 된다.

변경되는 클래스는 ByteSource 구현 객체를 생성하는 ByteSourceFactory에 집중되는 장점이 생겼다. (OCP 만족)

 

인터페이스는 해당 인터페이스를 사용하는 사용자 입장에서 만들어야 한다.

댓글