2024. 4. 17. 00:13 ㆍ개발 이야기
학생이었을 때, 추상화에 대해서 공부를 하다가 이해가 잘되지 않았던 부분들이 있었습니다. 추상화는 도대체 무엇이길래, 왜 해야하는걸까? 라는 생각을 했었습니다. 물론 이론 공부를 할때는 "그런가보다~", "이렇게 하는 것인가 보다"라는 생각으로 공부했지만 무엇인가 강렬하게 와닿지 않았던 기억이 납니다. 사실 그렇기 때문에 주니어 시절에는 추상화를 잘 생각하지 않았던 것 같습니다.
부끄럽지만, 꽤나 오랜시간이 지난 지금에서야, 조금이나마 이해하고 자연스럽게 개발하는데 사용하게 되는 것 같습니다.
추상화, 도대체 무엇이고, 왜 해야하는걸까요?
인터넷을 검색해보면 사전적으로는, "사물이나 표상을 어떤 성질, 공통성, 본질에 착안하여 그것을 추출하여 파악하는 것" 라는 의미를 나타냅니다. 조금 더 개발자스럽게 표현하면 복잡성을 관리하는 기술적 방법이며, 프로그래밍에서 추상화는 개발자가 복잡한 코드를 신경 쓰지 않고도 특정 기능을 사용할 수 있게 해줍니다.
학생일때는 이런 의미가 잘 이해가 되지 않았습니다만, 지금 생각해보면 둘다 정확하게 맞는 말 같습니다.
우선, 우리는 어떤 기능을 하는 프로그램을 만들 때,
- 머릿속으로 크게 어떤 기능을 하는 프로그램을 만들지 생각합니다.
- 대략적으로 어떤 단계를 거쳐서 동작하게 되는지 생각합니다.
- 대략적으로 어떤 함수들로 이루어질지 생각합니다.
- 이 함수들의 Input과 Output을 생각합니다.
그리고나서, 더 세부적인 설계와 개발이 진행된다고 생각합니다. 물론 각자만의 방식이 있을 수도 있을 것 같습니다.
예를들어, 1분 동안 서버의 리소스 사용량을 수집하고 1분마다 어디엔가 수집한 데이터를 저장하는 프로그램을 만든다고 가정하겠습니다.
1. 머릿속으로 크게 어떤 기능을 하는 프로그램을 만들지 생각합니다.
- 1분 동안 서버의 리소스 사용량 수집
- 1분마다 수집한 데이터 저장
2. 대략적으로 어떤 단계를 거쳐서 동작하게 되는지 생각합니다.
- CPU 사용량을 15초마다 수집
- Memory 사용량을 15초마다 수집
- 1분 동안 수집된 서버의 CPU, Memory 사용량의 Average를 계산
- 데이터를 저장할 저장소의 커넥션 생성
- 저장 동작 실행
- 1분간 수집된 데이터 초기화 후 다시 데이터 수집
3. 대략적으로 어떤 함수들로 이루어질지 생각합니다(실제 추상화 과정)
- 리소스 사용량을 수집하는 함수
- collectResource() -> CPU, Memory 리소스를 수집할때 공통적으로 실행될만한 함수명을 지었습니다.
- 수집된 리소스 사용량을 Aggregation 해주는 함수
- addResource() -> 15초마다 수집되는 데이터를 버퍼에 저장하여 Average를 계산하는 함수입니다.
- 1분간 수집된 데이터를 초기화하는 함수
- flush() -> 데이터를 저장하고 있는 버퍼를 비우고 저장소에 저장하게되는 함수입니다.
- 데이터 저장소 관리 함수(연결, 저장 등)
- connect() -> 데이터 저장소를 어떤 것을 사용할지 모르지만, 연결을 담당하는 함수입니다.
- save() -> 실제 데이터를 저장하는 함수입니다.
- close() -> 안전하게 저장소와의 연결을 종료하는 함수입니다.
우리는 3번 과정을 통해 대략적으로 어떤 함수들로 우리의 프로그램이 동작하는지 이해할 수 있습니다.
4. 이 함수들의 Input과 Output을 생각합니다.
앞서 생각한 함수들의 Input과 Output을 생각하여 대략적인 인터페이스(interface)를 구성해봅니다.
Collector 인터페이스
public interface Collector {
String getName();
float collectResource();
}
ResourceManager 인터페이스
public interface ResourceManager {
void addResource(float resource);
void flush();
}
Storage 인터페이스
public interface Storage {
Object connect();
void save(String meta, float resourceValue);
void close();
}
이렇게 우리가 개발해야하는 15초마다 리소스를 수집하여 1분마다 Average를 저장소에 저장하는 프로그램의 추상화를 해봤습니다. 자 그러면 이 인터페이스를 이용해서 유사 프로그램을 간단하게 만들어보겠습니다.
CPUCollector 구현
public class CPUCollector implements Collector {
@Override
public String getName() {
return "cpu";
}
@Override
public float collectResource() {
// return 실제 CPU 사용량을 반환하는 코드;
return 0;
}
}
MemoryCollector 구현
public class MemoryCollector implements Collector{
@Override
public String getName() {
return "memory";
}
@Override
public float collectResource() {
// 실제 메모리 사용량을 반환하는 코드;
return 0;
}
}
PrintStorage - 실제 저장하지 않고, 데이터를 STDout Print하는 구현체
public class PrintStorage implements Storage{
@Override
public Object connect() {
return new Object();
}
@Override
public void save(String meta, float resourceValue) {
// 데이터 저장 로직. 여기서는 콘솔에 출력하여 저장 행위를 시뮬레이션합니다.
System.out.println("Saving " + meta + " with value " + resourceValue);
}
@Override
public void close() {
System.out.println("Storage connection closed.");
}
}
SimpleResourceManager - 수집한 리소스를 Buffer에서 Aggregation하고, 저장소에 주기적으로 저장하는 구현체
Collector 인터페이스 구현체 List를 통해 데이터를 수집하여 Buffer에 저장하고, flush() 메서드를 통해 flush될때 저장소에 저장하도록 구현
public class SimpleResourceManager implements ResourceManager {
private HashMap<String, List<Float>> resourceMap = new HashMap<>();
private List<Collector> collectors = new ArrayList<>();
private Storage storage;
public SimpleResourceManager() {
collectors.add(new CPUCollector());
collectors.add(new MemoryCollector());
storage = new PrintStorage();
}
@Override
public void addResource() {
for (Collector collector : collectors) {
if (collector.collectResource() > 0) {
List<Float> resources = resourceMap.get(collector.getName());
if (resources == null) {
resources = new ArrayList<>();
resourceMap.put(collector.getName(), resources);
}
resources.add(collector.collectResource());
}
}
}
@Override
public void flush() {
for (Collector collector : collectors) {
List<Float> resources = resourceMap.get(collector.getName());
if (resources != null) {
float sum = 0;
for (Float resource : resources) {
sum += resource;
}
storage.save(collector.getName(), sum / resources.size());
}
}
resourceMap = new HashMap<>(); // 초기화
}
}
이렇게 간단한 구현체를 작성해봤습니다. 하지만, 추상화를 통해 어떤 이점을 얻었는지 명확하게 이해가 되지 않을 수 있습니다.
우리는 SimpleResourceManager의 addResource() 메서드를 통해 CPU, Memory 리소스를 수집하고 Buffer에 저장하고 있습니다. 만약 Disk 사용량도 수집해야한다고 한다면 어떻게 하면 될까요?
public SimpleResourceManager() {
collectors.add(new CPUCollector());
collectors.add(new MemoryCollector());
collectors.add(new DiskCollector());
storage = new PrintStorage();
}
DiskCollector를 구현하여 collectors라는 Collector 구현체 리스트에 추가해주기만 하면 기존의 로직에 변경없이 Disk 리소스를 수집하고 1분 간 Average를 저장소에 저장할 수 있게됩니다. 이 과정을 얻게된 몇 가지 이점을 나열해보겠습니다.
- 복잡성 감소
=> 추상화를 통해 관리하지 않았다면 우리는 CPU, Memory, Disk 수집을 위해 수많은 if 문을 이용해야했을 수도 있습니다. - 재사용성 향상
=> 기존에 구현해둔 addResource 메서드를 변경 없이 사용할 수 있습니다. 만약 CPU, Memory를 수집하지 않더라도 Disk 리소스를 수집하는 프로그램으로 손쉽게 변경할 수 있습니다. - 유지보수성
=> 추상화된 부분만을 수정하여 프로그램에 다른 부분에 영향을 최소화 할 수 있습니다. - 확장성
=> Disk 사용량을 추가한 것과 같이 새로운 기능을 추가하거나, 기존 기능을 변경하기 쉽습니다.
더해서, 만약 Storage 인터페이스를 PostgreSQL에 저장하도록 하는 구현체를 구현하여 아래와 같이 변경한다면 우리는 SimpleResourceManager의 최소한의 코드 변경을 통해 PostgreSQL에 수집한 데이터를 저장할 수 있습니다.
public SimpleResourceManager() {
collectors.add(new CPUCollector());
collectors.add(new MemoryCollector());
collectors.add(new DiskCollector());
storage = new PostgresStorage();
}
첨언하면, Database와 관련된 추상화 프로젝트 중 가장 유명한 것은 JDBC(Java Database Connectivity)입니다. 실제 프로젝트는 Database 사용에 필요한 메서드들을 추상화 해두었고, 이를 각 DB 진영(PostgreSQL, MySQL 등)에서 구현한 것이 JDBC입니다.
일련의 과정을 통해 우리가 프로그램을 개발하면서 추상화를 하는 과정과 추상화를 통에 얻게되는 이점들을 몸소 익혀보는 시간을 가져봤습니다. 사실 이런 과정들은 실제로 개발을 자주 해보지 않는다면 자연스럽게 몸에 익히기는 어려운 부분이 있습니다.
하지만 그래도 본 포스팅을 통해 제가 공유하고자하는 추상화 과정과 이점이 잘 전달되었기를 바래봅니다.
'개발 이야기' 카테고리의 다른 글
Prompt 엔지니어링: AI와 소통하는 기술 (2) | 2024.11.21 |
---|---|
TDD(Test-Driven Development) 테스트 주도 개발 (2) | 2024.11.13 |
Understanding Lock-Free Queues with Code Examples (6) | 2024.11.04 |
LLMs and RAG: Collaborating for Better Knowledge (1) | 2024.11.02 |
LLM과 RAG: 더 나은 지식을 위한 협업 (2) | 2024.11.02 |