클래스와 객체
20261007 클래스와 객체
클래스는(class)는 서로 관련된 데이터와 기능을 하나의 이름 아래로 묶는 틀임. 클래스를 중심으로 프로그램을 짜는 방식을 객체 지향 프로그래밍(Object-Oriented Programming, OOP) 라고 함. 4가지 기둥이 있음
| 기둥 | 뜻 |
|---|---|
| 캡슐화 | 관련된 데이터와 기능을 한 덩어리로 묶고, 밖에 보일 것만 골라 내놓음 |
| 상속 | 이미 만든 클래스의 내용을 물려받아 확장함 |
| 다형성 | 같은 이름의 기능이 대상에 따라 다르게 동작함 |
| 추상화 | 안쪽 사정을 감추고 쓰는 쪽에 필요한 것만 드러냄 |
데이터와 기능을 묶는다는 것
흩어진 데이터
게임 캐릭터 두 명을 변수로 관리해 봄
// class 없이 캐릭터의 데이터를 관리 하려고 할 때의 예시
string heroName = "용사";
int heroHealth = 100;
int heroAttack = 15;
string mageName = "마법사";
int mageHealth = 80;
int mageAttack = 25;
Console.WriteLine($"{heroName} - HP {heroHealth}, ATK {heroAttack}");
Console.WriteLine($"{mageName} - HP {mageHealth}, ATK {mageAttack}");
두 명뿐인데 변수가 여섯 개임. 캐릭터가 열 명이면 서른 개가 된다.
무엇보다 heroName과 heroHealth가 같은 캐릭터의 것이라는 사실이 이름의 앞 부분에만 적혀 있음.
배열로 묶어도 남는 문제
배열로 묶으면 개수 문제는 풀림. 그러나 각 배열의 같은 인덱스가 같은 캐릭터라는 약속은 여전히 사람 몫임. 한 배열에만 정렬을 걸면 짝이 통째로 어긋남.
// 배열로 묶었을 때 생기는 문제 예시
string[] names = { "용사", "마법사", "궁수" };
int[] healths = { 100, 80, 90 };
Array.Sort(names); // 이름만 정렬함
for (int i = 0; i < names.Length; i++)
{
Console.WriteLine($"{names[i]} - HP {healths[i]}");
}
클래스로 묶기
이름, 체력, 공격력을 하나로 묶어 캐릭터라는 새 타입을 만듦.
// 클래스로 묶은 예시
Character hero = new Character();
hero.Name = "용사";
hero.Health = 100;
hero.Attack = 15;
Character mage = new Character();
mage.Name = "마법사";
mage.Health = 80;
mage.Attack = 25;
Console.WriteLine($"{hero.Name} - HP {hero.Health}, ATK {hero.Attack}");
Console.WriteLine($"{mage.Name} - HP {mage.Health}, ATK {mage.Attack}");
class Character
{
public string Name;
public int Health;
public int Attack;
}
hero와 mage는 각각 이름, 체력, 공격력을 함께 들고 다니는 하나의 값임. 짝이 어긋날 일이 없음.
클래스 선언하기
class 키워드
가장 단순한 형태는 다음과 같음
class Character
{
}
class뒤에 클래스 이름을 씀- 이름은 PascalCase - 첫 글자를 대문자로, 단어마다 대문자로 시작함
- 중괄호 안에 클래스가 가질 것들을 나열함
클래스의 이름은 곧 새로운 타입 이름이 됨. int나 string을 쓰던 자리에 그대로 쓸 수 있음.
클래스 안에 넣는 것
클래스 안에 놓는 것들을 통틀어 맴버(member)라고 함.
| 멤버 | 하는 일 | 생긴 모습 |
|---|---|---|
| 필드 | 데이터를 담음 | 클래스 블록에 바로 놓인 변수 선언 |
| 메서드 | 기능을 담음 | 클래스 블록에 놓인 함수 |
class Character
{
public string Name; // 필드
public int Health; // 필드
public void ShowInfo() // 메서드
{
Console.WriteLine($"{Name} - HP {Health}");
}
}
데이터형 앞에 붙은 public은 클래스 밖에서도 쓸 수 있게 열어둔다는 표시임.
클래스 선언 위치
최상위문을 쓰는 파일에서는 클래스 선언이 최상위 실행문 뒤에 와야함.
class Character
{
public string Name;
}
Character c = new Character(); // 실행문이 클래스 선언 뒤에 있음
객체 만들기
new로 실체를 만듦
클래스는 설계도임. 설계도만으로는 아무것도 담을 수 없고, 그 설계도로 찍어 낸 실체가 있어야 값을 담을 수 있음. 그 실체를 객체(object) 또는 인스턴스(instance)라고 함.
객체는 new로 만듦.
객체마다 데이터가 따로임
같은 클래스로 만들어도 객체마다 자기 값을 가짐
flowchart TB
subgraph Design[Character 클래스]
D1[Name 자리]
D2[Health 자리]
end
subgraph Objs[만들어진 객체들]
O1[a — 용사 / 70]
O2[b — 마법사 / 80]
end
Design -->|new| O1
Design -->|new| O2
객체를 만들면 필드가 이미 자리를 잡고 있음
new가 끝난 시점에 필드는 이미 존재함. 값을 넣지 않았어도 타입마다 정해진 값이 들어가 있음.
객체를 만드는 짧은 표기
타입이 이미 왼쪽에 적혀 있으면 오른쪽의 클래스 이름을 생략할 수 있음.
using System;
Character a = new Character(); // 온전한 형태
Character b = new(); // 왼쪽 타입을 보고 채워 넣음
a.Name = "용사";
b.Name = "마법사";
Console.WriteLine($"{a.Name}, {b.Name}");
class Character
{
public string Name;
}
클래스 안의 기능
메서드가 자기 필드를 봄
클래스 안에 놓인 함수는 같은 객체의 필드를 이름만으로 쓸 수 있음.
using System;
Character hero = new Character();
hero.Name = "용사";
hero.Health = 100;
hero.ShowInfo();
class Character
{
public string Name;
public int Health;
public void ShowInfo()
{
Console.WriteLine($"{Name} - HP {Health}");
}
}
ShowInfo안의 Name은 매개변수도 지역 변수도 아님. 자기를 호출한 객체의 필드임.
호출하는 쪽에는 hero.ShowInfo()라고 적었으므로, 그 안의 Name은 hero의 Name임.
필드를 바꾸는 메서드
메서드는 필드를 읽기만 하는 것이 아니라 바꿀 수도 있음
using System;
Character hero = new Character();
hero.Name = "용사";
hero.Health = 100;
hero.TakeDamage(30);
hero.TakeDamage(50);
hero.TakeDamage(40);
class Character
{
public string Name;
public int Health;
public void TakeDamage(int damage)
{
Health = Health - damage;
if (Health < 0)
{
Health = 0;
}
Console.WriteLine($"{Name}이(가) {damage} 피해를 입음. 남은 HP {Health}");
}
}
체력이 음수로 내려가지 않게 막는 규칙이 캐릭터 안쪽에 들어있음. 쓰는쪽은 TakeDamage만 부르면 되고, 그 규칙을 매번 기억하지 않아도 됨.
값을 돌려주는 메서드
using System;
Rectangle r = new Rectangle();
r.Width = 8;
r.Height = 5;
Console.WriteLine($"넓이: {r.GetArea()}");
Console.WriteLine($"둘레: {r.GetPerimeter()}");
Console.WriteLine($"정사각형인가: {r.IsSquare()}");
class Rectangle
{
public int Width;
public int Height;
public int GetArea()
{
return Width * Height;
}
public int GetPerimeter()
{
return (Width + Height) * 2;
}
public bool IsSquare()
{
return Width == Height;
}
}
매개변수를 하나도 받지 않는데 계산이 됨. 필요한 값이 이미 객체 안에 있기 때문임. 메서드를 클래스 밖에 두면 같은 일을 하려고 값을 매번 넘겨야 함.
참조 의미론
변수가 담고 있는 것
int를 담은 변수는 값 자체를 담고 있음. 클래스 타입의 변수는 다름-객체가 놓인 자리를 가리키고 있음.
객체를 복사하려면
같은 내용의 다른 객체가 필요하면 new로 하나 더 만들어 값을 옮겨 담아야 함.
using System;
Character a = new Character();
a.Name = "용사";
a.Health = 100;
Character b = new Character(); // 새 객체
b.Name = a.Name;
b.Health = a.Health;
b.Name = "마법사";
Console.WriteLine($"a: {a.Name} / {a.Health}");
Console.WriteLine($"b: {b.Name} / {b.Health}");
class Character
{
public string Name;
public int Health;
}
함수에 넘길 때
객체를 함수에 넘기면 가리키는 자리가 복사됨. 함수 안에서 필드를 바꾸면 넘긴 쪽에도 보임.
using System;
Character hero = new Character();
hero.Name = "용사";
hero.Health = 100;
Heal(hero, 50);
Console.WriteLine($"{hero.Name}: {hero.Health}");
void Heal(Character target, int amount)
{
target.Health = target.Health + amount;
}
class Character
{
public string Name;
public int Health;
}
그러나 매개변수에 새 객체를 대입하면 넘긴 쪽에는 보이지 않음. 가리키는 자리를 갈아 끼운 것은 함수 안의 변수뿐이기 때문임.
using System;
Character hero = new Character();
hero.Name = "용사";
Replace(hero);
Console.WriteLine($"호출한 쪽: {hero.Name}");
void Replace(Character target)
{
target = new Character();
target.Name = "마법사";
Console.WriteLine($"함수 안: {target.Name}");
}
class Character
{
public string Name;
}
| 함수 안에서 한 일 | 넘긴 쪽에 반영되는가 |
|---|---|
target.Health = 150-객체의 필드를 바꿈 | 반영됨 |
target = new Character();-변수를 갈아 끼움 | 반영되지 않음 |
아직 아무것도 가리키지 않는 상태
클래스 타입의 변수는 아무 객체도 가리키지 않는 상태를 가질 수 있음. 그 상태를null이라고 씀.
using System;
Character c = null;
Console.WriteLine(c == null);
c = new Character();
c.Name = "용사";
Console.WriteLine(c == null);
Console.WriteLine(c.Name);
class Character
{
public string Name;
}
null인 변수에 점을 찍어 필드에 닿으려 하면 실행 중에 오류가 남. 가리키는 객체가 없어 필드가 놓일 자리도 없기 때문임.
따라서 null인 변수에 점을찍으면 안된다는 것 기억하기.
==는 같은 객체인지를 봄
클래스 타입끼리 ==로 비교하면 내용이 아니라 같은 객체를 가리키는지를 봄.
using System;
Character a = new Character();
a.Name = "용사";
Character b = new Character();
b.Name = "용사";
Character c = a;
Console.WriteLine($"a == b : {a == b}"); // 내용은 같지만 다른 객체
Console.WriteLine($"a == c : {a == c}"); // 같은 객체
Console.WriteLine($"이름 비교 : {a.Name == b.Name}");
class Character
{
public string Name;
}
내용이 같으면 같다고 보게 만드는 방법이 따로 있음.
값 타입과 참조 타입
지금까지 본 두 가지 성질에 이름이 붙어 있음.
| 분류 | 변수가 담는 것 | 대입할 때 | 지금까지 본 타입 |
|---|---|---|---|
| 값 타입 | 값 자체 | 값이 복사됨 | int, double, bool, char |
| 참조 타입 | 객체가 놓인 자리 | 가리키는 자리가 복사됨 | 클래스, 배열, string |
| 클래스는 참조 타입임. 배열을 함수에 넘겼을 때 원본이 바뀌던 것도 배열이 참조 타입이기 때문임. |
- 참고:
string은 참조 타입이지만 대입해놓고 한쪽을 고쳐도 다른쪽이 바뀌지 않음. 문자열은 한번 만들어지면 내용이 바뀌지 않고 새 문자열이 만들어지기 때문.
클래스 배열
객체 여러 개를 배열로 다루기
클래스 이름도 타입이므로 배열의 요소 타입이 될 수 있음.
using System;
Character[] party = new Character[3];
party[0] = new Character();
party[0].Name = "용사";
party[0].Health = 100;
party[1] = new Character();
party[1].Name = "마법사";
party[1].Health = 80;
party[2] = new Character();
party[2].Name = "궁수";
party[2].Health = 90;
for (int i = 0; i < party.Length; i++)
{
party[i].ShowInfo();
}
class Character
{
public string Name;
public int Health;
public void ShowInfo()
{
Console.WriteLine($"{Name} - HP {Health}");
}
}
요소는 처음에 null임
new Character[3]이 만드는 것은 객체 세개가 아니라 객체를 가리킬 자리 세 개임. 각 자리에 new로 객체를 넣기 전까지는 null임
객체를 넣지 않은 자리를 쓰면 실행 중에 오류가 남.
배열을 순회하며 객체 다루기
객체 배열에도 배열에서 쓰던 방법을 그대로 씀.
using System;
Character[] party = new Character[3];
party[0] = new Character();
party[0].Name = "용사";
party[0].Health = 100;
party[1] = new Character();
party[1].Name = "마법사";
party[1].Health = 40;
party[2] = new Character();
party[2].Name = "궁수";
party[2].Health = 75;
int total = 0;
Character weakest = party[0];
foreach (Character c in party)
{
total = total + c.Health;
if (c.Health < weakest.Health)
{
weakest = c;
}
}
Console.WriteLine($"전체 체력: {total}");
Console.WriteLine($"평균 체력: {total / party.Length}");
Console.WriteLine($"가장 약한 동료: {weakest.Name} (HP {weakest.Health})");
class Character
{
public string Name;
public int Health;
}
weakest에 닮기는 것은 객체의 복사본이 아니라 배열 요소가 가리키는 그 객체임. 그러므로 weakest를 통해 체력을 바꾸면 배열 안의 객체가 바뀜.
using System;
Character[] party = new Character[2];
party[0] = new Character();
party[0].Name = "용사";
party[0].Health = 100;
party[1] = new Character();
party[1].Name = "마법사";
party[1].Health = 40;
Character weakest = party[1];
weakest.Health = weakest.Health + 60; // weakest 를 통해 회복시킴
Console.WriteLine($"{party[1].Name}: {party[1].Health}");
class Character
{
public string Name;
public int Health;
}
조건에 맞는 객체 세기
using System;
Item[] inventory = new Item[4];
inventory[0] = new Item();
inventory[0].Name = "장검";
inventory[0].Price = 500;
inventory[1] = new Item();
inventory[1].Name = "방패";
inventory[1].Price = 300;
inventory[2] = new Item();
inventory[2].Name = "포션";
inventory[2].Price = 50;
inventory[3] = new Item();
inventory[3].Name = "붕대";
inventory[3].Price = 20;
int expensiveCount = 0;
int totalPrice = 0;
foreach (Item item in inventory)
{
totalPrice = totalPrice + item.Price;
if (item.Price >= 300)
{
expensiveCount++;
}
}
Console.WriteLine($"보유 개수: {inventory.Length}");
Console.WriteLine($"총 가치: {totalPrice}골드");
Console.WriteLine($"300골드 이상: {expensiveCount}개");
class Item
{
public string Name;
public int Price;
}
객체를 그대로 출력하면
객체를 Console.WriteLine에 그대로 넘기면 필드 값이 아니라 타입 이름이 나옴.
모든 클래스는 자신을 문자열로 바꾸는 기능을 기본으로 가지고 있고, 그 기본 동작이 타입 이름을 돌려주는 것임. 값을 보려면 필드를 직접 꺼내 씀.
클래스 여러 개를 함께 쓰기
한 프로그램에 클래스가 여럿 있어도 되고, 클래스가 다른 클래스를 필드로 가져도 됨.
using System;
Weapon sword = new Weapon();
sword.Name = "장검";
sword.Damage = 12;
Character hero = new Character();
hero.Name = "용사";
hero.Health = 100;
hero.Weapon = sword; // 다른 클래스의 객체를 필드에 담음
Character dummy = new Character();
dummy.Name = "허수아비";
dummy.Health = 50;
hero.Attack(dummy);
hero.Attack(dummy);
class Weapon
{
public string Name;
public int Damage;
}
class Character
{
public string Name;
public int Health;
public Weapon Weapon;
public void Attack(Character target)
{
target.Health = target.Health - Weapon.Damage;
Console.WriteLine($"{Name}이(가) {Weapon.Name}(으)로 공격! {target.Name} HP {target.Health}");
}
}
Attack이 받는 매개변수 target도 Character임. 객체가 다른 객체를 받아 그 필드를 바꾸는 것이 클래스로 짠 프로그램의 기본 모양임.
using System;
Weapon sword = new Weapon();
sword.Name = "장검";
sword.Damage = 12;
Weapon axe = new Weapon();
axe.Name = "전투도끼";
axe.Damage = 25;
Character hero = new Character();
hero.Name = "용사";
hero.Weapon = sword;
Character dummy = new Character();
dummy.Name = "허수아비";
dummy.Health = 100;
hero.Attack(dummy);
hero.Weapon = axe; // 가리키는 무기를 바꿈
hero.Attack(dummy);
class Weapon
{
public string Name;
public int Damage;
}
class Character
{
public string Name;
public int Health;
public Weapon Weapon;
public void Attack(Character target)
{
target.Health = target.Health - Weapon.Damage;
Console.WriteLine($"{Name}: {Weapon.Name} 공격 → {target.Name} HP {target.Health}");
}
}
무기 객체를 두 캐릭터가 함께 가리킬 수도 있음. 참조 타입이므로 한쪽에서 무기의 공격력을 바꾸면 다른 쪽에도 보임.
using System;
Weapon shared = new Weapon();
shared.Name = "낡은 검";
shared.Damage = 10;
Character a = new Character();
a.Name = "용사";
a.Weapon = shared;
Character b = new Character();
b.Name = "기사";
b.Weapon = shared; // 같은 무기 객체를 가리킴
shared.Damage = 30; // 무기를 강화함
Console.WriteLine($"{a.Name}의 공격력: {a.Weapon.Damage}");
Console.WriteLine($"{b.Name}의 공격력: {b.Weapon.Damage}");
class Weapon
{
public string Name;
public int Damage;
}
class Character
{
public string Name;
public Weapon Weapon;
}




붕어빵과 붕어빵틀....? 인가용