Deprecating Atlassian account password for Bitbucket API and Git activity - Bitbucket

Starting Sep 13, 2021, new Bitbucket users will not be able to use their personal Atlassian account password when using Basic…

bitbucket.org

지난 21년 9월, Bitbucket에서 3rd party app에 대해 아이디/패스워드 인증을 중단했다. git 저장소 권한이 명시된 key값과 application password를 따로 생성해서 쓰지 않으면 clone은 물론이고 접근이 불가능한 상황이 되었고, 3월 1일부터 완전 지원중단을 했다고 한다. 그런데 마지막 빌드 일자가 3월 4일인건 웨지

 

 

어쨌든 빌드머신을 돌리던 와중에 아니나다를까, 빨간불이 들어와 있었다.

 

빌드 실패쯤이야 이젠 익숙한 문장이다.

 

그냥 접속이 불가하다- 정도가 아니라 저렇게 해결방법을 너무나도 친히 알려주시니 다행이다 싶었지만, 인생이 그렇게 쉽게 풀릴리는 없지..

 

해당 작업의 설정란에서, 일단 key를 추가한다. Config - Manage Credentials로 이동

 

Global Credential에서 Bitbucket App Password Key를 따로 생성한다. 그리고 Repository에 대한 접근 권한을 따로 또 부여해주어야 했는데 이게 어디에 숨어있었냐면.....

 

 

해당 빌드 스케쥴의 설정창에서, Git을 사용하는 Repository 주소에 

https://<Your_Account_Name>:<App_Password>@bitbucket.org/<Your_Account_Name>/<Repo_Name>.git

형식으로 입력하면 된다. 끗!

 

 

 

 

 

 


참고 :

https://stackoverflow.com/questions/39886995/how-to-access-bitbucket-using-app-password

니델바가 자주 사용하는 디자인 패턴은 뭐가 있나요?

 새로운 동료와 팀을 만나는 자리, 구인 면접 자리에서 가장 많이 들은 질문을 꼽으라면 바로 이것일 것이다. 그때마다 멍청하고 자신있게 '싱글톤이요!' 하고 대답하면 이 대답을 예상했다는 듯이 '아~ 그러시구나, 그거 말고 다른건요?' 하고 물어오던 사람도 부지기수였다. 그만큼 싱글톤 패턴은 게임 디자인에서 아주 중요한 패턴이고, 전역 Instance 접근이라는 매력적인 설계방법 때문에 '누구나 쓰는 디자인 패턴'이 되어버린 것이다. (물론 단점도 있겠지만). 디자인 패턴은 단순 알고리즘을 말하는 것이 아니라 상황에 따라 자주 쓰이는 설계방법론 같은 것이다. 이 말은 내가 여지것 하고있던 프로그래밍은 예전의 누군가가 설계하고 시도해봤을 것이란거고, 마음대로 이름을 붙일 수도 있다는 것. 사실 디자인 패턴의 이름을 대지 못하고 있다 뿐이지 내 프로그래밍은 항상 누군가의 디자인 패턴을 따라 설계되고 있었다. 그 중 하나가 관찰자 패턴(observer pattern)이었는데, 동시처리가 많은 게임 클라이언트 특성상 아주 필수적으로 설계단계에 포함되고, 내가 특히 유용하게 쓰고 있다는 것을 알고 정리해 둔다.

 


업적 달성

 업적achievement 시스템을 추가한다고 해보자. '게임 100판 승리하기', '다리에서 떨어지기', '특정 조건으로 레벨 완료하기', '일일 미션 5개 완수하기'.. 같은 특정 기준을 달성하면 배지 혹은 보상을 얻을 수 있는데, 이 종류는 수백개가 넘는다고 하자.

 

 업적 종류가 광범위하고 달성할 수 있는 방법도 다양하다 보니 깔끔하게 구현하기가 어렵다. 물론 '다리에서 떨어지기'같은 업적은 어떻게든 물리 엔진과 연결해서 충돌 검사 알고리즘의 선형대수 계산 코드 한가운데에서 unlockFallOffBridge()를 호출하고 싶지는 않을 것이다.

 

 특정 기능을 담당하는 코드는 항상 한데 모아두는 게 좋다. 문제는 업적을 여러 게임 플레이 요소에서 발생시킬 수 있다는 점이다. 이런 코드 전부와 커플링되지 않고도 업적 코드가 동작하게 하려면 어떻게 해야 할까? 이럴 때 관찰자 패턴을 쓰면 된다. 어떤 코드에서 흥미로운 일이 생겼을 때 누가 받든 상관없이 알람을 보낼 수 있다.

 

 예를 들어 물체가 평평한 표면에 안정적으로 놓여 있는지, 바닥으로 추락하는지를 추적하는 중력 물리 코드가 있다고 해보자. (보통 유한 상태머신으로 해결한다) '다리에서 떨어지기' 업적을 구현하기 위해 업적 코드를 물리 코드에 곧바로 밀어 넣을 수도 있지만 앞서 말한 것 처럼 코드가 매우 지저분해진다. 대신 다음과 같이 해보자.

void updateEntity(Entity entity){
	bool wasOnSurface = entity.isOnSurface();
	entity.accelerate(GRAVITY);
	entity.update();
	if(wasOnSurface & !entity.isOnSurface()){
		notify(entity, EVENT_START_FALL);
	}
}

 이 코드는 '이게 방금 떨어지기 시작했으니 누군지는 몰라도 알아서 해라'라고 알려주는 게 전부다. 업적 시스템은 물리 엔진이 알림을 보낼 때마다 받을 수 있도록 스스로를 등록한다. (물리 엔진을 예로 들었지만 서버에서 무언가를 준다면 제일 편하고, 그걸 사용하면 된다!) 떨어지는 물체가 불쌍한 내 캐릭터가 맞는지, 떨어지기 전에 다리 위에 있었는지 등등을 확인한 뒤에 축포와 함께 업적을 잠금해제하면 된다. 이런 과정을 물리 코드 자체는 알 필요가 전혀 없다.

 

 이렇게 물리 엔진 코드는 전혀 건드리지 않은 채로 업적 목록을 바꾸거나, 업적 시스템을 아예 떼어낼 수도 있다. 물리 코드는 누가 받든 말든 계속해서 알림을 보낼 것이다. 

 


"너무 느려"

 사실 이 포스팅을 하게 된 이유이다. 아니, 관찰자 패턴은 결코 느리지 않다. 개발자 몇몇은 '디자인 패턴' 비슷한 이름만 붙어 있어도 쓸데없이 클래스만 많고 우회나 다른 희한한 방법으로 CPU를 낭비할 것으로 지레짐작한다는 것이다. 괜히 '유식한 척' 방법론을 들먹이지 말라고 비꼬기도 한다. 그래서, 관찰자 패턴이 정말 느린 것인가? 에 대해 고찰을 해보자면.

 

 관찰자 패턴은 특히 '이벤트', '메시지', 심지어 '데이터 바인딩'같은 좀 어두운(ㅋㅋ) 친구들과 어울려 다닌다는 얘기 때문에 부당한 평을 받아온듯 하다. 이런 시스템 중 일부는 알림이 있을 때마다 동적 할당을 하거나 큐잉queuing하기 때문에 실제로 느릴 수도 있다. 이래서 패턴의 문서화가 중요하다! 용어가 모호하면 분명하고 간결하게 의사소통할 수 없다. 같은 걸 놓고 사람마다 '관찰자'니 '이벤트'니 '메시징'이니 하는 식으로 다르게 부르는 이유는 누구도 이들 차이를 써두지 않았거나 아무도 그런 글을 읽지 않아서다. 정말 느리다고 하는 것은 그놈의 관찰자 '패턴' 탓인가?

 

 코드 패턴 자체는 전혀 느리지 않다. 그냥 목록을 돌면서 필요한 가상 함수를 호출하면 알림을 보낼 수 있다. 정적 호출보다야 약간 느리긴 하겠지만.. 진짜 성능에 민감한 코드가 아니라면 이 정도 느린게 문제가 되지 않는다. 게다 성능에 민감하지 않은 곳에 가장 잘 맞기 때문에, 동적 디스패치를 써도 크게 상관없다. 이 점만 제외하면 성능이 나쁠 이유가 전혀 없는 것이다. 그저 인터페이스를 통해 동기적으로 메소드를 간접 호출할 뿐 메시징용 객체를 할당하지도 않고, 큐잉도 하지 않는다. 느리다고 표현한 것은 이 패턴을 무시하고 막연히 할당시키는 객체들 덕이리라.

 

 

"동적 할당을 너무 많이 해"

 게임 개발자를 포함한 많은 프로그래머 무리가 가비지 컬렉션을 지원하는 언어로 이주한 뒤로는, 동적 할당은 더 이 상 예전만큼 무서운 존재가 아니다. 하지만 아무리 관리 언어로 만든다고 해도 게임같이 성능에 민감한 소프트웨어에서는 메모리 할당이 여전히 문제가 된다. 저절로 된다고는 하나 메모리를 회수하다 보면 동적 할당이 당연히 오래 걸릴 수 있다.

 

 실제 게임 코드에서는 고정배열이 아니라 관찰자가 추가, 삭제될 때 크기가 알아서 늘었다 줄어드는 동적 할당 컬렉션을 쓴다. 그럼 눈치챘을 것이다. 대부분 프로그래머들은 이렇게 메모리가 왔다갔다 하는 것을 굉장히 두려워한다(.....)!

 

 물!론. 실제로는 관찰자가 추가될 때만 메모리를 할당하는게 맞다. 알림을 보낼 때는 메소드를 호출할 뿐 동적 할당은 전혀 하지 않는다. 만약 할당을 하고 있다면 패턴을 다시 짜야 한다. 게임 코드가 실행될 때 처음 관찰자를 등록해놓은 뒤에는(유니티 같은 경우 frame 앞단에서!) 그 후 건드리지 않는다면 메모리 할당 자체는 거의 일어나지 않는다.

 

 

남은 문제점들

 관찰자 패턴을 꺼리게 하던 우려는 대충 해소가 되었다. 앞에서 본 것처럼 관찰자 패턴은 꽤 간단하고 빠르며 메모리 관리 측면에서도 깔끔하게 만들 수 있다. 그렇다면 항상 관찰자 패턴을 써야 하는 걸까?

 

 그건 전혀 다른 얘기다. 싱글톤도 마찬가지지만 다른 모든 디자인 패턴과 마찬가지로 관찰자 패턴 역시 만능은 아니다. 제대로 구현했다고 해도 올바른 해결책이 아닐 수도 있다. 디자인 패턴의 평판이 나빠진 것은 사람들이 좋은 패턴을 상황에 맞지 않는 문제에 적용하는 바람에 문제가 더 심각해진 경우가 많기 때문이라 생각한다. 그런 사람들이 항상 디자인 패턴을 욕하고는 한다. 그렇게 욕먹은 사례가 하나 있다면.

 

 유저의 재화 상태를 보여주는 UI 화면이 있었다. 유저가 상태창을 열면 상태창 UI 객체를 생성한다. 상태창을 닫으면 UI 객체를 따로 삭제하지 않고 GC가 알아서 정리하게 한다. 유저가 게임을 하면서 돈을 얻거나 잃으면 그럴 때마다 알림을 보낸다. 유저를 관찰하던 UI 창은 알림을 받아 재화를 갱신한다. 여기까지는 좋다. 이제 유저가 상태창을 닫을 때 관찰자를 등록 취소하지 않는다면 어떻게 될까?

 

 UI는 더 이상 보이지 않지만 유저 재화의 관찰자 목록에서 여전히 상태창 UI를 참조하고 있기 때문에 GC가 수거해 가지 않는다. 상태창을 열 때마다 상태창 인스턴스를 새로 만들어 관찰자 목록에 추가하기 때문에 관찰자 목록은 점점 커진다. 유저는 게임을 하는 동안 계속해서 이 모든 상태창 객체에 알림을 보낸다. 상태창은 더 이상 화면에 없지만 알림을 받을 때마다 눈에 보이지도 않는 UI요소를 업데이트하느라 CPU 클럭을 낭비한다. 상태창에서 효과음이라도 난다면, 연속해서 같은 효과음이 나는 걸 듣고서야 '아 뭔가 X됐는데..' 라고 눈치챌 것이다.

 

 이는 알림 시스템에서 굉장히 자주 일어나는 문제다 보니 사라진 리스너 문제lapsed listner problem라는 고유한 이름이 붙었을 정도. 얼마나 중요한지 심지어 위키피디아에 따로 페이지가 있을 정도다. 대상이 리스너 레퍼런스를 유지하기 때문에, 메모리에 남아 있는 좀비 UI 객체가 생긴다. 제발, 등록 취소를 제대로 하자. 주의해야 한다!

 

 

 

유니티 C#, 오늘날의 관찰자

 옛날(?) 프로그래머 분들의 얘기를 들어보면 이렇다. 그 당시에는 객체지향 프로그래밍이 최신 기법이었다. 모든 프로그래머가 '한 달 안에 OOP 배우기'를 원했고, 관리자는 클래스를 몇 개나 만들었느냐에 따라 프로그래머를 평가했다고는 한다. 프로그래머는 클래스 상속 구조의 깊이와 자신의 실력을 동일시했다.

 

 그런 시절에 관찰자 패턴이 알려지다 보니 당연히 클래스에 많이 의존하게 되었다. 하지만 요즘 프로그래머는 함수형 언어에 더 익숙하다. 당연히 나만 하더라도 함수를 모아놓은 클래스를 따로 관리하는게 편하다고 생각할 정도이니. 알림 하나 받겠다고 인터페이스를 상속받는 건 요즘 기준으로는 아름답지 않다고 표현하더라 (ㅋㅋㅋㅋㅋ). 이런 방식은 무겁고 융통성 없어 보이지만....... 실제로 무겁고 융통성이 없다(?). 한 클래스가 대상 인스턴스별 알림 메소드를 다르게 정의할 수 없다는 점만 해도 그렇다. 그래서 조금만 더 고치면.

 

 메소드나 함수 레퍼런스만으로 '관찰자'를 만드는 것이다. 일급 함수, 그 중에서도 클로저를 지원하는 언어에서는 이렇게 관찰자를 만드는 게 훨씬 일반적이다. 패턴 인터페이스를 상속받지 않고, 함수만으로 만이다! 아주 다행이도 유니티 C#에서는 언어 자체에 event가 있어서 메소드를 참조하는 delegate로 관찰자를 등록할 수 있다. 자바스크립트 메인 이벤트 시스템에서는 EventListener 프로토콜을 지원하는 객체 자체가 관찰자가 되는데, 이것 역시 함수로도 가능하다.

 

 

저 관찰자 패턴을 즐겨 써요

 어쨌거나 자주 쓰이는 패턴이다. 관찰자 패턴은. 이벤트 시스템이나 다른 유사 관찰자 패턴들이 이제는 너무 흔하고 정형화되어 있다. 싱글톤 만큼이나 유명할 것 같다. 하지만 관찰자 패턴을 이용해 대규모 프로그램을 만들다 보면 관찰자 패턴 관련 코드 중에서 많은 부분이 결국에는 다음과 같은 공통점이 있다는 걸 알게 된다.

1. 어떤 상태가 변했다는 알림을 받는다.
2. 이를 반영하기 위해 UI 상태 일부를 변경한다.

 '엥? 유저 재화가 70이 줄었다고? 그럼 보유 재화 Slider 너비를 0.3% 줄일게!' 라고 하는 식이다. 하다 보면 상당히 지겹다. 소프트웨어 엔지니어들은 이런 지루함을 제거하기 위해 오랫동안 노력해 왔다. 그게 함수형 반응형 프로그래밍이니 뭐니 하는 결과로 나타났기도 했고. 이런 프레임워크에서는 보통 '데이터 바인딩'을 지원하는데, 게임의 핵심 코드에 적용하기는 너무 느리고 복잡하다. 하지만 UI같이 게임에서 성능에 덜 민감한 분야에서는 대세가 될 지도..?

 

 기존 방식의 관찰자 패턴 역시 충분히 훌륭하기 때문에 계속 사용될 것이다. 이름에 '함수형'과 '반응형'을 같이 붙여놓은 무언가의 최신 방식(....)보다야 덜 흥미롭겠지만 여전히 단순하고 잘 동작한다. 적어도 내가 맡은 프로젝트에서는. '단순하고 잘 동작한다'는 것은 내가 해결책을 고를 때 가장 중요하게 보는 기준이기도 하다. 그래서 관찰자 패턴은 알게 모르게 나에게 신임을 쌓아 왔고, 이제 나는 관찰자 패턴을 즐겨 쓴다고 이야기한다.

 

코드 리뷰가 뭐예요?

..라고 물어보면 질문받는 사람마다 다른 대답이 돌아왔었다. 용어 자체의 뜻이 모호해서 그런 걸까, 코드의 기술적 분석을 포함한다는 공통분모는 있었지만 그것 말고는 전부 생각하는 정의가 제각각이라는 게 참 신기하게 느껴졌다. 마치 '리팩토링Refactoring'과 비슷한 느낌인데, 이 역시 생각하는 뜻이 다 달라서 한 번은 이런 곤혹을 치룬 적이 있었지.


니델바 : 저 코드 리팩토링 경험이 있습니다!

시니어 : 오 그래요? 어떤 식으로 하셨죠?

니델바 : 상속구조를 개선하고, 변수명과 스크립팅 방식을 통일하고.. 뭔가 쓰레기더미로 가득 찬 집을 정리하는 느낌이었네요.

시니어 : 그래서요?

니델바 : 네?

시니어 : 그건 코드 리팩토링이 아니죠.

니델바 : 


물론 내가 겪은 상황을 단면으로 일축하다 보니 저 대화 양상에서의 상대방이 예의 없는 사람(...)처럼 보일 수는 있겠으나 요지는 그거였다. 업무 혹은 Progress의 용어도 사람마다 다 다를 수 있겠구나 하는 것. 물론 나는 저 시니어에게 '코드 리팩토링이란 무엇인가-' 에 대하여 자그마치 20분간 일장 연설을 들어야 했다. 이런 상황이 '코드 리뷰'라는 단어에서도 그대로 이어졌는데, 멘붕하게 된 이유는 다음 대화에서였다.


친구 : 이번에 회사에서 코드 리뷰 있어서 바빠..

니델바 : 헐 그래? 그럼 너가 PT하니?

친구 : 아니. 소집자는 내가 아냐.

니델바 : (소집자..?) 에엫 그럼 누가 리뷰하는데?

친구 : 코드 리뷰를 한 사람만 하는건 아니니까.. 아이고 귀찮아 죽겠다.

니델바 : 아..!


친구가 생각하는 코드 리뷰의 정의는 내가 생각하는 것과 달랐다. 저 때 나는 대충 눈치빠른 척을 하며 '다 같이 모여 공통개발코드에 대한 피드백을 하는 자리겠거니-' 하고 넘어갔던 기억이다.


 

코드 리뷰는 숙제 검사 맡는 자리?

그렇다면 나는 이제껏 코드 리뷰를 뭐라고 생각하고 있었는가? 돌이켜본다면.. 코드 리뷰라고 부르는 n번의 자리에서 공통으로 내가 했던 것들을 떠올리게 되는 것이다. 나는 이 프로젝트와 코드가 대체 어떻게 굴러가는지 기술적 인과관계를 설명해야 했고, 듣는 사람은 단순히 고개만 끄덕거리거나 '그거 그렇게 하는 거 아닌데.' 로 응수하는, 그야말로 숙제를 검사 맡는 듯한 자리였다. 코드 리뷰를 하는 사람이 '해당 주제에 대해 얼마나 잘 알고 있나'를 테스트받는 느낌? 그렇기에 내가 경험한 '코드 리뷰'는 항상 주니어들의 몫이었다.

 

피드백을 준다는 것

지금은 생각이 좀 바뀌었지만 역시 누군가가 '코드 리뷰가 뭐예요?' 라고 묻는다면 아직 대답하기 어렵다. 이상적인 코드 리뷰가 무엇인지는 어렴풋이 알고 있으나 이게 막상 오프라인 회의실에 자리 깔고 앉으면 서로 의미 없는 논쟁하기 바쁘다는 이미지가 아주 크다. 코드 컨벤션을 지키지 않았다는 둥 괄호 위치가 잘못되었다는 둥 하등 쓸모없는 얘기가 나온다는 것은 코드 리뷰의 질이 그만큼 나쁘단 의미이기도 하다. 피드백을 주고받는 것은 곧 토론과 논쟁의 소지가 있는 것이다. 각자 가지고 있는 개발 스타일에 반하는 것이기도 하고, 때에 따라서는 과도한 논쟁으로 이어지기 쉬우니 그냥 '저 사람은 스타일이 그래' 하며 넘어가려는 분위기가 되고 만다.

 

가령 이슈(버그)가 하나 터졌다고 치자. '이 버그는 누가 만들었는가' 라고 질문 하는 것과 '왜 지금까지 이 버그가 발견되지 않았는가' 라고 질문하는 것의 차이는 매우 매우 크다. 다른 사람이 짠 코드를 들여다보고 피드백을 주고 받는 것이 코드 리뷰의 기본 골자가 된다면 그 기대 효과가 더 크지 않을까? 문제는 이러한 코드 리뷰 문화를 어떻게 정착시킬 것인가에 대해서다.

 

어쩌면 꼭 사람들을 회의실에 소집해서 프로젝터를 켜지 않아도 우리들은 일상에서 이미 코드 리뷰를 꾸준히 하고 있을지도 모른다. 피드백을 줄 때는 남의 자리에 가서 마우스를 뺏어 스크롤 몇 번 휘적거리면 '음 이런 식이군!' 이해할 수 있는 짬이 있다. 그 과정에서 '이렇게 짠 이유가 있어요?' 물어볼 수도 있고, '그거 그렇게 하는 거 아닌데' 하고 이야기할 수도 있고, 따봉을 날려줄 수도 있다. 반대로 피드백을 받는 상황에서도 마찬가지일 것이다. 나의 경우 조금 푸념을 늘어놓자면 지금껏 내 코드를 리뷰해주는 사람이 거의 없다시피 했다. 내가 만났던 팀원들 성격은 너무나 완강했고, 남의 코드 들여다보기를 싫어하는 사람들 투성이였다. 예를 들어 유니티에서 모종의 이유로 Inspector 기능을 사용하지 않는 사람, 모종의 이유로 자료형 Serializable을 걸지 않는 사람이 있었는데, 이유를 물어보니 '개발자는 코드로만 개발해야 한다' 라는 해괴망측한 대답을 한 경우도 있었다. 나는 이런 부류의 사람에게는 피드백을 줄 수도, 받을 수도 없다 느끼고 무조건 이해하려는 방향으로 맘을 틀게 된다.

 

그건 내 책임이 아냐

어쨌든 나는 지금도 코드 리뷰를 준비하고 있다. 하지만 이번 자리는 단순히 숙제를 검사 맡는 자리는 아닐 것이다. 내가 왜 이렇게 코드를 짰는지에 대해 충분히 설명할 근거가 있음은 물론이고 반드시 유의미한 피드백을 받아낼 것이다. 이는 나중에 문제가 발생했을 때 코드를 짠 나만 책임을 지는 것이 아니라 그때 리뷰를 했던 사람들까지 책임을 같이 지게 하리라 생각하면서.

 

앞서 적었지만 피드백이란 건 무섭다. 논쟁으로 번지기 아주 쉽다.(특히 개발자들끼리 모이면 더더욱) 최대한 모나지 않게 진행해야 하고, 상처받을 각오도 해야 한다. 코드 리뷰를 받으며 내가 잘못되었단 생각은 뒤로 접어두자. 더 좋은 코드를 만들기 위한 집단지성을 활용하는 것뿐이다. 코드는 내가 아니다. 내가 작성한 것일 뿐이니까.

유니티에서 쓰레드

Unity3d 엔진은 단일 쓰레드를 사용한다. 다중 쓰레드를 사용함으로써 발생하는 여러가지 경합조건등을 신경쓰지 않아서 좋다. 하지만 게임을 만들다보면 어쩔 수 없이 쓰레드가 필요한 경우들이 생긴다. 멀티 쓰레드를 꼭 사용하고 싶다면 사용할 수있다. 하지만 유니티에서는 쓰레드 대신 코루틴 사용을 권장한다. 멀티 쓰레드를 사용하는 경우, 메인 쓰레드 이외의 쓰레드에서 리소스에 접근하려고 하면 에러가 발생한다. 코루틴은 이런 문제가 발생하지 않고 사용할 수 있다. 코루틴에서 Update() 처럼 주기적으로 또는 일정 시간, 프래임 등의 간격을 지정하여 일을 처리할 수 있다. 쓰레드 처럼 여러개의 코루틴을 한꺼번에 동작시키는 일도 가능하다. 하지만 쓰레드 처럼 중간에 UI 접근시 에러를 발생시키지는 않는다. 그러니까 코루틴을 적극 활용해야 한다.

 

프로파일러를 통해 메인쓰레드가 열심히 일하고 있는 모습이 보인다. 그 아래 랜더링 쓰레드 워커 쓰레드 들이 보인다.

 

메인 쓰레드 이외의 쓰레드에서 리소스에 접근시 에러 내용

get_enabled can only be called from the main thread.

Constructors and field initializers will be executed from the loading thread when loading a scene.

Don't use this function in the constructor or field initializers, instead move initialization code to the Awake or Start function.

 

어쩔 수 없이 멀티 쓰레드를 사용하는 경우

게임을 만드는 도중 여러 플러그인들을 사용하게 된다. 광고,결제,통계,소셜 등등.. 많은 플러그인들의 서비스가 돌아갈 때 각자의 쓰레드를 사용하는 경우가 많다. 또는 게임서버와 소켓 통신을 하는 경우 소켓 라이브러리를 사용해야한다. 게임서버와 소켓 라이브러리를 사용하여 소켓통신하는 경우를 예로 들어보자. 소켓 라이브러리는 서버와 통신하는 로직을 별도의 쓰레드를 돌려서 사용한다. 소켓을 통해 게임서버와 데이터를 주고 받는 과정이 이 쓰레드에서 처리된다. 그런데 주고받은 데이터를 UI에 표시하거나 게임에서 사용하려고 할 때 에러가 발생한다. 유니티의 UI나 리소스는 메인 쓰레드 이외에 다른 쓰레드에서는 사용할 수 없기 때문이다.

 

 

 

구현 예제

클라이언트

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/*
 * 서버에 데이타를 요청하고 응답받아 UI에 표시
*/
 
using UnityEngine;
using UnityEngine.UI;
 
public class Client : MonoBehaviour
{
    //Inspector
    public Text textUI;         //데이타를 표시할 UI
    //Concrete classes
    private SocketLib socket;   //서버와 통신하는 소켓라이브러리
 
    void Awake ()
    {
        socket = new SocketLib();
 
        RequestData();
    }
 
    //서버에 데이타 요청
    private void RequestData()
    {
        ClientDele requestDele = new ClientDele(ResponseData);
 
        socket.Request(requestDele);
    }
 
    //서버에서 응답 받음
    public void ResponseData(string data)
    {
        textUI.text = data;
    }
}

소켓라이브러리

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/*
 * 서버와 통신하는 소켓 라이브러리 흉내
*/
 
using System.Threading;
 
public delegate void ClientDele(string data);
 
public class SocketLib
{
    //서버 작업 처리 동안 대기할 쓰레드
    private Thread thread;
     
    private ClientDele dele;
 
    public SocketLib()
    {
        thread = new Thread(new ThreadStart(Run));
    }
 
    //서버에 요청
    public void Request(ClientDele dele)
    {
        this.dele = dele;
        thread.Start();
    }
 
    //서버 작업 진행
    void Run()
    {
        //서버 작업 시작
        string result;
 
        Thread.Sleep(3000);
 
        //서버 작업 종료
        result = "This is result data.";
 
        Response(result);
    }
 
    //응답 받음
    public void Response(string result)
    {
        //결과 클라이언트에게 전달
        dele(result);
    }
}

설명

문제가 되는 부분만 집중하여, 클라이언트와 소켓 라이브러리를 간략화 하였다. 클라이언트에서는 소켓 라이브러리를 생성하고 바로 데이타를 소켓을 통해 요청한다. 소켓라이브러리에서는 실제 서버처리는 없고 쓰레드를 통한 작업지연을 흉내내었다. 소켓 라이브러리에서는 데이타를 다시 클라이언트에 돌려줄 때 3초의 대기시간을 가졌다. 그리고 생성한 값을 클라이언트에게 돌려줬다. 클라이언트는 절달 받은 데이타를 UI에 출력했다. 에러는 UI에 데이타를 출력하는 부분에서 발생한다.하지만 데이타를 출력하는 부분이 문제가 아니다. 문제는 쓰레드를 통해 UI에 접근하려 한것이다. 

 

해결책

이 문제는 유니티 뿐만아니라 안드로이드,아이폰 등 많은 플렛폼에서 똑같이 발생한다. 에러의 원인은 대개 메인 쓰레드 이외의 쓰레드가 감히 메인쓰레드의 허락 없이 UI에 접근하려했기 때문이다. 해결책도 플랫폼마다 비슷하다. 소켓 라이브러리 쓰레드가 UI 수정에 접근할 수 없으니 메인 쓰레드에게 허락을 받거나, UI를 변경 해달라고 메인 쓰레드에게 요청하는 것이다. 구체적인 방법으로는, Queue를 하나 만들고 이것을 메인쓰레드와 이외의 쓰레드가 함께 공유하는 것이다. 

첫번째, 소켓 라이브러리는 서버로 부터 받은 데이타를 이 Queue에 push 한다. (받을 때마다 push하여 쌓아둠)

두번째, 유니티의 메인 쓰레드가 이 Queue의 크기를 주지적으로 체크하여 데이타가 있으면 그 데이타를 pop하여 UI를 변경하는 등의 행위를 한다. 

이렇게 간단하게 서로 데이타를 직접적으로 주고받지 않고 Queue를 사용하는 것이다. 유니티에서 Queue를 주기적으로 탐색하기 위해서는 Update()에서 Queue를 체그하거나 이것이 부담스럽다면 코루틴을 사용해도 좋다. Update()나 코루틴은 유니티 메인쓰레드에서 관리하기 때문에 데이타 접근시 에러가 발생하지 않는다.

 

 

 

클라이언트

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
/*
 * 클라이언트
 * 서버에 데이타를 요청하고 응답받아 UI에 표시
*/
 
using System.Collections;
using UnityEngine;
using UnityEngine.UI;
 
public class Client : MonoBehaviour
{
    //Inspector
    public Text textUI;         //데이타를 표시할 UI
    //Concrete classes
    private SocketLib socket;   //서버와 통신하는 소켓라이브러리
    private Postbox postbox;    //메세지 큐를 관리하는 우편함
 
    void Awake ()
    {
        socket = new SocketLib();
        postbox = Postbox.GetInstance;
         
        //큐 탐색 시작
        StartCoroutine(CheckQueue());
        //데이타 요청 시작
        RequestData();
    }
 
    //서버에 데이타 요청
    private void RequestData()
    {
        socket.Request();
    }
 
    //서버에서 응답 받음
    public void ResponseData(string data)
    {
        textUI.text = data;
    }
 
    //큐를 주기적으로 탐색
    private IEnumerator CheckQueue()
    {
        //1초 주기로 탐색
        WaitForSeconds waitSec = new WaitForSeconds(1);
 
        while (true)
        {
            //우편함에서 데이타 꺼내기
            string data = postbox.GetData();
 
            //우편함에 데이타가 있는 경우
            if (!data.Equals(string.Empty))
            {
                //데이타로 UI 갱신
                ResponseData(data);
                yield break;
            }
 
            yield return waitSec;
        }
    }
}

포스트 박스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
using System.Collections.Generic;
 
public class Postbox
{
    //싱글턴 인스턴스
    private static Postbox instance;
    //싱글턴 인스턴스 반환
    public static Postbox GetInstance
    {
        get
        {
            if (instance == null)
                instance = new Postbox();
 
            return instance;
        }
    }
 
    //데이타를 담을 큐
    private Queue<string>  messageQueue;
 
    private Postbox()
    {   //큐 초기화
        messageQueue = new Queue<string>();
    }
 
    //큐에 데이타 삽입
    public void PushData(string data)
    {
        messageQueue.Enqueue(data);
    }
 
    //큐에있는 데이타 꺼내서 반환
    public string GetData()
    {
        //데이타가 1개라도 있을 경우 꺼내서 반환
        if (messageQueue.Count > 0)
            return messageQueue.Dequeue();
        else
            return string.Empty;    //없으면 빈값을 반환
    }
}

소켓라이브러리

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/*
 * 서버와 통신하는 소켓 라이브러리 흉내
*/
 
using System.Threading;
 
public class SocketLib
{
    //서버 작업 처리 동안 대기할 쓰레드
    private Thread thread;
     
    public SocketLib()
    {
        thread = new Thread(new ThreadStart(Run));
    }
 
    //서버에 요청
    public void Request()
    {
        thread.Start();
    }
 
    //서버 작업 진행
    void Run()
    {
        //서버 작업 시작
        string result;
 
        Thread.Sleep(3000);
 
        //서버 작업 종료
        result = "This is result data.";
 
        Response(result);
    }
 
    //응답 받음
    public void Response(string result)
    {
        //결과 데이타를 큐에 넣기
        Postbox.GetInstance.PushData(result);
    }
}

설명

포스트 박스를 추가하였다. 기존에 클라이언트와 소켓 라이브러리가 데이터를 직접 전달 받았지만 이제 중간에 큐를 통해 데이터를 전달하게 되었다. 직접 전달할 필요가 없으니 델리게이트는 제거했다. 클라이언트에서 소켓라이브러리로 데이터를 요청하는 것은 큐를 통하지 않아도 된다. 리소스나 UI에 접근하는 일과 상관 없기 때문이다. 하지만 소켓라이브러리에서 데이타를 클라이언트에게 전달하는 방법은 중간에 큐를 통하는 것으로 바뀌었다. 소켓라이브러리는 서버에서 받은 데이타를 큐에만 쌓는 것으로 역할은 끝이다. 클라이언트가 가져다 쓰건 말건 상관하지 않는다. 마치 등기 우편과 일반우편의 차이같다. 등기 우편은 집배원이 수신자의 집에 찾아가 수신자에게 직접 편지를 전달한다. 하지만 일반 우편은 집배원이 수신자의 집 우편함에 편지만 두고 떠난다. 수신자가 우편함을 열고 편지를 찾아가야 한다. 집배원은 편지를 넣고 떠난뒤 수신자가 편지를 찾아가건 말건 상관하지 않는다. 수신자는 주기적으로 우편함을 열어서 편지가 왔는지 체크해야한다. 위의 예제에서는 코루틴을 사용하여 1초 주기로 큐를 검사하도록 하였다. 코루틴 말고 Update()을 사용해도 괜찮다. 둘 모두 에러를 발생하지 않는다.

CBT인원과 기간이 확정되면서 고난의 앱심사 과정의 문을 두드렸다. 그렇게 맞은 첫 번째 Reject. 앱 출시 심사보다야 훨씬 강도가 마이너(?)하겠지만 그래도 한 번에 통과될리는 없을 줄 알고 있었다. 하나씩 처내려가는 과정을 기록해 둔다.

 

ITMS-90683 : Missing Purpose String in Info.plist

해달라는 대로, Plist Info에 해당 키값을 추가하면 된다. 그런데 앱에는 딱히 사용자 photo library를 사용하는 부분이 없긴 한데.. 아마 사용중인 API 일부에서 이 프로토콜을 쓰는지도 모르겠다. 정상적인 처리방법이라면야 이 API들을 긁어내 Deprecate시키는게 이상적이겠으나, 나에겐 시간이 없었다. 고분고분히 말을 들어주자.

 

ITMS-90809 : Deprecated API Usage

 

이 포스팅을 쓴 이유이기도 하다. 더 이상 지원하지 않는 API를 쓰고 있단 사유로, 소셜로그인에 쓰인 "UIWebView"가 이에 해당했다. 

이 경우는 간단했다. UIWebView를 우리는 어디에 쓰고 있는지 이미 알고 있었으니, 시간이 걸릴지라도 그것을 교체해주면 된다. WebView SDK중 WKWebView를 추천하길래 알아보다가, 시간을 절약할 셈으로 NHN에서 내놓은 오픈소스 라이브러리인 GPM을 차용하기로 결정했다.

 

GPM(Game Package Manager) : https://github.com/nhn/gpm.unity

 

GitHub - nhn/gpm.unity: A brand of NHN providing free services required for game development.

A brand of NHN providing free services required for game development. - GitHub - nhn/gpm.unity: A brand of NHN providing free services required for game development.

github.com

잠시 한탄을 해보자면.. 새로운 API 혹은 SDK를 프로젝트에 추가한다면 정말정말 면밀한 검토가 필요하겠구나, 느꼈다. 이 오픈소스 혹은 라이브러리가 검증된 것인지, Reference는 충분한지, 인지도가 있는지, 사용하기 쉬운지 등등.. 새로운 SDK가 추가된다면 해당 추가된 빌드를 가지고 수시로 TestFlight에 등록해두는 편이 나을 것 같다. 그것을 간과했으니 일정계산에 차질이 생길 수 밖에.

 

어쨌든 이걸로 해결봤다고 생각했으나 문제는 더 있었다. UIWebView Plugin을 프로젝트에서 모조리 삭제했으나 다시 한번 심사거부가 난 것. 아니, 우리는 안 쓰고 있다니까요?! 그래. 한번 Project에서 찾아보기로 했다. 콘솔에서 grep -r "UIWebView" /Dir 명령어로 해당 프로젝트의 문자열 참조를 모두 검색할 수 있다.

grep으로 문자열을 메타데이터 단위로 검색

지금은 이미 해결이 된 상태에서 쓰는 글이기 때문에 ^^ 표시는 되지 않지만 만약 프로젝트에 문자열이 있다면 어디에 포함이 되어있는지 표시가 된다. 우리의 경우엔 GoogleSignIn (또..또!!!)이 문제였고, 해당 SDK 문서에서 WebView 항목을 찾을 수 있었다. GoogleSignIn은 5.0.0 이후로 WebView의 참조를 중단하고 있었고 현재 쓰는 버전은 4.3.0이었다. pod으로 해당 SDK를 업데이트해주고, 몇 가지 이슈를 해결보았다.

 

사실 이 과정에서도 좀 앓았는데, SDK 업데이트 이후 xCode에서 자꾸 빌드실패가 뜨는 것. Library 참조가 이상한건지, Swift 코드들이 서로 오류를 일으키는 것... 그래서 github의 changeLog를 보고 직접 코드를 수정했다.

https://github.com/googlesamples/google-signin-unity/pull/126/files

 

Update iOS pod to >= 5.0.0 and apply migrations for native plugin code. by lukezbihlyj · Pull Request #126 · googlesamples/goo

Fixes for the updated GoogleSignIn iOS pod version 5.0.0+, see issue #122

github.com

 다만 위 로그는 iOS Native코드이기 때문에 Unity에서 사용되는 Object-C코드를 봐야 한다. GoogleSignInAppController.mm에서 해당 코드를 수정봤다. 그리고 빌드성공이 뜨고, 업로드!

 

.

.

.

.

.

 

 

내 인생이 그렇지 뭐. 3번째 Reject

그래 다음 리젝 사유가 왔다. Push Notification Certificate가 잘못되었다고 하는 그런 종류의. 나는 이 인증서가 만료되었거니~ 하고 개발자콘솔에 들어가봤지만 인증서와 키들은 그대로 온전히 남아있는 상태였다. 아니 왜지? 싶으면서 몇십분 삽질을 하다가 아! 젠킨스 빌드가 xCode프로젝트를 덮어씌우면서 Capability 속성이 초기화가 된 것이렸다.

 

Capability에 Push Notifications 추가

 

그리고 다시 심사를 위해 업로드했고, 그리고 통과됐다. 저 망할 GoogleSignIn때문에 이틀간 삽질한걸 생각하니 열불이 뻗쳐.

최근 UnityEngine의 개발환경을 그대로 차용한 VRChat 게임에 관심을 가지고 있었다. Unity Tool로 구현이 가능한 그래픽적 요소가 바로 게임 내에서 반영된다는 점 덕분에 수많은 창작자들이 UnityEngine을 탐구하며 별도의 포럼을 이루고 있는 듯 했다. 당연하게 게임 내엔 Compiler가 없기 때문에 코드적인 접근은 불가능하지만 VRChat에서 제공하는 별도의 SDK 내의 Class들은 전부 접근을 용이하게 해 놓았더라. 정말 대놓고 창작 자체를 유니티로만 하라 라는 뜻이리라. 장단은 잘 모르겠지만.

 

배경은 뒤로 하고, 어쨌건 내가 원하는 아바타를 만들기 위해선 좋든 싫든 유니티 Tool을 만져야 한다. 그래픽과 Legacy/Mechanim 애니메이션 요소의 Customize는 충분히 코드 없이도 가능하다. 하지만 지식이 없어도 되는것은 아니다. 방대한 창작의 자유도를 전부 창작자 본인에게 맡기는 방식이 누군가에겐 독이 될 수도 있다는 생각을 했다.

 

제한적인 Preset 형태의 커스터마이징조차 누군가에겐 스트레스일지도.

 

단순히 커스터마이징이 아닌 기능 구현의 경우는 더 골머리가 아프다. Animation Controller를 기반으로 작동하는 Descripter의 FX는 이 기능 자체를 세세하게 파고들지 않으면 습득이 어려운 축이다. 내가 캐릭터의 안경을 벗고 쓰는 동작을 만들기 위해 변수와 Parameter의 개념을 이해할 필요가 있나 싶은 것. 수개월 수년동안 툴을 만진 사람이 포럼 내에서는 "고인물"로 통하며, 누군가에게 돈을 받고 대신 작업을 해주기도 한다. 오 맙소사.

 

앞서 말한 VRC SDK의 경우 Humanoid 기반의 Avatar Object의 Serialization과 Upload를 담당한다. 그 과정 속에 당연히 Anim Controller 관련 Parameter 정보를 포함하며, 이는 코드로 접근이 가능하다고 했다. 안경을 벗고 쓰는 메뉴와 동작을 손쉽게 좀 바꾸어 보자. 기본적으로 Bool 형태의 Parameter를 이용해 구현하는 방법은 다음과 같다.

 

1. 해당 아바타의 FX 레이어를 찾는다.

2. VRC Parameter를 추가한다.

3. Animation Controller를 생성하고, State와 Transition을 설정한다.

4. Expression Menu에서 해당 Parameter를 변경하는 메뉴를 추가시킨다.

 

이 과정의 목표를 따라, 한번 자동화를 구현해보도록 하자. 기능 구현에 초점을 맞추기보단 VRC SDK가 유저편의를 위해 어떤 Class들을 제공하는지 알아보는 것이 목표긴 했지만 (..)

 

 

1. FX Layer / Parameter 파일 탐지

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
bool fxFind = false;
            for (int i = 0; i < avatarDescriptor.baseAnimationLayers.Length; i++)
            {
                var test = avatarDescriptor.baseAnimationLayers[i];
                switch (test.type)
                {
                    case VRCAvatarDescriptor.AnimLayerType.FX:
                        var tempFX = avatarDescriptor.baseAnimationLayers[i];
                        if (!tempFX.isDefault)
                        {
                            fxFind = true;
                        }
                        else
                        {
                            fxFind = false;
                        }
                        if (tempFX.animatorController.ToString() == "null")
                        {
                            fxFind = false;
                        }
                        else
                        {
                            fxFind = true;
                        }
                        FX = avatarDescriptor.baseAnimationLayers[i].animatorController as UnityEditor.Animations.AnimatorController;
                        break;
                }
            }
cs

Avatar Descriptor Class엔 CustomAnimLayer[] 형태의 AnimLayer가 존재한다. 그리고 Enum Type의 AnimLayerType.FX로 해당 FX 레이어를 탐색하면 손쉽게 아바타의 FX Controller를 찾을 수 있었다.

 

Parameter 탐지의 경우 좀더 쉬웠다. Descriptor Class만 찾을 수 있다면 VRCAvatarDescriptor.VRCExpressionParameter로 바로 접근이 가능하다. 이를 생성해주면 ,FX와 Parameter의 할당은 끝이 난다.

 

 

2. Animation State, Transition 생성

 

여기서부턴 VRChat의 영역이 아니다. 손쉽게 작업할 수 있는 Tool을 버리고 코드를 짠다는게 웃기긴 하겠지만 개발자는 5분 편하자고 5시간을 고생하는 똥멍청이들이다. 어쨌든, 1번에서 찾은 Parameter와 FX Layer에 해당 이름을 추가하는데 성공했다면 안경을 벗고 끄는것에 대한 Anim State를 자동으로 생성해줘야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
FX.AddLayer(targetLayer);
 
var nullState = FX.layers.Last().stateMachine.AddState("null");
 
var turnonState = FX.layers.Last().stateMachine.AddState(turnOn.name);
turnonState.motion = turnOn;
turnonState.writeDefaultValues = true;
 
var turnoffState = FX.layers.Last().stateMachine.AddState(turnOff.name);
turnoffState.motion = turnOff;
turnoffState.writeDefaultValues = true;
 
var nullToTurnOnTrans = nullState.AddTransition(turnonState);
var nullToTurnOffTrans = nullState.AddTransition(turnoffState);
 
nullToTurnOnTrans.hasExitTime = false;
nullToTurnOnTrans.exitTime = 0;
nullToTurnOnTrans.duration = 0;
 
nullToTurnOnTrans.AddCondition(UnityEditor.Animations.AnimatorConditionMode.If, 0, resultParamName);
 
nullToTurnOffTrans.hasExitTime = false;
nullToTurnOffTrans.exitTime = 0;
nullToTurnOffTrans.duration = 0;
cs

FX에 해당 Layer를 추가하고, Transition을 일일히 설정해주는 노가다를 진행한다. 단순히 옷을 입고 벗는 과정에서의 Transition의 시간은 필요없으니 hasExitTime과 duration은 전부 0으로 설정한다. 이렇게 앞뒤로, If와 Ifnot을 설정해준다면 원하는 형태의 Parameter로 조정할 수 있게 된다.

 

생성 완료! 위치는 신경쓰지 말것 :D

이제 이 자동화 기능을 가지고, Editor 단에서 UI를 입혀주면 끝이 난다. 내가 캐릭터에게 안경을 벗고 씌우는 기능을 구현하기 위해서 필요한것은 FX Layer와 Parameter Control이 아닌, 안경 하나여야만 한다는게 취지였다. 창작 자유도의 경계를 제한하려는 것이 아닌, 단순한 접근성과 편의성에 대한 고민이다. 이 포스팅에선 단순 bool 구현에서 그쳤지만 다른 많은 부분에서도 창작자들의 편의를 위해 SDK가 꾸준히 업데이트되길 바란다.

 

 

 

참고

https://forum.unity.com/threads/animatorcontroller-addlayer-doesnt-create-default-animatorstatemachine.307873/

https://answers.unity.com/questions/1023907/how-can-i-create-a-animatorstatetransition-conditi.html

 

니델바, Firebase 써 봤어요?

 

음, 아니. 분명 대학다닐 시절 언젠가 DB서버로 한 번 활용한 적이 있었을 것이다. 구글 모바일 플랫폼으로 굉장히 자주 쓰인다 들어는 봤지만 이렇게 빨리 경험하게 될 줄은. 유니티의 파이어베이스 연동과 구글 로그인까지 구현하며 했던 삽질을 기록해 둔다.

 

사실 이 프로젝트에는 이미 로그인시스템이 구현되어 있었다. 여타 앱에서 사용하는 페북, 구글, 애플, 그리고 본사 계정 로그인 방식까지 지원하는. 다만 이번에 프로젝트를 파이어베이스로 이전하게 된 가장 큰 이유는 Unity WebView로 구현된 로그인창이 계속 Google Auth Token을 못 받아온다는 문제가 있어서인것 같았다. DontDestroyOnLoad에 걸어둔 Singleton의 Instance가 자꾸 초기화가 된다나 뭐라나. 겸사겸사 더 좋은 플랫폼으로 이사도 할 겸, 주니어인 나에게 한번 구현해보라 시킨 것. 실패해도 프로젝트 진행엔 별 타격이 없을테니 말이다.

 

사실 문제가 하나 더 있었다. 단순 유니티와 파이어베이스의 연동은 구글에게 물어봐도 대답을 잘 해준다. 어렵게 없을거라 생각했다. 위에서 말했던 Google Auth Token을 받아오는 그 과정이 문제였다. 이것을 무슨 수로 따냐 하니, 대부분의 사람들은 Google GamePlay Service를 이용했다. 아니, 나는 Google GamePlay가 아니라 단순 Google E-Mail 로그인을 원했다고! 그것마져 별도의 Activity를 사용하는 안드로이드와 다르게 IOS엔 적용조차 불가능했다. 그래서 했다. 삽질을..

 

다시 한 번 적지만 이 글은 Google Play Console을 사용하는것이 아닌, 단순 Google 계정을 통한 연동에 관한 글이다.

 

 

 

1. Unity Project에서 Keystore 생성, SHA-1 인증서 지문 추출

 

기존 프로젝트라면 이미 Keystore쯤은 가지고 있었을 것이다. 있다면 그것을 쓰면 되고, 나는 새 프로젝트의 테스트 용도차 처음부터 세팅을 해야하니 Keystore를 가져오기로 한다.

<Android Switch Platform>

 

Player Settings - Publishing Settings - Keystore 파트에서 Keystore를 생성한다.

keystore의 생성, 그리고 파일 저장 위치를 설정 후 Password를 설정한다.

Alias 부분은 이 키의 식별자인데 프로젝트 이름으로 해도 좋고 아무렇게나 적는다.

Certificate는 이 인증에 사용할 개인정보를 입력하는데, 이 정보는 앱에 당연히 표시되지 않지만 apk 파일 안엔 일부로 들어간다고 한다. 나의 경우는 생략했다.

keystore가 정상적으로 만들어줬다면, cmd창에서 keystore 파일이 있는 위치로 디렉터리를 이동 후

keytool -list -v -keystore [keystore이름] 을 입력한다. SHA1 그리고 SHA256 인증서 fingerprint를 확인할 수 있다.

이 인증서 지문은 파이어베이스 프로젝트 세팅이나 API등 여러 곳에서 이용되니, 보안을 지켜 적절한 곳에 메모해두도록 하자.

 

아 참, keytool 명령어를 찾을 수 없는 경우엔 '시스템 환경 변수'의 path에 사용중인 jdk/bin 폴더를 추가해주면 된다.

 

 

2. Firebase Console 프로젝트 설정 (https://console.firebase.google.com/)

 

파이어베이스 콘솔로 접속해, 새로운 프로젝트를 생성한다.

이 Cloud System에 적당한 이름을 짓고

Google Analytics를 이용할 것인지 묻는다. 나중 api 설정에서도 다시 바꿀 수 있으므로, 나는 사용하지 않고 다음.

이예에~!!

 

Firebase Console - Project Settings에서

앱을 추가! 내 경우는 Android를 먼저 세팅했다.

 

Android Package Name, 그리고 앱 이름을 입력한 후 등록

 

구성파일 다운로드는 현재 Console에서의 프로젝트 설정이나 api값이 바뀌면 다시 다운로드해서 넣어줘야하기에 지금은 패스한다. 나머지도 지금은 패스하고, 앱 설정을 완료한다.

 

앱 설정이 완료되면, 위와같은 창에서 아까 기록해둔 SHA 인증서 지문을 추가한다. 나는 SHA1만 써놨는데, SHA256도 필요할지는 잘 모르겠다. 여기까지 하면, Firebase Console의 세팅은 끝이 난다.

 

 

 

3. Google Cloud Playform API 세팅 (https://console.cloud.google.com/)

 

파이어베이스 프로젝트 세팅이 되면, 해당 프로젝트의 ID와 번호를 그대로 승계받아 Google Cloud Playform의 프로젝트에서 찾을 수 있다.

 

만약 프로젝트가 보이지 않는다면 아까 파이어베이스에 만들어둔 프로젝트 이름으로 검색해 직접 등록한다

해당 프로젝트의 API - 사용자 인증 정보(Credential) 파트에서 OAuth Web Client에 대한 정보를 찾을 수 있다. 클릭하여 클라이언트 ID를 메모해 두자.

 

 

 

 

4. Unity Setting

 

몇 가지 준비물이 필요하다.

 

google-signin-plugin-1.0.4.unitypackage
0.52MB

본디 인증 Token을 얻는 과정에서 따로 서버와 통신하는 Script를 짜거나, 별도의 Activity를 여는 과정에서 삽질이 많을텐데 역시나 누가 만들어둔 오픈소스가 존재했다. 받아서 Import 시켜주자

 

그리고 Firebase 페이지에서 SDK를 준비한다. 유니티 2017버전 이상이라면 dotnet4 폴더의 FirebaseAuth.unitypackage를 Import 시켜준다. 만약 인증이 아니라 다른 기능이 필요하다면, 동봉된 별도의 unitypackage를 차곡차곡 심어주면 된다.

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Firebase;
using Firebase.Auth;
using Google;
using UnityEngine;
using UnityEngine.UI;
 
public class GoogleSignInDemo : MonoBehaviour
{
    public Text infoText;
    public string webClientId = "<your client id here>";
 
    private FirebaseAuth auth;
    private GoogleSignInConfiguration configuration;
 
    private void Awake()
    {
        configuration = new GoogleSignInConfiguration { WebClientId = webClientId, RequestEmail = true, RequestIdToken = true };
        CheckFirebaseDependencies();
    }
 
    private void CheckFirebaseDependencies()
    {
        FirebaseApp.CheckAndFixDependenciesAsync().ContinueWith(task =>
        {
            if (task.IsCompleted)
            {
                if (task.Result == DependencyStatus.Available)
                    auth = FirebaseAuth.DefaultInstance;
                else
                    AddToInformation("Could not resolve all Firebase dependencies: " + task.Result.ToString());
            }
            else
            {
                AddToInformation("Dependency check was not completed. Error : " + task.Exception.Message);
            }
        });
    }
 
    public void SignInWithGoogle() { OnSignIn(); }
    public void SignOutFromGoogle() { OnSignOut(); }
 
    private void OnSignIn()
    {
        GoogleSignIn.Configuration = configuration;
        GoogleSignIn.Configuration.UseGameSignIn = false;
        GoogleSignIn.Configuration.RequestIdToken = true;
        AddToInformation("Calling SignIn");
 
        GoogleSignIn.DefaultInstance.SignIn().ContinueWith(OnAuthenticationFinished);
    }
 
    private void OnSignOut()
    {
        AddToInformation("Calling SignOut");
        GoogleSignIn.DefaultInstance.SignOut();
    }
 
    public void OnDisconnect()
    {
        AddToInformation("Calling Disconnect");
        GoogleSignIn.DefaultInstance.Disconnect();
    }
 
    internal void OnAuthenticationFinished(Task<GoogleSignInUser> task)
    {
        if (task.IsFaulted)
        {
            using (IEnumerator<Exception> enumerator = task.Exception.InnerExceptions.GetEnumerator())
            {
                if (enumerator.MoveNext())
                {
                    GoogleSignIn.SignInException error = (GoogleSignIn.SignInException)enumerator.Current;
                    AddToInformation("Got Error: " + error.Status + " " + error.Message);
                }
                else
                {
                    AddToInformation("Got Unexpected Exception?!?" + task.Exception);
                }
            }
        }
        else if (task.IsCanceled)
        {
            AddToInformation("Canceled");
        }
        else
        {
            AddToInformation("Welcome: " + task.Result.DisplayName + "!");
            AddToInformation("Email = " + task.Result.Email);
            AddToInformation("Google ID Token = " + task.Result.IdToken);
            AddToInformation("Email = " + task.Result.Email);
            SignInWithGoogleOnFirebase(task.Result.IdToken);
        }
    }
 
    private void SignInWithGoogleOnFirebase(string idToken)
    {
        Credential credential = GoogleAuthProvider.GetCredential(idToken, null);
 
        auth.SignInWithCredentialAsync(credential).ContinueWith(task =>
        {
            AggregateException ex = task.Exception;
            if (ex != null)
            {
                if (ex.InnerExceptions[0is FirebaseException inner && (inner.ErrorCode != 0))
                    AddToInformation("\nError code = " + inner.ErrorCode + " Message = " + inner.Message);
            }
            else
            {
                AddToInformation("Sign In Successful.");
            }
        });
    }
 
    public void OnSignInSilently()
    {
        GoogleSignIn.Configuration = configuration;
        GoogleSignIn.Configuration.UseGameSignIn = false;
        GoogleSignIn.Configuration.RequestIdToken = true;
        AddToInformation("Calling SignIn Silently");
 
        GoogleSignIn.DefaultInstance.SignInSilently().ContinueWith(OnAuthenticationFinished);
    }
 
    public void OnGamesSignIn()
    {
        GoogleSignIn.Configuration = configuration;
        GoogleSignIn.Configuration.UseGameSignIn = true;
        GoogleSignIn.Configuration.RequestIdToken = false;
 
        AddToInformation("Calling Games SignIn");
 
        GoogleSignIn.DefaultInstance.SignIn().ContinueWith(OnAuthenticationFinished);
    }
 
    private void AddToInformation(string str) { infoText.text += "\n" + str; }
}
cs

 

DemoScript를 작성한다. 메소드별로 굵직굵직하게 OOP로 나뉘었으니 SignIn, Out, 그리고 Disconnect 정도를 할당해 쓰면 되게끔 만들었다. 그리고 추후 GooglePlay와의 연동 확장성을 생각해서 GoogleSignIn.Configuration.UseGameSignIn 변수를 사용해서도 하나 만들었으니 나중에 이용할 수 있도록 했다.

 

그렇게 대충 Canvas에 쭉쭉 긋고 프로토타입 빌드 후, 앱 내에서 테스트 진행.

 

구글 ID Token값을 정상적으로 받아오고

 

파이어베이스 Authentication에서도 정상적으로 로그인 기록이 남는걸 확인할 수 있다.

이렇게 구현 완료!!!!

 

 

 

-----추가

위에서 사용한 그 오픈소스의 경우, 별도의 구글 로그인창을 띄우기 위해 AndroidJavaObject의 Activity를 활용한다.

IOS의 경우는 그대로 zero를 return해버리는데, 이에 대한 대응이 되어있는지 몰라 직접 물어봤다. 지금은 재택근무 중이라 ios빌드테스트를 위한 환경도 셋팅되어있지 않아 답답한데.. ㅠㅠ

 

돌아온 개발자의 답변

답변을 받았다. '직접 테스트해보셈. 아마 될거임. 나 사과폰도 없어 ㅋ'

ios쪽에서 Activity를 별도로 만들수 있는 방법을 알아보고, 진전이 있다면 다음 포스팅에서.

 

----------------

참고

[Unity 이론] Google + Firebase 로그인 (https://inyongs.tistory.com/35)

Firebase Authentication in unity with Google Provider (https://youtu.be/pqJLHWFGhH4)

 

 사실 나는 새로운 시스템 혹은 라이브러리의 도입/사용을 굉장히 꺼려하는 타입이다. NGUI보다 UGUI를 선호하며, IDE는 확장기능 없이 순정만을 선호하고, 단축키를 바꾸는 일도 드물다. 코딩에 그렇게나 편하다던 해피해킹 키보드는 Fn키 사용에 벽을 느끼고 2주만에 다시 되팔아버렸고. 어떻게 보면 개발자로선 아주 못된 마음가짐일지도 모른다. 조금의 항변을 해보자면 확장/기능성 라이브러리에 익숙해지면, 그 라이브러리 없이는 코딩을 할 수 없다. 또는 개발 도중에 라이브러리가 도입되면 소스분석에 시간이 더 들어간다, 뭐 그런 이유 때문이리라.

 

 UniRx에 대한 배경과 기초설명은 조금만 찾아보면 아주 쉽게 찾아볼 수 있다. 실제로 현업에선 17년도부터 알음알음 써왔던것 같기도 하고. 이 포스트에선 UniRx의 사용방법이나 기초설명 보다는 사용기/예제에 초점을 맞추고자 한다. 

 

 실제로 UniRx는 굉장히 편리하지만 내가 느낀 단점도 있다. 나름의 학습 코스트가 높고, 개념적으로도 어렵다. 기존의 OOP 프로그래밍 패러다임과 워낙 다르다보니 바로 와닿지 않는 부분도 많다. Observable과 Stream을 사용한 비동기의 개념이다. 유니티 UI 내 OnClick 메소드는 잊어버리는게 나을 정도고, 이를 위해서는 설계 단계에서부터 UniRx를 포함해 설계를 해야하는게 좋을 정도이다. 즉, UniRx는 굉장히 편리한 라이브러리가 아닌 언어 확장이라고 생각할 필요가 있다. 몇 가지 예제를 기록해 둔다.

 

1. 더블클릭 판정 ( Observable / Operator / Subscribe )

 UniRx의 핵심이 되는 3가지 개념이고, 이를 한번에 잘 나타내는 예제는 바로 더블클릭 판정이다.

더블클릭 판정 예제

 생성된 Observable 객체는 유니티 MonoBehaviour의 매 프레임마다 값을 관찰하는 스트림을 생성한다. Where 람다절에서 매 Update 함수 호출시 마우스 왼쪽 버튼 클릭 여부를 감시하고, clickStream 스트림으로 값을 바로 흘려 보내지 않고 버퍼에다 저장한다. 즉, 버퍼에 모인 값의 개수가 2개 이상인 경우를 체크하는데, Throttle 밸브를 250ms 동안 열어두고 있다는 뜻. 그리고 최종적으로 Subscribe 절에서 감지 성공시 이벤트를 넣어주면, 끝난다.

 

 

2. Update() 없애기

 아마 Update 쓰기 좋아하는 유니티 개발자 없을거라 생각한다. 프레임마다 호출되는 메소드는 사용 부담이 너무나 크다. 대부분 코루틴을 활용한 비동기 처리를 던지거나, 실시간 변수감지를 탐지하는 무언가의 OOP 확장 메소드를 도 사용하고 여간 불편한게 아니다. 그러다보면 또 if-else절로 도배가 될 것이도, 가독성에도 좋지 않고 가정이 무너지고 사회가 무너지고...

무려 Awake/Start 단에서 한 번만 등록해두면 끝난다!!

 이걸 보면 LINQ를 몰라도 '아 LINQ는 이렇게 쓰는거구나' 감이 올 정도로 굉장히 간단명료해진다. (ㅋㅋㅋㅋㅋㅋ) 저렇게 캐릭터의 유한상태머신을 컨트롤하는 경우에도 매 Input값을 감지하기 위해 Update문을 돌릴 필요가 전혀 없어진다. 단지 Observe하는 Stream을 열어둘 뿐이므로, 자원 관리에도 탁월하다. 그리고 무엇보다, 병렬적으로 쉽게 알아볼 수 있게 된다. 즉, 기능 추가, 제거, 변경이 훨씬 용이해지게 되고 코드가 선언적이 되며 처리 의도를 쉽게 알아볼 수 있다. 이 얼마나 깔끔한가!!!

 

 단, Observable.EverUpdate에는 한가지 주의점이 있는데 유니티 MonoBehavior가 Destroy될 때에 OnCompleted가 발생되지 않는다는 것. 위에 예제로 적어둔 UpdateAsObservable과는 좀 다른 녀석인데, 해당 스크립트가 있는 gameObject가 파괴되면 해당 오브젝트를 참조하고있는(transform 프로퍼티라던가..) 녀석들이 죄다 Null이 되며 예외가 발생한다.

 

3. Reactive Property 활용

 

 이 Reactive Property의 가장 두드러지는 장점은, 유니티 에디터 자체에서 직렬화Serializable이 가능하단 뜻이다. 당연히 Inpector에 표시되고, 편집 가능하고, 커스텀이 가능하다. 당연히 Stream을 제어하므로 즉각적인 값 반영도 이루어진다. 캐릭터의 HP, 남은 시간 등등 값이 바뀜에 따라 분기가 달라지는 코드를 수동으로 작성하지 않고 ReactiveProperty를 쓰면 아주 손쉽게 Stream을 먹일 수 있다!

"모닥불 정령이 언제 방문했는지 알 수 있는 방법이 있을까?"


2011년에 오픈한, 유용한 마비노기 데이터를 제공해오던 사이트 싴갤러스가 지금은 트럭 시위의 여파로 운영이 중단되었다고 한다. (1차 충격)



가만 생각해보니 온라인게임 채팅 확성기(사자후) 기능을 했던 뿔피리는 채팅 로그에 밀려 올라가 순식간에 시야에서 사라지기 때문에

싴갤러스의 '뿔피리러'는 응당히 있어야 할 기능이긴 했다. 싴갤러스가 문을 닫은 지금 사람들이 굉장히 망연자실 ; _;하고 있는 듯 했고..



"싴갤러스는 어떻게 인게임 내 정보(채팅, 뿔피리 로그)를 수집하는가?" 정말 궁금하긴 했었다.


아주 단순히 추측컨데 마비노기 컨텐츠 데이터 서버에 접속, 서버 자체에 저장된 데이터를 파싱할거라 생각했다. 

별도의 API를 제공하지 않는다 해도 노가다의 영역이니까. '그렇게 하지 않을까~~ 하지만 나는 안 할거야~' 라고 유야무야 넘어갔던 탐구심이 이제 필요에 의해 발동되기 시작했다 ㅋ_ㅋ 드릉드릉.


일단 마비노기의 데이터 서버에 접속해서 정보를 받아낸다는 추측은 틀렸다. 왜냐하면 넥슨은 17년도부터 컨텐츠 서버의 접근을 제한한 이력이 있었고

당시부터 마비노기 서버로의 Direct Connection을 약관 위법으로 명시했다. 싴갤 역시 당시에 특별조 관리 툴의 진척도를 자동으로 반영하는 시스템을

수동 체크로 전환시키기도 했었다. 어쨌거나 저쨌거나, 지금 사용할 수 없는 방법임엔 명확하다.



호기심에 좀 찾아보기로 하던 와중 마탐에서 흥미로운 댓글을 하나 발견했다.



실시간 패킷 데이터 캡쳐. 매우 현실성있는, '아 정말 그럴 수 있겠는데?' 싶은 방법이었다.

어쨌든 TCP통신을 하고 있다면 패킷데이터는 수집이 가능할테고, 시간정보와 복호화 키만 있다면 노가다긴 하겠지만 얼마든지 가능할테니까.


모니터링용의 캐릭터를 상시 로그인 시켜놓아야한다는 제한사항..이 있긴 했는데


'마비노기 서버가 열리고 얼마간은 로그수집이 안되더라'

'싴갤러스의 컴퓨터는 24시간 돌아간다'

'만약 로그수집이 안된다면 관리자에게 문의를 해야한다'


주변 친구에게서 이런 말들도 들었으니 이건 합리적 의심이 가능하다. 정말 놀랍게도 근 10년간 싴갤러스는, 혼자든 아니든 이 '모니터링 시스템'을 수동으로 운영해왔단 소리다. 이 말이 맞다면 정말 새삼 대단한..






그래 어쨌거나 저쨌거나, 패킷 데이터 수집이라는 단서를 찾았으니 한번 실마리를 잡아보자.


일단 긴가민가한 고민을 해결하기 위해 패킷수집용 오픈소스를 사용했다. 더 윗단 프론트엔드에 맞는 SW로 Wireshark를 선택.

Wire Shark...피 한방울만 떨어져도 귀신같이 냄새를 맡으며 패킷을 감시한다는 컨셉. 이름 잘 지었다 싶음


사실 Wireshark는 장난감으로 많이 썼다. 아니, 제공되는 기능에 비하면 장난감이라 이름붙일만한건 아니지만, 초보 해커들이

'네이버 검색창에 무얼 쳤나' 이런 장난을 할때 자주 쓰는 뭐 그런.





패킷 수집할 이더넷을 선택해준다.


Wireshark는 Capture FilterDisplay Filter를 제공하는데 전자는 수집할 데이터의 필터, 후자는 그 데이터가 보여지는 영역을 취사선택해 필터링 하는 구조다.

나는 어느 서버에서 어떻게 들어오는 패킷이 마비노기 클라이언트의 것인지 모르므로, 캡쳐필터는 적용하지 않고 그대로 패킷수집을 시작했다.






엄청난 양의 패킷데이터가 전부 수집이 되고 있다. 마비노기 서버를 찾기위해 한땀한땀 데이터를 꺼내 뒤저보기 시작하다가......







찾았다! Ping과 Pong이 오는 데이터, 그리고 외부 서버 ip를 기준으로 탐색을 하니 금방 찾을 수 있었다. 

마비노기에서 L을 눌러 나오는 메세지 로그에서 출력되는 모든 메세지의 패킷 RAW DATA가 출력이 되었다.


 


이어서 뿔피리, 파티게시챗, 개인상점 등의 로그도 쉽게 수집할 수 있었다. 그리고 무엇보다 놀랐던 점은 패킷 데이터에 암호화가 되어있지 않았다!!!

단순히 HEX값을 한글로 바꾸는게 일이긴 하겠지만, 어찌됐든 싴갤러스의 '뿔피리러' '던배른 와쳐' '현상범' 등등의 기능구현이 가능하다고 볼 수 있겠다.





 

https://www.online-toolz.com/langs/ko/tool-ko-text-hex-convertor.html


패킷 내용과 인게임 채팅이 동일한지도 확인. HEX DATA는 유니코드를 통한 변환이 필요하니, 당장엔 사이트를 이용한다.






이제 방법이 뚜렷해졌다. 패킷을 계속 감시하며 특정 DATA 단에서 '모닥불'이라는 키워드를 찾으, 언제 모닥불 정령히 방문했는지 알 수 있을 것이다.


이 부분은 wireshark의 filter기능을 이용하면 손쉽게 가능할 것임.



대신.. 이걸 Front-End단의 SW로 만들기엔 좀 무리가 있어보인다. 일단 나는 패킷분석 오픈소스인 PCAP등의 라이브러리를 만저본 경험이 없다.

단순히 앞단의 데이터가공을 위해 필수적으로 필요할 터인데, 지금 나는 한낱 다른 소프트웨어에 의존을 하고 있을 뿐이니.. ; w; 이 데이터를 전송/가공하는게 더 어렵다는 판단.


ToDo List는 정해졌다. 어떻게 Filter를 먹여 키워드를 감시할것인지, 채팅 로그와 사념파, 전광판 등의 송신단 패키지가 다른지 여부를 알아보는게 숙제.


만약 진전이 있으면 또 글을 써보도록 하겠다 :> 


오전에 QA팀에서 연락이 왔다.

 

음, 퀴즈 20문제를 내놓고 답을 맞추는 그런 컨텐츠인데, 특정 퀴즈의 답이 보기에 없다는 것이다. 심지어 4개의 보기 중 어떤것을 선택해도 오답 처리.

 

별거 아닌 걸로 생각을 했다.. 정답지를 단지 xml 형태로 저장된 고정파일에서 당겨 파싱하는 것이니, 단지 무언가 오타가 났겠거니 하고.

 

여차저차 디버깅을 하면서 xml 소스엔 문제가 없다는게 밝혀졌고, 그 버그는 아주 간헐적으로 고정된 패턴(ㅋㅋ)을 가지고 발생한다는걸 알아냈다.

 

 

뭐 디버깅 과정이야 중요하지 않고, 위 버그를 해결하며 너무 어이없는 상황이 있어 기록차 남겨둠..

 

 

문제의 소스 되시겠다. 버그 발생부를 간단히 재현한 메소드.

 

코드는 단순히 JToken 객체를 하나 만들고(두개든 세개든 상관 없었음), 새로이 또 생성된 Jarray객채의 value값을 담아,

이 상태로 loop문을 돌면서 Jarray의 정보를 갱신해 또 담아주고, 삭제하고, 담아주고.. 뭐 그런 느낌이었다.

jtoken이 전체 문제 json 데이터였고, jarray는 각 문항의 보기(와 정답) 데이터를 포함한..

 

 

 

어쨌거나, 이 부분에서 각 token의 key값을 하나 만들어두고 array를 할당한다. 여기서 Jarray에는.. 그냥 string만 add되어있움.

 

 

디버깅해보면 각각 Key값에 착실하게 Jarray Value도 들어갔다.

 

그런데..

 

밑의 array.Clear()를 돌리는 순간

 

....??? Jarray 속성이 clear되는건 둘째치고,  첫번째 Key값 "Hi"에 할당한 value값도 같이 날아감.

 

아니, 다 날아가면 상관이 없다. 첫번째만 날아간다는게 문제다.

 

 

 

심지어 다시 string format을 add시키면 또 첫번째에만 들어와있음. 대환장 트루먼쇼.

 

 

---

 

 

Json을 디컴파일해서 해당 부분을 살펴봤더니, ValueChanged 구간이 콜되며 좀 이상하게 나누어진 것 같아 계속 살펴보고있다.

 

아, 참고로 array.RemoveAll(); 도 당연히 똑같은 결과를 나타낸다. 일단 이를 코드상으로 해결하려면

 

Clear()메소드가 아닌 단지 array = null;을 먹여주면 된다. 해당 객체만 초기화한다는 느낌으로. 단지 Clear()의 메소드가 뭔가 내가 아는 형태가 아닌 것이다.

 

뭔가 알아내는게 있다면 이 포스트에 추가하는걸로.

 

----------

 

 

3:58 추가

 

JToken을 디컴파일해서 따라 따라가다가 이런 메소드를 발견했다.

아, parent...!!! 계속 JToken에 객체를 '할당'한다고만 생각했지만 사실은 Tree구조를 하고 있었던것.

처음으로 Jtoken에 할당(이라고 쓰고 설정이라고 읽음)될때 해당 JArray의 부모는 ["Hi"]가 되는 것이고,

같은 Parent를 참조하고 있는 이상 계속 따라가게 된다. 새로운 노드를 받지 않는 이상.

 

네. 궁금증 끝 ㅎ vㅎ

+ Recent posts