오늘의 학습 내용
에이타니 + 코드카타 + 학습
에이타니: 객체지향 프로그래밍(OOP)의 4대 특성과 메모리·설계적 관점의 이해
객체지향 프로그래밍(Object-Oriented Programming)은 데이터와 그 데이터를 처리하는 논리를 하나의 단위인 객체로 묶어 프로그램을 구성하는 패러다임이다. 복습한 4대 특성을 정리한다.
캡슐화(Encapsulation): 데이터 보호와 은닉화
기본 개념
- 변수와 함수를 하나의 클래스 내부에 묶고, 외부에 공개할 필요가 없는 내부 구현 세부 사항을 감추는 기술이다.
- public, protected, private 등 접근 제어자를 활용해 객체의 외벽을 세운다.
추가로 알아야 할 핵심 포인트: 객체의 무결성(Integrity) 확보
- 캡슐화의 핵심 목적은 외부에서 객체 내부의 멤버 변수를 임의로 수정하여 객체의 상태가 파괴되는 것을 막는 것이다.
- 예를 들어, 게임 캐릭터의 체력(HP) 변수가 public으로 열려 있다면 외부의 엉뚱한 로직이 HP = -9999;와 같은 비정상적인 값을 주입할 수 있다. 변수를 private으로 숨기고 SetHP(int newHP) 같은 Setter 함수를 통해서만 수정하도록 제한하면, 함수 내부에 데이터 검증 로직(예: 0 미만이 되면 0으로 보정)을 강제할 수 있어 객체의 무결성이 안전하게 유지된다.
상속(Inheritance): 코드 재사용과 계층 구조 설계
기본 개념
- 이미 정의된 부모 클래스(Base/Super Class)의 속성과 기능을 그대로 물려받아 새로운 자식 클래스(Derived/Sub Class)를 확장하는 기술이다.
추가로 알아야 할 핵심 포인트: 접근 제어와 메모리 배치
- 복습 요점대로 부모의 private 멤버는 자식 클래스 내부에서도 직접 접근할 수 없다. 자식에게는 접근을 허용하면서 외부의 접근만 차단하고 싶다면 반드시 protected 제어자를 사용해야 한다.
- 메모리 관점의 상속: 자식 클래스의 인스턴스를 생성하면, 메모리(힙 또는 스택) 공간에는 부모 클래스가 가진 멤버 변수들이 먼저 배치되고, 그 뒤를 이어 자식 클래스가 독자적으로 정의한 멤버 변수들이 연속적으로 배치된다. 즉, 자식 객체는 내부적으로 부모 객체를 통째로 품고 있는 물리 구조를 가진다.
다형성(Polymorphism): 동일한 인터페이스, 다양한 동작
기본 개념
- 같은 이름의 함수나 연산자가 호출되는 객체의 실제 타입(Context)에 따라 다르게 작동하는 특성이다. 대표적으로 함수 오버로딩(Overloading)과 오버라이딩(Overriding)이 있다.
추가로 알아야 할 핵심 포인트: 가상 함수 테이블(V-Table)의 원리
- C++에서 부모 포인터로 자식 객체를 가리킬 때, 자식의 재정의된 함수가 올바르게 호출되도록 하려면 부모 함수에 virtual 키워드를 붙여 가상 함수(Virtual Function)로 선언해야 한다.
- 컴파일러는 virtual 함수가 포함된 클래스를 만나면, 해당 클래스만을 위한 가상 함수 테이블(Virtual Method Table, V-Table)을 메모리에 할당한다. 이 테이블에는 실제 실행되어야 할 함수의 메모리 주소(포인터)들이 배열 형태로 저장된다.
- 런타임에 프로그램은 객체 내부에 숨겨진 가상 함수 테이블 포인터(vptr)를 역참조하여 동적 바인딩(Dynamic Binding)을 수행한다. 이 메커니즘 덕분에 서버 가동 중 유저의 직업(전사, 마법사 등)이 무엇이든 상관없이 동일한 Attack() 패킷 함수 하나로 각기 다른 공격 연산을 수행할 수 있다.
추상화(Abstraction): 복잡성 제어와 핵심 인터페이스 추출
기본 개념
- 시스템의 복잡한 물리적 세부 구현을 숨기고, 사용자가 알아야 하는 본질적인 기능적 핵심(Interface)만을 개념화하여 모델링하는 과정이다.
추가로 알아야 할 핵심 포인트: 순수 가상 클래스(Interface)
- C++에서는 함수의 구현부 없이 선언만 존재하는 순수 가상 함수(Pure Virtual Function, virtual void Move() = 0;)를 하나 이상 포함한 클래스를 추상 클래스(Abstract Class)라고 부른다.
- 추상 클래스는 본체가 불완전하므로 단독으로 인스턴스(객체)를 생성할 수 없으며, 오직 자식 클래스들이 구현해야 할 표준 설계도 역할만 담당한다. 복잡한 시스템 내부 코드를 알 필요 없이, 추상화된 상위 레이어의 인터페이스 규격만 맞춰주면 되므로 대규모 프로젝트의 협업과 모듈화에 필수적이다.
헷갈리는 핵심 개념 바로잡기
캡슐화와 추상화의 차이
- 잘못된 생각: 두 개념 모두 내부의 무언가를 숨기는 것이므로 완전히 같은 뜻이다.
- 올바른 개념: 목적성과 방향성이 다르다. 캡슐화는 데이터를 보호하고 감싸기 위해 '보안 및 은닉(Hiding)'에 초점을 맞춘 성벽 같은 개념이다. 반면 추상화는 복잡한 전체 시스템 중에서 개발자에게 당장 필요한 '핵심 개념만 추출(Extracting)'해내는 설계적 개념이다.
직렬화(Serialization)의 오해
- 잘못된 생각: 직렬화는 객체를 다루는 기술이므로 객체지향 프로그래밍의 핵심 4대 특성 중 하나이다.
- 올바른 개념: 퀴즈 정리대로 직렬화는 OOP의 패러다임적 특성이 아니라, 메모리에 흩어져 있는 객체 데이터를 파일로 저장하거나 네트워크(서버-클라이언트) 환경으로 전송하기 위해 직렬 형태의 바이트 스트림(Byte Stream)으로 다듬는 '기술적 프로세스'일 뿐이다.
코드카타: 프로그래머스 - 달리기 경주
이번 코드 카타 문제는 해설진이 추월한 선수의 이름을 부를 때마다 실시간으로 등수를 스왑(Swap)하여 최종 순위를 도출하는 양방향 매핑 및 시뮬레이션 유형의 문제이다.
작성한 코드는 std::unordered_map과 원래의 players 벡터를 연동하여, "이름을 알면 등수를 찾고(O(1)), 등수를 알면 이름을 찾는(O(1))" 구조를 구현했다.
알고리즘 분석 및 접근 방식
#include <string>
#include <vector>
#include <unordered_map>
using namespace std;
vector<string> solution(vector<string> players, vector<string> callings) {
vector<string> answer;
unordered_map<string, int> rank;
for(int i = 0; i < players.size(); ++i)
{
rank[players[i]] = i;
}
for(int i = 0; i < callings.size(); ++i)
{
int current_index = rank[callings[i]];
string current_player = players[current_index];
int prev_index = current_index - 1;
string prev_player = players[prev_index];
rank[current_player] = prev_index;
rank[prev_player] = current_index;
swap(players[current_index], players[prev_index]);
}
return players;
}
핵심 로직 작동 원리
코드 내부에서 일어나는 데이터 동기화 과정은 크게 두 단계의 교환(Swap)으로 쪼개어 이해할 수 있다. 배열과 해시 맵이 동시에 일목요연하게 움직여야 데이터 무결성이 깨지지 않는다.
데이터 스왑 파이프라인 (2단 동기화)
- 해시 맵(rank) 갱신: 추월한 선수(current_player)의 등수를 하나 낮추고(prev_index), 추월당한 앞 선수(prev_player)의 등수를 하나 올린다(current_index).
- 실제 배열(players) 갱신: std::swap을 사용해 배열 내부의 두 물리적 위치를 맞바꾼다. 최종 반환값은 순서대로 정렬된 players 배열 그 자체이므로, 배열의 최신화가 실시간으로 보장되어야 한다.
코드 개선 및 최적화 전략
#include <string>
#include <vector>
#include <unordered_map>
#include <algorithm>
using namespace std;
vector<string> solution(vector<string> players, vector<string> callings) {
// 1. 해시 맵 버퍼 미리 확보 (Rehash 방지)
unordered_map<string, int> rank;
rank.reserve(players.size());
for (int i = 0; i < players.size(); ++i) {
rank[players[i]] = i;
}
// 2. 실시간 추월 시뮬레이션 (O(M))
for (const string& calling_name : callings) {
// 추월한 선수의 정보
int current_idx = rank[calling_name];
// 추월당할 앞 선수의 정보
int prev_idx = current_idx - 1;
string prev_name = players[prev_idx];
// 데이터 정합성을 위한 해시 맵 등수 스왑 (O(1))
rank[calling_name] = prev_idx;
rank[prev_name] = current_idx;
// 실제 결과를 반환할 벡터의 물리 위치 스왑 (O(1))
swap(players[current_idx], players[prev_idx]);
}
return players;
}
개선 포인트
- std::unordered_map::reserve 호출
- 해시 맵 역시 내부 버퍼 크기를 초과하여 데이터가 삽입되면 재할당(Rehash) 오버헤드가 발생한다. players.size()만큼 공간을 미리 예약(rank.reserve(players.size());)해 두면 맵 빌드 속도를 조금 더 끌어올릴 수 있다.
- 반복문 내 불필요한 대입 및 가독성 정돈
- 범위 기반 for 문(const string& name : callings)을 활용하면 인덱스 변수 i를 다루는 수고를 덜고 코드가 한결 세련되지며, current_player 변수 등 중복된 참조 구조를 정리할 수 있다.
공부를 마치며 느낀 점
다형성을 뒷받침하는 가상 함수 테이블 메커니즘을 이해하고 나니, 무분별한 가상 함수 남발이 메모리 포인터 역참조 오버헤드를 유발할 수 있다는 시야를 확보할 수 있었다. 앞으로 컴포넌트를 설계하거나 클래스 계층 구조를 짤 때, 이 4가지 대원칙을 기준 삼아 단단하고 유지보수하기 좋은 코드를 작성해야겠다.
이번 문제는 "실시간으로 순위나 인덱스가 뒤바뀌는 대상을 추적할 때, 배열과 해시 맵을 연동하여 양방향 링크 구조를 만드는 테크닉"의 정석을 보여주는 예제였고, 자료구조의 장단점을 적재적소에 결합하는 감각을 고스르게 복습할 수 있었던 유익한 세션이었다.