추상 클래스는 순수 가상 함수가 포함된 클래스를 말한다. 추상 클래스는 객체를 생성할 수 없다.

C++에서는 인터페이스라는 키워드는 없지만 public 접근 제어인 순수 가상 함수만으로 이루어진 클래스를 인터페이스라고 보면 된다.

추상 클래스는 전체적인 로직은 고정되지만, 상황에 따라 세세한 로직이 달라질 경우, 사용할 수 있다.

인터페이스는 정말 다양한 용도로, 매개 변수의 타입 제한(특정한 행동을 할 수 있어야 한다)를 걸어야 한다면 쓴다.

추상클래스를 예를 들 때는, 다른 컴퓨터에 있는 파일을 전송받는 (다운로드) 기능을 하는 클래스를 생각할 수 있다.

#include<string>
using namespace std;
using byte = uint8_t;
template<typename T>
using List = vector<T>;
class NetRequest
{
public:
	Option<List<byte>> DownloadFile(const string & fileName)
	{
		//1단계, 서버에 연결한다. 실패하면 끝
		if (!Connect())
		{
			return Option<List<byte>>();
		}
		//2단계, 파일을 요청한다.
		Option<int> res = RequestFile(fileName);
		if (!res.ok())
		{
			return Option<List<byte>>();
		}
		//파일을 읽는다.
		List<byte> file;
		int fileSize = *res;
		byte buffer[4096] = { 0 };
		do
		{
			int readSize = ReadFile(buffer, 4096);
			for (int i = 0; i < readSize; i++)
			{
				file.push_back(buffer[i]);
			}
		} while (file.size() != fileSize);
		//다 읽으면 파일을 반환한다.
		return Option<List<byte>>(std::move(file));
	}
};

C++를 모르고 Java만 안다면 위의 using같은 건 신경 끄고 DownloadFile함수만 보자. Option은 레퍼런스변수 같은 거라 생각하자.

일반적으로 서버에서 파일을 요청하는 단계를 이런 식일 것이다. 전체적인 구조는 달라지지 않을 것이다.그럼 서버에 접근하는 방식(프로토콜)이 HTTP인지, FTP인지 혹은 SMB의 차이만 있을 뿐이다. 그럼 실제로 NetRequest는 Connect함수나 RequestFile, ReadFile이 어찌 구현되어 있는 지 상관하지 않고 방식에 따라(프로토콜) 결과값만 제대로 나오면 된다.

class NetRequest
{
protected:
	//순수 가상함수들
	virtual bool Connect() = 0;
	virtual Option<int> RequestFile(const string & fileName) = 0;
	virtual int ReadFile(byte * buffer, int length) = 0;
public:
	Option<List<byte>> DownloadFile(const string & fileName)
	{
		//1단계, 서버에 연결한다. 실패하면 끝
		if (!Connect())
		{
			return Option<List<byte>>();
		}
		//2단계, 파일을 요청한다.
		Option<int> res = RequestFile(fileName);
		if (!res.ok())
		{
			return Option<List<byte>>();
		}
		//파일을 읽는다.
		List<byte> file;
		int fileSize = *res;
		byte buffer[4096] = { 0 };
		do
		{
			int readSize = ReadFile(buffer, 4096);
			for (int i = 0; i < readSize; i++)
			{
				file.push_back(buffer[i]);
			}
		} while (file.size() != fileSize);
		//다 읽으면 파일을 반환한다.
		return Option<List<byte>>(std::move(file));
	}
};

순수 가상 함수를 선언하자. 그럼 NetRequest의 객체는 만들 수 없다. 그럼 NetRequest를 상속받은 자식 클래스들이 순수 가상 함수를 정의해 주어야 한다.

//HTTP
class HTTPFileRequest : public NetRequest
{
protected:
	bool Connect() override
	{
		struct addrinfo hints, *res, *p;
		memset(&hints, 0, sizeof(struct addrinfo));
		hints.ai_family = AF_UNSPEC;
		hints.ai_socktype = SOCK_STREAM;
		getaddrinfo(m_host.c_str(), NULL, &hints, &res);
		for (p = res; p != NULL; p = p->ai_next)
		{
			void *addr;
			char *ipver;
			// Get the pointer to the address itself, different fields in IPv4 and IPv6
			if (p->ai_family == AF_INET)
			{
				// IPv4
				struct sockaddr_in *ipv4 = (struct sockaddr_in *)p->ai_addr;
				ipv4->sin_port = htons(80);

				addr = &(ipv4->sin_addr);
			}
			else
			{
				// IPv6
				struct sockaddr_in6 *ipv6 = (struct sockaddr_in6 *)p->ai_addr;
				ipv6->sin6_port = htons(80);
				addr = &(ipv6->sin6_addr);
			}
			
			if (!FAILED(connect(m_socket, p->ai_addr, p->ai_addrlen)))
			{
				freeaddrinfo(res);
				return true;
			}
		}
		freeaddrinfo(res);
		return false;
	}
	Option<int> RequestFile(const string & fileName) override
	{
		string httpHeader;
		httpHeader.append("GET ");
		httpHeader.append(fileName);
		httpHeader.append(" HTTP/1.1\r\n");
		httpHeader.append("Host:");
		httpHeader.append(m_host);
		httpHeader.append("\r\n");
		httpHeader.append("\r\n");
		//send header;
		int sendByteSize = 0;
		do 
		{
			sendByteSize += 
				send(m_socket,
				httpHeader.c_str() + sendByteSize,
				httpHeader.size() - sendByteSize, 0
				);

		} while (sendByteSize != httpHeader.size());
		//...생략...
		recv(m_socket, nullptr, 0, 0);
		return Option<int>();
	}
	int ReadFile(byte * buffer, int length)
	{
		//...생략...
		return 0;
	}
public:
	HTTPFileRequest(const string & host)
	{
		m_host = host;
		m_socket = socket(PF_INET, SOCK_STREAM, 0);
	}
private:
	string m_host;
	SOCKET m_socket;
};

이렇게 HTTPFileRequest외에도 FTPFileRequest, SmbFileRequest같은 프로토콜마다 그에 맞는 자식 클래스를 만들 수 있고, 자신이 직접 만든 프로토콜을 구현하는 것도 가능하다.

그럼 이렇게 하면 어떤 장점이 생길까? 바로 프로그램의 로직이 단순해지고, 또 확장이 쉬워진다는 점이다.HTTPFileRequest같이 FTPFileRequest, SmbFileRequest에 더해서, 직접 만든 프로토콜인 Simple프로토콜로 파일을 받는 SimpleFileRequest를 구현했다 치자,

int main()
{
	std::string server;
	std::string fileName;
	std::string protocol;
	std::cin >> protocol >> server >> fileName;
	NetRequest * request = nullptr;

	if (protocol == "HTTP")
		request = new HTTPFileRequest(server);
	else if (protocol == "FTP")
		request = new FTPFileRequest(server);
	else if (protocol == "SMB")
		request = new SMBFileRequest(server);
	else if (protocol == "SIMPLE")
		request = new SimpleFileRequest(server);

	if (request == nullptr)
		return 0;

	if (request->DownloadFile(fileName))
	{
		//성공
	}
	else
	{
		//실패
	}
	delete request;
	return 0;
}

각 실제 프로토콜로 파일을 받는 역할을 할 때는 역할에 맞는 가상함수들을 구현하므로 전체 로직이 매우 단순해진다.인터페이스는 어떨 때 장점을 가질까? 인터페이스가 가지는 특징은 추상클래스 또한 가질 수 있는데, 위의 예를 좀 변형해보자

class IFileWrite
{
public:
	virtual bool Begin() = 0;
	virtual void Write(byte * byte, int count) = 0;
	virtual void EndWrite() = 0;
};

C++에서는 인터페이스라는 명시적인 키워드는 없지만 위에서 말했듯이 public으로 지정된 순수 가상 함수만 있으면 그게 인터페이스다. 더하여 윈도우 개발쪽에서는 인터페이스에 Interface의 I를 붙이는 게 일반적이다.기존 NetRequest는 파일 데이터를 리스트에 받아서 반환했다면, 이번에는 이 인터페이스에 맞게 데이터를 저장하게 변형해보자

bool DownloadFile(const string & fileName, IFileWrite & writer)
{
		
	//1단계, 서버에 연결한다. 실패하면 끝
	if (!Connect())
		return false;
	//2단계, 파일을 요청한다.
	Option<int> res = RequestFile(fileName);
	if (!res.ok())
		return false;

	//파일을 읽는다.

	int fileSize = *res;
	int totalReadSize = 0;
	byte buffer[4096] = { 0 };

	if (!writer.BeginWrite(fileSize))
		return false;
	do
	{
		int readSize = ReadFile(buffer, 4096);
		writer.Write(buffer, readSize);
		totalReadSize += readSize;
	} while (totalReadSize != fileSize);

	writer.EndWrite();
	return true;
}

이렇게 반환형도 바뀌었지만, HttpFileRequest나, FTPFileRequest등등의 자식 클래스가 안 바뀐다는 점을 한 번 생각하면 좋다.이렇게 IFileWriter에 맞게 만들었다면, 다음에는 IFileWriter를 구현하는 클래스를 만들어 보자.

class MemoryWrite : public IFileWrite
{
public:
	MemoryWrite()
	{
		m_map = nullptr;
		m_size = 0;
		m_offset = 0;
	}
	~MemoryWrite()
	{
		if (m_map != nullptr)
		{
			delete[] m_map;
		}
	}
	bool BeginWrite(int fileSize) override
	{
		if (m_map != nullptr && m_size < fileSize)
		{
			delete[] m_map;
		}
		m_size = fileSize;
		m_offset = 0;
		m_map = new byte[fileSize];
	}
	void Write(byte * buffer, int count) override
	{
		byte * cur = m_map + m_offset;
		for (int i = 0; i < count; i++, cur++)
			*cur = buffer[i];
		m_offset += count;
	}
	void EndWrite() override
	{

	}
public:
	const byte * const GetByte() const
	{
		return m_map;
	}
	int GetSize() const
	{
		return m_size;
	}
private:
	byte * m_map;
	int m_size;
	int m_offset;
};

이렇게 메모리에 파일 데이터를 저장하는 클래스도 있을 수 있고

class HTTPPostWrite : public IFileWrite
{
public:
	HTTPPostWrite(const string& host, const string& path)
	{
		m_host = host;
		m_path = path;
	}
	bool BeginWrite(int fileSize) override
	{
		//...생략...
	}
	void Write(byte * buffer, int count) override
	{
		//...생략...
	}
	void EndWrite() override
	{
		//...생략...
	}
private:
	string m_host;
	string m_path;
	SOCKET m_socket;
};

이렇게 데이터를 받으면 아예 다른 사이트에 Post로 업로드하는 클래스도 있을 수 있다.두 클래스는 행동이 전혀 다르지만 IFileWrite라는 인터페이스에 맞게 행동하기 때문에 NetRequest는 IFileWrite가 구현된 클래스의 객체가 실제로 무슨 행동을 하는 지 신경 쓰지 않고, 자신이 할 일을 한다.

int main(int argc, char* args[])
{
	std::string server;
	std::string fileName;
	std::string protocol;
	std::cin >> protocol >> server >> fileName;
	
	HTTPPostWrite postWrite("damedame", "/uploads");
	MemoryWrite memoryWrite;
	string method = args[1];
	IFileWrite * writer = nullptr;
	if (method == "http")
		writer = &postWrite;
	else if (method == "memory")
		writer = &memoryWrite;
	if (writer == nullptr)
		return 0;

	NetRequest * request = nullptr;
	if (protocol == "HTTP")
		request = new HTTPFileRequest(server);
	else if (protocol == "FTP")
		request = new FTPFileRequest(server);
	else if (protocol == "SMB")
		request = new SMBFileRequest(server);
	else if (protocol == "SIMPLE")
		request = new SimpleFileRequest(server);

	if (request == nullptr)
		return 0;
	request->DownloadFile(fileName, (IFileWrite&)writer);
	delete request;
	return 0;
}

이런식으로 객체지향 프로그래밍에서 인터페이스와 추상 클래스는 프로그램의 로직을 나누고 통일시키는 역할을 하는데 아주 중요한 역할을 한다.