1. 소개
너 컴퓨터나 스마트폰 매일 쓰지? 별 생각 없이. 브라우저 열고, 사이트 들어가고, 사진 찍고. CPU가 어떻게 생겼는지, 램이 어떻게 동작하는지, 칩에 어떤 신호가 흐르는지 알 필요 없잖아. 이게 바로 사용자 레벨 — 편하고 직관적이고, 쓸데없는 건 다 가려주는 거지.
근데 뭔가 잘못됐다고 해보자. 예를 들어, 앱이 실행이 안 돼. 다시 설치하려면 최소한 어디서 다운받고 어떻게 설치하는지 정도는 알아야 해. 이건 또 다른 추상화 레벨 — 시스템, 기술적인 레벨이지. 만약 하드웨어 문제라면? 저장장치나 메인보드가 고장났다면? 그땐 기기의 물리적 구조까지 알아야 할 수도 있어.
추상화 레벨은 여러 개가 있을 수 있고, 각 레벨은 복잡함을 숨기고 네가 진짜 필요한 것만 보여줘.
프로그래밍도 똑같아. 택시를 탄다고 상상해봐. "프로그래머 거리 42번지로 가주세요"라고 말하지. 운전사가 어떤 경로로 가는지, 기어를 어떻게 바꾸는지, 연료가 뭔지 신경 안 써. 넌 그냥 목적지에 도착하면 돼. 나머지는 다 숨겨져 있지. 이게 바로 추상화의 정수야: 네가 시스템과 상호작용할 때, 내부 구현은 신경 안 쓰고, 명확한 인터페이스만 쓰는 거지.
또 다른 예시 — 네 핸드폰 카메라. 아이콘 누르고, 사진 찍으면 갤러리에 저장돼. 빛이 렌즈를 어떻게 통과하는지, 센서랑 칩이 어떻게 동작하는지, 데이터가 메모리에 어떻게 저장되는지 몰라도 돼. 이 모든 게 편한 인터페이스 아래에 숨어있지. 이게 바로 추상화 — 내부 구조 몰라도 강력한 도구를 쓸 수 있게 해주는 거야.
프로그래밍 세계, 특히 C#이랑 .NET에선 추상화가 단순한 편의가 아니라 진짜 필수야. 이게 없으면 크고 이해하기 쉬우며 유지보수 가능한 프로젝트를 만들기 힘들어. 추상화 덕분에 개발자들이 구현 세부사항에 빠지지 않고 같은 언어로 소통할 수 있지.
2. 프로그래밍에서 추상화가 왜 필요할까?
"그래, 멋져 보이긴 하는데, 나 같은 미래의 코드 고수한테 이게 왜 필요해?" 좋은 질문이야! 추상화는 그냥 멋져 보이려고 쓰는 게 아니라, 아주 실용적인 장점들이 있어:
- 복잡한 시스템 단순화: 우리 뇌는 거대한 프로그램의 모든 디테일을 한 번에 다 기억 못 해. 추상화는 복잡한 문제를 더 작고 관리하기 쉬운 부분으로 쪼갤 수 있게 해줘. 각 부분은 내부 복잡함을 숨기고, 진짜 중요한 것만 보여주지. 마치 레고 블록처럼: 각 블록은 단순하지만, 그걸로 뭐든 만들 수 있어. 각 블록이 어떻게 만들어졌는지는 신경 안 써도 되지.
- 코드 가독성 및 유지보수성 향상: 추상화 원칙으로 짠 코드는 훨씬 읽기 쉽고 이해하기 쉬워. device.TurnOn() 같은 메서드 호출을 보면, 그게 뭘 하는지 바로 알 수 있지. 전구나 선풍기에서 실제로 어떻게 동작하는지 수백 줄 코드를 볼 필요 없어. 이 덕분에 버그 수정이나 새로운 기능 추가도 쉬워져.
- 모듈 간 결합도 감소: 네 코드가 손전등을 켜는 데 특정 모델에만 맞는 저수준 연산을 직접 쓴다고 해봐. 모델을 바꾸면? 코드를 다 뜯어고쳐야 해! 추상화 덕분에 "아무 손전등"이든 공통 인터페이스로 다룰 수 있어. 내부를 바꿔도, 켜는 코드는 바뀐 걸 눈치도 못 채지. 왜냐면 추상적 표현으로만 작업하니까.
- 유연성과 무통증 변경 가능성: 추상화 덕분에 어떤 컴포넌트의 내부 구현을 바꿔도, 그걸 쓰는 나머지 프로그램은 영향 안 받아. 대형 프로젝트에서 여러 개발팀이 동시에 각자 파트를 작업할 때 진짜 소중하지.
- 책임 분리: 프로그램의 각 요소(클래스, 메서드)는 자기 역할이 명확해. LightBulb 클래스는 자기 기능만, SmartHomeManager 클래스는 디바이스 관리만 담당하고, 세부사항은 몰라도 돼.
추상화는 프로그램에 "첨가"하는 재료가 아니야. 사고방식이야. 코드를 설계할 때 공통점을 보고, 차이점은 숨기는 능력이지.
3. C# (그리고 OOP 전반)에서 추상화가 어떻게 나타날까?
놀랄 수도 있는데, 우리 이미 추상화를 엄청 쓰고 있었어! 이름만 안 붙였을 뿐이지. C# 프로그램에서 여러 레벨로 나타나:
클래스와 객체
클래스라는 개념 자체가 이미 추상화야. LightBulb 클래스는 "전구"라는 아이디어를 추상화한 거지. 켜고, 끄고, 밝기 조절할 수 있어. LightBulb myLamp = new LightBulb(); 이렇게 객체를 만들면, 우리는 전자나 원자 같은 실제 물질이 아니라 추상화된 전구랑 일하는 거야.
예시: LightBulb 클래스에 TurnOn() 메서드가 있어. myLamp.TurnOn()을 호출하면 전구가 켜져. 근데 네가 직접 전기를 제어하거나, 미세한 셔터를 열거나, 필라멘트에서 핵융합을 일으키는(장난이야!) 코드를 쓰는 건 아니지. 이런 세부사항은 TurnOn() 메서드 내부에 다 숨겨져 있어.
접근 제한자: private 필드나 메서드를 쓰는 건 바로 캡슐화의 예시고, 이게 추상화를 이루는 중요한 방법 중 하나야. 어떤 데이터나 동작을 외부에서 못 보게 막아서, 클래스 사용자에게 내부 복잡함을 숨기는 거지. 예를 들어, 뱅킹 앱에서 _updateBalance() (언더바로 내부용임을 표시) 메서드는 복잡한 잔액 갱신 로직을 처리하지만, 외부에선 Deposit()이나 Withdraw()만 쓸 수 있어. 이게 바로 추상화야.
메서드와 함수
메서드 호출 하나하나가 추상화의 사용이야. 네가 메서드한테 어떤 일을 맡기면, 그걸 어떻게 하는지는 신경 안 써도 돼.
예를 들어, 우리 모두 아는 Console.WriteLine("안녕, 세상!"); 있지? 그냥 호출하면 텍스트가 화면에 뜨지. 구현 세부사항 — OS가 메모리 버퍼를 어떻게 할당하는지, 폰트가 픽셀로 어떻게 바뀌는지, 그래픽카드가 어떻게 출력하는지 — 이런 건 몰라도 돼.
이런 거까지 다 생각하면, 짧은 프로그램 하나 짜는 데도 몇 시간 걸릴걸.
Console.WriteLine — 이게 진짜 강력한 추상화야. 엄청난 작업량을 다 숨겨주지.
상속과 다형성
여기서 추상화가 진짜 빛을 발해! 우리가 Animal이라는 기본 클래스를 만들고, Dog랑 Cat을 상속받으면, "소리를 내는 동물"이라는 공통 개념을 추상화하는 거야.
예를 들어, Animal myPet = new Dog(); 그리고 myPet.MakeSound(); 이렇게 쓰면, Animal이라는 추상화랑 일하는 거지. 실제로 myPet이 Dog라는 건 신경 안 써. 다형성 덕분에 MakeSound() 호출이 각 타입마다 다르게 동작해(개는 짖고, 고양이는 야옹). "무엇을 할지"만 프로그래밍하고, "어떻게 할지"는 각 클래스에 맡기는 거지. 이게 바로 추상화의 승리!
인터페이스 (지금은 언급만, 자세한 건 나중에)
아직 배우진 않았지만, 기억해둬: C#에서 인터페이스는 "무엇을, 어떻게는 모름"을 표현하는 가장 순수한 방법이야. 인터페이스는 메서드, 속성, 이벤트의 집합만 정의하고, 구현은 안 해. "이 인터페이스를 구현하는 애는 반드시 이거랑 저거 할 줄 알아야 해"라는 계약이지. 111강에서 자세히 다룰 거지만, C# 추상화의 끝판왕이야.
추상 클래스 (이것도 언급만, 자세한 건 다음 강의에서)
추상 클래스는 일반 클래스와 인터페이스의 중간쯤이야. 구현된 메서드도 있고, 추상 메서드도 있어(105강에서 잠깐 봤지), 이건 몸체가 없고 반드시 자식 클래스에서 구현해야 해. 추상 클래스는 공통 뼈대, 공통 기능을 만들고, "구멍"(추상 메서드)은 자식 클래스가 채우게 하는 거지. 다음 강의에서 아주 자세히 다룰 거야!
정리하자면: 추상화는 C#의 특정 문법 요소가 아니라, 메서드부터 복잡한 클래스 계층까지 코드의 모든 레벨에 스며든 강력한 개념이야.
4. 코드 예시: 스마트홈 관리 시스템
우리 앱으로 돌아가서, 추상화가 어떻게 더 유연하게 만들어주는지 보자. "스마트홈" 시스템을 만든다고 해보자. 처음엔 그냥 전구랑 선풍기만 있어:
public class LightBulb
{
public string Name;
public LightBulb(string name) => Name = name;
public void TurnOn() => Console.WriteLine($"{Name}: 불 켜짐");
public void ChangeBrightness(int level) => Console.WriteLine($"{Name}: 밝기 {level}%");
}
public class Fan
{
public string Name;
public Fan(string name) => Name = name;
public void TurnOn() => Console.WriteLine($"{Name}: 선풍기 켜짐");
public void AdjustSpeed(int speed) => Console.WriteLine($"{Name}: 속도 {speed}");
}
class Program
{
static void Main()
{
var lamp = new LightBulb("주방");
var fan = new Fan("침실");
lamp.TurnOn();
lamp.ChangeBrightness(75);
fan.TurnOn();
fan.AdjustSpeed(3);
}
}
이 코드는 각 디바이스를 따로 다룰 땐 잘 돌아가. 근데 진짜 스마트하게, 모든 디바이스를 한 번에 중앙에서 관리하고 싶으면? 예를 들어, 집에 오기 전에 모든 기기를 켜고 싶으면?
object[]에 디바이스를 넣으려고 하면 문제가 생겨: object 타입은 TurnOn() 메서드를 몰라. 호출하려면 타입을 일일이 검사하고 캐스팅해야 하는데, 이건 진짜 번거롭고 별로야:
// 추상화랑 다형성 없이:
foreach (object device in allDevices)
{
if (device is LightBulb bulb)
{
bulb.TurnOn();
}
else if (device is Fan fan)
{
fan.TurnOn();
}
// 새로운 디바이스마다 계속 추가해야 함... 끔찍!
}
여기서 상속과 다형성이 등장하지! 이 둘은 캡슐화와 함께 추상화를 실현하는 도구야. "스마트 디바이스"라는 공통 개념을 SmartDevice라는 기본 클래스로 추상화하고, 전구랑 선풍기를 상속받게 해보자.
class SmartDevice
{
public string Name;
public SmartDevice(string name) => Name = name;
public virtual void TurnOn() => Console.WriteLine($"{Name}: 기기 켜짐");
public virtual void TurnOff() => Console.WriteLine($"{Name}: 기기 꺼짐");
}
class LightBulb : SmartDevice
{
public LightBulb(string name) : base(name) { }
public override void TurnOn() => Console.WriteLine($"{Name}: 불 켜짐");
public override void TurnOff() => Console.WriteLine($"{Name}: 불 꺼짐");
public void ChangeBrightness(int x) => Console.WriteLine($"{Name}: 밝기 {x}%");
}
class Fan : SmartDevice
{
public Fan(string name) : base(name) { }
public override void TurnOn() => Console.WriteLine($"{Name}: 선풍기 켜짐");
public override void TurnOff() => Console.WriteLine($"{Name}: 선풍기 꺼짐");
public void AdjustSpeed(int s) => Console.WriteLine($"{Name}: 속도 {s}");
}
class Program
{
static void Main()
{
SmartDevice[] devices =
{
new LightBulb("주방"),
new Fan("침실"),
new SmartDevice("센서")
};
foreach (var d in devices) d.TurnOn();
foreach (var d in devices) d.TurnOff();
// 특화 메서드 호출 예시
foreach (var d in devices)
{
if (d is LightBulb b)
b.ChangeBrightness(50);
if (d is Fan f)
f.AdjustSpeed(2);
}
}
}
봐봐, 코드가 훨씬 깔끔하고 유연해졌지! 이제 smartHomeDevices에 SmartTV나 SmartThermostat처럼 SmartDevice를 상속받는 새 디바이스를 추가해도, foreach (SmartDevice device in smartHomeDevices) 루프는 그대로 돌아가. 이게 바로 추상화의 위력! 우리는 구체적인 디바이스 타입에서 벗어나, "켜고 끄는" 공통 능력에만 집중할 수 있어.
이 예시는 우리가 전에 배운 상속과 다형성이 추상화를 실현하는 도구라는 걸 잘 보여줘. SmartDevice라는 일반화된 표현을 만들어서, LightBulb, Fan 같은 다양한 구체적 디바이스를 똑같이 다룰 수 있게 됐지.
근데 한 가지: 지금 SmartDevice의 TurnOn()과 TurnOff()는 "공통 구현"만 있어. 모든 디바이스에 의미 있는 "공통 구현"이 없을 땐 어떡하지? 예를 들어, "일반 디바이스"(SmartDevice 자체)는 온도 센서라서 켜고 끄는 버튼이 없을 수도 있어. 또는 모든 자식 클래스가 반드시 이 메서드를 구현하게 강제하고 싶을 수도 있지. 이럴 때 추상 클래스와 추상 메서드가 등장해. 다음 강의에서 자세히 다룰 거야. 이건 추상화 원칙을 코드에 강제로 적용하는 더 강력한 방법이야. 어떤 메서드는 반드시 자식에서 구현하게 할 수 있거든.
이렇게 해서 OOP의 기본 원칙인 추상화에 대한 탐험은 끝! 다음 강의에선 C#이 우리에게 주는 특별한 도구 — 추상 클래스와 추상 메서드 — 를 어떻게 활용하는지 깊게 파볼 거야. 기대해, 더 재밌어질 거야!
GO TO FULL VERSION