Monday, August 21, 2023

AOSP 멀티유저 데이터 아키텍처와 시스템 앱의 상호작용

Android Open Source Project(AOSP)는 단순한 모바일 운영체제를 넘어, 태블릿, 자동차, 웨어러블 등 다양한 폼팩터에서 수십억 대의 기기를 구동하는 거대한 생태계의 기반입니다. 이러한 유연성의 핵심에는 단일 기기에서 여러 사용자의 환경을 안전하게 분리하고 관리하는 '멀티유저(Multi-user)' 기능이 있습니다. 이 기능은 가족이 함께 사용하는 태블릿, 기업용 기기의 업무 프로필 분리, 공용 키오스크의 게스트 모드 등 다양한 시나리오에서 필수적인 역할을 합니다. AOSP를 기반으로 시스템 레벨의 기능을 개발하는 엔지니어에게 멀티유저 아키텍처에 대한 깊이 있는 이해는 선택이 아닌 필수입니다.

본 문서는 시스템 앱(System App)의 관점에서 안드로이드 멀티유저 환경의 데이터 관리 메커니즘을 심층적으로 분석합니다. 단순히 현재 활성 사용자의 데이터를 읽고 쓰는 기초적인 방법을 넘어, 시스템이 어떻게 각 사용자의 데이터를 격리하는지, 시스템 앱이 어떤 특권을 가지며 다른 사용자의 데이터에 접근하기 위해 어떤 API와 패턴을 사용해야 하는지에 대한 구조적인 이해를 제공하는 것을 목표로 합니다. 사용자 컨텍스트 전환, ContentProvider를 통한 데이터 공유, 사용자 간 통신 등 고급 주제를 다루며, 실제 AOSP 개발 환경에서 마주할 수 있는 문제에 대한 실용적인 해결책을 제시할 것입니다.

1. 안드로이드 멀티유저 아키텍처의 핵심 원리

안드로이드의 멀티유저 기능은 리눅스 커널의 사용자 관리 기능을 기반으로 구축되었습니다. 각 안드로이드 사용자는 고유한 UID(User Identifier) 범위를 할당받아, 파일 시스템과 프로세스 수준에서 완벽한 격리를 보장받습니다. 이 구조를 이해하는 것은 시스템 앱이 사용자 데이터와 상호작용하는 방식을 파악하는 첫걸음입니다.

1.1 사용자 유형과 역할

안드로이드는 여러 유형의 사용자를 정의하며, 각각 다른 역할과 권한을 가집니다.

  • 시스템 사용자 (System User / User 0): 기기 소유자(Owner)라고도 불리며, ID는 항상 0입니다. 기기에서 생성된 첫 번째 사용자로, 다른 사용자를 생성 및 삭제하고 전체 시스템 설정을 관리할 수 있는 유일한 사용자입니다. 많은 시스템 서비스(System Service)들이 이 사용자의 컨텍스트에서 실행됩니다.
  • 보조 사용자 (Secondary User): 시스템 사용자가 생성한 일반 사용자입니다. 각자 고유의 앱, 데이터, 설정을 가집니다. 다른 사용자의 데이터에는 접근할 수 없습니다.
  • 게스트 사용자 (Guest User): 임시 사용자를 위한 프로필입니다. 게스트 세션이 종료되면 모든 데이터(앱 설치, 파일 등)가 삭제되어 개인정보를 보호합니다.
  • 관리 프로필 (Managed Profile / Work Profile): 주로 기업 환경(BYOD, Bring Your Own Device)에서 사용됩니다. 단일 사용자 환경 내에 개인 프로필과 격리된 업무용 공간을 생성합니다. 사용자는 동일한 앱을 개인용과 업무용으로 각각 설치할 수 있으며(예: Gmail), 데이터는 완전히 분리됩니다.

1.2 데이터 격리(샌드박싱)의 구현

안드로이드의 앱 샌드박스는 사용자 격리에도 동일하게 적용됩니다. 시스템은 각 사용자를 위해 별도의 데이터 디렉토리를 생성하여 이를 구현합니다.

  • 내부 저장소: 각 사용자의 앱 데이터는 /data/user/{USER_ID}/ 디렉토리 아래에 저장됩니다. 예를 들어, 사용자 10이 설치한 com.example.app 패키지의 데이터는 /data/user/10/com.example.app/ 에 위치합니다. 시스템 사용자(User 0)의 데이터는 예외적으로 /data/data/ 아래에 직접 저장됩니다. 이는 초기 안드로이드 버전과의 하위 호환성을 위함입니다.
  • 프로세스 분리: 사용자가 전환되거나 다른 사용자의 앱이 백그라운드에서 실행될 때, 안드로이드는 Zygote 프로세스를 통해 해당 사용자의 UID로 새로운 앱 프로세스를 생성(fork)합니다. 이로 인해 A 앱의 프로세스가 사용자 0의 권한으로 실행될 때와 사용자 10의 권한으로 실행될 때는 완전히 다른 프로세스가 되며, 파일 시스템 접근 권한 또한 달라집니다.

1.3 에뮬레이트된 저장소(Emulated Storage)와 사용자별 뷰

공용 외부 저장소(사진, 다운로드 파일 등)는 멀티유저 환경에서 약간 더 복잡하게 동작합니다. 실제 물리적 파티션은 하나지만(예: /sdcard), 시스템은 각 사용자에게 마치 자신만의 외부 저장소를 가진 것처럼 '에뮬레이트된 뷰'를 제공합니다.

이 경로는 /storage/emulated/{USER_ID}/ 형태로 나타납니다. 예를 들어, 사용자가 파일 탐색기 앱에서 보는 /sdcard/Download 디렉토리는 실제로는 시스템 사용자(User 0)에게는 /storage/emulated/0/Download, 보조 사용자(User 10)에게는 /storage/emulated/10/Download 에 매핑됩니다. 이러한 가상화 계층 덕분에 사용자는 다른 사용자의 공용 파일을 볼 수 없으며, 앱은 사용자 전환에 상관없이 일관된 경로로 파일에 접근할 수 있습니다.

2. 시스템 앱의 특권과 책임

일반적인 서드파티 앱은 자신이 실행 중인 사용자의 샌드박스 안에 엄격하게 갇혀 있습니다. 하지만 AOSP 소스 트리에서 시스템 이미지와 함께 빌드되고 특정 권한을 부여받은 시스템 앱은 이러한 제약을 넘어설 수 있습니다. 이는 시스템 전반의 기능을 관리해야 하는 본질적인 역할 때문입니다.

2.1 시스템 앱의 정의

시스템 앱은 다음과 같은 특징을 가집니다.

  • 사전 설치: 기기의 시스템 파티션(/system, /system_ext, /product 등)에 설치됩니다. 사용자가 임의로 삭제할 수 없습니다.
  • 플랫폼 서명: 안드로이드 플랫폼과 동일한 개인 키로 서명됩니다. 이를 통해 서명 기반(signature-level)의 보호된 권한을 획득할 수 있습니다.
  • 특수 권한: 일반 앱에 허용되지 않는 강력한 권한(예: MANAGE_USERS, INTERACT_ACROSS_USERS)을 AndroidManifest.xml에 선언하고 부여받을 수 있습니다.

2.2 `android:sharedUserId`의 이해

시스템 앱 개발에서 가장 강력한 도구 중 하나는 AndroidManifest.xml<manifest> 태그에 선언하는 android:sharedUserId 속성입니다. android:sharedUserId="android.uid.system"을 선언하고 플랫폼 키로 서명된 앱은 'system'이라는 리눅스 사용자 ID(UID 1000)로 실행됩니다. 이는 해당 앱이 안드로이드 시스템 자체의 권한을 가지게 됨을 의미하며, 파일 시스템의 거의 모든 영역에 접근하고, 대부분의 시스템 서비스를 직접 제어할 수 있는 막강한 권한을 얻게 됩니다. 당연히 이 기능은 극도의 주의를 기울여 사용해야 하며, 일반 앱에서는 절대 사용할 수 없습니다.

2.3 사용자 간 상호작용을 위한 핵심 권한

멀티유저 환경에서 시스템 앱이 다른 사용자와 상호작용하기 위해서는 명시적인 권한이 필요합니다.

  • android.permission.INTERACT_ACROSS_USERS: 이 권한은 앱이 현재 사용자가 아닌 다른 사용자와 상호작용할 수 있도록 허용하는 가장 기본적인 권한입니다. 다른 사용자의 컴포넌트(서비스, 브로드캐스트 리시버)를 시작하거나, 다른 사용자의 데이터에 접근하는 API를 호출할 때 필수적입니다.
  • android.permission.MANAGE_USERS: 사용자를 생성, 삭제, 전환하는 등 사용자 관리 기능을 수행하는 데 필요한 권한입니다. '설정' 앱과 같은 시스템 UI 관리 앱에서 주로 사용합니다.
  • android.permission.CREATE_USERS: `MANAGE_USERS`의 하위 집합으로, 사용자 생성만 가능한 권한입니다.

이러한 권한들은 보호 수준이 매우 높아 플랫폼 서명이 있는 시스템 앱에만 부여될 수 있습니다.

3. 활성 및 비활성 사용자 정보 획득

다른 사용자의 데이터에 접근하거나 상호작용하기 전에, 먼저 기기에 어떤 사용자가 존재하고 현재 활성 상태인 사용자가 누구인지 정확히 파악해야 합니다. 이를 위해 안드로이드는 `UserManager`와 `ActivityManager` 서비스를 제공합니다.

3.1 `UserManager` 서비스를 통한 사용자 정보 조회

`UserManager`는 사용자 프로필과 관련된 모든 정보를 관리하는 핵심 시스템 서비스입니다. 시스템 컨텍스트에서 이 서비스의 인스턴스를 얻을 수 있습니다.


UserManager userManager = (UserManager) context.getSystemService(Context.USER_SERVICE);

`UserManager`는 다음과 같은 유용한 API들을 제공합니다.

  • List<UserInfo> getUsers(): 기기에 존재하는 모든 사용자(시스템, 보조, 게스트, 관리 프로필 등)의 목록을 `UserInfo` 객체 리스트로 반환합니다.
  • UserInfo getUserInfo(int userId): 특정 사용자 ID에 해당하는 `UserInfo` 객체를 반환합니다. `UserInfo` 객체는 사용자의 이름, 플래그(게스트, 관리자 등), 생성 시간 등의 상세 정보를 담고 있습니다.
  • boolean isUserRunning(UserHandle userHandle): 해당 사용자가 현재 '실행 중' 상태인지 확인합니다. 사용자가 잠금 해제되어 활성 상태이거나 백그라운드에서 프로세스가 동작 중인 상태를 의미합니다.
  • boolean isSystemUser(): 현재 코드를 실행하는 프로세스가 시스템 사용자(User 0)의 프로세스인지 확인합니다.
  • UserHandle getProfileParent(UserHandle userHandle): 주어진 사용자가 관리 프로필일 경우, 그 부모가 되는 주 사용자의 `UserHandle`을 반환합니다.

3.2 현재 포그라운드 사용자 확인: `ActivityManager`의 역할

가장 중요한 정보 중 하나는 '현재 사용자가 보고 있는 화면의 주인', 즉 포그라운드 사용자(Foreground User)가 누구인지 아는 것입니다. 이 정보는 `UserManager`가 아닌 `ActivityManager`가 관리합니다.

과거에는 `ActivityManager.getRunningTasks()` 와 같은 API를 사용했지만, 개인정보 보호 강화로 인해 더 이상 사용할 수 없습니다. 시스템 앱에서는 다음과 같이 `ActivityManager`의 내부 API 또는 직접적인 접근을 통해 현재 사용자를 얻을 수 있습니다.


// 시스템 앱에서 현재 포그라운드 사용자를 얻는 가장 정확한 방법
import android.app.ActivityManager;

...

// ActivityManager는 @SystemService 어노테이션을 통해 직접 얻을 수 있습니다.
// 혹은 context.getSystemService(Context.ACTIVITY_SERVICE)를 사용합니다.
ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
int currentUserId = am.getCurrentUser();

Log.d(TAG, "Current foreground user ID: " + currentUserId);

UserManager.getUserHandle() 이라는 `@hide` API도 존재하지만, 이는 API를 호출한 프로세스의 사용자 ID를 반환합니다. 만약 시스템 사용자(User 0)로 실행되는 영구적인 서비스에서 이 API를 호출하면, 현재 포그라운드 사용자가 누구든 상관없이 항상 0을 반환할 것입니다. 따라서, 현재 화면을 사용하는 사용자를 알아내려면 반드시 `ActivityManager.getCurrentUser()`를 사용해야 합니다.

4. 다른 사용자의 데이터 영역에 접근하는 고급 기법

이제 시스템 앱이 다른 사용자의 데이터에 접근하는 핵심적인 방법을 알아볼 차례입니다. 단순히 파일 경로를 문자열로 조작하는 것은 매우 위험하며, 안드로이드 버전 변화에 취약합니다. 가장 올바르고 안정적인 방법은 '사용자 컨텍스트(User Context)'를 획득하여 작업하는 것입니다.

4.1 핵심 개념: 사용자 컨텍스트(User Context)의 전환

안드로이드에서 `Context` 객체는 앱의 환경 정보에 대한 접근을 제공하는 핵심적인 인터페이스입니다. `Context`는 리소스, 데이터베이스, 파일 시스템 경로, 시스템 서비스 등 모든 것에 대한 접근점을 제공합니다. 중요한 것은 `Context` 객체가 특정 사용자와 연결되어 있다는 점입니다.

시스템 앱이 사용자 10의 데이터 디렉토리에 접근하고자 할 때, 단순히 경로를 "/data/user/10/com.example.app/files/" 와 같이 하드코딩하는 대신, 사용자 10에 해당하는 `Context` 객체를 생성해야 합니다. 이 새로운 `Context` 객체를 통해 `getFilesDir()`와 같은 메서드를 호출하면, 시스템은 자동으로 올바른 경로(/data/user/10/com.example.app/files/)를 반환해 줍니다.

4.2 `Context.createPackageContextAsUser()`: 정석적인 접근법

다른 사용자의 `Context`를 얻는 표준 API는 `createPackageContextAsUser()` 입니다.


/**
 * 지정된 사용자의 컨텍스트에서 특정 패키지의 설정 파일을 읽는 예제.
 * 이 코드는 android.permission.INTERACT_ACROSS_USERS 권한이 필요합니다.
 *
 * @param context 현재 컨텍스트 (보통 시스템 서비스나 시스템 앱의 컨텍스트)
 * @param targetPackageName 대상 패키지 이름
 * @param targetUserId 대상 사용자 ID
 * @param fileName 읽어올 파일 이름
 * @return 파일의 내용 또는 오류 발생 시 null
 */
public String readSettingFileFromAnotherUser(Context context, String targetPackageName, int targetUserId, String fileName) {
    try {
        // 1. 대상 사용자를 위한 UserHandle 객체 생성
        UserHandle targetUserHandle = UserHandle.of(targetUserId);

        // 2. 대상 패키지와 사용자에 대한 Context 생성
        // CONTEXT_IGNORE_SECURITY 플래그는 시스템 앱이 개인 데이터에 접근할 때 필요할 수 있음
        Context targetUserContext = context.createPackageContextAsUser(
            targetPackageName,
            Context.CONTEXT_IGNORE_SECURITY, // 또는 0
            targetUserHandle
        );

        // 3. 획득한 '사용자 컨텍스트'를 통해 파일 작업 수행
        File userFile = new File(targetUserContext.getFilesDir(), fileName);
        if (!userFile.exists()) {
            Log.w(TAG, "File not found in user " + targetUserId + ": " + userFile.getPath());
            return null;
        }

        // 파일을 읽는 로직 (StringBuilder, BufferedReader 등 사용)
        // ... (생략) ...
        
        Log.d(TAG, "Successfully read file from " + userFile.getPath());
        return "file content";

    } catch (PackageManager.NameNotFoundException e) {
        Log.e(TAG, "Target package not found for user " + targetUserId, e);
        return null;
    }
}

이 방법을 사용하면 안드로이드의 파일 시스템 구조가 향후 변경되더라도 코드를 수정할 필요가 없습니다. 항상 시스템이 제공하는 올바른 경로를 사용하게 되므로 안정성이 크게 향상됩니다.

4.3 구식 방법의 문제점과 Scoped Storage

초기 자료들에서 볼 수 있는 Environment.getExternalStorageDirectory() 를 사용하여 경로를 조합하는 방식은 이제 사용해서는 안 됩니다.


// !! 잘못된 예시 !! 사용하지 말 것
File dataDirectory = Environment.getExternalStorageDirectory();
File userData = new File(dataDirectory, "user/" + currentUser); // 불안정하고 잘못된 접근 방식

이 방법은 여러 문제를 가집니다. 첫째, 안드로이드 10 (API 29)부터 도입된 Scoped Storage로 인해 앱은 더 이상 외부 저장소의 임의 경로에 자유롭게 접근할 수 없습니다. 둘째, 'user/' 라는 하위 디렉토리 구조는 안드로이드의 내부 구현에 해당하며, 향후 버전에서 변경될 수 있습니다. 항상 `Context`를 통해 얻는 경로를 사용해야 합니다.

5. ContentProvider를 통한 구조화된 데이터 공유

단순 파일 접근을 넘어, 여러 사용자와 구조화된 데이터(예: 데이터베이스 레코드)를 공유해야 하는 경우가 있습니다. 대표적인 예가 '설정' 앱입니다. 사용자는 각자의 프로필에서 벨소리, 배경화면 등을 설정하지만, 일부 시스템 전역 설정(예: 비행기 모드)은 모든 사용자에게 영향을 미칩니다. 이러한 시나리오를 위해 `ContentProvider`가 사용됩니다.

5.1 `android:singleUser="false"` 속성

기본적으로 `ContentProvider`는 그것을 실행시킨 사용자의 데이터만 볼 수 있습니다. 그러나 `AndroidManifest.xml`에서 `ContentProvider`를 선언할 때 `android:singleUser="false"` 속성을 추가하면, 이 `ContentProvider`는 여러 사용자로부터의 쿼리를 처리할 수 있는 인스턴스로 동작하게 됩니다.


<provider
    android:name=".MySystemSettingsProvider"
    android:authorities="com.example.settings"
    android:exported="true"
    android:singleUser="false" />

5.2 사용자 ID를 포함한 URI 처리

`singleUser`가 아닌 `ContentProvider`는 들어오는 `ContentResolver` 호출의 URI를 파싱하여 어떤 사용자를 위한 요청인지 식별해야 합니다. `ContentUris` 클래스와 `ContentResolver.SCHEME_CONTENT + "://" + AUTHORITY + "/users/" + userId + "/..."` 와 같은 URI 스키마를 설계하여 이를 구현할 수 있습니다.

`ContentProvider`의 `query()`, `insert()`, `update()`, `delete()` 메서드 내에서 `Binder.getCallingUid()`를 호출하여 요청을 보낸 프로세스의 UID를 얻고, `UserHandle.getUserId(uid)`를 통해 사용자 ID를 알아내어, 해당 사용자의 데이터베이스나 파일에 접근하도록 로직을 분기할 수 있습니다. 이는 시스템 앱이 다른 사용자에게 안전하고 통제된 방식으로 데이터를 노출하는 표준적인 방법입니다.

6. 멀티유저 환경에서의 Broadcast와 Service 동작 방식

시스템 앱은 종종 백그라운드 서비스나 브로드캐스트를 통해 여러 사용자와 상호작용합니다.

6.1 브로드캐스트 전파

시스템 앱은 특정 사용자 또는 모든 사용자에게 브로드캐스트를 보낼 수 있습니다.

  • 특정 사용자에게 보내기: `Context.sendBroadcastAsUser(Intent intent, UserHandle user)` 메서드를 사용하면 지정된 사용자에게만 브로드캐스트가 전달됩니다.
  • 모든 사용자에게 보내기: `Intent`에 `UserHandle.ALL`을 지정하여 `sendBroadcastAsUser`를 호출하면 실행 중인 모든 사용자에게 브로드캐스트가 전달됩니다. 이는 시스템 상태 변경(예: 시간대 변경)을 모든 사용자에게 알려야 할 때 유용합니다.
  • 포그라운드 사용자에게만 보내기: `Intent.FLAG_RECEIVER_FOREGROUND` 플래그를 추가하면 현재 활성 상태인 포그라운드 사용자에게만 브로드캐스트가 전달됩니다.

6.2 서비스의 실행 컨텍스트

서비스가 어떤 사용자 컨텍스트에서 실행되는지는 매우 중요합니다.

  • 시스템 사용자로 실행되는 영속적인 서비스: 대부분의 핵심 시스템 서비스(예: `ConnectivityService`)는 시스템 사용자(User 0) 컨텍스트에서 단일 인스턴스로 실행됩니다. 이러한 서비스는 기기 부팅 시 시작되어 종료될 때까지 계속 실행되며, 모든 사용자에 대한 정책을 중앙에서 관리하는 역할을 합니다.
  • 각 사용자로 실행되는 서비스: 시스템 런처나 키보드 앱과 같이 사용자별 UI/UX를 제공하는 시스템 앱의 서비스는 사용자가 활성화될 때마다 해당 사용자의 컨텍스트에서 별도의 프로세스로 실행될 수 있습니다. 이를 통해 각 사용자의 데이터를 안전하게 처리할 수 있습니다.

7. 실전 시나리오 및 모범 사례

이론을 바탕으로 실제 개발 시나리오와 주의해야 할 점들을 살펴보겠습니다.

7.1 시나리오: 모든 사용자에게 공통 정책 적용하기

특정 통신 정책(예: 특정 앱의 데이터 사용량 제한)을 기기의 모든 사용자에게 적용해야 하는 시스템 서비스를 개발한다고 가정해 봅시다.

  1. 서비스는 `android:sharedUserId="android.uid.system"`으로 빌드되고, `INTERACT_ACROSS_USERS` 권한을 가집니다.
  2. 서비스는 시스템 사용자(User 0)로 실행되는 영속적인 서비스입니다.
  3. 정책을 적용해야 할 때, 서비스는 `UserManager.getUsers()`를 호출하여 기기의 모든 사용자 목록을 가져옵니다.
  4. 각 `UserInfo`에 대해 루프를 돌며, `UserHandle.of(userInfo.id)`를 사용하여 `UserHandle`을 생성합니다.
  5. `Context.createPackageContextAsUser()`를 사용하여 각 사용자의 컨텍스트를 얻고, 해당 사용자의 데이터 디렉토리에 있는 설정 파일을 수정하거나, 해당 사용자를 대신하여 시스템 API를 호출합니다.
  6. 사용자가 새로 생성되거나 삭제될 때 `Intent.ACTION_USER_ADDED`, `Intent.ACTION_USER_REMOVED` 브로드캐스트를 수신하여 동적으로 정책을 업데이트합니다.

7.2 보안 및 프라이버시 고려사항

  • 최소 권한의 원칙: 시스템 앱이라 할지라도 필요한 권한만 요청해야 합니다. 다른 사용자의 모든 데이터에 접근할 수 있다고 해서 무분별하게 접근해서는 안 됩니다.
  • 사용자 데이터 프라이버시: 한 사용자의 개인 정보(연락처, 메시지 등)가 다른 사용자에게 노출되지 않도록 각별히 주의해야 합니다. 데이터 접근은 반드시 기능 구현에 필수적인 경우로 제한되어야 합니다.
  • 명시적인 동의: 만약 사용자 간 데이터 공유가 필요한 기능이라면, 사용자에게 명확하게 알리고 동의를 얻는 UI/UX를 고려해야 합니다.

7.3 호환성 및 테스트

멀티유저 관련 API는 안드로이드 버전에 따라 미묘하게 다를 수 있습니다. 다양한 사용자 유형(보조, 게스트, 관리 프로필)을 생성하고, 사용자 전환을 반복하며, 여러 사용자가 동시에 활성화된 상태(백그라운드 실행)에서 기능이 올바르게 동작하는지 철저한 테스트가 필요합니다.

결론

AOSP의 멀티유저 환경에서 시스템 앱을 개발하는 것은 안드로이드 프레임워크의 깊은 곳을 탐험하는 것과 같습니다. 단순한 파일 경로 조작을 넘어, 사용자 ID, UID, 프로세스 격리, 컨텍스트 전환의 개념을 명확히 이해해야 합니다. `UserManager`와 `ActivityManager`를 통해 사용자 상태를 정확히 파악하고, `createPackageContextAsUser`를 사용하여 다른 사용자의 데이터에 안전하게 접근하며, 필요에 따라 `ContentProvider`나 사용자 간 브로드캐스트를 활용하는 것은 견고하고 안정적인 시스템 기능을 구현하는 핵심입니다. 이 글에서 다룬 원리와 기법들을 바탕으로, 개발자 여러분이 AOSP의 강력한 멀티유저 기능을 완벽하게 제어하고 혁신적인 사용자 경험을 만들어 나가기를 기대합니다.


0 개의 댓글:

Post a Comment