이 튜토리얼에서는 Android 앱에서 FusedLocationProviderClient를 사용하여 GPS 위치를 가져오고 실시간 위치 업데이트를 구현하는 방법을 단계별로 설명합니다. Google Play 서비스 위치 API를 활용하면 배터리 효율을 높이며 정확한 위치 정보를 얻을 수 있습니다.
1단계: 새 프로젝트 생성
Android Studio에서 File → New Project를 선택하고, 기본 설정(Empty Activity 등)을 따라 새 프로젝트를 만듭니다. 패키지 이름은 예제에서 app.com.sample로 사용되었으나 자유롭게 지정하세요.
2단계: 레이아웃 구성 (activity_main.xml)
메인 화면에는 위치 업데이트 시작/중지 버튼, 마지막 위치 조회 버튼, 그리고 위도/경도와 갱신 시간을 표시할 TextView를 배치합니다.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="https://schemas.android.com/apk/res/android"
xmlns:tools="https://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:orientation="vertical"
tools:context=".MainActivity">
<Button
android:id="@+id/btn_start_location_updates"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="40dp"
android:onClick="startLocationButtonClick"
android:text="@string/start_updates" />
<Button
android:id="@+id/btn_stop_location_updates"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:enabled="false"
android:onClick="stopLocationButtonClick"
android:text="@string/stop_updates" />
<Button
android:id="@+id/btn_get_last_location"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:onClick="showLastKnownLocation"
android:text="@string/get_last_location" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="50dp"
android:gravity="center_horizontal"
android:text="위치 업데이트는 앱이 포어그라운드에 있을 때만 수신됩니다" />
<TextView
android:id="@+id/location_result"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:textColor="#333"
android:textSize="18sp" />
<TextView
android:id="@+id/updated_on"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:textSize="12sp" />
</LinearLayout>
3단계: 메인 액티비티 구현 (MainActivity.java)
권한 요청에는 Dexter 라이브러리를 사용했으며, 위치 설정 확인과 업데이트 요청은 Google Play 서비스의 SettingsClient와 FusedLocationProviderClient로 처리합니다.
package app.com.sample;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import android.Manifest;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Intent;
import android.content.IntentSender;
import android.content.pm.PackageManager;
import android.location.Location;
import android.net.Uri;
import android.os.Bundle;
import android.os.Looper;
import android.provider.Settings;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import com.google.android.gms.common.api.ApiException;
import com.google.android.gms.common.api.ResolvableApiException;
import com.google.android.gms.location.FusedLocationProviderClient;
import com.google.android.gms.location.LocationCallback;
import com.google.android.gms.location.LocationRequest;
import com.google.android.gms.location.LocationResult;
import com.google.android.gms.location.LocationServices;
import com.google.android.gms.location.LocationSettingsRequest;
import com.google.android.gms.location.LocationSettingsResponse;
import com.google.android.gms.location.LocationSettingsStatusCodes;
import com.google.android.gms.location.SettingsClient;
import com.google.android.gms.tasks.OnCompleteListener;
import com.google.android.gms.tasks.OnFailureListener;
import com.google.android.gms.tasks.OnSuccessListener;
import com.google.android.gms.tasks.Task;
import com.karumi.dexter.Dexter;
import com.karumi.dexter.PermissionToken;
import com.karumi.dexter.listener.PermissionDeniedResponse;
import com.karumi.dexter.listener.PermissionGrantedResponse;
import com.karumi.dexter.listener.PermissionRequest;
import com.karumi.dexter.listener.single.PermissionListener;
import java.text.DateFormat;
import java.util.Date;
public class MainActivity extends AppCompatActivity {
private static final String TAG = MainActivity.class.getSimpleName();
// UI 컴포넌트
private TextView txtLocationResult, txtUpdatedOn;
private Button btnStartUpdates, btnStopUpdates;
// 위치 관련 변수
private String mLastUpdateTime;
private static final long UPDATE_INTERVAL_IN_MILLISECONDS = 10000; // 10초
private static final long FASTEST_UPDATE_INTERVAL_IN_MILLISECONDS = 5000; // 5초
private static final int REQUEST_CHECK_SETTINGS = 100;
// Google Play 서비스 위치 API
private FusedLocationProviderClient mFusedLocationClient;
private SettingsClient mSettingsClient;
private LocationRequest mLocationRequest;
private LocationSettingsRequest mLocationSettingsRequest;
private LocationCallback mLocationCallback;
private Location mCurrentLocation;
// 상태 플래그
private Boolean mRequestingLocationUpdates = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
init();
restoreValuesFromBundle(savedInstanceState);
}
private void init() {
txtLocationResult = findViewById(R.id.location_result);
txtUpdatedOn = findViewById(R.id.updated_on);
btnStartUpdates = findViewById(R.id.btn_start_location_updates);
btnStopUpdates = findViewById(R.id.btn_stop_location_updates);
mFusedLocationClient = LocationServices.getFusedLocationProviderClient(this);
mSettingsClient = LocationServices.getSettingsClient(this);
// 위치 콜백 설정
mLocationCallback = new LocationCallback() {
@Override
public void onLocationResult(LocationResult locationResult) {
super.onLocationResult(locationResult);
mCurrentLocation = locationResult.getLastLocation();
mLastUpdateTime = DateFormat.getTimeInstance().format(new Date());
updateLocationUI();
}
};
// 위치 요청 설정
mLocationRequest = new LocationRequest();
mLocationRequest.setInterval(UPDATE_INTERVAL_IN_MILLISECONDS);
mLocationRequest.setFastestInterval(FASTEST_UPDATE_INTERVAL_IN_MILLISECONDS);
mLocationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);
LocationSettingsRequest.Builder builder = new LocationSettingsRequest.Builder();
builder.addLocationRequest(mLocationRequest);
mLocationSettingsRequest = builder.build();
}
/** 저장된 인스턴스 상태에서 값 복원 */
private void restoreValuesFromBundle(Bundle savedInstanceState) {
if (savedInstanceState != null) {
if (savedInstanceState.containsKey("is_requesting_updates")) {
mRequestingLocationUpdates = savedInstanceState.getBoolean("is_requesting_updates");
}
if (savedInstanceState.containsKey("last_known_location")) {
mCurrentLocation = savedInstanceState.getParcelable("last_known_location");
}
if (savedInstanceState.containsKey("last_updated_on")) {
mLastUpdateTime = savedInstanceState.getString("last_updated_on");
}
}
updateLocationUI();
}
/** UI 업데이트: 위치 표시 및 버튼 토글 */
private void updateLocationUI() {
if (mCurrentLocation != null) {
txtLocationResult.setText(
"위도: " + mCurrentLocation.getLatitude() + ", " +
"경도: " + mCurrentLocation.getLongitude()
);
// 깜빡임 애니메이션
txtLocationResult.setAlpha(0);
txtLocationResult.animate().alpha(1).setDuration(300);
txtUpdatedOn.setText("마지막 업데이트: " + mLastUpdateTime);
}
toggleButtons();
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean("is_requesting_updates", mRequestingLocationUpdates);
outState.putParcelable("last_known_location", mCurrentLocation);
outState.putString("last_updated_on", mLastUpdateTime);
}
private void toggleButtons() {
if (mRequestingLocationUpdates) {
btnStartUpdates.setEnabled(false);
btnStopUpdates.setEnabled(true);
} else {
btnStartUpdates.setEnabled(true);
btnStopUpdates.setEnabled(false);
}
}
/** 위치 업데이트 시작: 설정 확인 후 요청 */
private void startLocationUpdates() {
mSettingsClient
.checkLocationSettings(mLocationSettingsRequest)
.addOnSuccessListener(this, locationSettingsResponse -> {
Log.i(TAG, "모든 위치 설정이 충족되었습니다.");
Toast.makeText(getApplicationContext(), "위치 업데이트 시작!", Toast.LENGTH_SHORT).show();
mFusedLocationClient.requestLocationUpdates(mLocationRequest, mLocationCallback, Looper.myLooper());
updateLocationUI();
})
.addOnFailureListener(this, e -> {
int statusCode = ((ApiException) e).getStatusCode();
switch (statusCode) {
case LocationSettingsStatusCodes.RESOLUTION_REQUIRED:
Log.i(TAG, "위치 설정이 필요합니다. 설정 변경을 시도합니다.");
try {
ResolvableApiException rae = (ResolvableApiException) e;
rae.startResolutionForResult(MainActivity.this, REQUEST_CHECK_SETTINGS);
} catch (IntentSender.SendIntentException sie) {
Log.i(TAG, "PendingIntent 실행 실패.");
}
break;
case LocationSettingsStatusCodes.SETTINGS_CHANGE_UNAVAILABLE:
String errorMessage = "위치 설정이 부적절하며 여기서 수정할 수 없습니다. 설정 앱에서 수정하세요.";
Log.e(TAG, errorMessage);
Toast.makeText(MainActivity.this, errorMessage, Toast.LENGTH_LONG).show();
}
updateLocationUI();
});
}
/** '시작' 버튼 클릭: 권한 요청 후 업데이트 시작 */
public void startLocationButtonClick(View view) {
Dexter.withActivity(this)
.withPermission(Manifest.permission.ACCESS_FINE_LOCATION)
.withListener(new PermissionListener() {
@Override
public void onPermissionGranted(PermissionGrantedResponse response) {
mRequestingLocationUpdates = true;
startLocationUpdates();
}
@Override
public void onPermissionDenied(PermissionDeniedResponse response) {
if (response.isPermanentlyDenied()) {
openSettings(); // 영구 거부 시 앱 설정 화면으로 이동
}
}
@Override
public void onPermissionRationaleShouldBeShown(PermissionRequest permission, PermissionToken token) {
token.continuePermissionRequest();
}
}).check();
}
/** '중지' 버튼 클릭 */
public void stopLocationButtonClick(View view) {
mRequestingLocationUpdates = false;
stopLocationUpdates();
}
private void stopLocationUpdates() {
mFusedLocationClient
.removeLocationUpdates(mLocationCallback)
.addOnCompleteListener(this, task -> {
Toast.makeText(getApplicationContext(), "위치 업데이트 중지!", Toast.LENGTH_SHORT).show();
toggleButtons();
});
}
/** '마지막 위치 보기' 버튼 클릭 */
public void showLastKnownLocation(View view) {
if (mCurrentLocation != null) {
Toast.makeText(getApplicationContext(),
"위도: " + mCurrentLocation.getLatitude() + ", 경도: " + mCurrentLocation.getLongitude(),
Toast.LENGTH_LONG).show();
} else {
Toast.makeText(getApplicationContext(), "마지막 알려진 위치를 사용할 수 없습니다!", Toast.LENGTH_SHORT).show();
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_CHECK_SETTINGS) {
switch (resultCode) {
case Activity.RESULT_OK:
Log.e(TAG, "사용자가 위치 설정 변동에 동의했습니다.");
// onResume에서 startLocationUpdates()가 다시 호출됨
break;
case Activity.RESULT_CANCELED:
Log.e(TAG, "사용자가 위치 설정 변경을 거부했습니다.");
mRequestingLocationUpdates = false;
break;
}
}
}
private void openSettings() {
Intent intent = new Intent();
intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
Uri uri = Uri.fromParts("package", BuildConfig.APPLICATION_ID, null);
intent.setData(uri);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
}
@Override
public void onResume() {
super.onResume();
if (mRequestingLocationUpdates && checkPermissions()) {
startLocationUpdates();
}
updateLocationUI();
}
private boolean checkPermissions() {
int permissionState = ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION);
return permissionState == PackageManager.PERMISSION_GRANTED;
}
@Override
protected void onPause() {
super.onPause();
if (mRequestingLocationUpdates) {
stopLocationUpdates();
}
}
}
4단계: 매니페스트 권한 추가 (AndroidManifest.xml)
정밀 위치 권한(ACCESS_FINE_LOCATION)을 선언합니다. Android 10(API 29) 이상에서는 백그라운드 위치 권한(ACCESS_BACKGROUND_LOCATION)이 별도로 필요할 수 있으니 참고하세요.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="https://schemas.android.com/apk/res/android"
package="app.com.sample">
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
실행 및 테스트
실제 Android 기기를 USB로 연결한 뒤 Android Studio 툴바의 Run 아이콘을 눌러 앱을 설치·실행합니다. 기기 화면에 버튼들이 나타나며, "위치 업데이트 시작" 버튼을 누르면 권한 요청 다이얼로그가 뜨고 허용 시 위도/경도가 실시간으로 갱신됩니다.

핵심 포인트 요약
- FusedLocationProviderClient: 배터리 효율적인 위치 제공자 자동 선택
- LocationRequest: 업데이트 간격(
setInterval), 최대 빈도(setFastestInterval), 정확도(PRIORITY_HIGH_ACCURACY) 설정 - SettingsClient: 기기 위치 설정(GPS 켜기 등) 충족 여부 확인 및 해결 다이얼로그 표시
- Dexter 라이브러리: 런타임 권한 요청 로직 간소화
- Lifecycle 관리:
onResume/onPause에서 업데이트 시작/중지로 배터리 절약