5 분 소요

GridMap의 코드를 살펴보자.

Before ```csharp using System.Collections.Generic; using UnityEngine; namespace Data.Map { /// /// 그리드 맵을 나타내는 클래스입니다. /// public class GridMap { public Vector2Int MapSize { get; private set; } public Vector2Int RoomSize { get; } = new Vector2Int(11, 7); private Room[,] rooms; private Cell[,] cells; private readonly List _roomCache = new List(); private readonly List _cellCache = new List(); public IReadOnlyList Rooms => _roomCache; public IReadOnlyList Cells => _cellCache; /// /// 그리드 맵을 초기화합니다. /// /// 방의 가로 개수</param> /// 방의 세로 개수</param> public GridMap(int roomCols, int roomRows) { MapInit(roomCols, roomRows); RoomInit(roomCols, roomRows); } public GridMap(int roomCols, int roomRows, Vector2Int roomSize) { RoomSize = roomSize; MapInit(roomCols, roomRows); RoomInit(roomCols, roomRows); } private void MapInit(int roomCols, int roomRows) { MapSize = new Vector2Int(roomCols * RoomSize.x, roomRows * RoomSize.y); cells = new Cell[MapSize.x, MapSize.y]; for (int x = 0; x < MapSize.x; x++) { for (int y = 0; y < MapSize.y; y++) { cells[x, y] = new Cell(new Vector2Int(x, y)); _cellCache.Add(cells[x, y]); } } } private void RoomInit(int roomCols, int roomRows) { rooms = new Room[roomCols, roomRows]; for (int x = 0; x < roomCols; x ++) { for (int y = 0; y < roomRows; y ++) { rooms[x, y] = new Room(new Vector2Int(x, y), RoomSize, cells); _roomCache.Add(rooms[x, y]); } } } /// /// 지정된 좌표의 방을 반환합니다. /// /// X 좌표</param> /// Y 좌표</param> /// 지정된 좌표의 방 public Room GetRoom(int x, int y) { if (!IsValidPosition(x, y)) return Room.NRoom; return rooms[x / RoomSize.x, y / RoomSize.y]; } /// /// 지정된 그리드 좌표의 방을 반환합니다. /// /// 그리드 좌표</param> /// 지정된 좌표의 방 public Room GetRoom(Vector2Int position) { return GetRoom(position.x, position.y); } public Room GetRoomByIndex(int roomX, int roomY) { if (roomX < 0 || roomX >= rooms.GetLength(0) || roomY < 0 || roomY >= rooms.GetLength(1)) { return Room.NRoom; } return rooms[roomX, roomY]; } /// /// 지정된 좌표의 셀을 반환합니다. /// /// X 좌표</param> /// Y 좌표</param> /// 지정된 좌표의 셀 public Cell GetCell(int x, int y) { if (!IsValidPosition(x, y)) return Cell.NCell; return cells[x, y]; } /// /// 지정된 그리드 좌표의 셀을 반환합니다. /// /// 그리드 좌표</param> /// 지정된 좌표의 셀 public Cell GetCell(Vector2Int position) { return GetCell(position.x, position.y); } /// /// 그리드 맵 전체에 대해 네비게이션 맵을 베이크합니다. /// public void BakeNavigationMap() { NavigationMap.Bake(cells); } private bool IsValidPosition(int x, int y) { return x >= 0 && x < MapSize.x && y >= 0 && y < MapSize.y; } } } ``` </details> --- 이 코드에서 제일 먼저 눈에 띄는 것은 생성자 속 숨어있는 매직 넘버와 XML이다. ``` csharp public Vector2Int RoomSize { get; } = new Vector2Int(11, 7); /// /// 그리드 맵을 초기화합니다. /// /// 방의 가로 개수</param> /// 방의 세로 개수</param> public GridMap(int roomCols, int roomRows) { MapInit(roomCols, roomRows); RoomInit(roomCols, roomRows); } public GridMap(int roomCols, int roomRows, Vector2Int roomSize) { RoomSize = roomSize; MapInit(roomCols, roomRows); RoomInit(roomCols, roomRows); } ``` GridMap(int, int) 생성자를 사용하면, 11과 7이라는 매직 넘버를 가지게 된다. 다른 생성자를 이용하면 생성하는 클래스에서 확인과 수정이 쉽고 대체될 수 있는 생성자다. 파라미터 int roomCols와 int roomRows는 방의 개수 cols/rows인지, roomSize 인지 모호하다. XML을 통해 이를 해결하고자 했지만, 막상 XML은 처음 생성자 오버로드에만 작성되어 있어 혼란을 준다. 다음과 같이 바꿨다. ``` csharp public Vector2Int RoomSize { get; } public GridMap(Vector2Int roomCount, Vector2Int roomSize) { RoomSize = roomSize; MapInit(roomCount); RoomInit(roomCount); } ``` 첫 번째 생성자는 삭제되고, 남아있던 생성자가 유일한 생성자로 대체되었다. 이미 다른 파라미터에서 Vector2Int를 사용하였기에, Vector2Int로 row와 col을 합치면서 목적인 roomCount로 추상화 단계를 동일하게 하였다. 이를 통해 독자는 roomCols와 roomRows가 뭔지 XML을 확인할 이유도 없어지고 한눈에 확인할 수 있기에, XML 주석도 제거하였다. 다음은 쓸모 없는 주석이다. ``` csharp /// /// 지정된 좌표의 방을 반환합니다. /// /// X 좌표</param> /// Y 좌표</param> /// 지정된 좌표의 방 public Room GetRoom(int x, int y) { if (!IsValidPosition(x, y)) return Room.NRoom; return rooms[x / RoomSize.x, y / RoomSize.y]; } /// /// 지정된 그리드 좌표의 방을 반환합니다. /// /// 그리드 좌표</param> /// 지정된 좌표의 방 public Room GetRoom(Vector2Int position) { return GetRoom(position.x, position.y); } public Room GetRoomByIndex(int roomX, int roomY) { if (roomX < 0 || roomX >= rooms.GetLength(0) || roomY < 0 || roomY >= rooms.GetLength(1)) { return Room.NRoom; } return rooms[roomX, roomY]; } ``` XML 문서화로 함수에 올려두면 설명이 나오니 굉장히 편하다고 생각해 한창 내가 AI가 자동으로 생성하는 XML 주석에 몸을 맡기고 문서화를 진행하던 시절이 있었다. 그러나 마우스로 올리는 것보다, 코드를 읽거나 수정할 때 기다란 XML 주석을 스크롤하며 내리고 읽는 것은 코드에 무게를 더한다. GridMap.GetRoom이 어떤 일을 하는지는 코드를 모르는 사람도 안다. GetRoomByIndex의 조건문은 GetRoom 보다 파악하는데 오랜 시간이 걸리고 통일성도 떨어진다. GetRoom에서 조건문을 캡슐화 했으니, GetRoomByIndex도 캡슐화 하는게 이상적이고 가독성도 좋아진다. 추가로 GetRoomByIndex라는 이름도 충분히 좋지만 GetRoomAtIndex가 공간에서 더 통용되는 표현이다. ``` csharp public Room GetRoom(int x, int y) { if (!IsValidPosition(x, y)) return Room.NRoom; return _rooms[x / RoomSize.x, y / RoomSize.y]; } public Room GetRoom(Vector2Int position) => GetRoom(position.x, position.y); public Room GetRoomAtIndex(int roomX, int roomY) { if (!IsValidRoomIndex(roomX, roomY)) return Room.NRoom; return _rooms[roomX, roomY]; } private bool IsValidRoomIndex(int roomX, int roomY) => roomX >= 0 && roomX < _rooms.GetLength(0) && roomY >= 0 && roomY < _rooms.GetLength(1); ``` 훨씬 깔끔하게 읽히는 것을 알 수 있다. 같은 쓸모없는 XML이 포함된 GetCell, BackNavigation들을 지운 다음, 내부 private 변수에서 _ 접두사 규칙을 일괄 적용한다. 메소드 순서를 보기 좋게 정리하면 다음과 같다.
After ```csharp using System.Collections.Generic; using UnityEngine; namespace Data.Map { public class GridMap { public Vector2Int MapSize { get; private set; } public Vector2Int RoomSize { get; } private Room[,] _rooms; private Cell[,] _cells; private readonly List _roomCache = new List(); private readonly List _cellCache = new List(); public IReadOnlyList Rooms => _roomCache; public IReadOnlyList Cells => _cellCache; public GridMap(Vector2Int roomCount, Vector2Int roomSize) { RoomSize = roomSize; MapInit(roomCount); RoomInit(roomCount); } public Room GetRoom(int x, int y) { if (!IsValidPosition(x, y)) return Room.NRoom; return _rooms[x / RoomSize.x, y / RoomSize.y]; } public Room GetRoom(Vector2Int position) => GetRoom(position.x, position.y); public Room GetRoomAtIndex(int roomX, int roomY) { if (!IsValidRoomIndex(roomX, roomY)) return Room.NRoom; return _rooms[roomX, roomY]; } public Cell GetCell(int x, int y) { if (!IsValidPosition(x, y)) return Cell.NCell; return _cells[x, y]; } public Cell GetCell(Vector2Int position) => GetCell(position.x, position.y); public void BakeNavigationMap() => NavigationMap.Bake(_cells); public bool IsValidPosition(int x, int y) => x >= 0 && x < MapSize.x && y >= 0 && y < MapSize.y; private bool IsValidRoomIndex(int roomX, int roomY) => roomX >= 0 && roomX < _rooms.GetLength(0) && roomY >= 0 && roomY < _rooms.GetLength(1); private void MapInit(Vector2Int roomCount) { MapSize = new Vector2Int(roomCount.x * RoomSize.x, roomCount.y * RoomSize.y); _cells = new Cell[MapSize.x, MapSize.y]; for (int x = 0; x < MapSize.x; x++) { for (int y = 0; y < MapSize.y; y++) { _cells[x, y] = new Cell(new Vector2Int(x, y)); _cellCache.Add(_cells[x, y]); } } } private void RoomInit(Vector2Int roomCount) { _rooms = new Room[roomCount.x, roomCount.y]; for (int x = 0; x < roomCount.x; x++) { for (int y = 0; y < roomCount.y; y++) { _rooms[x, y] = new Room(new Vector2Int(x, y), RoomSize, _cells); _roomCache.Add(_rooms[x, y]); } } } } } ``` </details>