네트워크에 연결

애플리케이션에서 네트워크 작업을 실행하려면 매니페스트에 다음 권한을 포함해야 합니다.

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

안전한 네트워크 통신을 위한 권장사항

앱에 네트워킹 기능을 추가하기 전에 네트워크를 통해 앱의 데이터와 정보를 전송할 때 안전하게 보호되는지 확인해야 합니다. 데이터와 정보를 안전하게 유지하려면 다음 네트워킹 보안 권장사항을 따르세요.

  • 네트워크를 통해 전송하는 데이터 중 민감하거나 개인적인 사용자 데이터의 양을 최소화합니다.
  • 앱의 모든 네트워크 트래픽은 SSL을 통해 전송합니다.
  • 네트워크 보안 구성을 만들어 봅니다. 이 구성을 사용하면 앱에서 안전한 통신을 위해 맞춤 CA(인증 기관)를 신뢰하거나 신뢰하는 시스템 CA 집합을 제한할 수 있습니다.

안전한 네트워킹 원칙을 적용하는 방법을 자세히 알아보려면 네트워킹 보안 도움말을 참고하세요.

HTTP 클라이언트 선택

네트워크에 연결된 앱의 대부분은 HTTP를 사용하여 데이터를 송수신합니다. Android 플랫폼에는 HttpsURLConnection 클라이언트가 포함되며 이 클라이언트는 TLS, 스트리밍 업로드 및 다운로드, 시간 제한 구성, IPv6, 연결 풀링을 지원합니다.

네트워킹 작업에 상위 수준 API를 제공하는 서드 파티 라이브러리도 사용할 수 있습니다. 이는 요청 본문의 직렬화, 응답 본문의 역직렬화와 같은 다양한 편의 기능을 지원합니다.

  • Retrofit: Square의 JVM용 유형 안전 HTTP 클라이언트로, OkHttp를 기반으로 빌드됩니다. Retrofit을 사용하면 클라이언트 인터페이스를 선언적으로 만들 수 있으며 여러 직렬화 라이브러리를 지원합니다.
  • Ktor: JetBrains의 HTTP 클라이언트로, Kotlin 전용으로 빌드되었으며 코루틴을 기반으로 합니다. Ktor는 다양한 엔진, 직렬 변환기, 플랫폼을 지원합니다.

DNS 쿼리 결정

Android 10(API 수준 29) 이상을 실행하는 기기에서는 일반 텍스트 조회와 DNS-over-TLS 모드를 통한 특수 DNS 조회가 기본적으로 지원됩니다. DnsResolver API는 일반적인 비동기식의 결정 방법을 제공하며 이를 사용하여 SRV, NAPTR 및 기타 레코드 유형을 조회할 수 있습니다. 응답 파싱은 앱에서 실행합니다.

Android 9(API 수준 28) 이하를 실행하는 기기의 플랫폼 DNS 리졸버는 AAAAA 레코드만 지원합니다. 이렇게 하면, 이름과 연결된 IP 주소를 조회할 수 있으며 다른 레코드 유형은 지원하지 않습니다.

NDK 기반 앱의 경우 android_res_nsend를 참고하세요.

저장소로 네트워크 작업 캡슐화

네트워크 작업 실행 프로세스를 단순화하고 앱의 다양한 영역에서 코드 중복을 줄이려면 저장소 설계 패턴을 사용하면 됩니다. 저장소는 데이터 작업을 처리하고 특정 데이터 또는 리소스에 관해 명확한 API 추상화를 제공하는 클래스입니다.

다음 예와 같이 Retrofit을 사용하여 네트워크 작업에 필요한 HTTP 메서드, URL, 인수 및 응답 유형을 지정하는 인터페이스를 선언할 수 있습니다.

Kotlin

interface UserService {
    @GET("/users/{id}")
    suspend fun getUser(@Path("id") id: String): User
}

Java

public interface UserService {
    @GET("/user/{id}")
    Call<User> getUserById(@Path("id") String id);
}

저장소 클래스 내에서 함수는 네트워크 작업을 캡슐화하고 결과를 노출할 수 있습니다. 이러한 캡슐화를 통해 저장소를 호출하는 구성요소는 데이터가 저장되는 방식을 알 필요가 없습니다. 또한, 이후 데이터 저장 방식에 관한 모든 변경사항은 저장소 클래스와 분리됩니다. 예를 들어 API 엔드포인트 업데이트와 같은 원격 변경사항이 있을 수도 있고 로컬 캐싱을 구현해야 할 수도 있습니다.

Kotlin

class UserRepository constructor(
    private val userService: UserService
) {
    suspend fun getUserById(id: String): User {
        return userService.getUser(id)
    }
}

Java

class UserRepository {
    private UserService userService;

    public UserRepository(
            UserService userService
    ) {
        this.userService = userService;
    }

    public Call<User> getUserById(String id) {
        return userService.getUser(id);
    }
}

UI가 반응하지 않는 상황을 피하려면 기본 스레드에서 네트워크 작업을 실행하지 마세요. 기본적으로 Android에서는 기본 UI 스레드가 아닌 스레드에서 네트워크 작업을 실행해야 합니다. 기본 스레드에서 네트워크 작업을 실행하려고 하면 NetworkOnMainThreadException이 발생합니다.

이전 코드 예에서 네트워크 작업은 실제로 트리거되지 않습니다. UserRepository의 호출자는 코루틴 또는 enqueue() 함수를 사용하여 스레드를 구현해야 합니다. 자세한 내용은 Kotlin 코루틴을 사용하여 스레드 구현 방법을 보여주는 Codelab 인터넷에서 데이터 가져오기를 참고하세요.

구성 변경사항 유지

구성에 변경사항(예: 화면 회전)이 생기면 프래그먼트 또는 활동이 소멸되고 다시 생성됩니다. 소량의 데이터만 유지할 수 있는 프래그먼트 활동의 경우 인스턴스 상태에 저장되지 않은 데이터는 손실됩니다. 이 경우 네트워크를 다시 요청해야 할 수도 있습니다.

ViewModel을 사용하여 구성 변경 후에도 데이터가 유지되도록 할 수 있습니다. ViewModel 구성요소는 수명 주기를 고려하여 UI 관련 데이터를 저장하고 관리하도록 설계되었습니다. 위에서 생성된 UserRepository를 사용하면 ViewModel에서 필수 네트워크 요청을 실행하고 LiveData를 사용하여 프래그먼트나 활동에 결과를 제공할 수 있습니다.

Kotlin

class MainViewModel constructor(
    savedStateHandle: SavedStateHandle,
    userRepository: UserRepository
) : ViewModel() {
    private val userId: String = savedStateHandle["uid"] ?:
        throw IllegalArgumentException("Missing user ID")

    private val _user = MutableLiveData<User>()
    val user = _user as LiveData<User>

    init {
        viewModelScope.launch {
            try {
                // Calling the repository is safe as it moves execution off
                // the main thread
                val user = userRepository.getUserById(userId)
                _user.value = user
            } catch (error: Exception) {
                // Show error message to user
            }

        }
    }
}

Java

class MainViewModel extends ViewModel {

    private final MutableLiveData<User> _user = new MutableLiveData<>();
    LiveData<User> user = (LiveData<User>) _user;

    public MainViewModel(
            SavedStateHandle savedStateHandle,
            UserRepository userRepository
    ) {
        String userId = savedStateHandle.get("uid");
        Call<User> userCall = userRepository.getUserById(userId);
        userCall.enqueue(new Callback<User>() {
            @Override
            public void onResponse(Call<User> call, Response<User> response) {
                if (response.isSuccessful()) {
                    _user.setValue(response.body());
                }
            }

            @Override
            public void onFailure(Call<User> call, Throwable t) {
                // Show error message to user
            }
        });
    }
}

이 주제에 관해 자세히 알아보려면 다음 관련 가이드를 참고하세요.