보관 규칙 추가

상위 수준에서 유지 규칙은 클래스(또는 서브클래스나 구현)와 해당 클래스 내에서 유지할 멤버(메서드, 생성자 또는 필드)를 지정합니다.

유지 규칙의 일반적인 문법은 다음과 같습니다. 하지만 특정 유지 옵션은 선택사항인 keep_option_modfier를 허용하지 않습니다.


-<keep_option>[,<keep_option_modifier_1>,<keep_option_modifier_2>,...] <class_specification>

다음은 keepclassmembers을 유지 옵션으로, allowoptimization을 수정자로 사용하고 com.example.MyClass에서 someSpecificMethod()을 유지하는 유지 규칙의 예입니다.

-keepclassmembers,allowoptimization class com.example.MyClass {
  void someSpecificMethod();
}

유지 옵션

유지 옵션은 유지 규칙의 첫 번째 부분입니다. 클래스의 어떤 측면을 보존할지 지정합니다. keep, keepclassmembers, keepclasseswithmembers, keepnames, keepclassmembernames, keepclasseswithmembernames의 6가지 유지 옵션이 있습니다.

다음 표에서는 이러한 보관 옵션을 설명합니다.

Keep 옵션 설명
keepclassmembers 최적화 후 클래스가 있는 경우에만 지정된 멤버를 유지합니다.
keep 지정된 클래스와 지정된 멤버 (필드 및 메서드)를 보존하여 최적화되지 않도록 합니다.

참고: keep는 일반적으로 keep 옵션 수정자와 함께만 사용해야 합니다. keep 자체는 일치하는 클래스에서 어떤 종류의 최적화도 발생하지 않도록 하기 때문입니다.
keepclasseswithmembers 클래스 사양의 모든 멤버가 클래스에 있는 경우에만 클래스와 지정된 멤버를 유지합니다.
keepclassmembernames 지정된 클래스 멤버의 이름 변경을 방지하지만 클래스 또는 해당 멤버의 삭제는 방지하지 않습니다.

참고: 이 옵션의 의미는 종종 오해를 받습니다. 대신 동등한 -keepclassmembers,allowshrinking을 사용하는 것이 좋습니다.
keepnames 클래스와 해당 구성원의 이름이 바뀌는 것을 방지하지만 사용되지 않는 것으로 간주되는 경우 완전히 삭제되는 것을 방지하지는 않습니다.

참고: 이 옵션의 의미는 종종 오해를 받습니다. 대신 동등한 -keep,allowshrinking을 사용하는 것이 좋습니다.
keepclasseswithmembernames 클래스와 지정된 구성원의 이름이 바뀌는 것을 방지합니다. 단, 구성원이 최종 코드에 있는 경우에만 해당합니다. 코드 삭제를 방지하지는 않습니다.

참고: 이 옵션의 의미는 종종 오해를 받습니다. 대신 -keepclasseswithmembers,allowshrinking을 사용하는 것이 좋습니다.

올바른 보관 옵션 선택하기

올바른 유지 옵션을 선택하는 것은 앱에 적합한 최적화를 결정하는 데 매우 중요합니다. 특정 유지 옵션은 참조되지 않은 코드가 삭제되는 코드 축소 프로세스를 통해 코드를 축소하는 반면, 다른 옵션은 코드를 난독화하거나 이름을 바꿉니다. 다음 표에는 다양한 보관 옵션의 작업이 나와 있습니다.

Keep 옵션 클래스 축소 클래스를 난독화합니다. 구성원 축소 구성원을 난독화합니다.
keep
keepclassmembers
keepclasseswithmembers
keepnames
keepclassmembernames
keepclasseswithmembernames

옵션 수정자 유지

보관 옵션 수정자는 보관 규칙의 범위와 동작을 제어하는 데 사용됩니다. 보관 규칙에 0개 이상의 보관 옵션 수정자를 추가할 수 있습니다.

keep 옵션 수정자의 가능한 값은 다음 표에 설명되어 있습니다.

설명
allowoptimization 지정된 요소를 최적화할 수 있습니다. 하지만 지정된 요소는 이름이 변경되거나 삭제되지 않습니다.
allowobfucastion 지정된 요소의 이름 바꾸기를 허용합니다. 하지만 요소가 삭제되거나 다른 방식으로 최적화되지는 않습니다.
allowshrinking R8에서 지정된 요소를 참조하는 항목을 찾지 못하는 경우 해당 요소를 삭제할 수 있습니다. 하지만 요소의 이름이 변경되거나 다른 방식으로 최적화되지는 않습니다.
includedescriptorclasses 보관되는 메서드 (매개변수 유형 및 반환 유형) 및 필드 (필드 유형)의 설명자에 표시되는 모든 클래스를 보관하도록 R8에 지시합니다.
allowaccessmodification R8이 최적화 프로세스 중에 클래스, 메서드, 필드의 액세스 수정자 (public, private, protected)를 변경 (일반적으로 확대)할 수 있도록 허용합니다.
allowrepackage R8이 기본 (루트) 패키지를 비롯한 다른 패키지로 클래스를 이동하도록 허용합니다.

클래스 사양

보관 규칙의 일부로 클래스, 상위 클래스 또는 구현된 인터페이스를 지정해야 합니다. java.lang.String과 같은 java.lang 네임스페이스의 클래스를 비롯한 모든 클래스는 정규화된 Java 이름을 사용하여 지정해야 합니다. 사용해야 하는 이름을 이해하려면 생성된 Java 이름 가져오기에 설명된 도구를 사용하여 바이트 코드를 검사하세요.

다음 예는 MaterialButton 클래스를 지정하는 방법을 보여줍니다.

  • 올바름: com.google.android.material.button.MaterialButton
  • 잘못됨: MaterialButton

클래스 사양은 유지해야 하는 클래스 내의 구성원도 지정합니다. 다음 규칙은 MaterialButton 클래스와 모든 구성원을 유지합니다.

-keep class com.google.android.material.button.MaterialButton { *; }

서브클래스 및 구현

인터페이스를 구현하는 서브클래스 또는 클래스를 타겟팅하려면 각각 extendimplements를 사용하세요.

예를 들어 다음과 같이 서브클래스 Foo이 있는 클래스 Bar이 있는 경우

class Foo : Bar()

다음 유지 규칙은 Bar의 모든 하위 클래스를 보존합니다. 유지 규칙에는 슈퍼클래스 Bar 자체가 포함되지 않습니다.

-keep class * extends Bar

Bar를 구현하는 Foo 클래스가 있는 경우:

class Foo : Bar

다음 유지 규칙은 Bar를 구현하는 모든 클래스를 보존합니다. 보관 규칙에는 인터페이스 Bar 자체가 포함되지 않습니다.

-keep class * implements Bar

액세스 수정자

public, private, static, final과 같은 액세스 수정자를 지정하여 보관 규칙을 더 정확하게 만들 수 있습니다.

예를 들어 다음 규칙은 api 패키지 및 하위 패키지 내의 모든 public 클래스와 이러한 클래스의 모든 공개 및 보호된 멤버를 유지합니다.

-keep public class com.example.api.** { public protected *; }

클래스 내의 멤버에 한정자를 사용할 수도 있습니다. 예를 들어 다음 규칙은 Utils 클래스의 public static 메서드만 유지합니다.

-keep class com.example.Utils {
    public static void *(...);
}

Kotlin 관련 수정자

R8은 internal, suspend과 같은 Kotlin 전용 수정자를 지원하지 않습니다. 이러한 필드를 유지하려면 다음 가이드라인을 따르세요.

  • internal 클래스, 메서드 또는 필드를 유지하려면 공개로 취급하세요. 예를 들어 다음 Kotlin 소스를 고려해 보세요.

    package com.example
    internal class ImportantInternalClass {
      internal f: Int
      internal fun m() {}
    }
    

    internal 클래스, 메서드, 필드는 Kotlin 컴파일러에서 생성된 .class 파일에서 public이므로 다음 예와 같이 public 키워드를 사용해야 합니다.

    -keepclassmembers public class com.example.ImportantInternalClass {
      public int f;
      public void m();
    }
    
  • suspend 멤버가 컴파일되면 keep 규칙에서 컴파일된 서명을 일치시킵니다.

    예를 들어 다음 스니펫에 표시된 대로 정의된 fetchUser 함수가 있는 경우

    suspend fun fetchUser(id: String): User
    

    컴파일되면 바이트 코드의 서명은 다음과 같이 표시됩니다.

    public final Object fetchUser(String id, Continuation<? super User> continuation);
    

    이 함수의 유지 규칙을 작성하려면 컴파일된 이 시그니처와 일치하거나 ...를 사용해야 합니다.

    컴파일된 서명을 사용하는 예는 다음과 같습니다.

    -keepclassmembers class com.example.repository.UserRepository {
    public java.lang.Object fetchUser(java.lang.String,  kotlin.coroutines.Continuation);
    }
    

    ...을 사용하는 예는 다음과 같습니다.

    -keepclassmembers class com.example.repository.UserRepository {
    public java.lang.Object fetchUser(...);
    }
    

회원 사양

클래스 사양에는 보존할 클래스 멤버가 선택적으로 포함됩니다. 클래스에 하나 이상의 구성원을 지정하면 해당 구성원에게만 규칙이 적용됩니다.

예를 들어 특정 클래스와 모든 멤버를 유지하려면 다음을 사용합니다.

-keep class com.myapp.MyClass { *; }

클래스만 유지하고 멤버는 유지하지 않으려면 다음을 사용하세요.

-keep class com.myapp.MyClass

대부분의 경우 일부 구성원을 지정하는 것이 좋습니다. 예를 들어 다음 예시에서는 공개 필드 text와 공개 메서드 updateText()을 클래스 MyClass 내에 유지합니다.

-keep class com.myapp.MyClass {
    public java.lang.String text;
    public void updateText(java.lang.String);
}

모든 공개 필드와 공개 메서드를 유지하려면 다음 예시를 참고하세요.

-keep public class com.example.api.ApiClient {
    public *;
}

메서드

유지 규칙의 멤버 사양에서 메서드를 지정하는 문법은 다음과 같습니다.

[<access_modifier>] [<return_type>] <method_name>(<parameter_types>);

예를 들어 다음 유지 규칙은 void를 반환하고 String를 사용하는 setLabel()라는 공개 메서드를 유지합니다.

-keep class com.example.MyView {
    public void setLabel(java.lang.String);
}

다음과 같이 <methods>를 바로가기로 사용하여 클래스의 모든 메서드를 일치시킬 수 있습니다.

-keep class com.example.MyView {
    <methods>;
}

반환 유형 및 매개변수 유형의 유형을 지정하는 방법을 자세히 알아보려면 유형을 참고하세요.

생성자

생성자를 지정하려면 <init>를 사용합니다. 보관 규칙의 멤버 사양에서 생성자를 지정하는 문법은 다음과 같습니다.

[<access_modifier>] <init>(parameter_types);

예를 들어 다음 유지 규칙은 ContextAttributeSet를 사용하는 맞춤 View 생성자를 유지합니다.

-keep class com.example.ui.MyCustomView {
    public <init>(android.content.Context, android.util.AttributeSet);
}

모든 공개 생성자를 유지하려면 다음 예시를 참고하세요.

-keep class com.example.ui.MyCustomView {
    public <init>(...);
}

필드

보관 규칙의 구성원 사양에서 필드를 지정하는 문법은 다음과 같습니다.

[<access_modifier>...] [<type>] <field_name>;

예를 들어 다음 유지 규칙은 userId라는 비공개 문자열 필드와 STATUS_ACTIVE라는 공개 정적 정수 필드를 유지합니다.

-keep class com.example.models.User {
    private java.lang.String userId;
    public static int STATUS_ACTIVE;
}

다음과 같이 <fields>을 단축키로 사용하여 클래스의 모든 필드를 일치시킬 수 있습니다.

-keep class com.example.models.User {
    <fields>;
}

패키지 수준 함수

클래스 외부에 정의된 Kotlin 함수 (일반적으로 최상위 함수라고 함)를 참조하려면 Kotlin 컴파일러가 암시적으로 추가한 클래스에 생성된 Java 이름을 사용해야 합니다. 클래스 이름은 Kotlin 파일 이름에 Kt가 추가된 것입니다. 예를 들어 다음과 같이 정의된 MyClass.kt라는 Kotlin 파일이 있다고 가정해 보겠습니다.

package com.example.myapp.utils

// A top-level function not inside a class
fun isEmailValid(email: String): Boolean {
    return email.contains("@")
}

isEmailValid 함수의 유지 규칙을 작성하려면 클래스 사양이 생성된 클래스 MyClassKt을 타겟팅해야 합니다.

-keep class com.example.myapp.utils.MyClassKt {
    public static boolean isEmailValid(java.lang.String);
}

유형

이 섹션에서는 keep 규칙 멤버 사양에서 반환 유형, 매개변수 유형, 필드 유형을 지정하는 방법을 설명합니다. Kotlin 소스 코드와 다른 경우 생성된 Java 이름을 사용하여 유형을 지정해야 합니다.

기본 유형

기본 유형을 지정하려면 Java 키워드를 사용합니다. R8은 boolean, byte, short, char, int, long, float, double 기본 유형을 인식합니다.

기본 유형이 있는 규칙의 예는 다음과 같습니다.

# Keeps a method that takes an int and a float as parameters.
-keepclassmembers class com.example.Calculator {
    public void setValues(int, float);
}

일반 유형

컴파일 중에 Kotlin/Java 컴파일러는 일반 유형 정보를 삭제하므로 일반 유형이 포함된 유지 규칙을 작성할 때는 원래 소스 코드가 아닌 컴파일된 코드 표현을 타겟팅해야 합니다. 일반 유형이 변경되는 방식에 대해 자세히 알아보려면 타입 삭제를 참고하세요.

예를 들어 Box.kt에 정의된 경계가 없는 일반 유형이 있는 다음 코드가 있다고 가정해 보겠습니다.

package com.myapp.data

class Box<T>(val item: T) {
    fun getItem(): T {
        return item
    }
}

타입 삭제 후 TObject로 대체됩니다. 클래스 생성자와 메서드를 유지하려면 규칙에서 일반 T 대신 java.lang.Object를 사용해야 합니다.

보관 규칙의 예는 다음과 같습니다.

# Keep the constructor and methods of the Box class.
-keep class com.myapp.data.Box {
    public init(java.lang.Object);
    public java.lang.Object getItem();
}

NumberBox.kt에 바운드된 일반 유형이 있는 다음 코드가 있는 경우:

package com.myapp.data

// T is constrained to be a subtype of Number
class NumberBox<T : Number>(val number: T)

이 경우 유형 삭제는 T을 바운드인 java.lang.Number으로 대체합니다.

보관 규칙의 예는 다음과 같습니다.

-keep class com.myapp.data.NumberBox {
    public init(java.lang.Number);
}

앱별 일반 유형을 기본 클래스로 사용하는 경우 기본 클래스의 keep 규칙도 포함해야 합니다.

예를 들어 다음 코드의 경우

package com.myapp.data

data class UnpackOptions(val useHighPriority: Boolean)

// The generic Box class with UnpackOptions as the bounded type
class Box<T: UnpackOptions>(val item: T) {
}

다음과 같이 단일 규칙으로 UnpackOptions 클래스와 Box 클래스 메서드를 모두 유지하기 위해 includedescriptorclasses와 함께 유지 규칙을 사용할 수 있습니다.

-keep,includedescriptorclasses class com.myapp.data.Box {
    public <init>(com.myapp.data.UnpackOptions);
}

객체 목록을 처리하는 특정 함수를 유지하려면 함수의 서명과 정확히 일치하는 규칙을 작성해야 합니다. 일반 유형은 삭제되므로 List<Product>과 같은 매개변수는 java.util.List로 표시됩니다.

예를 들어 다음과 같이 Product 객체 목록을 처리하는 함수가 있는 유틸리티 클래스가 있다고 가정해 보겠습니다.

package com.myapp.utils

import com.myapp.data.Product
import android.util.Log

class DataProcessor {
    // This is the function we want to keep
    fun processProducts(products: List<Product>) {
        Log.d("DataProcessor", "Processing ${products.size} products.")
        // Business logic ...
    }
}

// The data class used in the list (from the previous example)
package com.myapp.data
data class Product(val id: String, val name: String)

다음 keep 규칙을 사용하여 processProducts 함수만 보호할 수 있습니다.

-keep class com.myapp.utils.DataProcessor {
    public void processProducts(java.util.List);
}

배열 유형

배열의 각 차원에 대한 구성요소 유형에 []를 추가하여 배열 유형을 지정합니다. 이는 클래스 유형과 기본 유형 모두에 적용됩니다.

  • 1차원 클래스 배열: java.lang.String[]
  • 2차원 기본 배열: int[][]

예를 들어 다음과 같은 코드가 있다고 가정해 보겠습니다.

package com.example.data

class ImageProcessor {
  fun process(): ByteArray {
    // process image to return a byte array
  }
}

다음 보관 규칙을 사용할 수 있습니다.

# Keeps a method that returns a byte array.
-keepclassmembers class com.example.data.ImageProcessor {
    public byte[] process();
}

와일드 카드

다음 표에서는 와일드 카드를 사용하여 특정 패턴과 일치하는 여러 클래스 또는 멤버에 유지 규칙을 적용하는 방법을 보여줍니다.

와일드 카드 수업 또는 회원에 적용 설명
** 둘 다 가장 일반적으로 사용됩니다. 패키지 구분 기호 수를 포함한 모든 유형 이름과 일치합니다. 이는 패키지 및 하위 패키지 내의 모든 클래스를 일치시키는 데 유용합니다.
* 둘 다 클래스 사양의 경우 패키지 구분자 (.)가 포함되지 않은 유형 이름의 모든 부분과 일치합니다.
멤버 사양의 경우 모든 메서드 또는 필드 이름과 일치합니다. 단독으로 사용되는 경우 **의 별칭이기도 합니다.
? 둘 다 클래스 또는 구성원 이름의 모든 단일 문자와 일치합니다.
*** 멤버 기본 유형 (예: int), 클래스 유형 (예: java.lang.String), 모든 차원의 배열 유형 (예: byte[][])을 비롯한 모든 유형과 일치합니다.
... 멤버 메서드의 매개변수 목록과 일치합니다.
% 멤버 `int`, `float`, `boolean` 등 모든 기본 유형과 일치합니다.

다음은 특수 와일드 카드를 사용하는 방법의 몇 가지 예입니다.

  • 입력으로 다른 기본 유형을 사용하는 이름이 같은 메서드가 여러 개 있는 경우 %를 사용하여 모든 메서드를 유지하는 유지 규칙을 작성할 수 있습니다. 예를 들어 이 DataStore 클래스에는 여러 setValue 메서드가 있습니다.

    class DataStore {
        fun setValue(key: String, value: Int) { ... }
        fun setValue(key: String, value: Boolean) { ... }
        fun setValue(key: String, value: Float) { ... }
    }
    

    다음 유지 규칙은 모든 메서드를 유지합니다.

    -keep class com.example.DataStore {
        public void setValue(java.lang.String, %);
    }
    
  • 이름이 한 글자씩 다른 클래스가 여러 개 있는 경우 ?를 사용하여 모든 클래스를 유지하는 유지 규칙을 작성하세요. 예를 들어 다음과 같은 클래스가 있다고 가정해 보겠습니다.

    com.example.models.UserV1 {...}
    com.example.models.UserV2 {...}
    com.example.models.UserV3 {...}
    

    다음 유지 규칙은 모든 클래스를 유지합니다.

    -keep class com.example.models.UserV?
    
  • ExampleAnotherExample 클래스 (루트 수준 클래스인 경우)는 일치시키되 com.foo.Example는 일치시키지 않으려면 다음 유지 규칙을 사용하세요.

    -keep class *Example
    
  • *를 단독으로 사용하면 **의 별칭으로 작동합니다. 예를 들어 다음 보관 규칙은 동일합니다.

    -keepclasseswithmembers class * { public static void main(java.lang.String[];) }
    
    -keepclasseswithmembers class ** { public static void main(java.lang.String[];) }
    

생성된 Java 이름 검사

유지 규칙을 작성할 때는 Java 바이트 코드로 컴파일된 후의 이름을 사용하여 클래스와 기타 참조 유형을 지정해야 합니다 (예는 클래스 사양유형 참고). 코드에 생성된 Java 이름을 확인하려면 Android 스튜디오에서 다음 도구 중 하나를 사용하세요.

  • APK 분석 도구
  • Kotlin 소스 파일을 연 상태에서 Tools > Kotlin > Show Kotlin Bytecode > Decompile로 이동하여 바이트 코드를 검사합니다.