확인 문제

using System;

public class Logger
{
    public string LogMessages { get; private set; }

    public Logger()
    {
        LogMessages = string.Empty;
    }

    public void Log(string message)
    {
        LogMessages += message + "\n";
    }
}

public class Program
{
    public static void Main()
    {
        Logger logger = new Logger();
        
        for (int i = 0; i < 10000; i++)
        {
            logger.Log("This is log message number " + i);
        }

        Console.WriteLine("Logging completed. Total log length: " + logger.LogMessages.Length);
    }
}

1. 위의 코드가 문제가 되는 이유를 메모리 관점에서 설명해주세요.

내 답:
string은 문자 하나하나가 메모리를 많이 사용하기 때문에 이를 1만번 반복하며 쓰는 것은 메모리 관리에 악영향을 끼칩니다.

모범 답:
string은 참조 타입 객체이기 때문에, 문자열을 결합할 때마다 새로운 문자열 객체가 힙 메모리에 생성된다.

많은 수의 문자열 객체가 생성되면서 GC 호출이 자주 일어날 수 있으며, 이 때문에 프로그램 성능에 영향을 미칠 수 있다.

2. 아래와 같이 string이 아닌 StringBuilder가 권장되는 이유는 무엇일까요?

public class Logger
{
    private StringBuilder logMessages;

    public Logger()
    {
        logMessages = new StringBuilder();
    }

    public void Log(string message)
    {
        logMessages.Append(message).Append("\n");
    }

    public override string ToString()
    {
        return logMessages.ToString();
    }
}

내 답:
변경 가능성(Mutability): StringBuilder의 인스턴스는 생성 후에도 내용을 변경할 수 있습니다. 이는 String 객체와 대비되는 특징으로, String은 불변(Immutable)입니다.
성능: 문자열을 반복적으로 변경하거나 추가할 때, StringBuilder는 String에 비해 더 나은 성능을 제공합니다. String 타입의 경우, 문자열을 변경할 때마다 새로운 문자열 객체가 생성되어 메모리와 성능에 부담을 줄 수 있습니다.
메모리 효율성: StringBuilder는 내부적으로 문자 배열을 사용하여 문자열을 관리합니다. 필요에 따라 이 배열의 크기를 동적으로 조정하여 메모리 사용을 최적화합니다.

모범 답:
StringBulider를 사용하면 문자열을 조합할 때 마다 새로운 변수를 생성하지 않고 결합할 수 있기 때문에 string을 사용하는 것 보다 문자열 결합에 있어 메모리를 효율적으로 사용할 수 있다.

설명 문제

1. 가비지 컬렉터란 무엇인가요?

내 답:
가비지 컬렉터는 .NET 프레임워크와 같은 관리되는 환경에서 메모리 관리를 자동으로 수행하는 시스템입니다. 프로그래머가 직접 메모리를 할당하고 해제하는 대신, 가비지 컬렉터는 동적으로 할당된 메모리(힙 메모리)에서 더 이상 사용되지 않는 객체를 자동으로 찾아내고, 그 메모리를 회수합니다. 이 과정을 통해 메모리 누수를 방지하고, 프로그램의 안정성과 성능을 향상시킵니다.

모범 답:

가비지에 대해 먼저 설명해주면 좋습니다. → 예 : 객체 저장을 위해 “힙에 할당된 메모리”가 참조되고 있는 상태가 아니라면 이를 가비지라고 판단합니다.
그 후 가비지가 일정 이상 쌓이면 GC를 수행한다라고 대답하면 됩니다.

가비지 컬렉터는 CLR의 메모리 관리 소프트웨어로, 할당할 힙 메모리 공간이 부족할 때, 가비지 컬렉터가 힙 메모리를 돌며 사용하지 않는 메모리를 회수한다.

2. 가비지 컬렉터의 장점과 단점에 대해 설명해주세요.

내 답:
장점
메모리 관리의 자동화: 개발자가 메모리 할당과 해제를 직접 관리할 필요가 없어, 메모리 관련 버그와 메모리 누수의 위험이 줄어듭니다.
안정성 향상: 가비지 컬렉터는 더 이상 사용되지 않는 객체를 안전하게 제거함으로써, 애플리케이션의 안정성을 향상시킵니다.
개발 효율성 증가: 메모리 관리에 대한 부담이 줄어들어, 개발자는 애플리케이션의 핵심 기능 개발에 더 집중할 수 있습니다.

단점
성능 오버헤드: 가비지 컬렉터가 작동하는 동안 애플리케이션의 실행이 일시적으로 중단될 수 있으며, 이는 애플리케이션의 응답성에 영향을 줄 수 있습니다. 이를 "가비지 컬렉션 퍼즈"라고 합니다.
예측 불가능한 실행 시간: 가비지 컬렉터의 실행 시점과 지속 시간은 예측하기 어렵습니다. 이는 실시간 시스템이나 응답 시간이 중요한 애플리케이션에서는 문제가 될 수 있습니다.

모범 답:

  • 직접 메모리를 해제하는 것과 GC를 비교해서 설명하는 것이 좋습니다. (마찬가지로 C#과 C++의 차이를 물어볼 때, GC를 앞세워 설명해주면 좋습니다.)
  • 장점은 휴먼 에러(Human Error)를 줄일 수 있다로 시작해 직접 메모리를 관리하면 힘든 점을 기억나는대로 설명해주면 좋습니다.
    (이미 해제한 메모리에 접근, 해제된 메모리를 또 다시 해제 등등..)
  • 단점은 가비지 컬렉션의 실행 시 오버헤드가 발생한다. GC 시점의 예측이 힘들다
    (아무것도 생각나지 않는다면, “자동으로 관리해준다고 메모리를 아예 신경쓰지 않아도 되는건 아니다”라는 뉘앙스로 말을 이어나가봅시다.)

프로그래머가 직접 메모리 해제를 안해줘도 알아서 GC가 돌면서 메모리를 해제하기 때문에 편리하다.
가비지 컬렉터가 실행되는 동안 프로그램의 성능이 일시적으로 저하될 수 있으며, 가비지 컬렉터는 힙 메모리가 부족할 때, 알아서 실행되므로 프로그래머가 실행 시점을 예측하기 어렵다.

3. 가비지 컬렉터의 세대 개념에 대해 설명해주세요.

내 답:
가비지 컬렉터의 세대 개념은 .NET 프레임워크에서 메모리 관리를 최적화하기 위해 사용되는 기술입니다. 객체들은 생성된 시간에 따라 다른 "세대"에 할당되며, 이는 가비지 컬렉터가 메모리를 더 효율적으로 관리할 수 있도록 돕습니다. .NET의 가비지 컬렉터는 세대를 기반으로 한 가비지 컬렉션을 사용하여, 더 자주 수집되어야 하는 객체와 덜 자주 수집되어야 하는 객체를 구분합니다.

모범 답:

  • 가비지 컬렉터의 세대 개념은 지원 언어마다 그 세부사항이 다르기 때문에, C#이나 유니티 회사 면접일 경우 반드시 C#의 가비지 컬렉터에 대해 이야기해주셔야 합니다.
  • C#의 가비지 컬렉터는 0,1,2 세대로 나누어져 있는데, 0→1→2세대로 변하는 과정을 설명해주면 좋은 답이 됩니다.

CLR은 메모리 구역을 나누어 메모리에서 빨리 해제될 객체와 오래도록 살아남을 것 같은 객체들을 구분한다. 이때, 0, 1, 2의 3개의 세대로 나눈다.

  • 0세대는 가비지 컬렉터를 한 번도 겪지 않은 객체들이 위치한다. 주로, 단명 객체들이 많아 가비지 컬렉터가 자주 발생한다.
  • 1세대에는 0세대를 통과하고 살아남은 객체들이 위치한다. 이 세대는 객체들이 조금 더 오래 생존할 가능성이 높다.
  • 2세대에는 가비지 컬렉션을 여러 번 겪고도 살아남은 오래된 객체들이 위치한다. 이 세대에서 가비지 컬렉션은 가장 드물게 발생한다.

4. 박싱, 언박싱을 사용할 때 주의해야 할 점은 무엇일까요?

내 답:
성능 저하: 박싱과 언박싱 과정은 메모리 할당과 추가적인 CPU 사이클을 필요로 합니다. 박싱 시에는 힙에 객체를 할당하고, 언박싱 시에는 타입 검사를 수행해야 합니다. 따라서 빈번한 박싱과 언박싱은 애플리케이션의 성능에 부정적인 영향을 줄 수 있습니다.
메모리 사용 증가: 박싱된 객체는 힙에 저장되므로, 박싱을 과도하게 사용하면 가비지 컬렉터의 부담이 증가하고 메모리 사용량이 늘어날 수 있습니다.
타입 안정성 문제: 언박싱은 명시적 캐스팅을 필요로 합니다. 잘못된 타입으로 언박싱을 시도할 경우 InvalidCastException이 발생할 수 있습니다. 따라서 언박싱을 수행할 때는 항상 올바른 타입으로 캐스팅하는지 확인해야 합니다.
값의 불변성: 박싱 과정에서 값 타입의 인스턴스는 힙에 새로운 객체로 복사됩니다. 이후 박싱된 객체의 값을 변경해도 원본 값 타입의 인스턴스에는 영향을 주지 않습니다. 이는 값의 불일치를 초래할 수 있으므로 주의가 필요합니다.

모범 답:

박싱, 언박싱은 값 형식 ↔ 참조 형식을 오가는 작업이기 때문에, “객체의 복사”가 일어나 가비지를 더 자주 발생시킨다고 대답해주면 좋습니다.

박싱, 언박싱을 하는 과정에서 데이터를 메모리의 다른 위치로 이동시키는데, 이때, 추가적인 메모리 할당 및 해제 과정에서 힙에 쓰레기 메모리가 쌓여 GC에 부하를 줄 수 있다.

5. 오브젝트 풀을 사용하면 메모리 관리에 도움이 되는 이유가 무엇일까요?

내 답:
객체 생성 및 소멸 오버헤드 감소: 객체를 생성하고 소멸시키는 과정은 메모리 할당 및 해제와 같은 비용이 드는 작업을 포함합니다. 오브젝트 풀을 사용하면 이러한 객체의 생성과 소멸이 빈번하게 발생하는 것을 방지하여, 관련 오버헤드를 줄일 수 있습니다.
메모리 단편화 감소: 객체가 반복적으로 생성되고 소멸될 때, 메모리 단편화가 발생할 수 있습니다. 오브젝트 풀을 사용하면 객체가 미리 할당되어 재사용되므로, 메모리가 보다 일관된 방식으로 사용되어 단편화를 감소시킵니다.
가비지 컬렉션 부담 감소: .NET과 같은 관리되는 환경에서는 가비지 컬렉터가 더 이상 사용되지 않는 객체를 메모리에서 제거합니다. 빈번한 객체 생성과 소멸은 가비지 컬렉터의 작업량을 증가시키며, 이는 성능 저하로 이어질 수 있습니다. 오브젝트 풀을 사용하면 객체의 재사용으로 가비지 컬렉션의 부담을 줄일 수 있습니다.
응답성 향상: 특히 실시간 시스템이나 응답 시간이 중요한 애플리케이션에서는, 객체를 즉시 사용할 수 있어야 합니다. 오브젝트 풀에서는 객체가 미리 생성되어 있으므로, 요청 시 즉각적으로 객체를 제공할 수 있어 응답성이 향상됩니다.

모범 답:

  • 프로젝트에 오브젝트 풀을 사용했는데, 왜 사용했는지 그 이유를 대답하지 못한다면 정말 큰 감점요소가 될 수 있습니다.
  • 메모리 재사용을 초점으로 알고 있는 바에 대해 최대한 이야기해주면 좋습니다.
  • 경험을 물어보는 질문이 아니지만, 자신의 경험을 적절히 녹여 설명한다면 더 좋은 답변이 될 수 있습니다.
  • 오브젝트 풀(Object Pool)
    • 객체를 미리 생성해두고 필요할 때 재사용하며, 사용이 끝난 객체는 파괴하지 않고 풀에 반환하여 다시 사용할 수 있게 하는 디자인 패턴
  • 사용 이유
    • 오브젝트를 Destroy할 때, 오브젝트는 게임에서 삭제되지만, 메모리 상으로 미세한 쓰레기 메모리들이 쌓이기 때문에 생성/ 삭제를 자주할 때, 쓰레기 메모리가 쌓여 GC 호출이 자주 일어날 수 있다.
    • 오브젝트 풀을 이용하면 오브젝트를 사용하고 삭제할 때, 삭제가 아닌 비활성화 함으로써 해당 오브젝트를 계속 재사용하고, 삭제로 발생하는 쓰레기 메모리를 최소화하여 가비지 컬렉터 호출을 줄인다.
    • 그렇다고 해서 무조건 오브젝트 풀이 좋은 것은 아니니 생성/ 삭제가 자주 일어나는 오브젝트에 한해서 사용하는 것이 좋다.

실습 문제

💡 **[EnemyPool 구현]**

오브젝트 풀을 이용하여 적을 생성하는 EnemyPool을 구현해봅시다.

  • GetEnemy 메서드에서 재사용 가능한 Enemy 객체를 가져오고 초기화하는 코드를 작성하세요.
  • ReleaseEnemy 메서드에서 사용한 Enemy 객체를 반환하고, 재사용 리스트에 추가하는 코드를 작성하세요.
using System;
using System.Collections.Generic;

public class Enemy
{
    public int HP { get; set; }
    public int Attack { get; set; }

    public Enemy(int hp, int attack)
    {
        HP = hp;
        Attack = attack;
    }

    public void Reset(int hp, int attack)
    {
        HP = hp;
        Attack = attack;
    }
}

public class EnemyPool
{
    private List<Enemy> availableEnemies = new List<Enemy>();
    private List<Enemy> usedEnemies = new List<Enemy>();

    public Enemy GetEnemy(int hp, int attack)
    {
        Enemy enemy;
        if (availableEnemies.Count > 0)
        {
            // TODO : 재사용 가능한 Enemy 객체를 가져오고 초기화
            availableEnemies.RemoveAt(availableEnemies.Count - 1);
            enemy.Reset(hp, attack);            
            //
        }
        else
        {
            enemy = new Enemy(hp, attack);
        }
        usedEnemies.Add(enemy);
        return enemy;
    }

    public void ReleaseEnemy(Enemy enemy)
    {
        // TODO : 사용한 Enemy 객체를 반환하고, 재사용 리스트에 추가
        
        //
        usedEnemies.Remove(enemy);
        availableEnemies.Add(enemy);
    }
}

public class Program
{
    public static void Main()
    {
        EnemyPool enemyPool = new EnemyPool();
        List<Enemy> currentEnemies = new List<Enemy>();

        long beforeGcCount = GC.CollectionCount(0);

        for (int frame = 0; frame < 10000; frame++)
        {
            // 일정 프레임마다 새로운 적 객체 생성
            if (frame % 10 == 0) // 10프레임마다 적 생성
            {
                Enemy newEnemy = enemyPool.GetEnemy(100, 10);
                currentEnemies.Add(newEnemy);
            }

            // 일정 조건에서 적 객체 반환
            if (frame % 20 == 0 && currentEnemies.Count > 0) // 20프레임마다 적 반환
            {
                Enemy oldEnemy = currentEnemies[0];
                currentEnemies.RemoveAt(0);
                enemyPool.ReleaseEnemy(oldEnemy);
            }
        }

        long afterGcCount = GC.CollectionCount(0);

        Console.WriteLine("Total enemies created: " + (enemyPool.usedEnemies.Count + enemyPool.availableEnemies.Count));
        Console.WriteLine("GC collection count before: " + beforeGcCount);
        Console.WriteLine("GC collection count after: " + afterGcCount);
        Console.WriteLine("GC collections occurred: " + (afterGcCount - beforeGcCount));
    }
}
profile
스터디 로그

0개의 댓글

관련 채용 정보