[AndroidStudio,FireBase] KnockKnock 개발일지 - 0425 (비동기식 쿼리 Callback으로 동기식 처리하기)

Hyebin Lee·2022년 4월 25일
0

knockknock 개발일지

목록 보기
29/29
post-thumbnail

참고한 링크

https://stackoverflow.com/questions/48995536/how-to-continue-firebase-task-on-the-same-thread
https://medium.com/firebase-developers/why-are-firebase-apis-asynchronous-callbacks-promises-tasks-e037a6654a93
https://stackoverflow.com/questions/50434836/getcontactsfromfirebase-method-return-an-empty-list/50435519#50435519
[Android Studio] 파이어베이스 데이터 로드 대기 (Wait to load data from firebase in android)

문제상황

원래 구현하려는 방식은 유저가 메세지를 작성하고 전송버튼을 누를 때
onClick에서 인근 유저 리스트를 조회하는 query 메서드가 실행이 되고
조회된 유저의 명 수를 알린 뒤 전송을 할 건지 물어보는 기능을 구현하려고 했다.
그런데 query 메서드는 비동기식으로 처리되어서 query 조회가 실행되는 도중에 아직 완료되지 않았는데 onClick 에서 전송할건지 물어보는 기능이 먼저 실행되어 버리는 것이다.

인근 유저 리스트를 조회하는 query를 작성한 코드이다.

 public static List<String> findUserIdsNear(double targetlatitude, double targetlongitude){
        final GeoLocation target = new GeoLocation(targetlatitude,targetlongitude);
        final double radiusInM = 1*1000; // 1km 로 범위 설정
        userId.clear();
        List<GeoQueryBounds> bounds = GeoFireUtils.getGeoHashQueryBounds(target,radiusInM);
        List<Task<QuerySnapshot>> tasks = new ArrayList<>();
        for(GeoQueryBounds b: bounds){
            Query q = db.collection("userlocation")
                    .orderBy("geohash")
                    .startAt(b.startHash)
                    .endAt(b.endHash);
            tasks.add(q.get());
        }
        Tasks.whenAllComplete(tasks)
                .addOnCompleteListener(new OnCompleteListener<List<Task<?>>>() {
                    @Override
                    public void onComplete(@NonNull Task<List<Task<?>>> t) {
                        List<DocumentSnapshot> matchingDocs = new ArrayList<>();
                        for(Task<QuerySnapshot> task: tasks){
                            QuerySnapshot snap = task.getResult();
                            for(DocumentSnapshot doc: snap.getDocuments()){
                                double lat = doc.getDouble("lat");
                                double lng = doc.getDouble("lng");

                                GeoLocation docLocation = new GeoLocation(lat,lng);
                                double distanceInM = GeoFireUtils.getDistanceBetween(docLocation,target);
                                if(distanceInM<=radiusInM){
                                    matchingDocs.add(doc);
                                    userId.add(doc.getId());
                                }
                            }
                        }

                    }
                });
        for(String id: userId){
            Log.d("GeoQuery","target 위치 인근 유저 아이디: "+id);
        }
        return userId;
    }

onClick 메서드이다.

  sendMessage.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                String message = messagecontent.getText().toString();
                if(message.length()==0) Toast.makeText(context,"전송할 메세지가 없습니다.", Toast.LENGTH_LONG);
                else{targetusers = GeoQuery.findUserIdsNear(targetlatitude,targetlongitude);
                targetusers.remove(id);
                if(targetusers.size() == 0){
                    Toast.makeText(context,"메세지를 받을 목표 지점 인근 유저가 없습니다.", Toast.LENGTH_LONG);
                }
                else{
                    sendDialog();
                }}
                        }



        });

Callback 해결방안

이러한 문제는 Callback 방식으로 해결할 수 있다.
Call back 방식의 문제 해결은 Callback interface 선언 및 구현을 통해 task.whenAllComplete.onComplete에서 모든 task 처리가 마무리 되었을 때만 실행할 코드를 선언함으로써 기존의 비동기식으로 쿼리 처리가 제대로 이루어지지 않은 시점에서 실제로는 쿼리 처리가 마무리 되어야만 실행되는 코드들이 먼저 실행되는 것을 방지한다.

Callback 방안은 비동기식 쿼리 조회 방법을 동기식으로 바꾼 것은 엄밀히 따지자면 아니다.
그저 비동기식으로 쿼리 조회를 하되, 쿼리 조회가 끝난 이후의 구현만 선언한 것이다.
만약 onSuccess 외부에 코드가 구현되어 있다면 이는 비동기식 쿼리 조회의 영향을 받는 부분이기 때문에 쿼리조회 결과가 모두 load되기 전에 해당 코드가 먼저 실행될 것이다.

1. Callback Interface 구현하기

따로 interface 클래스를 만들어도 되고, 나는 그냥 기존에 있는 GeoQuery 클래스에 덧붙였다.

 public interface FindUserCallback {
        void onSuccess(List<String> userIds);
        void onFail();
    }

2. 동기식 로드할 메서드 매개변수에 callback interface 넣기

  • 동기식 로드로 구현할 메서드 매개변수에 아까 선언한 callback interface를 넣어주고
  • 메서드에서 task 모두 완료시 callback에서 성공을 부를 부분에서 onSuccess를 부른다.
  • 이 때 onSuccess의 매개변수로 메인 쓰레드에서 사용할 값을 준다 (그래야 main thread에서 해당 인터페이스를 구현하면서 해당 값을 꺼내 쓴다)
 public static void findUserIdsNear( double targetlatitude, double targetlongitude, FindUserCallback callback) throws ExecutionException, InterruptedException {
        final GeoLocation target = new GeoLocation(targetlatitude,targetlongitude);
        final double radiusInM = 1*1000; // 1km 로 범위 설정
        userId.clear();
        List<GeoQueryBounds> bounds = GeoFireUtils.getGeoHashQueryBounds(target,radiusInM);
        List<Task<QuerySnapshot>> tasks = new ArrayList<>();


            for (GeoQueryBounds b : bounds) {
                Query q = db.collection("userlocation")
                        .orderBy("geohash")
                        .startAt(b.startHash)
                        .endAt(b.endHash);
                tasks.add(q.get());
            }

        Tasks.whenAllComplete(tasks)
                .addOnCompleteListener(new OnCompleteListener<List<Task<?>>>() {
                    @Override
                    public void onComplete(@NonNull Task<List<Task<?>>> t) {
                        List<DocumentSnapshot> matchingDocs = new ArrayList<>();
                        for(Task<QuerySnapshot> task: tasks){
                            QuerySnapshot snap = task.getResult();
                            for(DocumentSnapshot doc: snap.getDocuments()){
                                double lat = doc.getDouble("lat");
                                double lng = doc.getDouble("lng");

                                GeoLocation docLocation = new GeoLocation(lat,lng);
                                double distanceInM = GeoFireUtils.getDistanceBetween(docLocation,target);
                                if(distanceInM<=radiusInM){
                                    matchingDocs.add(doc);
                                    userId.add(doc.getId());
                                }
                            }
                        }
                        for(String id: userId){
                            Log.d("GeoQuery","target 위치 인근 유저 아이디: "+id);
                        }

                        callback.onSuccess(userId);
                    }
                });
    }

3. 메인 쓰레드에서 Callback interface 매개변수 구현하기

  • onSuccess 오버라이드 하는 부분에서 동기식으로 데이터를 갖고 왔을 때 실행할 내용을 구현해주면 된다.
sendMessage.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                String message = messagecontent.getText().toString();
                if (message.length() == 0)
                    Toast.makeText(context, "전송할 메세지가 없습니다.", Toast.LENGTH_LONG).show();
                else {
                    try {
                        GeoQuery.findUserIdsNear(targetlatitude, targetlongitude, new GeoQuery.FindUserCallback() {
                            @Override
                            public void onSuccess(List<String> userIds) {
                                userIds.remove(id);

                                if (userIds.size() == 0) {
                                    Toast.makeText(context, "메세지를 받을 목표 지점 인근 유저가 없습니다.", Toast.LENGTH_LONG).show();
                                } else {
                                    sendDialog(userIds);
                                }
                            }

                            @Override
                            public void onFail() {

                            }
                        });
                    } catch (ExecutionException e) {
                        e.printStackTrace();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

0개의 댓글