카테고리 보관물: 미분류

[Docker] Ubuntu image timezone 설정

Docker에서 서버를 기동할 경우 기본적으로 UTC 시간이 Locatime으로 설정됩니다.

제가 사용했던 ubuntu:bionic 버전에 서버를 올린 경우 timezone을 설정하는 과정을 설명합니다.

우선 Timezone을 설정하기 위해서는 tzdata가 apt를 통해 설치되어야 합니다.

FROM ubuntu:bionic
WORKDIR /server
COPY . .

RUN apt-get update &&\
 apt-get install -y  tzdata

CMD ["/server/run"]

이후 docker image를 실행 시 "TZ" 환경 변수에 Timezone 정보를 설정해 줍니다.

version: "3.9"

service:
  server:
    image: myserver
    environment:
      TZ: Asia/Seoul

Docker compose에서는 위와 같이 환경변수를 설정해주면 실행 시 Timezone이 설정됩니다.

[Postgresql] postgres_fdw

Postgresql에서는 여러 DB instance 사이에 join이 기본적으로 불가능 합니다.

이를 위해서 몇가지 extension이 존재하는데 dblink와 postgres_fdw 입니다.

dblink는 쿼리에서 DB instance의 Host 주소와 계정 정보를 입력하여 잠깐동안의 임시 연결을 생성하는 방식이고
postgres_fdw의 경우에는 연결 설정을 저장 해 두고 schema를 통기화 하여 사용하는 방식으로 dblink에 비해서 성능이 좀 더 좋습니다.

당연한 이야기지만 외부 db를 join 하는 것은 기본적으로 하나의 DB내 테이블을 Join 하는 것과 비교해서 크게 느리기 때문에 자주 Join을 해야하는 데이터라면 같은 DB Instance에 저장하도록 설계해야 합니다.

CREATE EXTENSION IF NOT EXISTS postgres_fdw
SCHEMA public;

우선 위 쿼리를 통해서 postgres_fdw를 설치 해 줍니다.
AWS에서도 지원하기 때문에 그냥 설치하면 됩니다.

CREATE SERVER server_name
FOREIGN DATA WRAPPER postgres_fdw
OPTIONS(
  host 'target server ip or url',
  port '5432',
  dbname 'target database name'
);

그 다음 연결할 instance를 server로 추가해 줍니다.

CREATE USER MAPPING
FOR public
SERVER server_name
OPTIONS(
  user 'target db instance user',
  password 'target db instance user password'
);

이 원격 DB Instance에 접근할 때 사용될 사용자 계정을 Mapping 해 줍니다.

IMPORT FOREIGN SCHEMA public
FROM SERVER foreign_server INTO schema_name;

그 다음 원격 DB Instance에서 연결할 Table들을 정의해 줘야 하는데, 하나하나 따로 정의할 필요가 없다면 위 쿼리를 통해서 전체 스키마를 퍼옵니다.
schema_name에 해당하는 스키마는 미리 생성해야 하며 public에 추가해도 무방하지만 EF Core를 사용하는 경우 Migration 테이블이 중첩되어 실패할 수 있기 때문에 해당 DB Instance의 이름에 맞는 Schema를 추가해서 Import를 하면 이후 편하게 사용이 가능합니다.

[Unity] iOS 에서 Http 다운로드 안되는 현상

Unity 2018.4.18f1 버전을 기준으로 정리합니다.

Unity에서 iOS 빌드를 했을 때 보통 AssetBundle을 다운로드 받기 위해서는 https가 아닌 http 프로토콜을 활용합니다.
무슨 보안 정보도 아닌데 https 씩이나 사용할 이유가 없으니까요.
그런데 iOS는 Secure하지 않다는 이유로 앱에서 Http 프로토콜을 사용하는 것을 기본적으로 차단하고 있습니다.
그렇기 때문에 아래와 같이 다운로드를 수행할 때 실패하게 됩니다.

var download = UnityWebRequest.Get("http://download.game.com/assetbundle");
yield return download.SendWebRequest();

if( download.isHttpError ) {
  Debug.LogError($"ResponseCode : {download.responseCode}");
}
else if( www.isNetworkError ) {
  Debug.LogError($"Error : {download.error}");
}
else {
  Debug.Log("Download Success");
}

위의 코드를 iOS에서 실행하면 "Error : Unknown Error" 라고 출력이 됩니다.

에러가 나면 왜 에러가 났는지를 메시지로 알려줘야 하는데 밑도끝도 없이 Unknown Error라고 해버리니 매우 답답하기 그지없는 상황이 됩니다.

iOS에서 Http를 사용하려면 Unity에서 빌드 후 XCode 프로젝트에서 info.plist를 오픈하여 App Transport Security Settings를 수정해 줘야 합니다.
그런데 문제는 Unity Project Setting에서 Allow downloads over HTTP 옵션을 설정해주면 info.plist에 자동으로 아래의 두가지 옵션을 추가해 줍니다.
- Allow Arbitrary Loads : YES
- Allow Arbitrary Loads in Web Content : YES

위의 옵션에 대한 자세한 내용은 아래의 링크를 참조하면 됩니다.
https://developer.apple.com/documentation/bundleresources/information_property_list/nsapptransportsecurity

문서상으로는 NSAllowArbitraryLoads를 true로 설정하면 Http 프로토콜의 사용이 허용된다고 되어 있습니다.
그런데 NSAllowArbitraryLoads는 전에 Http 프로토콜을 오픈해 버리는 것이기 때문에 여러가지 하위 옵션들을 제공하고 있습니다.
NSAllowsArbitraryLoadsForMedia, NSAllowsArbitraryLoadsInWebContent, NSAllowsLocalNetworking과 같은 옵션이 그렇습니다.

문제는 iOS 9.0과 iOS 10.0 이후의 동작이 약간 다릅니다. 이게 아주 심각한 문제와 혼란을 초래할 수 있습니다.

NSAllowArbitraryLoads는 Http 동작 전체를 허용하거나 허용하지 않는 최상위 옵션이고 기본값은 false입니다.
하지만 NSAllowArbitraryLoads와 동시에
NSAllowsArbitraryLoadsForMedia, NSAllowsArbitraryLoadsInWebContent, NSAllowsLocalNetworking 옵션이 설정되어 있을 때의 동작은 iOS 9.0 이전과 iOS 10.0 이후의 동작이 달라집니다.

iOS 9.0 이전에는 NSAllowArbitraryLoads가 true이면 하위 옵션들의 설정을 무시합니다. 최상위 옵션이니까 허용했으면 걍 다 허용되는 것이죠.

iOS 10.0 이후부터는 NSAllowArbitraryLoads가 true인데 하위 옵션들이 있으면 최상위 옵션을 무시합니다. 즉 NSAllowArbitraryLoads가 false가 되어버리는 것입니다. 왜냐하면 보안 안정성을 올리는 것이 이 옵션의 목적인데 최상위 하나만 설정하는 것으로 보안 설정을 모두 뚫어버리니까 하위 옵션이 설정되면 최상위 옵션을 내리고 좁은 범위의 허용을 우선하겠다는 목적입니다.

그런데 Unity는 Unity Ads를 활성화 한 상태로 빌드하면 NSAllowArbitraryLoads와 NSAllowArbitraryLoadsInWebContent가 동시에 설정됩니다. 그러면 NSAllowArbitraryLoads가 무시되어 버리면서 Http 프로토콜이 막혀 버립니다.
하지만 plist 옵션을 확인해 보면 NSAllowArbitraryLoads가 true로 설정되어 있으니 괜찮아야 할 것 같아서 다른 원인들을 찾아 헤메게 됩니다.

해결책은 NSAllowArbitraryLoads외의 하위 옵션들을 지워주기만 하면 됩니다.

이틀간의 삽질의 기록을 이렇게 남깁니다....

[Unity] Apple Login과 Firebase 인증

Apple이 익명 로그인 기능만 사용할게 아니라면 Apple Login을 의무화 하였기 때문에 Apple Login을 지원하지 않으면 리젝을 당하게 됩니다.

그래서 Apple Login을 프로젝트에 추가하는 과정을 간략하게 남깁니다.

우선 Unity에서 애플 로그인을 지원하는 플러그인이 두 가지가 있습니다.
첫번째는 Unity Technology에서 제공하는 SignInWithApple 입니다.
https://assetstore.unity.com/packages/tools/sign-in-with-apple-154202

두번째는 GitHub에 올라와 있는 AppleAuth 입니다.
https://github.com/lupidan/apple-signin-unity

당연히 처음에는 Unity에서 제공해 주는 플러그인을 사용하여 구현을 진행하였고 Apple Login이 문제없이 동작하는 것을 확인하였으나 Firebase 인증 단계에서 벽에 부딪혔습니다.
Firebase 인증 단계에서 Apple Login의 검증을 위해 nonce를 요구하는데, SignInWithApple은 Nonce을 얻어오는 API가 제공되지 않았습니다.
(현재 시점에 제가 발견하지 못했을 수 있습니다.)
그래서 눈물을 머금고 코드를 날린 뒤 AppleAuth를 통해 구현을 진행했습니다.

AppleAuth 설치

GitHub의 설명대로 진행하면 됩니다.
저는 2018.4 버전을 사용하고 있기 때문에 Packages/manifest.json 파일을 열어 아래의 패키지를 추가해 줍니다.

"dependencies": {
    "com.lupidan.apple-signin-unity": "https://github.com/lupidan/apple-signin-unity.git#v1.1.0",
}

AppleAuthorizer 구현

using AppleAuth;

public class AppleAuthorizer {
    // 취향 상 싱글턴을 사용합니다.
    public static AppleAuthorizer instance {get; private set;}

    public static string IdToken {get; private set;}
    public static string AuthCode {get; private set;}
    public static string RawNonce {get; private set;}
    public static string Nonce {get; private set;}

    private IAppleAuthManager appleAuthManager;
    public bool IsLoginSuccess = false;
    public bool IsLoginDone = false;

    private void Awake() {
        if( _instance == null && _instance != this) {
            Destroy(gameObject);
            return;
        }
        _instance = this;
        DontDestroyOnLoad(gameObject);
    }
    private void Start() {
        if(AppleAuthManager.IsCurrentPlatformSupported) {
            var deserializer = new PayloadDeserializer();
            appleAuthManager = new AppleAuthManager(deserializer);
        }
    }
    private void Update() {
        appleAuthManager?.Update();
    }

    // Nonce는 SHA256으로 만들어서 전달해야함
    private static string GenerateNonce(string _rawNonce) {
        SHA256 sha = new SHA256Managed();
        var sh = new StringBuilder();
        // Encoding은 반드시 ASCII여야 함
        byte[] hash = sha.ComputeHash(Encoding.ASCII.GetBytes(_rawNonce));
        // ToString에서 "x2"로 소문자 변환해야 함. 대문자면 실패함. ㅠㅠ
        foreach (var b in hash) sb.Append(b.ToString("x2"));
        return sb.ToString();
    }
    // 내부적으로 사용하는 인터페이스 통일을 위해 Coroutine으로 구현
    // 기존 인터페이스가 아니었다면 Async를 사용했을 듯
    public IEnumerator LoginProcess() {
        IsLoginSuccess = false;
        IsLoginDone = false;

        // Nonce 초기화
        // Nonce는 Apple로그인 시 접속 세션마다 새로 생성
        RawNonce = System.Guid.NewGuid().ToString();
        Nonce = GenerateNonce(RawNonce);

        // QuickLogin을 먼저 수행
        // 이전 로그인 기록이 없다면 실패 처리됨
        var quickLoginArgs = new AppleAuthQuickLoginArgs(Nonce);
        var isQuickLoginDone = false;
        appleAuthManager.QuickLogin(
            quickLoginArgs,
            credential => {
                try {
                    var appleIdCredential = credential as IAppleIDCredential;
                    AuthCode = Encoding.UTF8.GetString(appleIdCredential.AuthorizationCode);
                    IdToken = Encoding.UTF8.GetString(appleIdCredential.IdentityToken);
                    IsLoginSuccess = true;
                }
                catch(System.Exception e) {
                    Debug.LogException(e);
                    IsLoginSuccess = false;
                }
                isQuickLoginDone = true;
            },
            error => {
                IsLoginSuccess = false;
                isQuickLoginDone = true;
            });
        yield return new WaitUntil(() => isQuickLoginDone);
        // QuickLogin이 성공했다는 것은 이전 로그인 정보가 있었다는 의미
        // 일반 Login 과정을 진행할 필요가 없어짐.
        if(IsLoginSuccess) {
            IsLoginDone = true;
            yield break;
        }

        var loginArgs = new AppleAuthLoginArgs(LoginOptions.IncludeEmail, Nonce);
        appleAuthManager.LoginWithAppleId(
            loginArgs,
            credential => {
                try {
                    var appleIdCredential = credential as IAppleIDCredential;
                    AuthCode = Encoding.UTF8.GetString(appleIdCredential.AuthorizationCode);
                    IdToken = Encoding.UTF8.GetString(appleIdCredential.IdentityToken);
                    IsLoginSuccess = true;
                }
                catch(System.Exception e) {
                    Debug.LogException(e);
                    IsLoginSuccess = false;
                }
                IsLoginDone = true;
            },
            error => {
                IsLoginSuccess = false;
                IsLoginDone = true;
            });
        yield return new WaitUntil(() => IsLoginDone);
    }
}

위의 코드는 프로젝트에서 사용하는 인증 코드의 일부를 떼어낸 것이라 컴파일 오류가 날 수 있으나 기본적인 로직은 동일하기 때문에 사용 가능할 것이라고 생각됩니다.
QuickLogin에 대한 처리는 기존 로그인 정보가 있는지 여부를 별도로 파악해야할지 그냥 QuickLogin을 실패처리하면 될지 테스트가 필요한 상태.

Firebase에서 AppleLogin 사용

Firebase 쪽 코드는 잡코드가 많아서 Apple Login에 사용되는 부분만 추렸다.

// Firebase는 Google이나 Apple GameCenter와 달리 AppleLoginProvider를 제공하지 않고 있다. 그래서 OAuthProvider를 사용해야한다.
// RawNonce는 SHA256으로 변환하기 전 문자열을 의미한다.
var credential = Firebase.Auth.OAuthProvider.GetCredential("apple.com", AppleAuthorizer.IdToken, AppleAuthorizer.RawNonce, AppleAuthorizer.AuthCode);

var task = auth.SignInWithCredentialAsync(credential);
yield return new WaitUntil(() => task.IsCompleted || task.IsFaulted || task.IsCanceled);

FirebaseUser user = task.Result;

주의할 것은 AppleLogin 과정에서는 SHA256으로 만들어진 Nonce를 전달해야하고 Firebase 인증 시에는 SHA256으로 변환하기 전의 문자열을 전달해야 한다.

XCode 설정

AppleLogin을 위한 XCode 설정은 Unity Blog에서 영상으로 친절하게 설명하고 있으니 해당 내용을 참고하면 된다.
https://blogs.unity3d.com/kr/2019/09/19/support-for-apple-sign-in/

Reference

https://firebase.google.com/docs/auth/ios/apple?authuser=0
https://github.com/lupidan/apple-signin-unity
https://firebase.google.com/docs/auth/unity/apple

기타 삽질

UIApplicationExitsOnSuspend가 plist에 포함되어 있으면 ipa 업로드 중 에러남.
plist 열어서 삭제해 줘야함.

[Unity3D] Android 빌드에서 “Failure to Initialize! Your hardware does not support this application, sorry!” 라고 출력되면?

어느 날 갑자기 매일 하던대로 빌드를 해서 APK를 뽑아 설치를 하고 실행을 했더니

Failure to Initialize! Your hardware does not support this application, sorry!

 

라고 출력되었습니다.

이유는 모르겠지만 그냥 갑자기 발생하기 시작했습니다.

검색해보니 원인은 굉장히 다양한 것 같지만....

 

[해결책]

Play Service Resolver에서 Force Resolve를 한번 수행한 뒤에 빌드를 다시하니까 괜찮아졌습니다.

[C#] using 문 내부에서 yield return을 하면?

Disposable객체를 Coroutine 내부에서 사용할 경우 과연 어느 타이밍에 파괴될지 궁금해서 한번 간단하게 테스트 코드를 작성해 봤습니다.

설마... yield return 할 때 마다 파괴되고 다시 생기진 않겠지...

테스트 하는 김에 using static 도 한번 사용해 봅니다.

using System;
using System.Collections;
using static System.Console;

namespace YieldReturnInUsingStatement
{
    class DisposeTest : IDisposable
    {
        public DisposeTest() {
            WriteLine("Construct");
        }
        public void Dispose() {
            WriteLine("Dispose");
        }
    }
    class Test
    {
        public IEnumerator Run() {
            using (new DisposeTest()) {
                for (int i = 0; i < 10; ++i) {
                    WriteLine(i);
                    yield return null;
                }
            }
        }

        public Test() {
            var e = Run();
            while (e.MoveNext()) {

            }
        }
    }
    class Program
    {
        static void Main(string[] args) {
            var test = new Test();
        }
    }
}

결과는 다행히 using 블록을 완전히 빠져나간 타이밍에 Dispose 되었습니다.

Construct
0
1
2
3
4
5
6
7
8
9
Dispose

 

using static은 참 편하네요. 전역 함수처럼 만들어 줍니다.

많이 쓰면 몸에 나쁠것만 같은 느낌.

[Unity3D] Google Play Games Plugin에서 ERROR_NOT_AUTHORIZED 에러가 발생할 때

Google Play Games Plugin을 Unity에 붙이고 로그인을 시도하면 ERROR_NOT_AUTHORIZED 에러가 발생하면서 로그인이 안되는 경우가 있습니다.

이 문제는 Google Play Console의 설정을 제대로 하지 않은 경우게 발생합니다.

문제 해결을 위해서 두가지를 확인해야 합니다.

첫번째는 Unity에서 GPGS의 설정입니다.

  1. Unity에서 <Window-Google Play Games-Setup-Android setup...> 메뉴를 선택합니다.

  2. 아래와 같은 창이 뜹니다. 이 창의 항목들을 설정해 줘야 합니다.

  3. Directory to save constant, Constants class name은 적당하게 입력해 줍니다.
    Directory to save constant : Resource Definition 에 정의된 값들을 저장한 C# 파일을 생성할 폴더 위치
    Constants class name : Resource Definition에 정의된 값들을 저장한 C# Class의 이름
  4. Resources Definition은 Google Play Console에서 가져옵니다.
    업적, 이벤트 중 하무거나 하나 추가하고나면 '리소스 받기' 메뉴가 나타납니다. 업적과 이벤트를 하나도 추가하지 않고 받아올 수도 있을텐데 버튼이 등장하지 않네요.

  5. Web App Client ID는 입력을 해도 되고 안해도 되는 부분인데 Firebase나 외부 로그인 모듈을 사용하려면 입력해 줍니다.

두번째는 Google Play Console의 설정입니다.

  1. Google Play Console API 사이트에 접속합니다.
    https://console.developers.google.com
  2. OAuth 2.0 클라이언트 ID에서 Android 클라이언트 정보가 있는지 확인해 봅니다.
    (아마 별다른 셋팅이 없었다면 없을 겁니다.)

  3. '사용자 인증 정보 만들기' 를 클릭하고 'OAuth 클라이언트 ID'를 선택합니다.

  4. 아래와 같은 화면이 있고 '서명 인증서 지문' 과 '패키지 이름'을 입력해야 합니다.

  5. Google Play Console의 프로젝트에서 SHA-1 인증서 지문을 복사하여 4번 항목의 '서명 인증서 지문' 부분에 붙여넣습니다.

    만약 인증서를 업로드 하라거나 어쩌구 메시지가 뜨면 APK를 빌드할 때 사용한 key store를 등록해야 합니다. 관련 정보는 해당 메뉴의 도움말을 참고하세요.
    업로드 인증서를 실수로 입력하는 경우가 많으니 주의!

4번 항목의 '패키지 이름'은 Unity에서 APK를 빌드할 때 사용한 것을 넣어줍니다.

위의 두가지가 조금이라도 잘못 설정되어 있으면 로그인이 제대로 안됩니다.

AWS EC2에 ssh로 접속 시 timeout 될 때…

Aws lambda에 서버 로직을 업데이트 하기 위한 EC2 Instance를 생성했는데 ssh 접속을 시도할 때 마다 timeout 오류가 발생하여 원인을 한참동안 파악하다보니 RouteTable 설정의 문제였습니다.

이전에 생성했던 EC2 Instance를 Target으로 하는 RouteTable을 재활용했더니 Target이 사라진 BlackHole 상태가 되어 있었습니다.

Target을 새로 생성한 Instance로 교체해 주었더니 문제없이 잘 접속되었습니다.