static 이라 하면 main 함수 맨 앞에 정의된 이 부분이 가장 먼저 떠오를 것이다.
이 static이라 정의된 부분이 무엇일까? 알아보도록 하자.
static은 정적 변수 혹은 함수로 저장되는 것으로, 해당 구문으로 저장된 데이터는, 메모리의 데이터 영역에 저장된다.
(다만, 정젹 변수는 데이터 영역에 저장되는 것이 맞으나 함수와 클래스가 꼭 데이터 영역에 저장되지는 않음)
해당 위치에 저장되어 고정된 변수 및 함수는, 프로그램이 시작하고 종료할 때까지 사라지지 않고 유지되는 특성이 있다. 그러므로, 프로그램에 고정적으로 사용해야 하는, 없어져서는 안 되는 정보를 static으로 선언한다.
static 변수는 고정적인 위치에서 프로그램 시작부터 끝까지 남아있는 데이터이다.
따라서, 아래와 같은 특징을 가진다.
- 프로그램 시작부터 끝까지 유지되어야 하는 데이터에서 선언한다.
- 프로그램 최초 실행 시 바로 선언이 되는 변수이다. (우선적으로 저장됨)
- 어디서든 가져다가 쓸 수 있음 (전역적인 접근이 가능하다)
이런 static으로 선언되는 변수에는 특이한 점이 있는데, static으로 선언한 변수는 마치 참조 형식인 것처럼 값에 대한 계산이 실제 데이터에 반영되는 것이다.
하지만 실제로는 주소값을 받아 참조하여 값이 변하는 것과는 달리, static 변수는 그 원본 값 자체를 가지고 오는 것이기 때문에 발생하는 현상이다. 이에 따라 아래와 같은 주의점을 기억하도록 하자.
static 함수는 전역적으로 사용할 수 있는 함수를 말한다.
프로그램 시작 시 생성되고, 프로그램 사용 전반적으로 많이, 그리고 다양한 곳에 쓰이는 함수에 쓰기 적절하다.
사용 예시는 아래와 같은 상황을 생각할 수 있다.
ex) 아이템의 위치를 바꾸는 함수
아이템의 위치를 바꾸는 Swap 함수를 바꾼다고 해 보자. 플레이어는 인벤토리의 아이템의 위치를 Swap할 수도 있고, 스킬이나 장비를 Swap할 수도 있다. 심지어 몬스터도 무기를 Swap할 수 있다고 가정했을 때, 이 동일한 함수를 매번 선언하는 것은 코드 상으로는 비효율적이다.
그렇다면 해당 함수와 같이 전반적으로 많이 쓰이고 다양한 클래스에서 쓰일 함수들을 모아보자. static class Util 이라는 클래스를 선언하여, 이와 같이 유용한 함수를 모아두고 사용할 수 있는 방법을 생각해 볼 수 있다.
static 함수는 시작하자마자 선언되기 때문에, 아직 생성되지 않은 변수(멤버 변수)를 static 함수 안에 넣을 수 없다. 따라서 사용할 변수를 함수보다 전에 초기화하거나 static 변수를 초기화하도록 한다.
static 클래스는 모든 멤버 변수와 멤버함수가 static인 클래스 (아닌 것을 만들 수 없다)
중요한 특징 중 하나는, 정적 클래스는 인스턴스화 하는 것이 불가능하다는 것이다.
정확히는, 인스턴스 할 필요가 없다.
이를 설명하기 위해 대표적인 static 클래스의 예시를 보자.
지금까지 많이 써 왔던 Console의 정의로 이동해보자.
Console의 정의를 확인해보면, static으로 선언되어 있는 것을 확인할 수 있다.
이와 같이 Console은 static으로 선언되어 있기 때문에 instance가 불가능한 모습을 볼 수 있다.
상속이란 부모클래스에서 만든 멤버 변수와 멤버 함수를 자식클래스에 그대로 계승할 수 있는 기능이다.
프로그래밍의 분야에 따라서 상속의 중요성이 달라지기는 하지만,
게임 분야에서는 상속이 매우 중요하며, 상속을 잘 쓰면 코딩 잘하는 게임 프로그래머가 될 수 있다.
게임이라는 분야 특성상 상속을 자주 사용할 수밖에 없는, 공통적인 요소가 많기 때문이다.
이와 같은 점을 유념하고 상속에 대해서 알아보자.
상속(Inheritance)
- 부모클래스의 모든 기능을 가지는 자식클래스를 설계하는 방법이다
- 부모클래스를 만들고, 자식 클래스를 'class 자식클래스 : 부모클래스' 로 선언할 수 있다.
- is-a 관계 : 부모클래스가 자식클래스를 포함하는 상위개념일 경우 상속 관계가 적합함
이와 같은 상속을 사용할 수 있는 예시를 생각해 보자.
class Monster // '몬스터' 라는 부모 변수
{
public string name; // 모든 몬스터는 이름을 가진다
public int hp; // 모든 몬스터는 체력을 가진다
public float speed; // 모든 몬스터는 속도를 가진다
public void Move()
{
Console.WriteLine("{0} 이/가 {1} 의 속도로 움직입니다.", name, speed)
}
}
class Slime : Monster // 자식클래스-슬라임
{
name = "슬라임";
hp = 10;
speed = 3.5f;
}
class Dragon : Monster // 자식클래스-드래곤
{
name = "드래곤";
hp = 100;
speed = 5.5f;
}
이와 같은 상황을 구현해볼 수 있다.
모든 몬스터는 이름, 체력, 속도를 가지고, 움직인다.
이와 같은 공통적인 요소를 부모클래스에서 선언하고, 그 실제 이름과 체력, 속도에 대한 초기화를 자식클래스에서 선언할 수 있다. 이렇게 하면 부모클래스 없이 각자 클래스에서 선언하고 초기화하는 것보다 코드가 간결해진다. 또한 특정 변수나 함수를 추가해야 할 시, 반복을 줄이고 코드를 재활용할 수 있는 효과가 있다.
예를 들어 아래와 같은 상황을 생각해 보자.
'몬스터'라는 부모클래스를 선언한다. 그러면 자식클래스로 아래의 것들을 선언해보자.
-'슬라임'은 '몬스터'다 (o) -> 슬라임은 몬스터의 자식 클래스로 적합
-'드래곤'은 '몬스터'다 (o) -> 드래곤은 몬스터의 자식 클래스로 적합
-'오크'는 '몬스터'다 (o) -> 오크는 몬스터의 자식 클래스로 적합
-'포션'은 '몬스터'다 (x) -> 자식 클래스로 부적합하다.
-'포션'은 '아이템'이다 (o) -> 포션은 아이템의 자식 클래스로 적합
-'무기'는 '아이템'이다 (o) -> 무기는 아이템의 자식 클래스로 적합
다만 아래와 같은 실수를 하지 않도록 하자.
ex) 가령 타워디펜스 게임에서 아래와 같이 부모클래스와 자식클래스를 만든다고 하자
타워는 공격범위다 (이건 has - a (무엇을 갖고 있다) 라는 관계라서 부적절)
타워의 경우 공격범위를 가지고 있긴 하지만, 타워 자체가 공격범위는 아니므로 기능을 구현하는 과정에서 문제가 생길 수도 있다.
가령, 이와 같은 상황을 생각해볼 수도 있다.
어떤 변수를 외부에서 접근하지 못하게 선언하고 싶다. 따라서 private로 바꾸고 싶으나, private로 변수를 선언하게 되면, 해당 클래스와 연결된 모든 자식클래스 또한 해당 변수를 사용할 수 없게 된다.
name 변수를 private로 설정하였더니 아래와 같이 오류가 뜨는 것을 확인할 수 있다.
하지만 이런 상황에서 리스크를 안고 public으로 바꿔주는 대신에 protected로 선언해줄 수 있는 방법이 있다.
이와 같이 protected로 사용하면 자식클래스에서는 name 변수를 사용할 수 있다.
하지만 아래와 같이 main 함수에서 선언할 수 없으며, 임의로 이름을 변경하는 것을 방지할 수 있다.
상속의 원리는 부모의 클래스를 다 갖다 붙이고 추가로 클래스 내용을 덧붙이는 것이다.
그래서 부모를 만든 후 자식을 만든다는 특징이 있다.
부모에서 생성자를 만들면 (기본생성자가 생성되는 형태가 아니면) 자식 또한 생성자를 만들어줘야 한다.
ex)
class Car
{
string name;
float speed;
public Car(string name, float speed) // 부모에서 생성자를 선언
{
this.name = name;
this.speed = speed;
}
}
class Truck : Car
{
public int capacity;
public Truck() : base("트럭", 10.0f)
{
capacity = 10;
}
// 자식에서 생성자를 선언해야지 사용할 수 있다.
public Truck(string name, float speed, int capacity) : base(name, speed)
{
this.capacity = capacity
}
}
위의 몬스터 부모클래스와 슬라임, 드래곤 자식클래스의 예시를 다시 생각해 보자.
class Monster // '몬스터' 라는 부모 변수
{
public string name; // 모든 몬스터는 이름을 가진다
public int hp; // 모든 몬스터는 체력을 가진다
public float speed; // 모든 몬스터는 속도를 가진다
public void Move()
{
Console.WriteLine("{0} 이/가 {1} 의 속도로 움직입니다.", name, speed)
}
}
class Slime : Monster // 자식클래스-슬라임
{
name = "슬라임";
hp = 10;
speed = 3.5f;
}
class Dragon : Monster // 자식클래스-드래곤
{
name = "드래곤";
hp = 100;
speed = 5.5f;
}
우선은 부모클래스로부터 이름, 체력, 속도와 움직임 함수를 받아 사용하고 있다.
하지만 이와 같은 정보 외에도 몬스터 전체의 공통적인 특징이 아닌, 자식클래스 고유의 특징이 있을 수 있다. 가령 슬라임 같은 경우엔 분열이 가능하거나, 드래곤을 브레스를 뿜을 수 있다는 것 같이.
이와 같은 상황에서는 부모클래스에서 해당 함수를 선언하는 것이 아닌, 자식클래스에서 해당 함수를 선언한다.
class Slime : Monster
{
public Slime()
{
name = "슬라임";
hp = 10;
speed = 3.5f;
}
public void Split()
{
Console.WriteLine("슬라임이 분열합니다.");
}
}
class Dragon : Monster
{
public float speed;
public Dragon()
{
name = "드래곤";
hp = 100;
speed = 5.5f;
}
public void Breath()
{
Console.WriteLine("드래곤이 브레스를 뿜습니다");
}
}
이와 같이 선언하고 메인에서 함수를 호출해보자.
slime의 고유 기능인 Split이 구현되었고, dragon의 Breath가 구현되었다. 또한 슬라임이 드래곤의 스킬을 사용할 수 없는 것 또한 확인할 수 있었다.
1) 업 캐스팅
지금까지 몬스터 클래스를 열심히 만들었고, 이를 플레이어와 상호작용하게 하고 싶다고 하자.
class Player
{
public string name = "김전사";
public int hp = 50;
public int attackPoint = 10;
public void Attack()
{
Console.WriteLine("몬스터를 공격합니다.");
}
}
다만 이렇게만 하면 몬스터와의 상호작용을 구현할 수 없을 것이다. 또한, 슬라임을 공격해도, 드래곤을 공격해도 데미지가 들어가도록 하고 싶은데, 그 각각의 몬스터와의 상호작용을 작성하기에는 코드가 길어지는 문제점이 있을 것이다.
따라서 여기에서 '업 캐스팅'을 사용할 수 있다.
class Player
{
public string name = "김전사";
public int hp = 50;
public int attackPoint = 10;
public void Attack(Monster monster)
{
Console.WriteLine("몬스터를 공격합니다.");
monster.hp -= attackPoint;
}
}
이와 같이 작성하면, 플레이어가 상대하는 몬스터가 슬라임이든, 드래곤이든, 아니면 달리 구현된 몬스터이든 부모클래스가 몬스터인 모든 객체에게 Attack 할 수 있게 된다.
player.Attack(slime); 도 되고,
player.Attack(monster); 도 되는 것처럼
부모와 자식 사이의 자리를 바꾸어도 이상하지 않도록 구현된 상속은 잘 만든 상속이다.
2) 다운 캐스팅
다만 위에서 설명한 업 캐스팅의 경우에 다음과 같은 문제점이 있다.
위와 같이 Slime을 직접 instance화 하는 대신, 부모클래스인 Monster로 자식클래스를 instance할 수 있다. 다만 이렇게 했을 시에는 자식클래스의 고유 능력을 사용할 수 없는 것을 확인할 수 있다.
이는 부모클래스로 선언된 monster0가 실제로 슬라임인지 드래곤인지 판단할 수가 없기 때문에 고유능력의 사용 또한 막힌 것이다.
하지만 위의 Player의 업 캐스팅의 예시와 같이 부모클래스로 몬스터를 instance 해야 할 상황이 있을 수밖에 없다. 그러면 이런 상황에서 자식클래스의 고유 능력 사용은 불가능한 걸까?
클래스에서도 이런 식으로 강제 형변환은 가능하다. 하지만 이러한 방법은 실수가 발생할 시에 문제가 생길 수 있기 때문에 우리는 '다운 캐스팅'을 사용하고자 한다.
다운캐스팅 - 불가능한 경우가 있기 때문에 조건을 체크하고 변환 가능함
if(monster is Slime)
{
Slime slime = (Slime)monster;
slime.Split();
}
else if(monster is Dragon)
{
Dragon dragon = (Dragon) monster;
dragon.Breath();
}
Dragon dragon1 = monster as Dragon;
이와 같이도 작성할 수 있다.
변수를 선언할 때, 이름이 중복되면 안 된다는 사실을 알 것이다.
하지만 함수 같은 경우는, 매개변수가 다른 경우 동일한 이름이어도 선언이 가능하다.
위와 같이 함수의 이름이 같아도, 매개변수가 다르기 때문에 컴퓨터는 어떤 함수를 써야하는지 구분할 수 있다는 것이다. 이와 같이 매개변수가 다른 함수를 구분할 수 있는 특성을 '함수 오버로딩'이라 한다.
하지만 앞으로 게임을 구현할 때 굳이 같은 이름으로 함수를 써야 할 경우가 생길까?
아래와 같은 상황을 생각해보자.
class Player
{
public void UseItem(Item item) // 아이템을 사용한다
{
}
public void UseSkill(Skill skill) // 스킬을 사용한다
{
}
}
플레이어가 아이템을 사용하는 경우와, 스킬을 사용하는 경우를 각각 구분해서 만들 수도 있을 것이다. 하지만 코드가 길어지면, 나중에는 아이템을 사용할 때 뭐라고 해야하지? 스킬을 쓸 때는? 이렇게 생각하며 다시 함수명을 찾아봐야 할 수도 있다.
이렇게 쓰는 대신에 아래와 같이 작성 해보자.
class Player
{
public void Use(Item item) {}
public void Use(Skill skill) {}
}
이와 같이 쓰면 함수명을 일일히 찾지 않고 아이템을 쓰든, 스킬을 쓰든 문제가 생기지 않고, 컴퓨터가 판단할 수 있는 장점이 있다.
또한, 이런 함수 오버로드를 쓰는 대표적인 예시가 있다.
Console.WriteLine을 쓰면 그 내부에 어떠한 자료형의 내용을 넣어도 다 출력된다는 사실을 알고 있다. 이 함수를 자세히 살펴 보자.
Console.WriteLine은 17개의 함수 오버로드로 만들어진 것을 확인할 수 있다.
sealed는 특정 클래스를 상속하는 것을 제약하고자 할 때, class 앞에 사용한다
가령, 다른 협업자가 Player 클래스의 자식 클래스로 만들어서 사용하는 것을 원치 않는다고 하자. 그래서 sealed class Player라고 선언하면 아래와 같이 자식 클래스를 사용하는데 문제가 생긴다.
sealed를 앞에 붙인 클래스의 경우, 그 클래스를 상속하지 말고 그대로 써달라 라는 의미라고도 해석할 수 있다.
이와 같이 긴 내용을 다뤄 보았다. 상속의 사용 의미를 생각해 보며 어떻게 하면 상속을 잘 쓸 수 있을 지 생각해 보도록 하자.
상속의 사용의미1
- 상속을 진행하는 경우 부모클래스의 소스가 자식클래스에서 모두 적용된다
- 부모클래스와 자식클래스의 상속관계가 적합한 경우, 부모클래스에서의 기능구현이 자식클래스에서도 어울린다.
상속의 사용의미2
- 자식클래스는 부모클래스를 요구하는 곳에서 동일한 기능을 수행할 수 있다.
- 코드의 중복을 줄이고 유지보수성을 개선하는 기능이 있으며, 코드를 간결하게 작성하는 데 도움을 준다.
다형성(Polymorphism)
객체의 속성이나 기능이 상황에 따라 여러가지 형태를 가질 수 있는 성질
앞선 상속에서 부모클래스와 자식클래스로 다양한 기능을 구현해 보았다. 이런 상속 클래스에서, 객체의 속석에 있어 형태를 더욱 다양하게 만들 수 있는 방법에 대해 알아보고자 한다
가령, Skill이라는 부모클래스가 있고, 자식클래스로 화염구를 구현했다고 하자.
public class Skill
{
public string name;
public float coolTime;
public void Execute()
{
Console.WriteLine("{0} 스킬을 사용합니다.", name);
Console.WriteLine("{0} 쿨타임을 진행합니다.", coolTime);
}
}
public class Fireball : Skill
{
public Fireball()
{
name = "파이어볼";
coolTime = 3.5f;
}
public void Execute()
{
Console.WriteLine("화염구 발사!!!");
}
}
internal class Program
{
static void Main(string[] args)
{
Skill skill = new Fireball(); // 부모클래스로 선언
skill.Execute();
}
}
이를 출력해보면 아래와 같이 나온다.
부모클래스로 선언했기 때문에 자식클래스에서의 '화염구 발사!!!' 라는 출력이 나오지 않는 것을 확인할 수 있다. 이를 상속에서 배웠던 다운 캐스팅 방식으로 사용할 수도 있겠지만, 다른 방법이 있다.
- 부모클래스 함수를 public virtual void Execute() 로 선언
- 자식클래스 함수에서 public override void Execute() 로 선언
이와 같이 바꾼 후 출력해 보자.
부모클래스 함수의 내요이 자식클래스 함수의 내용으로 덮어씌워진 것을 볼 수 있다.
이것은 부모클래스의 함수를 가상함수로, 자식클래스의 함수를 오버라이딩하여 생긴 변화이다.
가상함수와 오버라이딩
- 가상함수 : 부모클래스의 함수 중 자식클래스에 의해 재정의할 수 있는 함수로 지정
- 오버라이딩 : 부모클래스의 가상함수를 같은 함수이름과 같은 매개변수로 재정의하여 자식만의 반응을 구현
이와 같은 방법을 이용하면, 부모클래스의 함수를 수정하지 않고 자식클래스의 함수에 고유의 내용을 적을 수 있는, 다형성의 실현이 가능하다. 이와 같이 덮어씌우는 작용은 내용 자체를 바꾸는 것은 아니다.
다만 위와 같이 사용하는 방식에 대해서는 잠깐 생각해 보도록 하자.
부모클래스에서 스킬 시전과 쿨타임이 기입되어 있었는데, 그대로 override 해 버리면 쿨타임과 관련된 내용이 날아갈 수 있다. 이런 문제를 해결하려면 어떻게 해야할까.
그러면 자식클래스에서 함수를 이렇게 만들자
public override void Execute()
{
base. Execute();
Console.WriteLine("화염구 발사");
}
이와 같이 작성하고 출력하면 아래와 같이 나온다.
다형성 사용의미 1
- 새로은 클래스를 추가하거나 확장할 때 기존 코드의 영향응 최소화함
- 새로운 클래스를 만들 때 기존의 소스를 수정하지 않아도 됨
다형성 사용의미 2
- 클래스 간의 의존성을 줄여 확장성은 높임
또한 virtual 클래스에 주목할 필요가 있다.
스킬을 구현하려면 -> 스킬 부모 클래스 확인 -> virtual 보고 아, 이거 사용해서 스킬 override를 한다, 라는 판단을 할 수 있다.
불가피할 때는 다운캐스팅 하지만 다형성으로 구현할 수 있는게 더 좋음 (다운캐스팅도 비권장은 아니고 충분히 활용할 수 있는 기술)
추상화
클래스를 정의할 당시 구체화시킬 수 없는 기능을 추상적으로 정의
추상화란 클래스의 내용을 당장 구체화하지 않고, 해당 클래스가 어떤 내용을 담아야 하는지 기능을 추상적으로 구현해 놓는 것이다. 따라서 내용을 구체적으로 작성하지 않는다.
추상클래스(Abstract Class)
- 하나 이상의 추상함수를 포함하는 클래스
- 클래스가 추상적인 표현을 정의하는 경우 자식에서 구체화시켜 구현할 것을 염두하고 추상화 시킨다
- 추상클래스에서 내용을 구체화 할 수 없는 추상함수는 내용을 정의하지 않으며,
추상클래스를 상속하는 자식 클래스가 추상함수를 재정의하여 구체화한 경우 사용 가능- 인스턴스가 불가능하고 완벽하게 부모 클래스의 역할이 가능한 클래스를 만드는 것
abstract 라는 키워드를 통해 추상클래스를 선언할 수 있다.
가령, 아래와 같이 아이템이란 부모클래스를 추상화를 한다고 해 보자.
public abstract class Item
{
public abstract void Use();
}
이제 여기에 포션이라는 아이템을 구현한다고 하자. 그런데 여기에서 abstract로 선언된 Use 함수에 대한 구체적인 구현이 없으면 아래와 같이 뜬다.
abstract 는 이와 같이 추상클래스로 정의된 부분에서, 구현되지 않은 함수에 관해 구현되지 않았다며 컴파일 오류를 출력시킨다.
이와 같이 Use함수를 구체화시키면 컴파일 오류가 사라지고, 부모클래스에서 구현되지 않은 함수에 관해 빠뜨리는 실수를 방지한다.
추상화 사용의미 1
- 객체들의 공통적인 특징을 도출하는데 의미를 둔다.
- 구현을 구체화하기 어려운 상위클래스를 설계하기 위한 수단으로 사용한다.
추상화 사용의미 2
- 상위클래스의 인터페이스를 구현하기 위한 수단으로 사용한다.
- 추상적인 기능을 구체화시키지 않은 경우 인스턴스 생성이 불가하다.
이를 통해 자식클래스에게 추상함수의 구현을 강제하여 실수를 줄일 수 있다.