ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 내울배움캠프 9일차 TIL - 개인과제 완성
    TIL/C# 2024. 4. 25. 19:53

     

    [학습목표]

    C# 개인 과제 추가기능을 모두 구현하여 제출한다.

    [학습내용]

    오늘은 개인 과제 제출일이기 때문에 개인 과제에 몰두하여 마무리했다.

    아래는 오늘까지 구현한 모든 기능이다. 오늘 새롭게 구현한 것들은 파란색으로 칠했다.

     

    필수기능

    • 게임 시작 화면 구성
    • 스테이터스 보기 기능
    • 인벤토리 기능
    • 장착 관리 기능
    • 상점 기능
    • 아이템 구매 기능

    추가기능

    • 아이템 정보 클래스화
    • 아이템 정보 배열(리스트)로 관리
    • 아이템 판매 기능
    • 장착 관리 기능 개선
    • 던전 (추가)
    • 휴식 기능 (추가)
    • 레벨업 (추가)
    • 저장 / 불러오기 (추가)

    항목 수는 많지 않지만 스케일이 큰 게 끼어있기 때문에 시간이 생각보다 오래 걸렸다.

     

    던전과 휴식, 레벨업 기능은 사실 크게 어렵지 않았다. 던전의 경우 구동 방식이 상점과 크게 다르지 않고, 씬매니저와 시스템 메시지 출력 등이 이미 구현되어 있기 때문에 막히지 않고 진행됐다. 휴식기능과 레벨업 기능은 수치만 건드려주는 작업이었기에 오히려 쉬운 편이었다.

    문제는 저장/불러오기 기능 구현이었다. I/O도 다뤄야 하고, 양식도 지정된 게 없기 때문에 설계부터 시작해야했다. 이전에 자바 I/O를 다뤘던 기억을 더듬고 구글링을 조금 보태서 진행했다.

     

    우선 무엇을 저장할지 정해야 했다. 상점이나 던전 등은 저장할 필요가 없다. 그런데 캐릭터는 계속 성장하면서 변화하므로 캐릭터의 상태는 반드시 저장해야 한다. 여기에는 HP나 레벨, 골드 등이 포함된다. 그리고 인벤토리도 캐릭터 클래스 안에 넣어놨다. 그래서 캐릭터 인스턴스를 통째로 저장하기로 했다.

     

    직렬화

    인스턴스를 저장하기 위해서는 직렬화 과정이 필요하다.

    직렬화(serialization)는 지속시키거나 전송할 수 있는 형태로 개체 상태를 변환하는 프로세스다. 대칭을 이루어 반대로 진행되는 프로세스는 스트림을 개체로 변환하는 역직렬화(deserialization)다. 이 프로세스를 함께 사용하여 데이터를 쉽게 저장하고 전송할 수 있다.

     

    그리고 닷넷(.NET)에서 지원하는 직렬화가 있어서 찾아보았다. 크게 3가지로 나눌 수 있었다.

    이진 직렬화는 형식 정확도를 유지하므로 애플리케이션의 여러 호출 간에 개체 상태를 유지하는 데 유용하다. 예를 들어, 개체를 클립보드로 직렬화하면 여러 애플리케이션 간에 개체를 공유할 수 있다. 개체를 스트림, 디스크, 메모리, 네트워크 등으로 serialize할 수 있습니다.
    XML 및 SOAP 직렬화는 public 속성과 필드만 직렬화하며 형식 정확도를 유지하지 않는다. 데이터를 사용하는 애플리케이션을 제한하지 않고 데이터를 제공하거나 사용하려고 할 때 유용하다. XML은 공개 표준이기 때문에 웹을 통해 정보를 공유할 때 적합하다. SOAP도 마찬가지로 공개 표준이어서 적합하다.
    JSON 직렬화는 public 속성만 직렬화하며 형식 정확도를 유지하지 않는다. JSON은 웹을 통해 데이터를 공유할 때 적합한 공개 표준이다.

     

    나는 이 중에서 JSON 직렬화를 사용했다. 인스턴스가 제공하는 기능 자체가 필요한 건 아니었기 때문에 모든 걸 그대로 가져올 필요가 없었다고 생각했다.

    using System.Text.Json;
    
    string saveInfo = JsonSerializer.Serialize(Program.character);

     

    JSON 직렬화는 JsonSerializer의 Serialize(Object obj)를 사용하면 자동으로 된다고 한다. 다른 언어 다룰 때에는 사용자 정의 인스턴스를 다룰 때 일일이 지정해줬던 거 같은데. 그런데 아니나 다를까 문제가 하나 생겼다.

     

    ! 문제 발생 1

    캐릭터 인스턴스를 JSON 직렬화할 때 필드 변수가 포함되지 않는 문제였다. 그래서 출력해보자 빈 껍데기만 나왔다.

     

    해결

    알고보니 내가 HP나 레벨 등의 정보를 필드변수로 담아둬서 포함되지 않는 거 같았다. 그래서 문서를 더 찾아보니, [JsonInclude] 키워드를 사용해서 포함할 필드를 일일이 지정해주면 된다고 한다. 그래서 Character.cs를 아래 같이 변경해줬다.

    [JsonInclude]
    public int level, hp, gold, exp, requiredExp;
    [JsonInclude]
    public float atk, enhAtk, amr, enhAmr;
    [JsonInclude]
    public string name, job;
    [JsonInclude]
    public List<MyItem> inventory;

     

    그런데 또 Character의 필드 변수 중에 MyItem 타입의 리스트도 있어서 MyItem.cs도 변경해줘야했다.

    [JsonInclude]
    public bool isEquiped;

     

    그런데 또 또 MyItem은 Item을 상속하는 클래스이기 때문에 Item.cs도 변경해줬다.

    [JsonInclude]
    public float atk, amr;
    [JsonInclude]
    public int price;
    [JsonInclude]
    public string name, part, description;

     

    그러자 문제 없이 직렬화에 성공했다.

     

    학습

    JSON 직렬화 과정에서 필드 변수를 직렬화에 포함시켜주기 위해서는 수동으로 [JsonInclude] 키워드를 사용해줘야 한다.

     

     

    출력

    직렬화 작업이 끝났으니 결과물을 어딘가에 저장해야 했다. 제일 먼저 떠오른 건 파일로 저장하는 방식이었다. 콘솔이 꺼져도 정적인 파일로 남아있다면 사용할 수 있을 거라 생각했다.

    다행히 자바와 비슷하게 StreamWriter를 사용하면 되었다. 자바는 무슨무슨-Writer를 계속 덧씌웠던 기억이 있는데 현재 상황에서는 그렇게 하지 않아도 되는 것 같았다. C#이라서 편한 걸까, 상황이 그런 걸까?

    StreamWriter sw = new StreamWriter(filePath);
    sw.WriteLine(saveInfo);
    sw.Close();

     

    여기서 filePath를 지정해줘야 했는데, 절대경로와 상대경로 중에 나는 상대경로를 택했다. 왜냐하면 다른 사람들의 환경에서 원하지 않는 곳(예를 들면 바탕화면)에 파일을 만들 순 없기 때문이다. 그렇다면 현재 디렉터리를 알아야 했다. 그래서 현재 디렉터리에 test 파일을 만들고 어디에 생성되는지 찾아보았다.

     

    학습

    최초 현재 디렉터리는 (프로젝트 폴더)\bin\Debug\net8.0다.

     

    아무래도 콘솔이 작업되는 곳 같았다. 나는 프로젝트 폴더 아래 sav 폴더를 만들어서 이 안에 저장하고자 했다. 그래서 

    string directory = ".\\..\\..\\..\\sav";
    string filePath = directory + "\\" + fileName;

     

    이런 식으로 지정해줬다.

     

    ! 문제 발생 2

    그런데 파일이 이미 존재하거나, 디렉터리가 존재하지 않는 경우가 있었다. 그래서 이러한 문제들에 대해 대응할 수 있는 코드를 작성해줘야 했다.

     

    해결

    찾아보니 DirectoryInfo와 FileInfo라는 클래스가 있다고 한다. 이것들을 활용하면 디렉터리와 파일 정보들을 받아올 수 있어서 쉽게 해결할 수 있었다.

    DirectoryInfo di = new DirectoryInfo(directory);
    if (!di.Exists) 
        di.Create();
    FileInfo fi = new FileInfo(filePath);
    if (fi.Exists)
        fi.Delete();

     

    파일이 중복 존재하는 경우 여러 선택지가 있었는데, 나는 기존 파일을 삭제하는 방향으로 진행했다. 일종의 덮어쓰기다.

     

    입력

    이제 출력해서 저장한 파일을 다시 입력으로 읽어와야 한다. 출력의 역순으로 진행해줬다.

     StreamReader sr = new StreamReader(filePath);
     fileInfo = sr.ReadToEnd();
     sr.Close();

     

    Writer를 Reader로 바꾼 정도다. 그리고 readLine()으로 읽을 수도 있었지만, 나는 한 파일 안에 여러 줄을 저장할 게 아니었기 때문에 그냥 시원하게 ReadToEnd()를 써서 끝까지 읽었다.

     

    역직렬화

    입력도 되었으니 역직렬화를 해서 다시 인스턴스로 만들어야 했다. 역직렬화는 되게 간단했다. 직렬화 때처럼 수동으로 지정해줄 것도 없었고, 그냥 적기만 하면 자동으로 되었다.

    Program.character = JsonSerializer.Deserialize<Character>(fileInfo);

     

    C# 최고!

     

     

    참고자료

    구글을 뒤져보다가 변수 뒤에 물음표(?)가 붙는 게 보였다. 이게 뭘까 궁금해서 찾아봤다.

    string? str = "This is Nullable";
    변수형 뒤에 붙는 ?는 Nullable의 뜻이다.
    변수는 기본적으로 그 값이 Null인 것을 허용하지 않는다. 그러나 Nullable로 만들어주면, Null을 허용한다. 다만 그것을 사용하는데 있어서는 당연히 알잘딱 해야 한다.

     

    코드를 작성하다보면 계속 초록줄이 뜨면서 Nullable 어쩌고 하는 것이 보였다. 크게 문제되지 않아서 급한대로 넘겼는데, 이 참에 알아봐서 좋았다.

     

    [결과물]

    깃허브 리포지터리

     

    GitHub - JNUSYJ/SpartaDungeonGame

    Contribute to JNUSYJ/SpartaDungeonGame development by creating an account on GitHub.

    github.com

     

    [회고]

    개인 과제는 성공적으로 마무리됐다. 다만 주석을 달아놓지 않아서 조금 마음이 쓰인다. 시간 나면 주석도 달아야겠다. 다음주에도 C# 주차이기 때문에 내일과 다음주에는 C# 강의를 더 수강하고 그곳에 올라오는 문제도 풀어봐야겠다.