[Apple] File System Programming Guide : File System Basics

J.Noma·2022년 2월 8일
0

iOS : Cocoa Touch

목록 보기
4/4

Reference


🌀 서론

🔘 파일시스템이란?
파일 시스템은 데이터파일/App/OS관련파일들을 위한 영구 저장소를 관리합니다. 그러므로 파일 시스템은 모든 프로세스가 사용하는 근본적인 리소스 중 하나입니다

🔘 APFS / HFS+
APFS(애플파일시스템)은 macOS/iOS/watchOS/tvOS의 디폴트 파일 시스템입니다. APFS는 HFS+를 대체하여 디폴트 파일 시스템이 되었는데 그 시기는 iOS 10.3부터/macOS Sierra부터입니다. macOS는 추가로 다른 포맷들도 지원합니다 (macOS 지원포맷)

🔘 파일시스템은 계층구조
본질 포맷이 무엇이든 간에, 디바이스에 달린 모든 디스크는 (물리적으로든 네트워크로든) 한 묶음의 파일을 생성하기 위해 공간을 할당합니다. 파일의 개수가 쉽게 수백만개씩 많아지기 때문에, 파일 시스템은 디렉토리를 사용하여 계층구조를 꾸립니다

🔘 iOS vs macOS
비록 기본적인 디렉토리 구조는 iOS/macOS가 서로 비슷하지만, 각 시스템이 App과 유저데이터를 조직하는데 있어 차이점들이 존재합니다

🔘 파일을 적절한 위치에 저장해야 한다
파일시스템과 상호작용하는 코드를 작성 시작하기 전에, 먼저 파일시스템의 구조와 당신의 코드에 적용되는 규칙들에 대한 몇가지를 이해해야 합니다. 적절한 보안 권한을 얻지 못한 디렉토리에는 파일을 쓰기할 수 없다는 상식 외에도, App은 파일을 적절한 위치에 놓기를 기대합니다. 파일을 정확히 어디에 놓아야 할지는 플랫폼에 달렸지만, 가장 중요한 목표는 유저의 파일을 쉽게 찾을 수 있어야 하고 당신의 코드가 내부적으로 사용하는 파일은 유저에게 방해가 되지 않도록 하는 것입니다


🌀 iOS 파일 시스템

iOS 파일 시스템은 자체적으로 실행하는 App에 맞춰져 있습니다. 시스템을 심플하게 유지하기 위해, iOS 유저는 파일시스템으로의 직접적인 권한을 갖지 않고 App이 이 규칙을 따르도록 권고합니다

🔸 iOS 표준 폴더

🔘 샌드박스
보안을 목적으로 iOS App과 파일시스템 간 상호작용은 App의 샌드박스 안에 있는 폴더들로 제한됩니다. 새로운 App을 설치하는 동안, 인스톨러는 샌드박스 내에 해당 App을 위한 많은 컨테이너 폴더들을 생성합니다

🔘 Container 종류
컨테이너 폴더들은 각자의 명확한 역할을 가집니다. Bundle Container 폴더는 App의 번들을 저장합니다. Data Container 폴더는 App과 유저를 위한 데이터를 저장합니다. 그리고 Data Container는 App이 데이터를 정렬/조직하는데 사용할 하위폴더들로 더 세분화됩니다. App은 필요에 따라 iCloud Container라던지 추가적인 컨테이너 폴더에 대한 접근권한을 런타임에 요청할 수 있습니다. 파일시스템에서 해당 App에 대한 primary view가 이 컨테이너 폴더들로 구성되게 됩니다

🔘 연락처/음악 등은 예외적으로 접근이 가능하다 (by. helper)
App은 보통 자신의 컨테이너 폴더 밖에서의 파일 접근/생성이 금지되어 있습니다. 이 규칙에 대한 예외 케이스 중 하나는 App이 연락처/음악 같은 것들에 접근하는 유저 public 시스템 인터페이스를 사용할 때입니다. 이런 케이스들에선 시스템 프레임워크가 helper를 사용하여 App이 데이터 읽기/쓰기 작업을 처리할 수 있게 도와줍니다

🔘 샌드박스 주요 하위폴더
아래는 샌드박스 폴더 안에 있는 중요한 하위폴더들입니다. 또한, usage와 추가적인 접근제한, 그리고 content가 iTunes/iCloud에 백업이 되는지 여부를 설명하고 있습니다

iOS App은 파일들을 더 잘 구조화하기 위해 아래에서 설명하는 Documents/Library/tmp 폴더에 추가적인 하위폴더를 만들수도 있습니다

  • AppName.app
    App의 번들입니다. 이 폴더는 App 자체와 그 모든 리소스를 포함합니다
    당신은 이 폴더에 쓰기를 할 수 없습니다. 조작을 막기위해 번들 폴더는 인스톨 타임에 서명됩니다. 이 폴더를 쓰기하는 것은 이 서명을 변경하고 App이 런치되는 것을 막아버립니다. 하지만 번들의 모든 리소스에 대한 읽기권한은 얻을 수 있습니다. 이에 대한 자세한 정보는 Resource Programming Guide를 참고합니다
    이 폴더의 content들은 iTunes/iCloud에 백업되지 않습니다. 하지만, iTunes는 App 스토어에서 구매한 모든 App에 대해 초기 동기화를 수행합니다

  • Documents/
    이 폴더는 유저가 생성한 content를 저장합니다. content들을 파일 형태로 유저가 사용할 수 있습니다. 그러므로, 이 폴더에는 유저에게 공개할 파일만을 담아야 합니다. 이 폴더의 content들은 백업됩니다

  • Documents/Inbox
    이 폴더는 외부 엔티티로부터 요청된 파일에 접근할 때 사용합니다. 특히, 메일 App은 이메일 첨부파일을 여기에 저장합니다. 또한 Document interaction controller들은 파일을 여기 저장합니다
    당신의 App은 이 폴더에 있는 파일들을 읽거나 삭제할 순 있지만 새로운 파일을 만들거나 이미 존재하는 파일에 쓰기를 할 순 없습니다. 만약 유저가 여기에 있는 파일 수정을 시도한다면 폴더 밖으로 꺼내서 해야 합니다
    이 content들은 백업됩니다

  • Library/
    이는 유저 데이터 파일을 제외한 모든 파일의 최상단 폴더입니다. Library 내에는 몇가지 표준 하위폴더들이 더 있는데, 일반적으론 이 표준 하위폴더 중 하나에 파일을 저장합니다 (iOS App은 보통 Application SupportCaches라는 하위폴더를 사용합니다). 하지만, 커스텀으로 하위폴더를 만들 수도 있습니다
    유저에게 노출되고 싶지 않은 파일은 Library의 하위폴더들을 사용합니다. 당신의 App은 유저 데이터 파일을 여기 저장하면 안됩니다
    Library 폴더의 content는 Caches를 제외하곤 백업됩니다
    Library와 그 하위폴더에 대한 더 많은 정보는 The Library Directory Stores App-Specific Files를 참고합니다

  • tmp/
    임시파일을 저장하기 위한 폴더이며 App이 재실행되었을 때 보존해야 할 필요가 없습니다. 당신의 App은 이 폴더에 있는 임시파일들이 더이상 필요없다면 삭제해야 합니다만, 시스템이 App이 not running일 때 이 폴더를 지워버리기도 합니다
    백업되지 않습니다

🔘 App에서 위 폴더들에 접근하는 방법
iOS App에서 위에서 설명한 폴더들에 접근하기 위한 참조값에 대해선 Locating Items in the Standard Directories를 참고합니다

🔸 그래서 이 폴더들 중 어디에 저장해야 할까

iOS device에서 프로세스들이 동기화하고 백업하는데 오랜 시간을 잡아먹지 않도록, 파일을 어디에 저장할지를 선별해야 합니다. 큰 파일들을 저장하는 App은 iTunes/iCloud 백업하느라 프로세스를 느리게 만들 수 있습니다. 또한 유저 저장공간을 많이 소모하여 유저로 하여금 App을 삭제하거나 백업기능을 비활성화하게 유도할 수 있습니다. 이런 점들을 염두에 두고, 아래 가이드라인을 준수하여 파일을 저장해야 합니다

🔘 유저 데이터는 Documents/에 둡니다
유저 데이터는 일반적으로 유저가 생성/삭제 등을 할 수 있도록 노출하길 원하는 파일들이 포함됩니다. 예로 drawing App에선, 유저 데이터에는 유저가 생성한 그림 파일들이 포함됩니다. 다른 예로 텍스트 에디터에선, 텍스트파일이 포함됩니다. 비디오/오디오 App은 심지어 유저가 나중에 보려고 다운로드 했던 파일들이 포함될 수도 있습니다

🔘 App이 생성한 support 파일은 Library/Application support/에 둡니다
일반적으로 이 폴더는 App 구동을 위해 필요하면서 유저에게는 숨기고 싶은 파일들이 담깁니다. 또한 App 번들 리소스에 대한 (데이터파일, configuration 파일, 템플릿, 수정버전)들도 포함될 수 있습니다

🔘 Documents/Library/Application support/는 디폴트로 백업됩니다
기본적으로 백업되므로, 백업을 원치 않는다면 해당 파일에 대한 URL에 대해 백업제외 프로퍼티를 설정하면 됩니다 (isExcludedFromBackup, NSURLIsExcludedFromBackupKey)

🔘 임시파일은 tmp/에 둡니다
임시 파일은 오래 보존할 필요가 없는 데이터들로 구성됩니다. device 공간을 낭비하지 않도록 할 일이 끝나면 잊지말고 삭제해줘야 합니다. tmp는 시스템이 주기적으로 청소를 해줍니다 (App이 not running일 때). 그러므로 App이 한번 종료된 이후에는 이 임시파일들이 제거되었을 수 있음을 인지해야 합니다

🔘 캐시파일은 Library/Caches/에 둡니다
캐시 데이터는 임시파일보다는 더 오래, support file보다는 적게 지속될 필요가 있습니다. 일반적으로 말해서, 캐시 데이터는 App 구동을 위해 필수적이진 않지만 성능 향상을 위해 사용됩니다. 캐시 데이터의 예로는, DB 캐시 파일이나 일시적이면서 다운로드가능한 content 등이 있습니다. Caches/ 역시 tmp/ 처럼 시스템이 디스크 공간 확보를 위해 삭제해버릴 수 있습니다. 그러므로 App은 이에 대비하여 필요에 따라 이 파일들을 재생성하거나 다운로드해올 수 있어야 합니다


🌀 macOS 파일 시스템

일단 패스 ㅎㅎ..


🌀 iCloud 파일 스토리지 매니저

🔸 primary / secondary 컨테이너

iCloud는 자신을 이용하는 App을 위해 파일 저장 시스템을 제공합니다. App은 네이티브 파일들을 저장하기 위해 primary iCloud 컨테이너 폴더를 가집니다

또한 Entitlements에 명시된 secondary iCloud 컨테이너 폴더들에 접근할 수 있습니다. 각 secondary 컨테이너 폴더 안에서, 파일들은 Document와 데이터로 구분됩니다

🔸 유저에게 공개할 파일 (Document)

Documents 폴더에 위치한 모든 파일/파일패키지는 iCloud UI를 통해 유저에게 개별적으로 삭제가능한 형태의 document로 보여집니다

Document는 유저가 생성하고 App의 UI에 보여집니다. 예로, Pages/Numbers/키노트 App에서 document 브라우저는 Documents 폴더에 저장되어야 합니다. 다른 예로, 나중에 불러오기가 가능한 "저장된 게임" 등도 있습니다. App이 선택을 위해 몇가지 방법을 제공할 수도 있는 것들이기 때문입니다

🔸 유저에게 숨길 파일 (Data)

Documents에 있지 않은 파일들은 데이터로 취급되며 iCloud UI에서 개별적인 엔트리로 보여집니다

유저에게 보여지거나 직접적으로 수정이 불가하길 원하는 파일들은 Documents 폴더 밖에 위치해야 합니다. App은 컨테이너 폴더 내에 얼마든지 하위폴더를 만들어 private 파일들을 정렬할 수도 있습니다

🔸 그 외 특징

App은 일반 로컬 파일/폴더를 생성하는 것과 완전히 같은 방식으로 iCloud 컨테이너에 파일/폴더를 생성합니다. 파일에 Attribute를 추가하면 이를 현재 device에 저장하고, iCloud와 유저의 다른 device에도 복사됩니다. 또한 iCloud 컨테이너는 key-value 쌍의 스토리지도 허용하여, document 포맷을 생성하지 않고도 쉽게 접근이 가능하도록 합니다


🌀 시스템은 파일 Content의 타입을 어떻게 식별할까

파일 content 타입을 식별하는 2가지 주요 테크닉이 있습니다

  • Uniform Type Identifiers (UTIs)
  • Filename extensions

🔸 Uniform Type Identifiers

Uniform Type Identifiers는 string 형태로, "type"을 가진 것이라 간주되는 엔티티의 class를 유일하게 식별합니다. UTIs는 모든 App/서비스가 인식할 수 있는, 일관성있는 데이터 식별자를 제공합니다. 또한 파일/폴더뿐만 아니라 모든 타입의 데이터를 표현할 수 있으므로, 다른 대부분의 테크닉들보다 유연합니다. 아래는 UTI의 예시입니다

  • public.text : 텍스트 데이터를 식별하는 public 타입
  • public.jpeg : JPEG 이미지 데이터를 식별하는 public 타입
  • com.apple.bundle : 번들 폴더를 식별하는 Apple 타입
  • com.apple.application-bundle : 번들된 App을 식별하는 Apple 타입

🔸 macOS/iOS에서의 UTIs 지원여부

가능한 한 UTI를 선호해야 합니다. 많은 macOS 인터페이스들이 대응되는 파일/폴더에 UTI를 명시할 수 있도록 허용하고 있습니다. 예로, 맥에 있는 Open Panel에서 UTI를 파일필터링에 사용하거나 유저가 선택하는 파일의 타입을 제한할 수 있습니다. NSDocument/NSPasteboard/NSImage 등의 일부 AppKit class들이 UTI를 지원하고 있으며, iOS에서는 pasteboard 타입 명시에만 사용됩니다

🔸 Filename extensions (파일 확장자)

시스템이 주어진 파일에 대한 UTI를 결정하는 한 가지 방법은 파일의 확장자를 보는 것입니다. 확장자는 string 형태로, 파일이름 끝에 붙으며 마침표로 구분됩니다. 각 고유한 string은 특정 타입의 파일을 식별합니다. 예로, .strings라는 확장자는 리소스 파일을 localizable string 데이터로 식별하고, .png는 리소스 파일을 png 이미지 데이터로 식별합니다

Note
macOS/iOS에서 마침표 자체가 파일이름의 일부로써 허용되므로, 파일이름의 마지막 마침표만이 확장자로 간주됩니다

🔸 커스텀 파일 포맷을 정의하는 경우

만약 App이 커스텀 파일 포맷을 정의한다면, 이 포맷과 확장자를 Info.plist에 등록해야 합니다. CFBundleDocumentTypes key는 App이 인식하고 open할 수 있는 파일 포맷을 명시합니다. 커스텀 파일 포맷에 대한 엔트리들은 확장자와 파일 content에 대응되는 UTI를 모두 포함하고 있어야 합니다. 시스템은 해당 App에 파일을 보낼 때 적절한 타입으로 보내기 위해 이 정보를 사용하게 됩니다

UTI와 사용법에 대한 더 많은 정보는 Uniform Type Identifiers Overview를 참고합니다. CFBundleDocumentTypes key에 대한 더 많은 정보는 Information Property List Key Reference를 참고합니다


🌀 Security : 생성한 파일을 보호

모든 유저 데이터와 시스템 코드는 디스크 어딘가에 저장되기 때문에, 파일의 무결성과 파일시스템을 보호하는 것이 중요합니다. 이를 위해 content를 보호하고 탈취되거나 다른 프로세스에 의해 손상되지 않도록하는 몇가지 방법이 존재합니다

(secure 코딩 예제에 대한 일반적인 정보는 Secure Coding Guide를 참고합니다)

🔸 샌드박스가 데미지 확산을 방지

iOS와 macOS 10.7부터는 샌드박스가 App이 금지된 파일시스템 부분에 쓰기를 하는 것을 방지합니다. 샌드박스화된 각 App은 자신만의 하나 이상의 쓰기가 가능한 컨테이너를 받습니다. 하나의 App은 다른 App의 컨테이너 혹은 샌드박스 외부의 대부분의 폴더에 쓰기를 할 수 없습니다. 이런 제약들이 App의 보안이 위반된 경우에 발생 가능한 데미지를 막아줍니다

🔘 개발자 권고사항
macOS 10.7 이상 타겟의 App을 작성하는 개발자는 보안 강화를 위해 App을 샌드박스 내에 놓을 것이 권고됩니다. iOS 개발자는 시스템이 인스톨 타임에 자동으로 그렇게 하므로 명시적으로 샌드박스 내에 놓을 필요가 없습니다

샌드박스와 파일시스템 접근에 도입된 제약들에 대한 더 많은 정보는 Mac App Programming GuideApp Sandbox Design Guide를 참고합니다

🔸 권한(Permission)과 접근제어목록

최종적인 파일/폴더로의 접근은 Access Controll List (ACL, 접근제어목록)BSD 권한의 합으로 결정됩니다

🔘 Access Controll List (ACL, 접근제어목록)
접근제어목록은 어떤 파일/폴더에 무엇을 행할 수 있는지와 누가 할 수 있는지를 정확히 정의합니다. 접근제어목록을 통해 어떤 파일/폴더에 대해 유저마다 다른 접근권한을 부여할 수 있습니다

🔘 BSD 권한
반대로, BSD 권한은 공개할 유저 범위를 3가지로만 분류합니다 (파일의 오너, 특정 유저 그룹, 모든 유저)

이에 대한 더 많은 정보는 Security Overview를 참고합니다

NOTE
네트워크 서버의 파일에 대해선, 해당 파일과 관련한 ACL/BSD에 대해 어떠한 가정도 하면 안됩니다. 일부 네트워크 파일시스템은 권한 관련 정보와 관련하여 요약된 일부만을 공개하기 때문입니다

🔘 iOS vs macOS
iOS App은 항상 샌드박스에서 실행되므로, 시스템은 특정 ACL과 권한을 각 App이 생성한 파일에 할당합니다. 하지만, macOS App은 ACL을 관리하기 위한 Identity Service를 사용할 수 있습니다. Identity Service의 사용법과 Collaboration 프레임워크에 대한 더 많은 정보는 Identity Services Programming Guide를 참고합니다

🔸 디스크에 저장하는 파일은 암호화될 수 있다

macOS와 iOS 모두 디스크에 저장할 파일에 대한 암호화를 지원합니다

🔘 iOS
iOS App은 디스크에 저장할 때 암호화되길 원하는 파일을 지정할 수 있습니다. 유저가 device를 잠금해제할 때, 시스템이 암호화된 파일에 접근할 수 있는 해독키를 생성합니다. 하지만 유저가 device를 잠그면, 해독키는 무효화됩니다

iOS에서, 디스크 기반 암호화를 사용하는 App은 유저가 device를 잠글 때 암호화된 파일 사용을 중단해야 합니다. device를 잠그면 해독키가 무효화되므로, 암호화된 파일로의 접근이 제한됩니다. 만약 iOS App이 device가 잠긴 동안 백그라운드에서 돌아갈 수 있다면, 해독키가 없으므로 암호화된 파일로 접근하는 것 없이 작업하도록 해야 합니다

iOS에서 암호화된 파일과의 작업에 대한 자세한 정보는 App Programming Guide for iOS를 참고합니다

🔘 macOS
유저는 Disk Utility App을 사용하여 디스크 특정 볼륨의 content를 암호화할 수 있습니다 (또한, Security & Privacy 시스템 환경설정에서 부팅 볼륨만 암호화할 수도 있습니다). 암호화된 디스크의 content는 컴퓨터가 running 상태일 때만 App에서 사용할 수 있습니다. 유저가 컴퓨터를 sleep시키거나 종료하면, 해독키는 무효화됩니다

macOS에서 iOS와 달리 device를 lock/unlock이 기준이 아니라 컴퓨터의 running 상태가 기준이므로 iOS의 백그라운드 작업처럼 해독키가 없는 상태에서 작업을 하는 경우가 없습니다. 따라서, macOS는 디스크 level 암호화를 관리하기 위한 별다른 처리가 필요하지 않습니다


🌀 파일시스템 동기화

🔸 동기화의 필요성

파일시스템은 시스템 App과 기타 서드파티 App들이 공유하는 리소스로, 여러 App들이 동시에 특정 파일/폴더에 접근이 가능합니다. 어떤 파일을 두 App이 공유하고 있는 상황에서, 한 App이 파일을 변경하면 두번째 App이 갖고 있던 것은 업데이트가 필요한 구식이 됩니다. 만약 두번째 App에 동기화가 구현되어 있지 않다면, 알 수 없는 상태에 빠지거나 crash가 발생할 수 있습니다. 이를 위해 동기화 인터페이스를 사용하여 파일에 대한 변경을 알림받을 수 있습니다

🔸 동기화 인터페이스

파일시스템 동기화는 유저가 Finder에서 직접적으로 조작할 수 있거나 수많은 App을 동시에 돌릴 수 있는 macOS에서 주로 문제가 됩니다. 다행히도 macOS는 동기화 문제를 위해 아래 인터페이스들을 제공하고 있습니다

🔘 File coordinator
(macOS 10.7부터) file coordinator는 App의 객체들에 직접적으로 동기화 지원을 설정하는 방법입니다. 이에 대해선 The Role of File Coordinators and Presenters를 참고합니다

🔘 FSEvents
(macOS 10.7부터) 파일시스템 이벤트는 어떤 폴더 혹은 내부 content를 감시할 수 있게 해줍니다. 이에 대해선 File System Events Programming Guide를 참고합니다


🌀 파일관련 동작에 대한 동시성 이슈

파일관련 동작들은 하드디스크와의 상호작용이 포함되어 다른 대부분의 동작에 비해 느리기 때문에, iOS/macOS에서 대부분의 파일관련 동작은 동시성을 염두에 두고 설계되었습니다. 일부 기술들은 설계에 비동기 작업이 통합되어 있고, 대부분의 기술은 DispatchQueue 혹은 보조 스레드로부터 안전하게 실행할 수 있습니다

🔸 파일관련 핵심 기술들과 Thread-safety

🔘 NSFileManager
대부분의 작업에서, "디폴트 NSFileManager 객체"는 여러 백그라운드 스레드에서 동시에 사용되어도 안전합니다. 하지만 파일매니저의 delegate와 상호작용하는 작업인 경우에는 안전하지 않습니다. 따라서, delegate를 쓰는 경우엔 한 번에 하나의 스레드에서 돌 수 있게 우리가 통제할 수 있도록 NSFileManager 인스턴스를 직접 생성하여 사용합니다

🔘 Grand Central Dispatch
GCD는 자체적으로는 어떤 스레드에서 사용되더라도 안전합니다만, 코드 블럭을 작성할 때는 여전히 thread-safe하게 고려하여 작성해야 합니다

🔘 NSFileHandle, NSData, Cocoa streams
파일 데이터를 읽고 쓰는데 사용되는 대부분의 Foundation 객체들은 단일 스레드에서만 사용될 수 있습니다. (여러 스레드에서 동시 사용하면 안됩니다)

🔘 Open and Save panels
(mac에서 파일 저장할 때 뜨는 창을 말함) 이 패널들은 UI의 일부이므로, 항상 메인스레드에서 띄우고 조작해야 합니다

🔘 POSIX routines
파일을 조작하는 POSIX 루틴들은 일반적으로 어떤 스레드에서도 안전하게 동작하도록 설계되었습니다. 이에 대한 구체적인 내용은 관련 reference를 참고합니다

🔘 NSURL, NSString
경로를 명시하기 위해 사용하는 상수 객체들은 어떤 스레드에서 사용하더라도 안전합니다. 상수이기 때문에 여러 스레드에서 동시에 접근하더라도 문제가 없습니다. 물론, 이 객체들의 변수버전들은 한 번에 하나의 스레드에서만 사용되어야 합니다

🔘 NSEnumerator와 그 subclass
Enumerator 객체들은 단일 스레드에선 안전하나 여러 스레드에서 동시에 사용하면 안됩니다

🔸 그럼에도 항상 안전하진 않다

한 파일을 조작하는데 thread-safe한 인터페이스를 사용하더라도, 여러 스레드/프로세스에서 동일한 파일에 대해 동작시키면 여전히 문제가 발생할 수 있습니다. 여러 곳에서 하나의 파일을 동시에 수정하는 것을 막기 위한 안전장치가 존재하긴 합니다만, 항상 독점적인 접근을 보장하진 않습니다. 또한, 다른 프로세스가 공유 파일에 접근하는 것을 막아선 안됩니다

이런 공유 파일에 변화가 생겼음을 코드가 알 수 있도록, file coordinator를 사용할 수 있습니다. 이에 대한 자세한 정보는 The Role of File Coordinators and Presenters를 참고합니다

profile
노션으로 이사갑니다 https://tungsten-run-778.notion.site/Study-Archive-98e51c3793684d428070695d5722d1fe

0개의 댓글