일반적으로 쓰는 정렬 알고리즘도 참 많은게 있다.

 

그중, 흔히 말하는 시간 복잡도 O(n²) 정렬들은 구현도 간단하여 자주 사용하는 편인데, 정작 각 정렬에 대한 차이를 명확하게 답변하지 못할때가 종종 있어서 복습 겸 정리를 좀 할까 싶다.

 

 

 

1. 해당 문서의 예시 코드는 C/C++ 기반으로 작성되었습니다.

2. 모든 설명은 오름차순을 기준으로 합니다.


삽입 정렬


삽입정렬은 "맨 처음부터 진행하면서 내 위치일 것 같은곳으로 삽입" 하는 정렬이다.

예를들어, [5 3 2 1 4] 의 순서로 자료가 있다고 하면

 

시작 - [5 3 2 1 4]

1회차 - [5 3 2 1 4] --> [3 5 2 1 4]

2회차 - [3 5 2 1 4] --> [2 3 5 1 4]

3회차 - [2 3 5 1 4] --> [1 2 3 5 4]

4회차 - [1 2 3 5 4] --> [1 2 3 4 5]

종료 - [1 2 3 4 5]

 

의 방식으로 정렬이 이루어진다.

 

삽입 정렬의 특징이자 문제점이, "삽입" 방식으로 이루어지다보니 중간에 데이터를 삽입하면 뒤로 밀어버려야 한다는 특징이 있는데..

 

코드를 보면서 설명하도록 하겠다.

 

void Insertion_Sort(int *data, int n)	//data = int 동적배열, n = 배열 길이
{
	int i, j;
    int temp;		//임시 기억용 변수
    
    for (i = 1 ; i < n ; i++)  //2번째 원소부터 검사하므로 index가 1부터 시작함.
    {
    	j = i;
    	temp = data[i];
        
        while (--j >= 0 && data[j] > temp)	//검사중인 원소부터
        {					//0번 인덱스 원소까지
        	data[j+1] = data[j];		//한칸씩 뒤로 당김
        }					//* while문 최초 실행때 [j+1] 원소는 temp에 들어간 원소와 같다.
        data[j+1] = temp;			// j = i = 1일때, j + 1은 0이 될 수 있다.
    }
}

 

흐름은 다음과 같다.

 

시작을 i 번 원소부터 했을때 

1. [i - 1] 원소가 [i] 보다 클때 = i 원소의 값을 [i - 1]로 바꾼다. 

2. [i - 1] 원소가 [i] 보다 작을때 = 값을 바꾸지 않고 종료한다.

3. while 루프 종료시 = 최종 위치에 임시값을 삽입한다. (1, 2번을 수행하면서 생긴 "중복 값" (=빈 자리) 에 삽입)

 

 

여기 나오는 모든 정렬들이 그렇겠지만, 삽입정렬도 N번째 값 정렬 시 N-1만큼의 순회를 돌아야 하므로,
시간 복잡도는 O (N(N-1)) = O(n² - n) = O(n²) 로 수렴한다.

 

구현이 간단하고, 나름 빠른 편 (선택 정렬과 버블정렬은 순수하게 O(n²) 의 시간 복잡도를 가진다) 이나, 배열이 길어질수록 효율이 떨어진다는 단점이 존재한다.

 

이미 정렬이 되어있는 경우에는 비교를 제외한 추가적인 연산이 필요 없으므로 최선의 케이스가 나오며, 이때 시간 복잡도는 O(n) 이 나온다.

 

 

 

 

 

 

선택 정렬


선택 정렬은 "리스트의 최소값부터 정렬하는" 알고리즘이다.

 

삽입정렬과 헷깔릴 수 있는데, 선택정렬은 N번째 자료를 정렬시에 N-1번까지는 이미 정렬이 완전히 끝난 상태라는것이 가장 큰 차이. (위에도 언급했지만 삽입정렬은 N번째 자료 정렬시 정렬이 끝난다는 보장을 할 수 없다.)

 

선택 정렬은 다음과 같이 작동한다.

 

시작 - [5 3 2 1 4]

1회차 - [5 3 2 1 4] --> [1 3 2 5 4]

2회차 - [1 3 2 5 4] --> [1 2 3 5 4]

3회차 - [1 2 3 5 4] --> [1 2 3 5 4]  // 이미 순서가 맞으므로 추가 정렬 X

4회차 - [1 2 3 5 4] --> [1 2 3 4 5]

종료 - [1 2 3 4 5]

 

 

선택 정렬은 N번째 자료 정렬시 그 앞까지 (N-1까지)는 정렬이 완료되었다는 보장을 할 수 있는데, 현재 값이 "어느 위치인지" 알기 위해서 결국 모든 자료를 검사해야한다는 불상사가 발생한다.

 

이게 무슨소리냐면... 시간 복잡도가 어떤 상황에서든 O(n²) 라는 것이다.

삽입 정렬이나 버블정렬의 경우에는, 비교하는 대상과 본인이 정렬이 되어있다면 추가적인 연산을 하지 않는데 선택정렬은 그런거 모르겠고 이미 끝난 정렬도 함수 안에서 정렬을 수행한다는 뜻이다.

 

선택정렬은 "다음 최소값"이 어떤 값인지 알아야 하는 문제가 있어, 삽입정렬이나 버블정렬보다는 로직이 조금 복잡한 편이다.

 

void SelectionSort(int *data, int n)
{
	int i, j, indexMin, temp;		//최소값 인덱스와 임시값 변수 2종이 추가로 필요함.
    
    for (i = 0 ; i < n-1 ; i++)
    {
    	indexMin = i;
        
        for (j = i+1 ; j < n ; j++)
        {
        	//해당 for문은 정렬되지 않은 구역에서 "최소값"을 찾는 로직이다.
            
        	if (data[j] < data[indexMin])
            {
            	indexMin = j;				
            }
        }
        temp = data[indexMin];	//찾아낸 최소값을 temp에 저장.
        data [indexMin] = data[i];	//위치 교환
        data[i] = temp;	//temp값을 위치에 저장
    }    
}

 

 

흐름은 다음과 같다.

 

시작을 i 번 원소부터 했을때 

1. i번 부터 N번까지 데이터 중 최소값을 찾는다.

2. 최소값이 발견되면 해당 값의 인덱스를 기록한다.

3. 최소값을 임시 변수에 저장한다.

4. i번 값과 최소값이 위치한 인덱스의 값을 교체한다.

5. i번 값에 임시 변수에 저장된 값을 집어넣는다.

 

해당 과정은 값이 이미 정렬이 되어있던, 아니던 상관없이 작동한다. 그러니까 시간 복잡도가 그모양이지

 

 

 

버블 정렬


참 자주 얘기하게 되는 정렬 중 하나인 버블 정렬이다.

 

버블 정렬도 길게 설명하면 어지러운 얘기가 많지만, 요약하면 단 한가지로 요약 가능하다.

 

N번째와 N+1번째가 정렬되어 있지 않으면 교체, 정렬 되어있으면 넘어간다.

 

이 문장이 바로 버블 정렬의 본질이라고도 볼 수 있겠다..

 

 

버블 정렬은 다음과 같이 작동한다.

 

시작 - [5 3 2 1 4]

1회차 - [5 3 2 1 4] -> [3 5 2 1 4] -> [3 2 5 1 4] -> [3 2 1 5 4] -> [3 2 1 4 5]

2회차 - [3 2 1 4 5] -> [2 3 1 4 5] -> [2 1 3 4 5] -> [2 1 3 4 5] // 3이 4보다 작으므로 종료

3회차 - [2 1 3 4 5] -> [1 2 3 4 5] -> [1 2 3 4 5] // 2가 3보다 작으므로 종료

종료 - [1 2 3 4 5]

 

void BubbleSort (int *data, int n)
{
	int i, j, temp;
    for (i = 0 ; i < n-1 ; i++)
    {
    	for (j = 0 ; j < n-1-i ; j++)		//이미 정렬된 후위 i번은 확인 할 필요 없음.
        {
        	if (data[j+1] < data[j])
            {
            	temp = data[j+1];
                data[j+1] = data[j];
                data[j] = temp;
            }        
        }    
    }
}

 

흐름은 다음과 같다.

 

시작을 i 번 원소부터 했을때 

1. i번 원소와 i+1원소를 비교한다.

2. i번 원소가 i+1 보다 크면 둘을 교환한다.

3. i번 윈소가 i+1 보다 작거나 같으면 교환하지 않는다.

4. 1~3을 n번 반복한다.

 

 

 

 

 

 

요약


 

  삽입 정렬 선택 정렬 버블 정렬
최악의 경우 O(n²) 비교 및 교환 O(n²) 비교, O(n) 교환 O(n²) 비교, O(n²) 교환
최선의 경우 (순정렬인 경우) O(n) 비교, O(1) 교환 (미실시) O(n²) 비교, O(n) 교환 O(n) 비교, O(1) 교환 (미실시)
평균인 경우 O(n²) 비교 및 교환 O(n²) 비교, O(n) 교환 O(n²) 비교, O(n²) 교환

 

 

 

 

자주 쓰이지만, 그렇다고 설명하려고 하면 종종 헷깔리는 O(n²) 3종류에 대해 간단히 비교해보았다.

 

 

최근 면접자리에서 짐벌락 관련 질문이 나온적이 있다.

 

정확히는, 내가 경력기술서에 작성한 내용 중

 

- 짐벌락 이슈에 대응하기 위해, 회전 정보 획득시 GetActorRotation이 아닌 GetComponentRotation 함수를 Root Component를 대상으로 호출하여 이용함.

 

라는 문장에 대한 질문이었는데, 내가 답변을 좀 에매모호하게 한거 같아서 정리를 좀 하고자 한다.

 

 


짐벌락?

 

짐벌락(Gimbal lock) 은 말 그대로 짐벌 (물체가 회전 가능하도록 중심축을 가진 물체)에 락이 걸린 상황을 의미한다.

 

보통, 3D 회전은 오일러 좌표계 (혹은 오일러 각 / Eular Angle)을 따르는데, 이때 3개의 축을 이용한다.

 

각각을 Roll, Pitch, Yaw로 표현하고, 보통은 각각의 수치가 X축 회전, Y축 회전, Z축 회전에 대응한다.

 

 

 

그래서 짐벌락은 언제, 왜 생기는가.

 

간단히 요약하는 짐벌락

 

 

간단히 요약하면, 축이 겹쳐서 발생하는 현상이다.

 

물체가 회전할때 명심해야 할 것이 있는데, 회전축이 회전할때는 다른 축의 회전에 영향을 받는다.

 

특히, 명심해야 하는 사항이 회전에는 순서가 있고, 순서에 따라 다른 결과값을 갖고온다는 점이다.

 

이러다보니 발생하는 문제가 바로 짐벌락. 임의의 축을 기준으로 회전했을때 회전한 축이 아닌 다른 축이 겹치는 현상이 발생하는데, 이렇게 되면 특정 축이 연산에 전혀 영향을 못주게 되면 문제가 발생한다.

 

예시로, X = 빨강, Y = 초록, Z = 파랑이라 생각해보자.

예를들어, X, Y축이 겹치는 현상이 발생했다면,  X축을 회전하던, Y축을 회전하던 결국 Z축을 기준으로 똑같이 빙빙 도는 결과가 발생한다.

 

짐벌락이 문제가 되는 경우는, 해당 상황과 같이 특정 축이 락이 걸려버린경우 해당 축의 연산값이 전혀 영향을 못 미치게 된다는것인데, 이로 인해 정상적인 회전을 보장할 수가 없게된다.

 

 

 

쿼터니언

 

선형대수학에서는 오일러 각을 이용할때 생기는 이러한 문제를 해결하기 위해 4개의 축을 이용하는 쿼터니언 이라는것을 이용하여 연산을 한다.

 

쿼터니언 표현식이 좀 많은데, 제일 많이 쓰는 식은

 

q = xi + yj + zk + w

 

의 식으로 기억하고 있다. 표현대로 각각 x축, y축, z축과.. 가상의 w축을 포함하여 4개의 축을 이용한다.

 

이를 이용해서 회전 처리를 할때 짐벌락이 걸리지 않도록 하는것인데... 쿼터니언 설명 자체는 다음에 필요하면 하도록 하겠다.

 

 


언리얼에서는

 

언리얼에서도 당연히 짐벌락을 막기 위해 많은 방법을 선택한다.

 

주로 사용되는 방식은 2가지인데,

 

첫번째. 회전시 쿼터니언을 사용한다. 

두번째. 회전값 표현시 축 분리를 한다.

 

 

우선 첫번째부터 설명하면, 언리얼에서 물체의 회전 및 위치, 스케일을 가지고 있는 "FTransform" 구조체를 뜯어보면 바로 답이 나온다.

 

protected:
	/** Rotation of this transformation, as a quaternion */
	TPersistentVectorRegisterType<T> Rotation;
	/** Translation of this transformation, as a vector */
	TPersistentVectorRegisterType<T> Translation;
	/** 3D scale (always applied in local space) as a vector */
	TPersistentVectorRegisterType<T> Scale3D;
public:
	/**
	 * The identity transformation (Rotation = TQuat<T>::Identity, Translation = TVector<T>::ZeroVector, Scale3D = (1,1,1))
	 */
	CORE_API static const TTransform<T> Identity;

그냥 대놓고 쿼터니언으로 변환해서 쓴다고 얘기를 한다.

 

언리얼에선 TQuat라는 쿼터니언 자료형이 있는데, 내부적으로는 해당 값을 사용하고, 오일러 좌표계로 표현되는 FRotator 값이 내부 함수에서 쿼터니언으로 변환되어 들어온다.

 

	FORCEINLINE explicit TTransform(const TRotator<T>& InRotation) 
	{
		TQuat<T> InQuatRotation = TQuat<T>::MakeFromRotator(InRotation);
		// Rotation = InRotation
		Rotation =  VectorLoadAligned( &InQuatRotation );
		// Translation = {0,0,0,0)
		Translation = GlobalVectorConstants::FloatZero;
		// Scale3D = {1,1,1,0);
		Scale3D = GlobalVectorConstants::Float1110;

		DiagnosticCheckNaN_All();
	}

 

 

두번째가 조금 설명하기 까다로운데,
우선 설명해야할것은, 쿼터니언 회전값은 해당 식만 보고는 이게 어떤 회전값을 가지고 있는지 바로 알기가 힘들다.
물론, FQuat와 FRotator를 변환해주는 함수가 없다는 뜻은 아니고, 쿼터니언 값만 가지고 원하는 오일러 각으로 변환하기가 쉽지 않다는 뜻이다. (물론, 지금은 시키면 할 수 있을거 같긴한데)

 

아무튼 그런 이유로, 언리얼 에디터에서는 회전값을 쿼터니언이 아닌 오일러 좌표계로 표현한다.

하지만, 오일러 좌표계를 이용하면 짐벌락을 피하기가 쉽지 않은데, 이를 "자동으로" 각도를 분할시키는 방식으로 해결하려고 했다.

 

무슨소리냐

 

예를들어, 멀쩡한 물체의 각도를 회전시키면

 

처음에는 0,0,0임

 

Y축 (Pitch)를 회전시키려고 하면

 

짜잔! 건들지도 않은 Yaw도 바뀌어버려요!

 

이처럼, 짐벌락을 피하기 위해 임의의 각을 연산하여 추가로 회전시키는 로직이 들어간다.

 

이러한 연산이 일반적인 회전 처리에서는 당연히 도움이 되지만...

회전이 들어간 이후 "인위적으로 회전값을 변경해야 하는 경우"에 문제가 발생했다.


그래서 뭐 한건데?


문제가 뭐였나면요...

 

 

위에서 말한것에서 이어서, 언리얼에서는 짐벌락을 피하기 위해 임의의 각 회전을 추가로 하는 경우가 있다고 했다.

하지만, 다음과 같은 경우에서 문제가 발생했었다.

 

 

예를들어, 특정 이동에서 X축값 (Roll)이 회전이 들어간 경우, Roll값을 0으로 만들고싶으면 Roll을 0으로 만드는 회전값 오버라이드를 하면 그만일것이다.

 

처음엔 당연히 그렇게 생각했다. 하지만 Roll을 회전 시킨 결과가 과연 Roll에만 있을까?

 

여기서 해당 회전 수치를 보장 할 수 없는 문제가 생겨버린다. 내가 임의의 축을 0으로 돌렸을때, 나머지 회전값은 내가 원하던 회전이 맞는가?

 

여기서 의문이 들 수 있다.

첫번째. "그냥 모든 축을 0으로 바꾸면 안됐나요?"

두번째. "내가 필요한 회전축을 제외하고 0으로 만들면 안되나요?"

 

둘다 답은 X다. 당연히 첫번째는 말도 안되고, 두번째의 경우, 처음 회전한 Yaw 값을 유지하는것이 목적이었는데
(언리얼에서 캐릭터를 조금이라도 만져보면 알겠지만, 서 있는 상태에서 회전은 Yaw값을 사용하여 움직인다)

 

임의의 축에서 회전이 발생 -> 짐벌락을 피하겠다고 언리얼에서 자체적으로 회전 처리 -> Yaw값에 손상 -> 나머지 회전값을 처리해도 Yaw값이 원하는 회전값이 아니게 되는 문제가 발생

 

이라는 문제가 발생해버리고 있었다.

 

 

 


해결방안

 

우선, Actor에서 회전값을 받아오는 GetActorRotation() 함수는 다음과 같은 구조로 작동한다.

template<class T>
static FORCEINLINE FRotator TemplateGetActorRotation(const T* RootComponent)
{
	return (RootComponent != nullptr) ? RootComponent->GetComponentRotation() : FRotator::ZeroRotator;
}

//---

/** Return rotation of the component, in world space */
FORCEINLINE FRotator GetComponentRotation() const
{
	return WorldRotationCache.NormalizedQuatToRotator(GetComponentTransform().GetRotation());
}

 

보통은 RootComponent의 회전값을 갖고와서 리턴해주는 방식인데, 당연히 대체로는 문제가 없다.

 

그런데 갖고오는 부분 어딘가에서 문제가 생기는지, RootComponent의 회전값과 다른 무언가를 갖고오는 현상이 있었는데, 이를 막기 위해 RootComponent에서 갖고오는 (정확히는 엔진 코멘트에 "해당 값은 RootComponent의 값과 동일함" 이라는 코멘트가 있는 부분이 있었다...) 방안을 선택했는데...

 

.....근데 분명 작업할때는 그런 코멘트를 본 기억이 있는데 (심지어 내부 문서에도 다 기록해놨다) 지금와서 찾으려니까 안보인다...???

 

그냥 해당 기록은 안쓰는거로 해야할거같다... 증거가 없으면 못쓰지요

 

사실 이제와서 해결하려면 그냥 쿼터니언 연산으로 해결하면 되긴 하는데 (...)

쿼터니언 연산으로 X축 성분, Y축 성분 제거하고 Z축 성분만 리턴해주면 될거다 아마도...

 

 


자료 출처

 

1.https://en.wikipedia.org/wiki/Euler_angles

2.https://en.wikipedia.org/wiki/Gimbal_lock

 

곰방와. 로켓곰군입니다.

 

 

일기라는걸 제대로 써본지 대충 10여년이 지나서 드디어 새롭게 일기라고 볼 수 있는 회고글을 써보려고 합니다.

 

일단..... 지금까지 제가 과거에 대해 너무 회피성으로 대응해왔던것도 있고 당연히 그게 안좋은거인건 잘 알고 문제에 직접 직면해야 현재 문제를 좀 이겨낼 수 있을거같아 좀 싸질러볼까 합니다.

 

 

지금까지 있었던 일


24년 4월 전까지 있었던 일

 

1. 직장 생활

1) 4월 전까지 있었던 일

알만한 지인들은 알고 있었겠지만, 첫 회사로 P사에 들어갔다가 여러가지 사정으로 인해 생각보다 일찍 나오게 되고, 한 4개월쯤... 방황하다 운좋게도 불러준 회사에서 언리얼을 배우면서 약 2년정도 있었다.

비공개 온라인 프로젝트에서 배우고 털려가면서 열심히 일을 해 나가고 있었다고 생각했... 으나...

 

2) 4월

정확히는 3월 중순, 팀 리더의 호출이 있었다.

간단히 요약하면, 회사 사정이 어려우니 팀이 정리될것이라는 통보......

 

저는 그만 기절해버리고 말았습니다...

나에게도 언젠간 올 상황이라 생각했지만... 이렇게 빠를줄은 몰랐지!

 

원래는 3월 되자마자 나가게 될 상황이었다고 했으나, 적당히 협상을 통해 4월 말까지 전부 채우는것으로 하고, 남은 휴가를 소비하고 5월 되자마자 퇴사하는것으로 확정.

그나마 다행스러운건, 회사측에서 딱히 억지 부리지 않고, 오히려 굉장히 잘 마무리지어주셔서 퇴직금 + 실업급여까지 전부 받고 나오게 되었다.

물론 얼마 없던 퇴직금은 죄다 카드값으로 나가버렸지만

 

3) 5월 직후

실업급여는 다행스럽게도 받을 수 있는 최대한도로 받게 되어서, 11월까지의 유예기간이 생기긴 했다.

그때까지는 굶지는 않을 수 있다는것.....

물론 이때까지만 해도 나는 금방 이직에 성공할 줄 알았다...

왜냐, 놀면서 일한것도 아니고 준비를 나름 한거라고 생각해서 "아 이정도면 금방 가겠지?" 했으나

 

내가 간과하던 세가지 문제가 있었다.

 

1. 내가 퇴사하던 시점은 공고가 적게 나오는 시즌.
보통 3, 9월쯤이 공고가 많이 나오는 편인데 (3월 = 연협에 불만을 가진 경력 이직이 다수, 9월 = 공채 다수) 하필 3월 공고가 거의 끝나가는 시점에 나오다 보니...

넣을 수 있는 공고 자체가 정말 희박했다. 사실 이 글을 쓰는 지금도 현황은 비슷하지만..

 

 

대충 설명을 하자면...

클라이언트 전체 -> 언리얼 프로그래머 한정

 

애초에 공고 자체도 적고, 언리얼 프로그래머로 줄이면 거의 20% 이하로 떨어진다.

 

저기에 대체로 경력직, TA 등으로 필터링이 걸리면.......

 

공고가 없잖아!!!

 

그래서 한달에 끽해야 10개 쓰면 많이 쓰는...? 그런 수준이었다.

 

2. 자리가 있다고 불러주긴 할까?

언제나 하는 소리. 서류를 막 돌리더라도 면접에 불러주는건 커녕 합/불합 통보라도 주는데가 많지가 않다는게 문제

 

그래도, 이력서랑 경력기술서를 최대한 다듬어가다보니 서류 합격률이 늘........ 어나는거 같기도 하고... 아닌거 같기도 하고... 모르겠다.

 

3. 님 그래서 준비는 끝남?

이게 제일 중요했는데, 현재 와서 생긴 결론 = 아직 준비가 덜됐다.

1리단님...

 

아직 너무 헛점이 많다. 생각을 안하고 작업해온것도 아니긴 한데... 깊은 고민을 많이 하지 않았던게 티가 나는건지, 자꾸 미끄러지고 있다.


좀 제대로 준비를 할 필요가 있어보임.

 

 

 

4) 그 이후...?

하지만 다행스럽게도, 면접 자체는 몇번 잡혔었다.

거기다, 이름만 부르면 다 아는 대기업도 포함되어있었고, 대기업 계열사 및 관련 업체들 몇군데를 포함해서 꽤 많은 면접이 잡혔었다.

면접 5곳

 

하지만... 이렇게 얘기하면 대충 눈치챘겠지만...

다 떨어졌다! (하나는 좀 다른 상황이 있긴 했는데, 따로 항목을 팔 수준이라 그냥 다 떨어졌다고 치자)

 

심지어 저 면접 5번중에 "아쉽네..." 하고 끝낸 면접이 단 한번, 나머지는 전부 "왜 이렇게 못봤지" 싶은 면접들 뿐이었다.

 

그러게 진작 잘하지

심지어 K사같은 경우에는... 면접 전 테스트에 나온 문제를 면접때 물어봤는데, 이걸 제대로 복기안한채로 들어가면서 생긴 대참사가 발생하고 말았다... 솔직히 그거만 아니었어도 가능성이 있었을거라 생각은 하는데 결국엔 스스로 불러온 재앙이 아닐까 싶은 생각.

 

그때 나온 문제가 바로 이 문제다.

https://locketgoma.tistory.com/77

 

단방향 링크드 리스트 뒤집기 (C/C++)

최근에 다들 이름만 들어도 알 회사에 면접을 볼 일이 있었다.그 전, 입사 테스트로 단방향 링크드 리스트 뒤집기라는 과제를 받았었는데, 손으로 짜려다가 머리가 고장나버리는 바람에 제대로

locketgoma.tistory.com

 

 

 

아무튼, 스스로를 좀 돌아보면 "이게 맞니 곰청아..." 하고 스스로를 질책할 수 밖에 없었다...

제정신이세요? 취업 안해요?

 

 

 


2. 운동

1) 시즌 N번째 다이어트

회사를 쉬면서, 이왕 이렇게 된거 제대로 다이어틀 하고자 하고 5월부터 본격적으로 헬스장을 나가기 시작했다...

 

생각보다 좀 자주는 안가고 있지만 (그래도 일주일 2~3번은 꼭 간다) 일단은 운동 하는 습관을 잡는걸 목표로 하고 있다.

 

 

 

2) 그래서 빠지고는 있으신가요?

근데 문제가 하나 있다. 운동을... 안하는건... 아닌거 같고... 식단도.. 조절을 하는 거 같긴... 한데

안빠진다?

왜죠?

 

인바디를 한달마다 재고 있는데, 인바디 수치상으로는 개선이 되고 있긴 하지만 (인바디 점수가 7점 올랐다)

전체적은 체중 숫자 자체는 변화가 없는 수준이라, 뭔가 이건 아닌거 같아서 다음주에 병원갈때 물어볼 예정이다.

 

...가능하다면 삭센다라도 처방받아서 써볼까 싶은 생각

 

 

 

3) 앞으로의 목표

정확한 숫자를 말할 수는 없지만, 올해까지는 20%정도 감량이 목표였다.

근데 20%는... 슬슬 힘든거 같고, 한 10~15%정도를 목표로 하고 있다.

 

그래서 우선 이번달은 두가지에 제한을 걸었다.

하나. 튀김 금지

둘. 배달 금지 (뭐 먹고싶으면 직접 가서 먹기)

 

물론, 야식 금지는 기본이다.

 

아직까지는 잘 참고 있는데, 과연 9월 끝날때까지 버틸 수 있을까?

 


3. 커피챗, 사람 만나기

1) 어쩌다 이렇게 된거지?

정확히 언제부터인지는 모르겠는데 어느 시점부터 트위터에 개발하는 트친들이 많이 늘었다.

인프런에서 열었던 인프콘 컨퍼런스와 같은 날짜에 했던 출판사 "인사이트" 에서 제공하는 단체 커피챗 (aka. 아웃프콘)에서 다양한 사람들을 만났고, 거기서도 추가로 몇몇 개발자 분들을 새로 알게 되었다.

 

2) 휘발성 인연보다는 가늘지만 길게라도!

원래 성격이 사람과 만나는걸 좋아해서 그런건진 몰라도, 대화를 나누다가 좀 맞는거 같은 분들에게 제의를 해서 정기 커피챗 + 기술 교류를 위한 단체채팅방을 만들었다.

 

지금은 본인 포함 11명 정도 있고, 최대 18~20명정도까지를 목표로 하고 있다.

 

3) 커피챗 커피챗

커피챗을 몇번 나가게 되었다.

지금까지는 8월에 2분 정도를 만난 정도고, 9월에 한분 더 만날 예정.

서울역에서 커피챗할때 사진. 만나신 분이 알고보니 동갑이라 의외였다 (!)

 

고향인 대구에서 만난 분과의 커피챗. 도움을 드릴 수 있을까 싶어 아는 지인에게 소개시켜드렸다.

 

 

뭐 말이 커피챗 (= 간단히 커피를 하면서 하는 개발 대화)이지, 사실상 친구/지인 만나서 밥먹고 간단히 노는 정도의 평범한 모임 정도로 대하고 있다.

만나게 되는 분들이 업종이 겹치는 경우가 많지가 않고, 단순히 비즈니스적인 관계보다는 편하게 대화할 수 있는 업계 지인을 원해서 그런거 같기도 하다.

 

4) 그냥 사람을 많이 만나는 편

애초에 지인들에게 밥 사주는걸 좋아하는 편이라 꽤 많은 사람들을 추가로 알게 되고 많이 만난것 같다.

 

전혀 다른 계열의 사람들을 좀 알게 되고 하다보니 시야가 넓어지는 느낌이 드는거같기도 하고 아닌거같기도 하고

 

 

 

5) 정기 모임을 가자! 아니면 만들자!

트위터에서 발견한 회고 모임에 합류해서 회고 다닌지 대충 3개월쯤 된거같다.

튜링의 사과 라는 뚝섬역의 개발자를 위한... 뭐라고 해야하지? 스터디카페? 그런곳에서 매달마자 주기적으로 모여서 회고를 진행한다.

 

당신이 아는 그 "엘런 튜링"의 사과를 의미하는것이 맞습니다.

 

 

그리고 위에서 말한 단체 채팅방에서도 주기적으로 (일단은 분기별 1회 정도로 생각중이다) 만나서 교류를 해볼까 싶다.

 

 


4. 그 외 이야기

1) 자기계발은 멈출 수 없다.

면접을 위한 공부, 운동, 사람 만나기

다 중요하지만 결국에는 게임 프로그래머는 "게임 프로그래밍이 재밌어야 오래 할 수 있다."

 

마침, 아는 기획자 지인분이 (요즘은 무려 모 대학교에서 강사로 계신다..) 같이 작업 하자는 제의를 주셔서 열심히 게임 개발 작업중이다.

 

몬스터가 쫒아와요

 

멘탈이 좀 많이 흔들려서 그런건지, 진도가 좀 안나가고 있었는데 추석 전까지 좀 빡빡하게 진행해서 추석 이후에는 본 작업을 진행 할 수 있도록 하는것이 목표...

 

2) 최대한 고립되지 않기

언제나 그랬듯이 멘탈이 흔들리기 시작하면 고립되고, 고립되면 될수록 상태가 안좋아지는 악순환이 발생한다.

이번에도 그런 악성 피드백으로 인해 면접을 와르르 조져먹은 느낌이라, 최대한 사람들을 만나고 밖에 나가고 운동도 하고 해 나갈 예정이다.

 

3) 뜬금없이 식물을 키우고 있습니다.

바질... 씨앗부터 키우다가 장마 맞고 다 죽어서 모종 사서 키운지 1.5개월차

 

바질과 로즈마리를 키우기 시작했습니다.

 

바질은 씨앗부터 키우려고 했으나 장마때 비 그대로 맞췄더니 과습으로 인해 다 죽어버렸고, 모종을 사서 새로 심어놨다

아직 먹거나 장식할만한 정도의 상태는 아니지만 한 10월 이후부터는 가능하지않을까? 싶다.

 

그리고 로즈마리...

 

죽...여줘...

로즈마리도 파종을 해서 시도했는데

 

5개 -> 4개 -> 3개 -> 2개 순으로 죽어나가다가

딱 하나만 살아남았다.

그마저도 웃자라기만 하고 생장을 하지 않아서 고민하다 결국 식물등까지 사서 쓰고 있다...

 

안 죽었으면 좋겠다... 오래오래 키우게

 


9월달 목표

 

1. 면접 + 커리어 관련 이야기

1) 면접 피드백 및 앞으로의 목표.

9월 2일에 한군데 면접을 더 봤었다.

거기서 지금 회고를 하는 계기였던 "영혼까지 털리는 상황" 이 펼쳐졌었는데, 적당히 요약을 하면 "기초를 다 까먹은건 아닌가?" 라는 상황인것.

 

물론, 해당 질문들을 다시 복기해보면 대부분 정답이긴 했는데, 면접관에게 피드백 받은 내용 중 "너무 떠시는것 같다" 라는 표현이 나온만큼... 기초가 좀 부족한것 같으니 기초부터 다시 하고자 마음을 먹었다.

 

우선은 CS 기초부터 하고, 다시 언리얼 기초, 경력기술서 내용 보충 하는 식으로 해나가지 않을까 싶다.

 

목표는 추석 전까지 다 끝내는것.

 

2) 각종 모임 나가기

튜사콘 (튜링의 사과에서 하는 미니 컨퍼런스)에 참여해서 지식 습득 및 교류를 해나가고자 한다.

 

프로그래머는.. 움직여야 제자리를 유지할 수 있으니까, 뛰지는 못하더라도 최소한 걷기라도 꾸준히 하려고 한다.

 

3) 다음 면접은.... 제발....!

총  5번의 면접을 통해 내가 뭐가 부족한지 명확해졌으니까, 다음에는 진짜 실수 안하고 후회하지 않도록 준비를 착실히 해 나갈것...

 

4) 우선은 추석전까지는 준비부터

그러기 위해서... 지금까지 피드백받은것들을 정리해서, 블로그에다가도 추가로 기록해둘 예정이다.

그러다보면 스스로 좀 덜 까먹겠지?

 

2. 개인적인 이야기

1) 다이어트 할거죠?

우선 다음주에 관련 과 진료를 보는데, 그때 "삭센다 처방 받고싶습니다" 하고 처방받고 해볼까 싶다.

식단도 좀 더 타이트하게 짜고, 운동량도 늘리고...

 

그래도 하루에 (스트레칭 빼고) 1시간 가량 하는 중!

 

9월 말에는 스트레칭 빼고 운동시간 70분 이상 찍는걸 목표로 하고 있다...

 

2) 그래서 연애는요?

뭔 연애야.....

싶긴 하지만, 솔직히 좀 외롭긴 하다.

 

그렇지만 그렇다고 아무에게나 들이대는건 당연히 아니고, 심리적으로 이끌리는 분이 있다면... 좀 적극적으로 고민해볼 생각.

 

지금까지 왜 연애를 못했나 생각해보니, 모 여지인분 왈 "님 제대로 좋아하는사람이 있긴 했어요?" 라고 하시더라...

그걸 스스로에게 물어보니, 맞는거 같더라...

 

그냥 연애하고싶다! 가 많았지, 이사람이 아니면 안돼! 가 있었던적은... 있었나? 싶은 생각

 

이 미련곰탱이야...

아무튼, 일단은 취업부터 하고 고민하자! 가 현재 마인드.

 


회고록 처음 써보는데 뭔가 횡설수설만 한거 같다...

이제 한달마다 한번씩 써봐야지

 

해당 문서는 다음 자료들을 참고하였습니다.

 


https://modoocode.com/229

 

씹어먹는 C ++ - <13 - 1. 객체의 유일한 소유권 - unique_ptr>

모두의 코드 씹어먹는 C ++ - <13 - 1. 객체의 유일한 소유권 - unique_ptr> 작성일 : 2018-09-18 이 글은 60741 번 읽혔습니다. 이번 강좌에서는 C++ 의 RAII 패턴unique_ptr안녕하세요 여러분! 지난번 강좌에서 다

modoocode.com

https://modoocode.com/252

 

씹어먹는 C ++ - <13 - 2. 자원을 공유할 때 - shared_ptr 와 weak_ptr>

모두의 코드 씹어먹는 C ++ - <13 - 2. 자원을 공유할 때 - shared_ptr 와 weak_ptr> 작성일 : 2018-12-21 이 글은 51016 번 읽혔습니다. 이번 강좌에서는 shared_ptrenable_shared_from_thisweak_ptr에 대해 다룹니다.안녕하

modoocode.com

 


 

C++에 들어오면서, "기존의 로우 포인터(=Raw Pointer) 에서 발생할 수 있는 문제들을 해결하기 위한 포인터"를 만들겠다는 명목하에 스마트 포인터라는 포인터가 추가되었다.

 

Raw Pointer 를 쓰면 생기는 문제점은 크게

1. 메모리를 사용했는데 해제 안한 경우
= 메모리가 낭비되는 현상 (=메모리 누수 / Memory Leak) 가 발생

2. 메모리를 사용하던 중에 해제한 경우, 해제한 메모리를 접근하는 경우

= 잘못된 참조  (Dangling Pointer) 발생으로 크래시 위험

 

둘다 문제가 많은 상황인데,

해제를 안한 경우가 "언제 터질지 모르는 시한폭탄" 이라면, 해제된 메모리 접근은 "지뢰" 라고 볼 수 있다.

둘다 터지면 머리아픈건 매한가지지만

 

 

주요 이론은, "객체는 (특별한 문제가 없는 한) 사라질때 소멸자가 반드시 호출되며, 이때 자원 해제를 하게 되면 자원 해제와 관련된 문제가 해결되지 않을까?" 라는 마인드에 입각하여(*1) 
포인터를 포인터 객체로 만들어서, 해당 객체가 소멸시 데이터까지 같이 delete 하도록 하여 각종 포인터 예외를 대응하고자 한것이다. 

 

다양한 스마트 포인터가 있고, 각자 다양한 목적을 위해 사용하는데 각각에 대한 정보를 정리를 한번 해야할것 같아 스스로 이해한 부분에 대해 정리를 해보고자 한다.

 

물론, 이미 Duplicated 된 Auto_Ptr은 생략한다...

 

 

 

 

해당 문서에는 각 스마트포인터의 사용법보단, 구조 및 원리에 대해 주로 설명합니다.

사용법이 필요하다면 적당히 찾아보시거나 직접 연구해보시는것을 추천합니다.

 

 

 

1) 해당 내용과 관련된 디자인 패턴이 RAII 라고 불리는 객체를 통한 자원 획득 디자인 패턴임.
RAII : Resource Acquisition Is Initialization


Unique_Ptr

 

unique_ptr은 특정 객체에 대한 유일한 소유권을 가지는 스마트 포인터이다.

대체로, 유일한 소유권을 가지는 경우에 대해 "이걸 왜 쓰냐" 라는 의문을 가질때가 종종 있는데

 

다음과 같은 경우에 주로 문제가 발생한다.

 

Data* data1 = new Data();
Data* data2 = data1;

delete data1;// = data1 에 연결된 객체 소멸 (=new data() 로 선언한 객체)
delete data2;// = data2 에 연결된 객체 소멸 (=data1 (= new data()로 선언한 객체)

 

간단히 설명하면, data1 포인터에 새로운 data 객체를 만들어줬고, 이를 data2에서도 참조할 수 있도록 해놨는데,

수행 이후 data1 포인터를 통해 객체를 소멸시켰으나, data2에서도 연결된 객체를 소멸시키려고 하는 상황이다.

이때, 이미 삭제된 객체를 삭제하려고 시도해서 메모리 오류가 발생하며 Crash가 발생하게 된다.

 

"그럼 객체 소멸을 한가지 포인터만 하게 하면 되지 않나요?" 란 의문에서 나온것이 바로 unique_ptr 되시겠다.

 

특정 객체의 소유권을 지정하여 해당 포인터에서만 객체 접근 및 소멸을 담당하게 하면, 다른 포인터가 임의로 객체를 파괴시키는 현상은 일어나지 않게 될것이다. 또한, 위에서 설명한 문제중 "포인터를 쓰고 해제 하지 않는 문제"를 RAII 패턴에 입각하여 대응을 한것이 unique_ptr이고, 이는 일반적인 포인터처럼 사용할 수 있으나 다음과 같은 특징을 가지고 있다.

 

1. 특정 객체 (= 특정 클래스로 이루어진 메모리 영역) 에 대해 유일한 소유권을 가진다.

2. 함수 스택에 포함된 객체로, 함수가 끝나면 소멸자가 호출되며 메모리를 자동으로 해제한다.

 

 

사실, 2번에 대해서는 이해하기가 쉬운데, 1번은 "그걸 어떻게 알 수 있는데?" 라는 의문이 생길 수 있다.

안그래도 궁금해서 좀 찾아보았는데, unique_ptr 구현부에 다음과 같은 항목이 있었다.

 

      /** Takes ownership of a pointer.
       *
       * @param __p  A pointer to an object of @c element_type
       * @param __d  A reference to a deleter.
       *
       * The deleter will be initialized with @p __d
       */
      template<typename _Del = deleter_type,
               typename = _Require<is_copy_constructible<_Del>>>
        unique_ptr(pointer __p, const deleter_type& __d) noexcept
        : _M_t(__p, __d) { }
      /** Takes ownership of a pointer.
       *
       * @param __p  A pointer to an object of @c element_type
       * @param __d  An rvalue reference to a (non-reference) deleter.
       *
       * The deleter will be initialized with @p std::move(__d)
       */
      template<typename _Del = deleter_type,
               typename = _Require<is_move_constructible<_Del>>>
        unique_ptr(pointer __p,
                   __enable_if_t<!is_lvalue_reference<_Del>::value,
                                 _Del&&> __d) noexcept
        : _M_t(__p, std::move(__d))
        { }

 

위는 할당, 아래는 "이동" 연산

 

여기서 보면 _M_t_ 라는 변수가 보이는데, 이는 __uniq_ptr_data 라는 구조체 변수이고, 해당 변수는 내부에 "__unique_ptr_impl" 라는 변수를 가지고 있다.

 

//__uniq_ptr_data 구현부
template <typename _Tp, typename _Dp,
            bool = is_move_constructible<_Dp>::value,
            bool = is_move_assignable<_Dp>::value>
    struct __uniq_ptr_data : __uniq_ptr_impl<_Tp, _Dp>
    {
      using __uniq_ptr_impl<_Tp, _Dp>::__uniq_ptr_impl;
      __uniq_ptr_data(__uniq_ptr_data&&) = default;
      __uniq_ptr_data& operator=(__uniq_ptr_data&&) = default;
    };
    
    
    
    
 // Manages the pointer and deleter of a unique_ptr
  template <typename _Tp, typename _Dp>
    class __uniq_ptr_impl
    {
      template <typename _Up, typename _Ep, typename = void>
        struct _Ptr
        {
          using type = _Up*;
        };
      template <typename _Up, typename _Ep>
        struct
        _Ptr<_Up, _Ep, __void_t<typename remove_reference<_Ep>::type::pointer>>
        {
          using type = typename remove_reference<_Ep>::type::pointer;
        };
    public:
      using _DeleterConstraint = enable_if<
        __and_<__not_<is_pointer<_Dp>>,
               is_default_constructible<_Dp>>::value>;
      using pointer = typename _Ptr<_Tp, _Dp>::type;
      static_assert( !is_rvalue_reference<_Dp>::value,
                     "unique_ptr's deleter type must be a function object type"
                     " or an lvalue reference type" );
      __uniq_ptr_impl() = default;
      __uniq_ptr_impl(pointer __p) : _M_t() { _M_ptr() = __p; }
      template<typename _Del>
      __uniq_ptr_impl(pointer __p, _Del&& __d)
        : _M_t(__p, std::forward<_Del>(__d)) { }
      __uniq_ptr_impl(__uniq_ptr_impl&& __u) noexcept
      : _M_t(std::move(__u._M_t))
      { __u._M_ptr() = nullptr; }
      __uniq_ptr_impl& operator=(__uniq_ptr_impl&& __u) noexcept
      {
        reset(__u.release());
        _M_deleter() = std::forward<_Dp>(__u._M_deleter());
        return *this;
      }
      pointer&   _M_ptr() { return std::get<0>(_M_t); }
      pointer    _M_ptr() const { return std::get<0>(_M_t); }
      _Dp&       _M_deleter() { return std::get<1>(_M_t); }
      const _Dp& _M_deleter() const { return std::get<1>(_M_t); }
      void reset(pointer __p) noexcept
      {
        const pointer __old_p = _M_ptr();
        _M_ptr() = __p;
        if (__old_p)
          _M_deleter()(__old_p);
      }
      pointer release() noexcept
      {
        pointer __p = _M_ptr();
        _M_ptr() = nullptr;
        return __p;
      }
    private:
      tuple<pointer, _Dp> _M_t;				//<<<---포인터 소유권을 관리하는 tuple
    };

 

해당 로직에서, _M_t 변수가 unique_ptr 내부의 포인터 변수임을 알 수 있었다.

 

그리고, unique_ptr이 특정 객체를 유일하게 소유하게 하는 방법은 다름이 아닌 "일부 생성자 함수를 삭제" 하는 방식이었는데, unique_ptr 구현부를 일부 뒤적대다보면 다음과 같이 기록되어 있다.

 

      // Disable copy from lvalue.
      unique_ptr(const unique_ptr&) = delete;
      unique_ptr& operator=(const unique_ptr&) = delete;

 

즉, 일반적인 복사 생성자나 대입 연산자는 삭제되어있으니, 당연히 접근하면 오류가 발생하는것이다.

 

'std::unique_ptr<A,std::default_delete<_Ty>>::unique_ptr(const std::unique_ptr<_Ty,std::default_delete<_Ty>> &)': attempting to reference a deleted function

 

오류 메시지는 다음과 같이 나온다. 삭제된 함수 (= 여기서는 복사, 대입 연산자)에 접근하려고 했다는 뜻.

 

 

이와 같은 방법으로 unique_ptr 간의 소유권을 지정할 수 있게 되었으며 (정확히는 조금.... 눈가리고 아웅 같긴 하지만) RAII 패턴에 힘입어 안전한 해제까지 보장되게 된 것이다.

 

 

 

p.s -

주의사항 : 물론, 설명에 없는것에서 눈치챘을수도 있지만 소유권이 이전된 빈 포인터가 된 unique_ptr에 포인터 연산을 시도 시 댕글링 포인터 문제로 인해 런타임 오류가 발생한다.

 


 

Shared_Ptr

 

근데 내가 특정 객체를 여러번 써야하는 경우가 있다면?

사실 이런 경우가 굉장히 흔하다. (오히려 unique_ptr을 쓰는 경우는 되게 드물었던것으로 기억한다.)

 

그렇다면 요구사항은 다음과 같이 정리된다.

 

1. 나는 한개의 객체를 여러개의 포인터로 접근하고 싶다.
2. 하지만, 그러면서 스마트 포인터의 특징 (= 안전한 해제)를 보장받고 싶다.

 

이를 위해 만들어진것이 바로 shared_ptr 되시겠다.

 

shared_ptr은 같은 객체를 다수의 스마트 포인터가 가리킬 수 있고, 내부적으로 레퍼런스 카운팅을 통해 몇번 참조중인지를 알 수 있는 스마트 포인터다.

레퍼런스 카운터가 0이 되면, 자동으로 해제되는 형태의 스마트 포인터로 알려져있는데.....

 

그럼 그 레퍼런스 카운터는 어떻게 되는가... 하면

__shared_count라는 서브 클래스가 따로 있고, (해당 클래스 내부에는 "_Sp_counted_base" 라는 클래스가 있다) 해당 클래스에서 카운트를 실시하는 스타일이다.

 

  template<>
    inline void
    _Sp_counted_base<_S_single>::_M_add_ref_copy()
    { ++_M_use_count; }
  template<>
    inline void
    _Sp_counted_base<_S_single>::_M_release() noexcept
    {
      if (--_M_use_count == 0)
        {
          _M_dispose();
          if (--_M_weak_count == 0)
            _M_destroy();
        }
    }

 

해당 정보는 같은 포인터를 가리키는 shared_ptr 끼리 공유하며 해당 공유 정보를 Control Block이라고 부른다.

즉, 레퍼런스 카운트 정보를 동일한 Control Block이 소유하고, 이를 각각의 shared_ptr이 공유하면서 사용하면 굳이 각 포인터에서 각자 카운터를 조작할 필요가 없게 되는것...

 

물론, 이를 위해서는 최초의 shared_ptr에서 접근을 늘려가야 하며 (즉, 최초의 shared_ptr을 제외한 N번째 shared_ptr은 최초의 shared_ptr을 가리켜야 한다), 1개의 raw pointer에서 다수의 shared_ptr을 새로 선언하면 shared_ptr이 자랑하는 레퍼런스 카운트 기능이 딱히 의미가 없어진다.

 

즉, shared_ptr에 raw pointer (=주소값)이 전달되면, 해당 shared_ptr은 본인이 해당 주소값을 최초로 소유한것으로 인지하게 된다는 것이다.

 

예시를 들자면, 다음과 같은 상황이다.

Data* data = new Data();	
    
std::shared_ptr<Data> p1(data);
std::shared_ptr<Data> p2(p1);
std::shared_ptr<Data> p3(data);


std::cout << p1.use_count() << std::endl;
std::cout << p2.use_count() << std::endl;
std::cout << p3.use_count() << std::endl;

 

이때의 결과값은

p1 = 2

p2 = 2

p3 = 1

 

이라는 결과가 나온다.

 

이 경우가 shared_ptr을 쓸때 가장 주의해야 하는 상황인데, 총 참조 회수는 3회임에도 불구하고,
시스템 상에서는 [p1,p2], [p3] 으로 2개의 제어 블록이 붙은 상황이라 카운팅이 다르게 발생한다.

이 경우, p3가 만일 사라진다면 p3에 연결된 원본 데이터도 같이 소멸하면서 p1, p2 포인터에서 오류가 발생한다.

 

해당 상황을 의도적으로 만들어야하는 경우가 아니라면, 동일한 raw pointer로 여러개의 shared_ptr을 만드는것은 지양해야 할것이다...

 

 

 

 

한가지 더. shared_ptr을 찾아보면 항상 나오는 얘기가 바로 순환참조이다.

순환참조를 아주 간단히 설명하면,
"A가 지워지려면 B에 있는 참조가 지워져야 하는데 B가 지워지려면 A에 있는 참조가 지워져야 됨"

즉, 각각의 객체에 다른 객체를 가리키는 shared_ptr이 존재하며 서로 레퍼런스 카운트를 차지하므로써 소멸이 이루어지지 않는 현상을 의미한다.

이 경우에는 시스템이 종료되기 전까지는 무슨짓을 해도 안 날아가기때문에, 메모리를 영원히 차지하는 문제가 생긴다.

 

 

 

 

주의사항 요약 : 

1. 동일 raw pointer로 다수의 shared_ptr 만들기 금지

2. shared_ptr을 서로 참조하는 순한참조 만들기 금지

 


Weak_ptr

 

이쯤되면, 이런 고민도 들 것이다.

"나는 그냥 포인터 값만 확인하고 싶고 레퍼런스 카운터에는 영향을 주고싶지 않아요"

"나는 순환 참조 문제를 피하면서 스마트 포인터를 쓰고 싶어요"

 

이럴때를 위한 스마트 포인터가 바로 weak_ptr 되시겠다.

 

weak_ptr은 shared_ptr과 유사하게 작동하나, 레퍼런스 카운터에는 영향을 미치지 않는다는 특징이 있다.

물론 weak_ptr에도 내부적으로 몇개의 weak_ptr이 사용중인가를 카운트하는 무언가가 있긴 하지만, shread_ptr의 레퍼런스카운트 보다는 크게 중요하지는 않아 보인다.

 

Data* data = new Data();	
    
std::shared_ptr<Data> p1(data);
std::shared_ptr<Data> p2(p1);
std::weak_ptr<Data> p3(p1);


std::cout << p1.use_count() << std::endl;
std::cout << p2.use_count() << std::endl;
std::cout << p3.use_count() << std::endl;

 

사용은 다음과 같이 할 수 있고, 카운트는 각각 2,2,2 로 동일한 숫자가 나오게 된다.

 

weak_ptr의 특이사항으로, weak_ptr로 객체를 할당받았음에도 불구하고 weak_ptr에서 임의로 객체에 접근할 수 는 없다.

이유는 어찌보면 당연한데, weak_ptr을 이용해서 직접 데이터를 접근하는 도중 원본 shared_ptr의 레퍼런스 카운트가 0이 된다면? 이라는 질문에 대한 답을 찾아보면 쉽게 이해할 수 있을것이다.

 

답은 뻔할것이다. 메모리 오류가 터지면서 Crash가 발생하게 될것이다.

 

이를 막기 위해, weak_ptr 에서 데이터에 접근하고 싶으면, weak_ptr에 포함된 Lock() 이란 함수를 이용해주면 된다.

해당 함수는 weak_ptr에서 값에 접근을 시도하겠다고 알리면서, shared_ptr을 하나 추가로 생성하는 (= 레퍼런스 카운트를 1 올리는) 연산을 수행한다.

 

__shared_ptr<_Tp, _Lp>
lock() const noexcept
{ return __shared_ptr<element_type, _Lp>(*this, std::nothrow); }

 

이렇게 되면, weak_ptr에 연결된 객체에 접근 가능한 임시 shared_ptr이 생성되며, 해당 shared_ptr을 통해 데이터를 사용할 수 있게 된다.

 

std::shared_ptr<Data> p2 = new Data();
std::weak_ptr<Data> p2 (p1)
    
p2.lock()->GetData();	//이렇게도 되고
std::shared_ptr<List> p3 = p2.lock();
p3->GetData();		//이렇게도 된다.

 

 


 

뭔가 사용법에 대한 설명은 쏙 빼놓고 설명하니까 반절정도 밖에 이해를 못한거 같은데,

사용법은... 사실 실제로 써보는게 더 빠르니까 그 방법을 택하는걸 추천 한다.

오늘 면접 진행하면서 C++ 및 컴퓨터 이론 기초에서 터지는 사고가 좀 발생했는데(...)

 

그중에 정렬문제가 있었다.

 

문제 자체는 퀵소트 관련 질문이었는데, 문득 "언리얼에서는 기본 정렬로직을 뭘 쓰더라?" 하고 찾아보니, C++ standard에 포함된 std::sort()를 사용한다고 되어있었다.

 

거기는 또 뭘 쓰냐 봤더니... 퀵 정렬이 아닌 "Intro Sort" 라는 개선형 퀵 정렬을 사용하고 있었다.

 

 


해당 자료는 온라인 코드 브라우저의 gcc 컴파일러 기준 C++ 11 로직을 참고하였습니다. : https://codebrowser.dev/gcc/include/c++/11/bits/stl_algo.h.htm


우선, std::sort를 검색하면 std::__sort 라는 inner func가 나온다.

 

  /**
   *  @brief Sort the elements of a sequence using a predicate for comparison.
   *  @ingroup sorting_algorithms
   *  @param  __first   An iterator.
   *  @param  __last    Another iterator.
   *  @param  __comp    A comparison functor.
   *  @return  Nothing.
   *
   *  Sorts the elements in the range @p [__first,__last) in ascending order,
   *  such that @p __comp(*(i+1),*i) is false for every iterator @e i in the
   *  range @p [__first,__last-1).
   *
   *  The relative ordering of equivalent elements is not preserved, use
   *  @p stable_sort() if this is needed.
  */
  template<typename _RandomAccessIterator, typename _Compare>
    _GLIBCXX20_CONSTEXPR
    inline void
    sort(_RandomAccessIterator __first, _RandomAccessIterator __last,
         _Compare __comp)
    {
      // concept requirements
      __glibcxx_function_requires(_Mutable_RandomAccessIteratorConcept<
            _RandomAccessIterator>)
      __glibcxx_function_requires(_BinaryPredicateConcept<_Compare,
            typename iterator_traits<_RandomAccessIterator>::value_type,
            typename iterator_traits<_RandomAccessIterator>::value_type>)
      __glibcxx_requires_valid_range(__first, __last);
      __glibcxx_requires_irreflexive_pred(__first, __last, __comp);
      std::__sort(__first, __last, __gnu_cxx::__ops::__iter_comp_iter(__comp));
    }


  // sort
  template<typename _RandomAccessIterator, typename _Compare>
    _GLIBCXX20_CONSTEXPR
    inline void
    __sort(_RandomAccessIterator __first, _RandomAccessIterator __last,
           _Compare __comp)
    {
      if (__first != __last)
        {
          std::__introsort_loop(__first, __last,
                                std::__lg(__last - __first) * 2,
                                __comp);
          std::__final_insertion_sort(__first, __last, __comp);
        }
    }

 

해당 함수에서는, 시작과 끝이 일치하지 않을때 introSort 재귀함수를 진행하고, 종료 시점에 삽입 정렬을 수행한다.

 

introsort_loop (Instro sort 재귀함수) 에서는, 미리 정해진 threadhold 상수값을 기준으로 정렬을 선택해서 사용한다.

 

  /// This is a helper function for the sort routine.
  template<typename _RandomAccessIterator, typename _Size, typename _Compare>
    _GLIBCXX20_CONSTEXPR
    void
    __introsort_loop(_RandomAccessIterator __first,
                     _RandomAccessIterator __last,
                     _Size __depth_limit, _Compare __comp)
    {
      while (__last - __first > int(_S_threshold))
        {
          if (__depth_limit == 0)
            {
              std::__partial_sort(__first, __last, __last, __comp);
              return;
            }
          --__depth_limit;
          _RandomAccessIterator __cut =
            std::__unguarded_partition_pivot(__first, __last, __comp);
          std::__introsort_loop(__cut, __last, __depth_limit, __comp);
          __last = __cut;
        }
    }

 

_S_threadhold 라는 상수가 문제의 상수인데, 파일을 조금 뒤져보니 "16" 이란 값으로 임의로 박혀있었다.

왜 하필 16인지에 대해서는.... 더 찾아봐야할거같긴 한데 일단은 16이라니까 16이라고 보자.

 

Introsort_loop 의 로직은,

1. 리스트의 길이가 16 초과 일때 로직을 수행하고

2. depth limit가 0이 아니라면 -> depth limit를 1 줄이고, 임의 pivot을 선택한 후 instroSort를 반복해서 선택한다.

3. 여기서, depth limit는 함수 시작 시 2log₂(length) (log 연산값의 정수 올림) 의 값을 가지고 시작한다.(해당 연산을 N번 반복하므로 정렬의 평균 속도가 O(n log₂n) 이 나오는것..)

4. 연산을 반복하다, depth limit가 0이 되었고 (= 연산의 깊이가 2⌈ log₂(length) ⌉  돌파시), 길이가 16이상일 시 힙 정렬을 수행한다.

 

  //introsort 내부의 힙 정렬
  template<typename _RandomAccessIterator, typename _Compare>
    _GLIBCXX20_CONSTEXPR
    inline void
    __partial_sort(_RandomAccessIterator __first,
                   _RandomAccessIterator __middle,
                   _RandomAccessIterator __last,
                   _Compare __comp)
    {
      std::__heap_select(__first, __middle, __last, __comp);
      std::__sort_heap(__first, __middle, __comp);
    }

 

5. 최종적으로 모든 introsort 재귀함수가 끝나면, 남은 값에 대해 삽입 정렬을 수행한다.

 

 


단순히 std::sort 가 퀵 정렬인줄 알고 찾아봤는데, 알고보니 퀵 정렬을 개선한 무언가였다는거는 조금 의외였다...

 

참고로, 해당 정렬의 속도는 대체로   O(n log⁡ n) 가 나온다고 알려져 있다.

 

하긴 퀵보단 좋아야지

 

 

 

 

회사 및 과거 프로젝트가 유추될 수 있는 사항은 제외했습니다.

 


언리얼 리플렉션 시스템 (https://www.unrealengine.com/ko/blog/unreal-property-system-reflection)

  • 프로그램이 실행시간에 자기 자신을 조사할 수 있는 기능. (본인이 어떤 클래스인지 등을 알 수 있음)
  • 언리얼 엔진에서 C++ 클래스, 구조체, 함수, 멤버 변수, 열거형 관련 정보를 수집, 질의, 조작하는 별도의 시스템
  • 리플렉션 시스템에 등록시키고자 하는 변수 등에 UProperty 키워드 등을 부착시 Unreal Header Tool (UHT) 에 의해 리플렉션 시스템에 등록됨
    • UProperty 사용시, 언리얼 GC에 포함이 됨.
    • 각종 메타데이터들을 추가하여 해당 변수가 어떤 속성을 가졌는지를 알 수 있음.

 

 

 

언리얼 최적화 관련

  • 언리얼에서는 자체적인 GC를 사용함.
  • GC는 다음과 같이 작동함 :: 
    Unreachable 플래그를 모든 UObject에 부착 -> RootSet에 걸려있는 UObject에서 플래그 회수 -> 플래그 남은 UObject 기록 -> 리스트업 된 UObject Destroy (Begin/Finish) -> UObject의 소멸자 호출
  • GC의 타겟이 되지 않게 하기 위해서는 관련 오브젝트에 "AddToRoot" 혹은 "SetFlags(RF_MarkAsRootSet)" 플래그를 붙이면 됨.
  • 주로, 인게임에서 이미 소멸된 액터거나 Replicate 범위 밖 (NetCullDistance)에 존재하는 경우 타겟이 될 수 있음.

 

 

 

언리얼 리플리케이션 관련

  • 기본 개념 : 서버-클라간 동기화를 위한 개념
    • 서버->클라로는 호출한 / 소지한 (Autonomous) 클라이언트에게, 혹은 모든 클라이언트에게 전달 (Multicast) 가능
    • 클라->서버로는 클라이언트가 소지한 액터가 Autonomous일때만 서버로 전송가능 (이것도 RPC만 가능)
    • 변수 리플리케이션은 "원칙적으로는" 서버->클라만 가능함.
  • Role
    • Role은 Local Role / Remote Role 2개가 있음.
    • 보통 서버 입장에서 Local Role은 Authority인 경우가 대부분
    • 클라에서는 Remote는 Authority, Local이 다를 수 있음.
    • 비교
      • 클라 입장에서 Autonomous - "현재 클라이언트가 컨트롤 중인 플레이어"
      • 클라 입장에서 Simulated - "현재 클라이언트의 영향 외의 동기화 액터"
  • 기타
    • RPC는 단발성 리플리케이션에 주로 사용, 변수 리플리케이션은 영구적 리플리케이션에서 주로 사용함.

 

 

언리얼 포인터 관련


기본적으로 언리얼 스마트 포인터는 Object 포인터와 일반 포인터 (비 Object 포인터 = C++ 노멀 포인터)가 있음

  • 오브젝트 포인터 (Actor 클래스 등의 언리얼 오브젝트에서 사용. 언리얼 GC 타겟이 된다.)
    • TWeakObjectPtr - 순환참조 이슈 등을 대응하기 위한 오브젝트 포인터. 참조 대상 파괴시 nullptr로 세팅됨.
    • TObjectPtr - 64bit 기반 신규 포인터.
      원시포인터 (Normal Pointer) 와 유사하나, 다이나믹 해상도 (액터에 대한 동적 해상도 전환), 엑세스 트래킹 등의 신규 기능 추가.
      • 5.0 되면서 추가된 신규 포인터임. 원시 포인터를 TObjectPtr로 바꾸는걸 언리얼에서는 권장함.

  • 노멀 포인터
    • TUniquePtr - 일반적인 Unique Pointer와 동일. (하나의 포인터로 단 하나의 메모리만을 참조)
    • TSharedPtr - 일반적인 Shared Pointer와 동일
    • TWeakPtr - 일반적인 Weak Pointer와 동일 (Shared Pointer의 보조)
      • TWeakPtr은 TSharedPtr를 통해서만 복사 생성, 대입 연산이 가능하다.
      • TWeakPtr은 TSharedPtr를 참조하되, 레퍼런스 카운트를 증가시키지 않는다.
      • TWeakPtr을 통해 객체를 참조하려면 반드시 TSharedPtr로 변환하여 사용해야 한다.

 

 

 

* 주의 *

1. 해당 작업은 StandAlone에서만 테스트되었습니다.

2. 해당 작업은 임시 연동을 위해 작업되었으며, 타 환경 (네트워크 환경 등)에서는 정상 작동을 보장하지 않습니다.

3. 필요시, 코드를 적당히 수정해서 사용해주세요!

 

 

(아마 기억이 맞다면) 언리얼 5.3에 들어오면서 큰 변화가 몇가지 생겼다.

그중에 프로그래머 입장에서 유의미하게 볼것이 바로 EnhancedInput의 정식 사용 시작 및 AbilitySystem의 공식 플러그인 화 일것이다.

 

근데 웃긴게, 이 두가지가 하나는 정식기능, 다른 하나는 플러그인이라서 그런가 공식적으로 지원하는 연동 코드가 없다.....

 

물론, 진짜 하나도 없진 않다. 대신, LyraStarterGame에 포함되어있을뿐...

 

LyraStarterGame에서는 해당 기능을 HeroComponent라는 연동 컴포넌트와, EnhancedInput을 위한 LyraInputComponent를 만들어서 사용하는데, 해당 컴포넌트 및 Lyra 식 사용에 대한 분석은 다음에 하기로 하고, 우선은 조금 빠른 사용법을 익혀보도록 하자.

 

 

* 해당 코드의 일부는 EpicGames에서 제공하는 Lyra Starter Game에서 발췌했습니다.


우선, 이전 글을 참고하여 AbilitySystemComponent를 정상적으로 사용할 수 있는 상태를 기준으로 한다.

 

https://locketgoma.tistory.com/80

 

짧막 팁 : AbilitySystem 관련 헤더를 불러오지 못할때

대충 이런 상황이다. AbilitySystemComponent 및 기타 관련 헤더를 사용해야하는데 못불러오는경우... 해당 문제는 모듈이 빠져있는 상황으로, 프로젝트 명칭으로 된 폴더에 있는 "(ProjectName).Build.cs"

locketgoma.tistory.com

 

 

두가지를 연동하려면 우선, InputAction과 GameplayTag를 연동시켜둔 데이터파일이 필요하다.

해당 파일 타입을 Lyra에서는 "FLyraInputAction" 라는 struct를 사용하는 ULyraInputConfig라는 DataAsset를 만들어 사용한다.

 

일단, 해당 방법을 그대로 사용하자.

 

UDataAsset를 상속받는 신규 클래스를 만들고, 해당 파일에

 

USTRUCT(BlueprintType)
struct FInputAction		//이름은 적당히 바꿔서 쓰면 됩니다.
{
	GENERATED_BODY()

public:

	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
	TObjectPtr<const UInputAction> InputAction = nullptr;

	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Meta = (Categories = "InputTag"))
	FGameplayTag InputTag;
};

해당 구조체를 추가, uproperty로 추가해주면 된다.

Lyra에서는 (EditDefaultsOnly, BlueprintReadOnly) 조건으로 사용중이나, 적당히 필요한대로 쓰시면 될것같다.

 

 

 

그 다음부터는 선택지가 갈리는데, EnhancedInputComponent 및 PanwComponent를 상속받아서 Lyra와 같이 InputComponent + HeroComponent를 만들어서 사용하거나, 간략하게 AbilitySystemComponent를 상속하여 구현해주는 방법 등이 있다.

 

여기서는 AbilitySystemComponent 을 상속하여 추가 함수를 만들어주는 방식으로 구현하고자 한다.

 

Lyra 방식은... 공부도 하실겸 Lyra 코드를 열어보시는걸 추천드립니다.

 

 

 

첫번째. UAbilitySystemComponent를 상속받은 컴포넌트를 추가한다.

두번째. 위에서 만든 DataAsset Class를 헤더에 추가한다.
(단순히 전방선언으로 사용하면 구조체의 사이즈를 알 수 없어 오류가 발생한다)

세번째. 다음과 같이 (혹은 적당히 수정하여) 헤더를 작성한다.

UCLASS()
class PROJECTCLOUD_API UCLAbilitySystemComponent : public UAbilitySystemComponent
{
	GENERATED_BODY()

public:
	//인풋 액션을 세팅하는 함수 (return bool인 이유는 오류 검출 용도)
	bool BindInputActions(const UCLAbilityInputConfig* InputConfig, UEnhancedInputComponent* EnhancedInputComponent);
	
	//인풋 액션을 호출하는 바인딩 함수
	void TryActiveAbilityFromInputAction(const FInputActionInstance& Value);

private:
	//인풋 액션 리스트
	TMap<TObjectPtr<const UInputAction>, FGameplayTag> InputactionList;
};

네번째. cpp 파일에 필요한 헤더들을 추가한다.
EnhancedInput관련, GameplayTag 관련 헤더를 추가해주면 된다.

다섯번째. 바인딩 코드 작성.

bool UCLAbilitySystemComponent::BindInputActions(const UCLAbilityInputConfig* InputConfig, UEnhancedInputComponent* EnhancedInputComponent)
{
	if (!ensure(EnhancedInputComponent))
	{
		UE_LOG(LogTemp, Error, TEXT("UCLAbilitySystemComponent :: EnhancedInputComponent is Null."));
		return false;
	}

	for (const FCLInputAction Action : InputConfig->AbilityInputActions)
	{
		InputactionList.Add(Action.InputAction, Action.InputTag);

		EnhancedInputComponent->BindAction(Action.InputAction, ETriggerEvent::Triggered, this, &UCLAbilitySystemComponent::TryActiveAbilityFromInputAction);
	}
	return true;
}

 

Input으로 받은 DataAsset에서 리스트를 가져와서 EnhancedInput에 바인드하고, 로직 검사를 위해 ASC에 추가한 Map에 등록해주는 로직이다.

 

여섯번째. 바인드 될 호출함수 작성

void UCLAbilitySystemComponent::TryActiveAbilityFromInputAction(const FInputActionInstance& Value)
{
	//1. 이벤트로 들어온 InputAction과 매치되는 Tag가 있는지 검사
	FGameplayTag* InputTag = InputactionList.Find(Value.GetSourceAction());
	
    //2. 매치되는 Tag가 있다면
	if (InputTag != nullptr)
	{
    	//3. Pay로드 작성 후
		FGameplayEventData TempPayload;
		TempPayload.EventTag = *InputTag;
		TempPayload.Instigator = GetOwner();
        
        //4. ASC에 포함된 HandleGameplaytEvent를 호출한다.
        HandleGameplayEvent(TempPayload.EventTag, &TempPayload);
	}
}

Payload값은 임의로 수정해도 된다. (단, EventTag 제외)

 

 

다음과 같이 작성하면 함수 준비는 끝났다.

 

이를 어떻게 사용하느냐...

 

사실 생각보다 별거 없다. EnhancedInput과 ASC를 사용하고자 한다면, 아마 여러가지 검색을 하면서 Player Pawn에다가 연동하는 작업도 수행했을것이다.

아니었다구요? 그렇다면..

 

	UEnhancedInputComponent* EnhancedInputComponent = GetComponentByClass<UEnhancedInputComponent>();

	if (ACLPlayerState * PS = Cast<ACLPlayerState>(GetPlayerState()))
	{
		PS->GetAbilitySystemComponent()->InitAbilityActorInfo(PS, this);
		PS->SetAbilitiesFromActionSet(AbilitySet);
		PS->GetAbilitySystemComponent()->BindInputActions(InputConfig, EnhancedInputComponent);
	}

다음과 같은 로직을 Player Pawn의 Beginplay 등의 Init 단계에서 호출하도록 추가해주면 된다.

 

당연한 얘기겠지만, 이미 EnhancedInput을 Player Pawn에 등록했어야 하고,
해당 로직의 경우에는 PlayerState에 ASC를 등록해서 사용했기에, 해당 과정도 거친 상태여야 한다.

(스텐드 얼론이어서 Pawn에 직접 ASC를 붙여서 사용한다면, 코드를 적당히 수정하면 된다.)

 

다음과 같이 등록하고, Player Pawn에 "InputConfig"  변수를 추가해주면 끝.

 

정상적으로 잘 따라왔다면,

 다음과 같은 InputConfig를 만들 수 있고,

해당 InputConfig를 추가할 수 있는 공간이 Player Pawn에 추가된다.

 

이제 자유롭게 InputAction과 InputTag를 부착하면, Player의 ASC에 부착된 GameplayAbility 중 Trigger Event로 호출되는 Ability를 사용할 수 있게 된다.

 

 


ASC 사용법은 저 말고도 다른 분들이 잘 설명해준 문서들이 많으니 참고 바랍니다...
EnhancedInput과 ASC 연동하는 방법은 어디에도 없길래 Lyra 코드 분석해가면서 간략화해서 만들어봤어요

 

틀린 내용에 대한 지적은 언제나 받습니다.

대충 이런 상황이다.

 

AbilitySystemComponent 및 기타 관련 헤더를 사용해야하는데 못불러오는경우...

 

해당 문제는 모듈이 빠져있는 상황으로, 프로젝트 명칭으로 된 폴더에 있는 "(ProjectName).Build.cs" 파일에 모듈을 추가해주면 된다.

 

	PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "HeadMountedDisplay" });

Build.cs 파일의 해당 위치에

 

 

	PublicDependencyModuleNames.AddRange(new string[] { "GameplayAbilities", "GameplayTags", "GameplayTasks", "Core", "CoreUObject", "Engine", "InputCore", "HeadMountedDisplay" });

다음과 같이 어빌리티 시스템 관련을 추가해주면 됨.

 

 

어빌리티 시스템 관련을 모듈로 추가해준 뒤, 프로젝트 파일을 Re-Generate 해주면 완료.

 

 

 

 

참고 자료 : https://dev.epicgames.com/documentation/en-us/unreal-engine/gameplay-ability-system-for-unreal-engine?application_version=5.0

+ Recent posts