(C#) 19. Static의 정체
static 필드로 객체 개수를 세는 예제로 클래스 소속과 인스턴스 소속의 차이를 확인했다. Clone도 생성자를 타서 번호가 올라가는 것, static 함수에서 필드에 못 닿는 이유, 여러 스레드에서 counter++가 어긋나는 것까지.
객체마다 있으면 안 되는 것
18편까지 만든 Knight는 객체마다 hp와 attack을 하나씩 갖는다. 그게 맞다. 기사마다 체력이 다르니까.
그런데 “지금까지 만든 기사가 몇 명인가”는 객체마다 있으면 안 된다. 클래스 전체에 하나만 있어야 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
using System;
namespace Static
{
class Program
{
class Knight
{
// 필드
static public int counter = 1; // 오로지 1개만 존재!
public int id;
public int hp;
public int attack;
// static 함수 -> 클래스에 종속적 (유일성)
static public void Test()
{
counter++;
}
static public Knight CreateKnight()
{
Knight knight = new Knight();
knight.hp = 100;
knight.attack = 1;
return knight;
}
public Knight()
{
id = counter;
counter++;
hp = 100;
attack = 10;
Console.WriteLine("생성자 호출!");
}
public Knight Clone()
{
Knight knight = new Knight();
knight.hp = hp;
knight.attack = attack;
return knight;
}
public void Move()
{
Console.WriteLine("Knight Move");
}
public void Attack()
{
Console.WriteLine("Knight Attack");
}
}
static void Main(string[] args)
{
Knight knight = Knight.CreateKnight(); // static
knight.Move(); // 일반
Console.WriteLine();
Random rand = new Random();
rand.Next(0, 2); //static이 아님.
}
}
}
부르는 방법이 다르다
1
2
Knight knight = Knight.CreateKnight(); // static -> 클래스 이름으로
knight.Move(); // 일반 -> 객체 이름으로
이 두 줄이 차이를 그대로 보여준다.
CreateKnight는 static이라 객체가 없어도 부를 수 있다. 오히려 객체를 만들어 주는 함수라 객체가 있을 리도 없다.
Move는 “누가 움직이는가”가 있어야 의미가 있다. 그래서 객체를 통해 부른다.
맨 아래 두 줄이 이 대비를 확인하는 부분이다.
1
2
Random rand = new Random();
rand.Next(0, 2); //static이 아님.
Next는 static이 아니라 인스턴스 메서드다. Random 객체가 자기 안에 수열 상태를 들고 있고, 부를 때마다 그 상태가 바뀌기 때문이다. 그래서 Random.Next(0, 2)처럼은 못 쓴다.
반대로 Math.Abs나 Console.WriteLine은 static이라 객체 없이 바로 부른다. 상태를 안 들고 있으니 객체를 만들 이유가 없다.
상태를 들고 있어야 하면 인스턴스, 아니면 static. 이 기준이 제일 명확했다.
static 함수에서는 필드에 못 닿는다
1
2
3
4
5
static public void Test()
{
counter++; // 된다. counter 도 static
hp = 100; // 컴파일 에러
}
static 함수에는 this가 없다. 어느 객체의 hp인지 정할 수가 없으니 접근 자체가 불가능하다.
10편에서 “Main이 static이라 거기서 부르는 함수도 static이어야 한다”고 적었던 게 이 이야기다. 이유를 이제 알 수 있다.
반대 방향은 된다. 일반 함수에서 static 필드는 자유롭게 쓴다. 생성자가 counter를 건드리는 게 그 예다.
Clone 도 번호를 하나 먹는다
1
2
3
4
5
6
public Knight()
{
id = counter;
counter++;
// ...
}
생성자가 자기 번호를 받고 카운터를 올린다. counter가 1부터 시작하니 첫 기사의 id는 1이다.
여기서 눈여겨볼 게 있다. Clone도 new Knight()를 부른다.
1
2
3
4
5
6
7
public Knight Clone()
{
Knight knight = new Knight(); // 생성자가 돈다 -> counter 증가
knight.hp = hp;
knight.attack = attack;
return knight;
}
복제본도 새 번호를 받는다. 그게 의도라면 맞는데, “원본과 같은 기사”를 만들려던 거라면 id까지 복사해야 한다. Clone 안에서 hp와 attack만 옮기고 id는 안 옮기는 게 의도적인지 아닌지가 코드에 안 드러난다.
CreateKnight도 마찬가지다. 생성자가 이미 hp = 100, attack = 10을 넣었는데 그 뒤에 attack = 1로 덮어쓴다. 생성자와 팩토리 함수 양쪽에서 초기화하니 어느 값이 최종인지 헷갈린다. 18편에서 적은 “초기화를 한 곳에 모은다”가 여기서도 적용된다.
static 필드는 언제 초기화되나
1
static public int counter = 1;
이 대입은 클래스를 처음 쓰는 시점에 한 번 실행된다. 객체를 만들 때가 아니다. 객체를 하나도 안 만들어도 Knight.counter를 읽으면 1이 나온다.
더 복잡한 초기화가 필요하면 static 생성자를 쓴다.
1
2
3
4
5
6
7
8
9
class Knight
{
static public int counter;
static Knight() // 접근 지정자도 매개변수도 없다
{
counter = LoadFromConfig();
}
}
이것도 클래스를 처음 쓸 때 딱 한 번 돈다. 언제 도는지 정확한 시점을 통제할 수 없다는 게 함정이라, 순서에 의존하는 코드를 여기 넣지 않는 게 좋았다.
여러 스레드에서 counter++ 가 어긋난다
static 필드는 모두가 공유한다. 공유한다는 건 여러 스레드가 동시에 건드릴 수 있다는 뜻이다.
1
counter++;
한 줄이지만 실제로는 읽고, 1을 더하고, 다시 쓰는 세 단계다. 두 스레드가 겹치면 둘 다 같은 값을 읽어서 증가가 하나 사라진다. 기사를 1000명 만들었는데 counter가 997인 상황이 나온다.
자주 일어나지 않아서 더 찾기 어렵다.
1
2
using System.Threading;
Interlocked.Increment(ref counter);
Interlocked는 이 세 단계를 쪼개지지 않게 한 번에 처리한다. 게임 서버처럼 여러 스레드에서 객체를 만드는 상황이면 필요해진다.
static 을 쓰기 전에
편해서 자꾸 쓰게 되는데, static 필드는 결국 전역 변수다.
- 어디서든 바꿀 수 있어서 값이 언제 왜 바뀌었는지 추적이 어렵다
- 테스트할 때 앞 테스트가 남긴 값이 뒤 테스트에 영향을 준다
- 프로그램이 끝날 때까지 메모리에 남는다
Unity를 쓴다면 하나 더 있다. static 필드는 씬을 바꿔도 초기화되지 않는다. 게임을 다시 시작했는데 점수가 이어지는 종류의 버그가 여기서 나온다. 에디터에서 플레이를 멈췄다 다시 시작해도 남아 있어서, 빌드에서는 되는데 에디터에서만 이상한 상황도 생긴다.
기준은 이렇게 잡았다.
- 상태가 없는 도구 함수 →
static(Math.Abs같은 것) - 진짜로 하나만 존재해야 하는 것 →
static(인스턴스 카운터, 설정) - 그 외 → 인스턴스
정리하면
static은 클래스에 속하고 일반 멤버는 객체에 속한다. 부르는 방법이 다르다- 상태를 들고 있어야 하면 인스턴스, 아니면
static이다.Random.Next가 인스턴스인 이유가 그것이다 static함수에는this가 없어서 인스턴스 필드에 접근할 수 없다. 반대는 된다- 생성자를 타는 모든 경로에서 카운터가 올라간다.
Clone도 포함된다 static필드는 클래스를 처음 쓸 때 한 번 초기화된다- 공유 자원이므로 여러 스레드에서
++하면 값이 어긋난다.Interlocked가 필요하다 static은 전역 상태다. 추적과 테스트가 어려워지니 꼭 필요할 때만 쓴다