네트워크에 연결

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

<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, 연결 풀링을 지원합니다. 이 주제에서는 Retrofit HTTP 클라이언트 라이브러리를 사용하며 이를 통해 HTTP 클라이언트를 명시적으로 생성할 수 있습니다. 또한, Retrofit은 요청 본문의 자동 직렬화와 응답 본문의 역직렬화를 지원합니다.

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
}

자바

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

저장소 클래스 내에서 함수는 네트워크 작업을 캡슐화하고 결과를 노출할 수 있습니다. 이러한 캡슐화를 통해 저장소를 호출하는 구성요소는 데이터가 저장되는 방식을 알 필요가 없습니다. 또한, 이후 데이터 저장 방식에 관한 모든 변경사항은 저장소 클래스와 분리됩니다.

Kotlin

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

자바

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 클래스에서 네트워크 작업은 실제로 트리거되지 않습니다. UserRepository의 호출자는 코루틴 또는 enqueue() 함수를 사용하여 스레드를 구현해야 합니다.

구성 변경사항 유지

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

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 will move execution off
                // the main thread
                val user = userRepository.getUserById(userId)
                _user.value = user
            } catch (error: Exception) {
                // show error message to user
            }

        }
    }
}

자바

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
            }
        });
    }
}

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