줍깅 SMS 설계

SeonKyum·2021년 12월 22일
0

백엔드 개념

목록 보기
13/23

Sms 설계

로직 타겟 설정

  1. 모임에 참가한 사람들에게 모임 시작 하루전에 알림 문자 전송
  2. 회원가입시 휴대폰 번호 인증 (보류)
  3. 모임 참가/취소 버튼 클릭 시 알림 문자 전송 (보류)
  4. 모임 시작시 후기 작성 페이지 유도 알림 문자 전송
  5. 모임 시작시 모임장에게 출석체크 url 보내기

기본적인 문자메세지 틀 구현

  • 다운 받았던 coolSms SDK를 인텔리제이로 열어보면 여러가지 예시 코드들이 담겨있다.
  • 개인 메세지(2, 3, 5)와 단체 메세지(1, 4)를 혼용할 수 있는 예시를 주목하여 코드를 짰다.
  • 기본 sms 로직 service / SmsService
    @Service
    @RequiredArgsConstructor
    public class SmsService {
    
        public void sendSms() {
            JsonObject params = new JsonObject();
            JsonArray messages = new JsonArray();
    
            JsonObject msg = new JsonObject();
            JsonArray toList = new JsonArray();
    
            toList.add("01099403102");
            msg.add("to", toList);
            msg.addProperty("from", "01099403102");
            msg.addProperty("text",
                    "안녕하세요 줍깅입니다. 오늘 참여하실[같이 줍깅해요!]모임의 모임날짜가 3일 남으셨습니다.");
            messages.add(msg);
    
            params.add("messages", messages);
    
            Call<GroupModel> api = APIInit.getAPI().sendMessages(APIInit.getHeaders(), params);
            api.enqueue(new Callback<GroupModel>() {
                @Override
                public void onResponse(Call<GroupModel> call, Response<GroupModel> response) {
                    // 성공 시 200이 출력됩니다.
                    if (response.isSuccessful()) {
                        System.out.println("statusCode : " + response.code());
                        GroupModel body = response.body();
                        System.out.println("groupId : " + body.getGroupId());
                        System.out.println("status: " + body.getStatus());
                        System.out.println("count: " + body.getCount().toString());
                    } else {
                        try {
                            System.out.println(response.errorBody().string());
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }
    
                @Override
                public void onFailure(Call<GroupModel> call, Throwable throwable) {
                    throwable.printStackTrace();
                }
            });
        }
    
    }
    util / coolsms / GroupModel
    @Getter
    public class GroupModel {
        private ArrayList<JsonObject> log;
        private JsonObject agent;
        private JsonObject count;
        private String accountId;
        private String apiVersion;
        private String groupId;
        private String dateCreated;
        private String dateUpdated;
        private String _id;
        private String status;
    }
    util / coolsms / CoolsmsMsgV4 (Coolsms서버와의 통신)
    public interface CoolsmsMsgV4 {
    
        // 심플 메시지
        @POST("messages/v4/send")
        Call<GroupModel> sendMessage(@Header("Authorization") String auth,
                                       @Body Message message);
    
        // 여러건 발송
        @POST("messages/v4/send-many")
        Call<GroupModel> sendMessages(@Header("Authorization") String auth,
                                      @Body JsonObject messages);
    util / coolsms / config.ini (api key, api secret 이 담겨져있는 파일)
    [AUTH]
    api_key = {api_key 넣기}
    api_secret = {api_secret 넣기} 
    [SERVER]
    domain = api.coolsms.co.kr
    protocol = https
    prefix =
    util / coolsms / APIInit (헤더를 만들어서 요청하는 로직, 서버에로 보내는 로직 )
    public class APIInit {
    
        private static Retrofit retrofit;
        private static CoolsmsMsgV4 messageService;
    
        public static String getHeaders() {
            try {
    						//실험결과 절대경로만되고 상대경로는 되지않음
                Ini ini = new Ini(new File("C:\\Users\\user\\Desktop\\gits\\joopging\\src\\main\\java\\com\\project\\joopging\\util\\coolsms\\config.ini"));
                String apiKey = ini.get("AUTH","api_key");
                String apiSecret =ini.get("AUTH","api_secret");
                String salt = UUID.randomUUID().toString().replaceAll("-","");
                String date = ZonedDateTime.now(ZoneId.of("Asia/Seoul")).toString().split("\\[")[0];
    
                Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
                SecretKeySpec secret_key = new SecretKeySpec(apiSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
                sha256_HMAC.init(secret_key);
                String signature = new String(Hex.encodeHex(sha256_HMAC.doFinal((date + salt).getBytes(StandardCharsets.UTF_8))));
                return "HMAC-SHA256 ApiKey=" + apiKey + ", Date=" + date + ", salt=" + salt + ", signature=" + signature;
            } catch (InvalidKeyException | NoSuchAlgorithmException | IOException e) {
                e.printStackTrace();
            }
            return null;
        }
    
        public static CoolsmsMsgV4 getAPI() {
            if (messageService == null) {
                setRetrofit();
                messageService = retrofit.create(CoolsmsMsgV4.class);
            }
            return messageService;
        }
    
        public static void setRetrofit() {
            HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
    //        Request 시 로그가 필요하면 추가하세요.
    //        interceptor.setLevel(HttpLoggingInterceptor.Level.HEADERS);
    //        interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
    
            String domain = "api.coolsms.co.kr";
            String protocol = "https";
            String prefix = "/";
    
            try {
    						//실험결과 절대경로만되고 상대경로는 되지않음
                Ini ini = new Ini(new File("C:\\Users\\user\\Desktop\\gits\\joopging\\src\\main\\java\\com\\project\\joopging\\util\\coolsms\\config.ini"));
                if (ini.get("SERVER", "domain") != null) domain = ini.get("SERVER", "domain");
                if (ini.get("SERVER", "protocol") != null) protocol = ini.get("SERVER", "protocol");
                if (ini.get("SERVER", "prefix") != null) prefix = ini.get("SERVER", "prefix");
            } catch (IOException e) {
                e.printStackTrace();
            }
    
            OkHttpClient client = new OkHttpClient.Builder()
                    .addInterceptor(interceptor)
                    .build();
            retrofit = new Retrofit.Builder()
                    .baseUrl(protocol + "://" + domain + prefix)
                    .addConverterFactory(ScalarsConverterFactory.create())
                    .addConverterFactory(GsonConverterFactory.create())
                    .client(client)
                    .build();
        }
    
    }

1. 모임에 참가한 사람들에게 모임 시작 하루전에 알림 문자 전송

schedule / SmsSchedule 추가로직

//스케쥴러 매일 9시
    //러닝데이트 1일 전에 알럿문자메세지
    @Scheduled(cron = "0 0 9 * * *")
    @Transactional(readOnly = true)
    public void sendRunningDateAlertToCrew() {
        List<Post> postList = postRepository.findAll();
        JsonArray toList = new JsonArray();
        String nowPlusOneDay = getLocalDateTimeNowToStringDay(LocalDateTime.now().plusDays(1));
        for (Post post : postList) {
            String runningDate = getRunningDateToStringDay(post);
          if (nowPlusOneDay.equals(runningDate)) {
              List<Crew> crewList = post.getCrew();
              String postTitle = post.getTitle();
              String message = "안녕하세요 줍깅입니다. " +
                      "신청하신"+ " [" + postTitle + "] " +"모임의 모임날짜가 하루전입니다.";
              for (Crew crew : crewList) {
                  User user = crew.getUserJoin();
                  Long userId = user.getId();
                  User myUser = userRepository.findById(userId).orElseThrow(
                          NullPointerException::new
                  );
                  String userNumber = myUser.getNumber();
                  toList.add(userNumber);
              }
              sendSms(toList, message);
          }
        }
    }

2. 회원가입시 휴대폰 번호 인증

//휴대폰 번호 인증
		@Transactional(readOnly = true)
    public CertificateNumberResponseDto certificatePhoneNumber(PhoneNumberRequestDto requestDto) {
        JsonArray toList = new JsonArray();
        String phoneNumber = requestDto.getPhoneNumber();
        toList.add(phoneNumber);

        Random random = new Random();
        String numStr = "";
        for(int i = 0; i < 4; i++) {
            String ran = Integer.toString(random.nextInt(10));
            numStr+=ran;
        }
        log.info("수신자 번호 :" + phoneNumber);
        log.info("인증번호 :" + numStr);
        String message = "안녕하세요 줍깅입니다. 인증번호는 [ " + numStr + " ] 입니다";
        sendSms(toList, message);
        CertificateNumberResponseDto responseDto = new CertificateNumberResponseDto();
        responseDto.setCertificationNumber(numStr);
        return responseDto;

    }

3. 모임 참가/취소 버튼 클릭 시 알림 문자 전송 (보류)

4. 모임 시작시 후기 작성 페이지 유도 알림 문자 전송

schedule / SmsSchedule 추가로직

//스케쥴러 5분마다 체크
    //Crew 전체에게 후기 쓰고 설문조사 유도 알럿문자메세지
    @Scheduled(cron = "0 0/5 * * * *")
    @Transactional(readOnly = true)
    public void sendInduceReviewAlertToCrew() {
        System.out.println("스케쥴러 시작");
        List<Post> postList = postRepository.findAll();
        JsonArray toList = new JsonArray();
        String now = getLocalDateTimeNowToStringMinute(LocalDateTime.now());
        for (Post post : postList) {
            String runningDate = getRunningDateToStringMinute(post);
            if (now.equals(runningDate)) {
                List<Crew> crewList = post.getCrew();
                String postTitle = post.getTitle();
                String message = "안녕하세요 줍깅입니다. 이번" +" ["+ postTitle + "] " + "모임은 어떠셨나요?" +
                        "후기를 작성하여 다른 사용자에게 플로깅이 얼마나 좋은지 알려주세요!" +
                        "[이벤트] 이벤트 기간 중 설문조사를 작성하시면 소정의 기프티콘을 드려요! " +
                        "https://forms.gle/X3nQmmbHiRwmmWtZ8";
                for (Crew crew : crewList) {
                    User user = crew.getUserJoin();
                    Long userId = user.getId();
                    User myUser = userRepository.findById(userId).orElseThrow(
                            NullPointerException::new
                    );
                    String userNumber = myUser.getNumber();
                    toList.add(userNumber);
                }
                sendSms(toList,message);
            }
        }
    }

5. 모임 시작시 모임장에게 출석체크 url 보내기

schedule / SmsSchedule 추가로직

//스케쥴러 5분마다 체크
    //CrewHead 에게 출석체크 url 알럿문자메세지
    @Scheduled(cron ="0 0/5 * * * *")
    @Transactional(readOnly = true)
    public void sendAttendanceCheckAlertToCrewHead() {
        System.out.println("스케쥴러 시작");
        List<Post> postList = postRepository.findAll();
        JsonArray toList = new JsonArray();
        String now = getLocalDateTimeNowToStringMinute(LocalDateTime.now());
        for (Post post : postList) {
            String runningDate = getRunningDateToStringMinute(post);
            if (now.equals(runningDate)) {
                User user = post.getWriter();
                Long userId = user.getId();
                User myUser = userRepository.findById(userId).orElseThrow(
                        NullPointerException::new
                );
                String number = myUser.getNumber();
                String postTitle = post.getTitle();
                Long postId = post.getId();
                String message = "안녕하세요 줍깅입니다." +" ["+ postTitle +"] "+ "모임의 모임원들은" +
                        "다 모이셨나요? 출석체크를 해주세요!" +
                        "출석체크는 앞으로 유저간의 신뢰도를 측정하는데 도움이 됩니다!" +
                        "http://joopgging.link/meetingcheck/" + postId;
                toList.add(number);
                sendSms(toList,message);
            }

        }
    }

뱃지

처음 구현하려한 의도는 노쇼나 출석에 관한 각종 사고를 방지함에 있어 표현이 너무 직설적이지 않은 뱃지를 부여함으로 출석률을 높이려는 의도였다.

스케쥴러를 돌리는 주기는 유저 입장으로써 뱃지의 기준을 도달했을 즉시 알림으로 알려주고 주는것이 가장 좋겠지만 러닝커브를 생각해 fixedDelay = 5000 (로직이 돌고난 5초후) 간격으로

빠르게 업데이트를 해주었다.

  • 출석률 계산

기본으로 출석체크를 누르면 false값이 유저에게 저장됨으로 단순하게 출석률을 계산할 경우 유저는 처음 모임참가를 눌렀을때 부터 지각생이라는 출석률이 저조하다는의미에 뱃지를 부여받게 된다.

//출석률 계산
//출석체크 default 값이 false 라서 2번째 모임신청을 누르는 순간 출석률이 50%가 되서 바로 지각생 뱃지를 받게됨으로
 //출석 5번이상 경우의 수 (ex. 출석 5 + default 1 = 83.3% (안받음), 출석5 결석1 + default 1 = 71.4% (최저 안받음))
//결석을 2회이상 경우의 수(ex. 출석1 결석1 + default 1 = 33% (받야야함) , 출석2 + default 1 = 66%(안받음))
 if (countAttendanceTrue > 5 || countAttendanceFalse > 2) {
			     Double attendanceRate =
           (double) (countAttendanceTrue / (countAttendanceTrue + countAttendanceFalse) * 100);
			     System.out.println(attendanceRate);
        //출석률 70프로 이하일때 지각생 뱃지
           Badge badAttendanceRateBadge = Badge.of(5, 5, user);
              if (attendanceRate <= 70) {
                    if (categoryList.contains(badAttendanceRateBadge.getCategory())) {

                   } else {
                            badgeList.add(badAttendanceRateBadge);
			                      badgeRepository.save(badAttendanceRateBadge);
	                 }

               } else {
                   Badge DubBadge = badgeRepository.findByUserBadgeAndCategoryAndLevel(user,5,5);
                   badgeList.remove(DubBadge);
                   badgeRepository.delete(DubBadge);
                }

           } else {
						
   }						

스케쥴러 어려웠던 점

스케줄러를 사용하면서 어려웠던 부분들

  • 오픈 api (sms sdk)를 가져와서 우리의 시스템에 맞게 수정, 변경해서 서비스에 녹여내는 작업
    • api 에서 제공하는 여러가지 방식중에 우리에게 맞는 기능을 먼저 선별한 후 테스트코드를 작성하는 것으로 해결
  • 얼마의 주기로 스케줄러를 돌리는것이 우리 서비스에 가장 적절한것인가에 대한 고민
    1. sms
    • sms같은 경우는 유저 flow의 경우의 수와 지금 현재 팀의 운용자금을 생각해 주기를 선택한다
    • sms 스케줄러의 주기는 유저가 특정 행동을 취할때(핸드폰 인증), 모임의 스케줄에 대한 알림(하루전, 모임의 시작시(출석체크, 후기작성) )
    1. 뱃지
    • 오프라인 만남에서 노쇼등에 대처하기 위해 유저에게 거부감이 들지 않게 뱃지로 표현하여 그사람의 출석률을 자연스럽게 알 수 있도록 최신화가 빨리 되야된다고 결정
    • 유저의 특정 액션에 반응하는 기능을 만들기에는 프로젝트 기간이 짧다고 판단되어 서버에 무리가 가지 않는선에서 5초에 한번씩 모든 유저를 체크하여 뱃지를 부여하는 방식으로 결정

요약 : 스케줄러를 적용하면서 어려웠던점은 저희 프로젝트의 특성에 맞게 REST API를 수정하는 것과 주기 설정이 였습니다. REST API는 기능선별을 한 후 테스트코드를 작성하여 하나하나 오류를 지워나가는 방법으로 해결을 하였습니다. 주기에 대한 설정은 sms의 경우 유저 flow 의 경우의 수, 운용 자금을 생각해 결정하였고 뱃지는 도입의도에 부합하게 실시간으로 업데이트 되야한다고 판단하여 스케줄러가 두번돌아가는 것을 방지하기 위해 이전 기능이 실행된 후 1초 주기로 모든 유저를 체크하여 뱃지를 부여하는 방식으로 결정하였습니다. (쓰레드 풀이 겹치지 않게)

profile
차근차근,,

0개의 댓글