부트캠프/본캠프

[내일배움캠프_2025SEP23] 모의면접 2차 스터디, EffectManager 시작

Young_A 2025. 9. 23. 23:40

목차

    모의면접 2차 스터디, EffectManager 시작


    모의면접 2차 스터디

    이번에도 모의면접을 위해 팀원들과 스터디를 했다.

    아직 문제 범위를 정해주진 않으셨지만, 지난번 1번부터 20번까지 내주셨으니 21번부터 40번까지 나눠서 5명이서 4문제씩 담당하기로 했다.

     

    내가 담당한 부분은 접은 글로 정리했다.

    더보기

    Q.가비지 컬렉터의 장점과 단점에 대해 설명해주세요.
    A.
    가비지 컬렉터(GC)는 더 이상 사용되지 않는 메모리를 자동으로 해제하여 관리하는 기술입니다
    장점으로는
    개발자가 직접 메모리를 할당하고 해제할 필요가 없으므로, 메모리를 해제하지 않아 발생하는 메모리 누수를 방지할 수 있고,
    메모리 관리에 대한 부담이 줄어들어 개발 편의성이 증가하고,
    잘못된 메모리 접근이나 이중 해제와 같은 위험을 줄여 프로그램의 안정성을 높일 수 있습니다.
    단점으로는
    GC가 동작하는 동안 프로그램 실행이 잠시 멈추거나 성능이 저하될 수 있는 'Stop-the-world'라고 불리는 성능 저하 현상이 일어날 수 있고,
    GC가 언제 동작할지 예측하기 어려워 실시간 처리나 고성능이 요구되는 애플리케이션에서는 문제가 될 수도 있습니다.
    또한 GC는 사용되지 않는 객체를 식별하고 해제하기 위해 추가적인 메모리를 사용하게 되어 메모리 사용량이 증가합니다.

    Q. 람다식(Lambda Expression)이 무엇인지 설명해주세요.
    A.
    람다식은 익명 함수를 간결하게 표현하는 방법입니다.
    코드를 더 간결하고 가독성 높게 작성할 수 있도록 돕기도 합니다.
    매개변수 => 식 또는 문장 불록 형태로 작성됩니다.
    주요 특징으로는
    불필요한 코드(예: 함수 이름, return 키워드)를 줄여 코드를 단순화하여 간결성이 좋아집니다.
    또한, C#의 LINQ와 함께 결합하여 데이터를 처리할 때 매우 유용합니다.
    델리게이트를 사용할때 객체를 생성하지 않고 바로 함수를 전달할 수 있다는 장점이 있습니다.

    Q. Update, FixedUpdate, LateUpdate의 차이점에 대해 설명해주세요.
    A.
    Update는
    매 프레임마다 호출되며,
    사용자 입력, 캐릭터 움직임, 애니메이션과 같이 프레임에 종속적인 로직을 처리하는 데 사용됩니다.
    프레임 속도에 따라 호출 횟수가 달라지므로, 물리 연산에는 적합하지 않습니다.
    (즉, 호출 빈도가 가변적임)
    FixedUpdate는
    고정된 시간 간격(기본 0.02초)마다 호출되며,
    물리 엔진 시뮬레이션(Rigidbody 관련)을 처리하는 데 사용됩니다.
    일정한 시간 간격으로 호출되므로, 물리 연산의 정확성과 안정성을 보장할 수 있습니다.
    (즉, 호출 빈도가 일정함)
    LateUpdate는
    Update 함수가 모두 실행된 후, 마지막에 호출됩니다.
    카메라 추적, 다른 객체의 위치를 따라가는 것과 같이 모든 오브젝트의 Update가 완료된 후에 실행되어야 하는 로직에 적합합니다.
    예를 들어, 플레이어의 움직임이 Update에서 처리된 후, 카메라가 그 위치를 LateUpdate에서 따라가야 부드러운 화면을 얻을 수 있습니다.

    Q. 유니티 최적화 기법은 어떤 것들이 있나요?
    A.
    최적화는 게임의 성능을 향상시키기 위해 불필요한 리소스 사용을 줄이는 과정입니다.
    우선,
    렌더링 최적화 방법으로는
    같은 재질을 사용하는 오브젝트들을 배치(Batching)하여 드로콜을 줄이는 방법,
    시야에 가려진 오브젝트는 렌더링하지 않고 GPU 부하를 줄이는 Occlusion Culling 기법,
    카메라와 가까운 오브젝트는 고품질 모델을, 먼 오브젝트는 저품질 모델을 사용하여 성능을 최적화하는 Level of Detail 방법들이 있습니다.
    메모리 최적화 방법으로는
    자주 생성하고 파괴하는 오브젝트를 미리 생성해두고 재활용하여 가비지 컬렉터의 부하를 줄일 수 있는 오브젝트 풀링 기법을 적용하거나,
    불필요한 텍스쳐, 오디오와 같은 에셋을 사용하지 않거나 압축하는 등 리소스를 관리하는 방법으로
    메모리 사용량을 줄일 수 있습니다.
    CPU 최적화 방법으로는,
    FixedUpdate를 신중히 사용하고 필요 없는 Rigidbody등을 제거하여 물리 연산을 최소화하는 방법,
    Update에서 불필요한 연산을 제거하거나 다른 함수로 분산시키는 등 Update 함수를 최적화하는 방법,
    new, Instantiate과 같이 힙 메모리를 사용하는 작업을 최소화하여 가비지 컬렉터의 호출 빈도를 줄이는 방법이 있습니다.
        Q. 최적화를 해본 적이 있나요? 없다면 어떤 최적화가 있는지 설명해주세요.
        A.
        프로젝트 중에 스테이지를 담당하게 된 적이 있습니다. 당시 스테이지는 최대 10마리의 몬스터가 존재할 수 있다는 제약이 있었습니다. 오브젝트 풀링을 통해 처치한 몬스터를 다시 Initialize하여 사용할 수 있도록 구현하였습니다. 이를 통해 Instantiate 및 Destory 함수의 호출 횟수를 최소화하여 GC 부하를 줄이고자 했습니다.
         또한, 일상적으로 코딩할 때도 Find 함수처럼 비용이 큰 함수들을 피하려고 노력합니다. Find는 게임 오브젝트의 계층 구조 전체를 탐색하기 때문에 성능에 큰 부하를 줄 수 있습니다. 주로 Awake나 Start 함수에서 필요한 컴포넌트나 오브젝트를 미리 찾아 변수로 캐싱해두는 습관을 들이고 있습니다.
        Q. 최적화에서 가장 중요한 부분은 무엇인가요?
        A.
        최적화에서 가장 중요한 것은 프로파일러를 통한 정확한 문제 진단입니다. 무작정 최적화 기법을 적용하는 것보다는 CPU, GPU, 메모리 중 어디에 부하가 걸리는지 파악하는 것이 우선입니다. 문제가 되는 부분을 정확히 찾아내야만 효율적인 최적화를 할 수 있기 때문입니다.
        Q. 최적화를 위해서 적용해본 텍스쳐 포맷이 있나요?
        A.
        아직 다양한 텍스처 포맷을 적용해본 경험은 없습니다. 개인 혹은 팀 프로젝트에서는 주로 PNG 포맷을 사용했는데, 이는 도트 그래픽 에셋이라 크기가 작아 부하가 크지 않았고, 알파 채널을 통해 투명도를 표현해야 했기 때문입니다.
        하지만 모바일 게임 최적화에서 VRAM(Video RAM) 사용량을 줄이는 것이 매우 중요하다는 것을 인지하고 있습니다. PNG와 같은 비압축 포맷은 용량은 작아도 메모리에 로드될 때 압축이 풀려 VRAM을 크게 차지하게 됩니다.
        따라서 앞으로는 각 플랫폼에 최적화된 하드웨어 압축 포맷을 적용해보고 싶습니다. 예를 들어, 안드로이드 기기에서는 ETC2나 ASTC가, iOS 기기에서는 PVRTC 포맷이 주로 사용되며, 이들이 런타임 메모리를 효율적으로 관리하는 데 도움이 된다는 것을 알고 있습니다. 만약 개발하게 될 플랫폼이 정해진다면, 해당 플랫폼에 맞는 텍스처 포맷을 적용하여 VRAM을 최적화하는 데 집중하겠습니다.
        (안드로이드는 ETC2, ASTC / iOS는 PVRTC)


    팩토리 메서드?

    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Linq;
    using UnityEngine;
    
    [Serializable]  //임시, 지워야됨.
    public class SkillSequenceNodeDataList: DataListBase<SkillSequenceNode>
    {
        /// <summary>
        /// Json 데이터로 불러오지 않기 때문에, 스킬 노드를 생성할때마다 이 곳에 추가해줘야함.
        /// todo. 하드코딩 피하고 자동 업데이트 할 수 있는 방법을 찾아볼 것. 팩토리 패턴? 같은거?
        /// </summary>
        public override void Initialize()
        {
            dataList = new List<SkillSequenceNode>();
            
            //스킬 시퀀스 노드 생성
            dataList.Add(new MetalBladeSkillSequenceNode(103001));
            dataList.Add(new HeavyDestroyerSkillSequenceNode(103002));
            dataList.Add(new ThreePointSkillSequenceNode(103003));
            dataList.Add(new EarthquakeSkillSequenceNode(103004));
            dataList.Add(new StompSkillSequenceNode(103005));
            dataList.Add(new UpperSlashSkillSequenceNode(103006));
            dataList.Add(new NormalAttackSkillSequenceNode(103000));
            dataList.Add(new WhirlWindSkillSequenceNode(103008));
            dataList.Add(new SharkSkillSequenceNode(103007));
            
            dataList.Add(new RumbleOfRuinSkillSequenceNode(103009));
    
            //일반 몬스터
            dataList.Add(new GoblinCommonAttackSkillSequenceNode(103101));
            dataList.Add(new GoblinRogueStrongAttackSkillSequenceNode(103102));
            
             
        }
        
        /// <summary>
        /// 데이터리스트에 id에 해당하는 스킬시퀀스노드가 있다면 true, 없다면 false 반환
        /// </summary>
        /// <param name="skillId"></param>
        /// <param name="skillSequenceNode"></param>
        /// <returns></returns>
        public bool TryGetSkillSequenceNode(int skillId, out SkillSequenceNode skillSequenceNode)
        {
            var tempData = dataList.FirstOrDefault(node => node.SkillId == skillId);
    
            switch (tempData)
            {
                case MetalBladeSkillSequenceNode:
                    skillSequenceNode = new MetalBladeSkillSequenceNode(skillId);
                    break;
                case EarthquakeSkillSequenceNode:
                    skillSequenceNode = new EarthquakeSkillSequenceNode(skillId);
                    break;
                case StompSkillSequenceNode:
                    skillSequenceNode = new StompSkillSequenceNode(skillId);
                    break;
                case UpperSlashSkillSequenceNode:
                    skillSequenceNode = new UpperSlashSkillSequenceNode(skillId);
                    break;
                case HeavyDestroyerSkillSequenceNode:
                    skillSequenceNode = new HeavyDestroyerSkillSequenceNode(skillId);
                    break;
                case ThreePointSkillSequenceNode:
                    skillSequenceNode = new ThreePointSkillSequenceNode(skillId);
                    break;
                case NormalAttackSkillSequenceNode:
                    skillSequenceNode = new NormalAttackSkillSequenceNode(skillId);
                    break;
                case WhirlWindSkillSequenceNode:
                    skillSequenceNode = new WhirlWindSkillSequenceNode(skillId);
                    break;
                case SharkSkillSequenceNode:
                    skillSequenceNode = new SharkSkillSequenceNode(skillId);
                    break;
                case RumbleOfRuinSkillSequenceNode:
                    skillSequenceNode = new RumbleOfRuinSkillSequenceNode(skillId);
                    break;
                case GoblinCommonAttackSkillSequenceNode:
                    skillSequenceNode = new GoblinCommonAttackSkillSequenceNode(skillId);
                    break;
                case GoblinRogueStrongAttackSkillSequenceNode:
                    skillSequenceNode = new GoblinRogueStrongAttackSkillSequenceNode(skillId);
                    break;
                default:
                    skillSequenceNode = null;
                    break;
            }
            
            if (tempData == null)
            {
                return false;
            }
            else
            {
                return true;
            }
        }
    }

    현재 스킬 노드들 MonoBehaviour나 SO를 상속 받지 않는다.

    따라서 인스펙터에 연결을 할 수가 없다.

    new 생성자를 사용해서 생성해야하는데, 형변환 때문에 저렇게 못생긴 switch 문이 나왔다.

    그래서 나중에 팩토리 패턴을 한번 살펴봐야겠다, 하고 오늘 살펴봤는데..

    switch문을 이용하는 것도 팩토리 패턴을 구현하는 방법 중 하나고,

    내가 원하는 건 리플렉션이나 Dictionary를 통해 구현하는 방법들이었다.

     

    다만, 오늘 모의면접 스터디에서 배운건 리플렉션은 성능에 부하를 줄 수 있다는 점이 신경쓰인다.

    아마 조금 더 번거로워도 Dictionary로 구현하지않을까 싶다. 

     

    아예 스킬시퀀스노드 리스트는 팩토리 패턴으로 관리하게 될 것 같다.


    Rigidbody 바닥을 뚫는 현상

    높은 곳에서 다운어택을 하면 바닥을 뚫는 현상이 있었다.
    Rigidbody의 Collision Detection을 discrete (이산) 말고 continuous (연속) 으로 바꾸면 된다.

     


    느낀점

    오늘 집중력이 너무 안좋아서 업무효율이 그렇게 좋진 않았다.

    중간에 내가 구현했던 부분들에 대한 질문이 들어온다거나 하는 경우도 많았고,

    팀원들끼리 다 같이 트러블슈팅을 하거나 기술적 의사결정을 해야하는 순간들도 있었기 때문이다.

     

    이 중에 내가 개선할 수 있는 점은,

    내가 구현하는 부분들에 대한 주석을 친절하게 달아두자는 것이다.

    다시 설명하려고 할때 보니까 주석 쓰는 부분이 많이 부족한 것 같다는 생각이 들었다. 

     

    +우수 TIL에 선정되었다!

    https://nbcamp.spartaclub.kr/blog/71123

     

    내일배움캠프 우수 TIL | Unity 11기 9월 1주차 - 내일배움캠프 블로그

    TIL 작성 방법이 궁금한가요? 내일배움캠프 수강생들의 우수 TIL을 참고해 보세요. | 내배캠 라이프

    nbcamp.spartaclub.kr

    두번째 우수 TIL 선정이라서 정말 뿌듯하고 기쁘다.

     

    한동안 TIL을 제대로 쓰지 않아서.. 아 못 받아도 할 말 없다!라고 생각했었고

    최근 들어 열심히 쓰기 시작해서 이번에는 받을만 하다!고 생각했다.

    정말 받은 걸 보니 최근 바꾼 TIL 작성 방식이 맞는 것 같아서 다행이다.


    내일 학습 할 것은 무엇인지

    EffectManager를 마무리하고 Effect 데이터를 관리할 SO와 SO list를 생성하려고 한다.

    이전에 3D Survival 프로젝트를 할때 사용했었던 커스텀 에디터 기능인 autopopulate을 적용하면 될 것 같다.

     

    그러고 남은 시간 동안은 특수 스킬 구현에 힘써야겠다.