Playground (우리동네, 아이랑 놀곳 Ver.서울)는 

서울 시내에서 아이랑 갈만한 실내외 놀이터, 박물관/미술관, 도서관등의 장소(약 3,700군데)들을 모아서, 

일목요연하게 보여 주는 웹 사이트로 

주말 같은 여유 시간에 아이랑 어디를 가야 할지 고민하는 부모들을 위해 제작한 웹 사이트이고, 

Playground 안드로이드(Android) 앱은 이 사이트의 앱 버전으로 

아직 개발 중이지만, 

다음과 같이 2 가지 핵심기능을 동일하게 제공하고 있다.

  • 우리 동네: 지도상에 보고싶은 지역의 좌표(위도, 경도)를 지정하면, 좌표를 중심으로 2Km 내의 시설/장소 정보 표시
  • 추천 코스: 특정 장소를 중심으로 갈만한 장소들을 묶어서 하나의 코스로 정리

반응형으로 제작된 웹 사이트와 앱의 핵심 기능은 동일하지만

지정된 위치의 시설들을 조회하는 [우리 동네]의 경우,

웹 사이트에는 SSL 문제 때문에 위치를 검색해서 지정하는 방식으로 구현하였다.

안드로이드 앱 버전은 GPS를 이용해서, 현재 위치를 지정하는 방식으로 구현하였다.

Playground 에 대한 웹 사이트와 기능에 대한 상세한 설명은 여기서 확인할 수 있고,

안드로이드 앱 버전의 프로그램 소스는 Github에서,

안드로이드 앱은 구글 play에서 다운 받아서 설치 할 수 있다.

 

주요 기능에 대한 사용법을 정리하면,

메뉴나 메인 화면에서 [우리동네] 를 클릭하면

다음과 같이 현재 위치나 원하는 위치를 지정하는 화면이 나타난다.

GPS로 현재 위치를 지정하거나 지도를 클릭해서 위치를 지정하고,

화면 우측 하단에 있는 조회(돋보기) 버튼을 클릭하면

지정된 위치를 중심으로 2Km내의 실내외 놀이터, 박물관등이

그림과 같이 색깔별로 표시 된다.

표시된 점(위치)를 선택하면,

해당 장소에 대한 간단한 정보가 하단에 표시된다.

 

[추천코스]도 메뉴나 메인 화면에서 클릭해서 실행할 수 있다.

서울 시내에 갈만한 곳을 묶어서 보여 주는 기능으로

현재는 관리자(개발자)가 개인 경험을 기반으로 작성한 내용들이 나타난다.

사용자가 등록하는 기능은 추가할 예정이다.

 

리스트에서 항목을 선택하면

코스에 해당하는 코스, 소개글, 지도 등의 상세한 내용을 확인할 수 있다.

안드로이드 앱은 웹 사이트에 비해

아직 세부 기능이 부족하지만, 계속 추가 개발되고 있다.

 

많은 Android앱들이 죽지 않는 서비스를(Immortal Service) 이용하는데,

개발 중인 제품에 필요하여 인터넷을 검색해 사용해 보니 제대로 작동하지 않거나 관련 자료가 부족해서 직접 개발하였다 (해야 했다...).

국내외국 자료를 검색해서 찾은 결과들을 토대로 구현하였고,

소스는 Github에서 받을 수 있다.

(따라서, 충분하게 검증된 기술이 아니기 때문에 문제가 발생할 수 있으며, 관련 문제를 공유하거나 더 나은 방법을 공유해줬으면 하는 기대로 이 글을 작성합니다.)


Android의 죽지 않는 서비스는 일반적인 서비스를 startForeground로 실행하면 간단하게 구현 할 수 있다.

이 방법의 문제는 Notification을 이용하여 알림창에 표시해야만 한다는 것이다.

알림창을 제거하기 위해 다양한 방법들이 논의 되었지만 [참고],

Android Oreo 버전에서는 작동되지 않는다.


이 내용들을 토대로 운영체제가 서비스를 죽이면 다시 실행하는 방법으로 죽지 않는 서비스를 구현하였다.

다시 실행시키는 방법은 알람(Alarm)을 이용한다.

서비스가 종료되면(onDestroy) 1초 뒤에 알람이 실행되게 하고,

알람에서 해당 서비스를 다시 실행한다.

이때, Android Oreo에서는 서비스의 백그라운드 실행을 금지하기 때문에 문제가 발생한다.

따라서, Oreo 이전과 이후 버전으로 나누어서 구현한다.


Oreo 이전 버전은 startService로 해당 서비스를 실행하고,

Oreo 이후 버전은 한 단계를 더 거쳐서 구현한다.

Oreo에서는 서비스를 백그라운드(Background)에서 실행하는 것을 금지하기 때문에

포그라운드(Foreground)에서 실행해야 한다.

즉, 알람은 백그라운드이기 때문에 서비스를 실행할 수 없다.

따라서 알람에서 포그라운드인 startForegroundService(Notification)으로 서비스를 실행하고, 이 서비스 안에서 해당 서비스를 실행한다.

그리고, startForegroundService으로 실행한 서비스는 죽이는 방식으로 구현한다.

그림으로 정리하면 다음과 같다.


기본 개념을 코드로 구현하면,

먼저, 앱을 실행하고 종료(onDestroy)할 때 서비스(RealService)를 종료(stopService)한다.

public class MainActivity extends AppCompatActivity {
private Intent serviceIntent;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

if (RealService.serviceIntent==null) {
serviceIntent = new Intent(this, RealService.class);
startService(serviceIntent);
} else {
serviceIntent = RealService.serviceIntent;//getInstance().getApplication();
Toast.makeText(getApplicationContext(), "already", Toast.LENGTH_LONG).show();
}
}

@Override
protected void onDestroy() {
super.onDestroy();
if (serviceIntent!=null) {
stopService(serviceIntent);
serviceIntent = null;
}
}

MainActivity.java


무엇인가 백그라운드에서 서비스를 제공할 서비스 클래스는 RealService.java이다.

메신저 앱의 경우 메시지를 수신하는 등의 처리를 구현하는데, 여기서는 1분에 한번씩 시간을 알려주도록 (Notification) 했다.


가장 처음에는 앱(MainActivity)이 실행되기(Foreground) 때문에 RealService는 startService로 실행하면 된다.

다만, 앱이 다시 실행되었을때 충돌을 방지하기 위해 RealService에 serviceIntent를 static 변수로 두어 서비스 실행시 intent를 가지고 있도록 한다.

serviceIntent의 값이 있으면 이미 서비스가 실행 중이니 넘어가고 없을 때만 서비스를 실행한다.


앱을 종료(onDestroy)할 때 서비스(RealService)를 종료(stopService)하여

서비스의 종료(onDestroy) 이벤트가 실행되게 작성한다.

public void onDestroy() {
super.onDestroy();

serviceIntent = null;
setAlarmTimer();
Thread.currentThread().interrupt();

if (mainThread != null) {
mainThread.interrupt();
mainThread = null;
}
}

protected void setAlarmTimer() {
final Calendar c = Calendar.getInstance();
c.setTimeInMillis(System.currentTimeMillis());
c.add(Calendar.SECOND, 1);
Intent intent = new Intent(this, AlarmRecever.class);
PendingIntent sender = PendingIntent.getBroadcast(this, 0,intent,0);

AlarmManager mAlarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
mAlarmManager.set(AlarmManager.RTC_WAKEUP, c.getTimeInMillis(), sender);
}

RealService.java

RealService 서비스가 종료(onDestroy)될때 마다 1초뒤 알람이 실행되게 작성한다(setAlarmTimer).

public class AlarmRecever extends BroadcastReceiver{

@Override
public void onReceive(Context context, Intent intent) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Intent in = new Intent(context, RestartService.class);
context.startForegroundService(in);
} else {
Intent in = new Intent(context, RealService.class);
context.startService(in);
}
}

}

AlarmRecever.java

알람(AlarmRecever)에서는

Android가 Oreo 이후 버전이면 (SDK_INT >= O)

Android에서 제공하는 죽지않는(Foreground) 서비스인 RestartService를 startForegroundService로 실행한다.

이전 버전은 RealService 서비스를 실행하면 끝이다.


Oreo 이후 버전은 startForegroundService로 실행한 RestartService의 코드는 다음과 같다.

public class RestartService extends Service {
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, "default");
builder.setSmallIcon(R.mipmap.ic_launcher);
~~ 생략 ~~

Notification notification = builder.build();
startForeground(9, notification);

/////////////////////////////////////////////////////////////////////
Intent in = new Intent(this, RealService.class);
startService(in);

stopForeground(true);
stopSelf();

return START_NOT_STICKY;
}

RestartService.java

RestartService 의 코드는 Android에서 제공하는 죽지않는(Foreground) 서비스의 코드이다.

포그라운드(Foreground)로 실행되지만, Android 알림창에 표시되는 문제가 있다.

따라서, RestartService를 startForeground로 실행하고,

RealService를 실행한다 (startService).

그리고 실행된 RestartService를 stopForeground와 stopSelf를 실행하여 RestartService를 종료한다.

RestartService가 실행되고 종료되는 시간이 짧기 때문에 알림창에는 표시가 생기지 않는다.


이외에 부팅후 실행을 위한 코드(RebootRecever)도 있지만 알람과 동일한 코드라 생략한다.


이상의 죽지 않는 서비스를 구현하기 위해 필요한 2개의 서비스와 2개의 Recever를

AndroidManifest.xml 파일에 다음과 같이 등록해야 한다.

<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

<application
~~ 생략 ~~
<activity android:name="com.damonet.service9.MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

<service android:name="com.damonet.service9.RealService"
~~ 생략 ~~
<service android:name="com.damonet.service9.RestartService"
~~ 생략 ~~
<receiver android:name="com.damonet.service9.AlarmRecever"/>

<receiver
android:name=".RebootRecever"
~~ 생략 ~~
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
</application>

AndroidManifest.xml

추가적으로 부팅이 완료되면 서비스를 시작하기 위하여 부팅이 완료되었다는 것을 수신할 권한(permission)을 등록한다(RECEIVE_BOOT_COMPLETED).

또, Oreo API 28 부터는 FOREGROUND_SERVICE권한을 등록해야 포그라운드(Foreground) 서비스를 실행할 수 있다.


테스트 결과

Oreo 이전 버전에서는 큰 이상을 찾을 수 없었고,

Oreo에서는 2~5분 마다 서비스가 다시 실행되는 것을 확인 할 수 있었다.


---------------------------- 2019년 8월 19일 추가 ----------------------------

2~5분 마다 서비스가 다시 실행되는 문제는 절전 모드(doze) 문제로,

REQUEST_IGNORE_BATTERY_OPTIMIZATION를 이용하여 절전 모드를 사용하지 않는 예외 앱으로 처리하면 된다.

상세한 내용은 다른 자료를 참고 하고,

이 기능은 채팅 또는 통화 앱 등에만 허용하는 것으로, 메신저가 아니라면 웹 스토어 등록이 거절 될 수 있다 (참고).


먼저, AndroidManifest.xml에 다음 권한을 추가한다.

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

그리고, MainActivity.java 파일에 절전 모드를 해제하는 권한을 얻는 코드를 추가해 준다.

protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

PowerManager pm = (PowerManager) getApplicationContext().getSystemService(POWER_SERVICE);
boolean isWhiteListing = false;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
isWhiteListing = pm.isIgnoringBatteryOptimizations(getApplicationContext().getPackageName());
}
if (!isWhiteListing) {
Intent intent = new Intent();
intent.setAction(android.provider.Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
intent.setData(Uri.parse("package:" + getApplicationContext().getPackageName()));
startActivity(intent);
}

MainActivity.java

추가된 코드는 기존의 github에 추가되어 있고,

계속 테스트 중이지만 현재까지 성능 문제 없이 잘 사용하고 있다.




그림과 같이 입력한 값을 보여주는 입력상자(EditText) 하나와 15개의 버튼으로 구성된 간단한 계산기를 만들면서 안드로이드 프로그래밍 기초를 정리한다.

정리 방법은 다음과 같이 단계별로 예제를 구현하고, 예제 소스는 GitHub에서 받을 수 있다.

  1. 계산기 디자인과 기본 코드 (branch: master)
  2. 생성한 클래스를 이벤트 리스너로 사용 (branch: step1)
  3. 코드 줄이기  (branch: step2)
  4. 실제 계산 기능 구현 (branch: step3)
  5. 후위 표기법으로 계산 기능 구현 (branch: step4)
  6. 동적 생성 (branch: step5)

각각의 단계별 예제는 GitHub의 branch에서 받을 수 있다.


먼저, 계산기를 그림과 같이 디자인 하면서

레이아웃(Layout) 사용법과 디자인된 개체에 이벤트를 연결하는 방법을 익힌다.


안드로이드 스튜디오에서 새로운 프로젝트를 생성하고,

디자인을 위해 activity_main.xml (app/src/main/res/layout/) 파일을 열어서 다음과 같이 코드를 작성한다.


전체코드

전체 레이아웃을 LinearLayout으로 지정하여 생성하고 [라인 1],

전체 레이아웃의 자식으로 EditText(입력상자) 하나 [라인 10]와 4개의 LinearLayout를 생성한다 [라인 18~].

하위에 있는 4개의 LinearLayout은 각각 4개(마지막은 3개)의 버튼을 자식으로 가진다 [라인 23~].

자식으로 가진다는 의미는 XML에서 <LinearLayout>와 </LinearLayout>사이에 코딩 한다는 의미이다.


간단하게 LinearLayout, EditText, Button의 주요 속성을 정리한다.

LinearLayout의 layout_width와 layout_height는 위젯(widget)의 크기를 의미한다.

너비(width)와 높이(height)를 지정하는 것으로 match_parent나 wrap_content로 지정하거나 직접 값을 dp단위로 입력할 수 있다.

match_parent는 부모 크기에 100%로 맞추는 것이고,

wrap_content는 그 위젯의 내용물(content)에 맞추어 자동으로 크기를 조절하는 것이다.

여기에서는 스마트폰의 해상도에 맞게 크기가 조절되도록 값을 직접 입력하지 않고,

match_parent나 wrap_content를 이용하였다.


최상위 LinearLayout은 너비와 높이를 모두 부모에 맞추었다 [라인 5, 6].

최상위 LinearLayout은 부모가 없기 때문에 휴대폰 크기에 맞추

orientation은 자식 위젯의 나열 방향을 의미하는 것으로

EditText와 4개의 LinearLayout는 수직(vertical)으로 쌓았고, 각 버튼들은 수평(horizontal)으로 쌓았다.

LinearLayout외에도 FrameLayout,TableLayout, GridLayout, RelativeLayout 등이 있다.


EditText는 사용자가 입력하는 값을 받는 위젯으로 너비와 높이는 레이아웃과 동일하게 사용한다 [라인 10].

너비는 화면 너비만큼[라인 12], 높이는 컨텐츠 높이로 지정하였다 [라인 13].

id는 Java에서 XML에 작성된 위젯을 찾기 위한 식별자를 의미한다 [라인 11].

실제 Id의 값은 @+id/위에 지정되고, 지정된 id는 안드로이드에서 관리된다.

gravity는 입력한 값에 대한 정렬을 의미하는 것으로 왼쪽(left), 오른쪽(right), 위쪽(top) 등으로 지정할 수 있다 [라인 16].


Button의 id, 너비, 높이는 EditText나 Layout과 동일한 개념이고,

버튼에 표시할 텍스트를 나타내는 text 속성과 가중치(layout_weight) 속성 등이 있다.

현재 Button이 4개 생성되었고, 버튼의 너비는 컨텐츠 길이만큼(wrap_content) 생성하도록 지정하였다.

이경우 화면이 Button의 너비 합보다 크기 때문에 우측에 여백이 생기게 된다.

이 여백을 각 Button의 가중치만큼 나누어 가지도록 하는 속성이 layout_weight이다 [라인 27,~].

모두 1씩 지정하였으니 같은 비율로 커지게 된다.


디자인을 마쳤으니, MainActivity.java (/src/main/java/com/gujc/calculatorsample/)을 열어서 다음과 같이 작성한다.


전체코드

먼저 사용할 위젯을 변수로 지정하여 Java에서 제어한다.

EditText는 TextView 클래스로 선언한 editText (인스턴스)변수로 제어한다 [라인2].

이 변수와 EditText 위젯을 연결하기 위해 findViewById 함수를 사용한다 [라인10].

위젯 생성시 지정한 id를 이용하여 해당 위젯을 찾아서 변수에 할당하고,

이 클래스 변수를 이용하여 값을 가지고 오거나 지정한다.


15개의 버튼 중 2개의 버튼만 코드로 작성했다 (나머지는 각자).

button0은 클래스내에서 전역 변수로 [라인 3],

button1은 onCreate 함수(이벤트)내에서 사용하는 지역 변수로 선언하였다 [라인20].

개념을 위해 이렇게 한 것으로 용도에 따라 전역으로 사용할 것인지, 지역으로 사용할 것인지 결정하면 된다.


setOnClickListener를 이용하여 각 버튼을 클릭했을 때 [라인 14, 22],

editText에 버튼의 값이 찍히도록 작성하였다.

클릭된 버튼의 값을 얻기(getText) 위해 클릭된 버튼을 알아야 한다.

여기서는 button0, button1을 각각 다르게 두가지 방식으로 작성했다.

button0은 클릭하면 button0의 값을 가지고 오도록 했고 [라인 16],

button1은 클릭 이벤트의 파라미터를 이용하였다 [라인 24, 25].

클릭 이벤트의 파라미터는 클릭된 개체(버튼)를 넘겨 주는데, Button 클래스가 아닌 클래스들의 부모(View)로 넘겨준다.

따라서 이것을 버튼으로 형변환 한 뒤에 사용하면 된다 [라인 24].

button0 코딩보다 button1 코딩이 더 깔끔하고 방어가 잘 된 코딩인데

그 이유는 나머지 13개의 버튼에 대하여 클릭 이벤트를 작성해 보면 알 수 있다.

버튼을 클릭하면 클릭된 버튼의 값만 출력된다.

앞서 클릭된 모든 버튼의 값이 이어서 출력되도록 작성해 보길 바란다.


모든 버튼에 클릭 이벤트를 작성하면,

두 번째 방식아 코드 양은 한 줄 더 많지만, 복사/붙여넣기 하고 수정하는 글자 수가 적다는 것을 알 수 있다.

이외에 더 많은 장점이 있다.

두 번째 예제로 이 장점을 정리하고, 이벤트 코드 양을 줄이는 방법을 정리한다.


전체 코드

앞서의 예제와 다르게 이상의 코드는 위젯을 찾고 이벤트 리스너를 등록하는 코드에서 button0, button1 등의 변수를 사용하지 않았다 [라인 2-16].

클래스 변수로 뭔가를 할 경우에는 사용하지만, 그렇지 않다면 생략하고 사용해도 된다.


첫 예제와 다른 것은 이벤트 클래스를 생성해서 사용하는 것이다.

첫 예제는 다음 코드처럼 OnClickListener 클래스를 생성(new)해서 변수에 담지 않고 버튼의 이벤트 리스너로 지정했다.

             button0.setOnClickListener(new View.OnClickListener() {

이번 예제는 생성해서 mClickListener 변수에 담아서 사용한다 [라인 19].

             Button.OnClickListener mClickListener = new View.OnClickListener() {

그리고 이 mClickListener변수를 모든 버튼에 지정했다.

즉, 15개의 버튼이 하나의 이벤트 리스너로 처리 되는 것이다.


OnClickListener 이벤트에서는 사용자가 클릭한 버튼을 파라미터(View)로 제공한다 [라인 20].

클릭한 버튼(view)의 고유번호(id)를 이용하여(getId()) [라인 21],

클릭한 버튼이 Button0이면 입력상자(editText)에 0을 넣고,

클릭한 버튼이 Button1이면 입력상자(editText)에 1… 등을 넣어 준다.

코드를 2개의 버튼에 대해서만 작성했지만, 15개 버튼에 대해서 모두 작성해야 한다.


이렇게 클릭된 버튼이 무엇인지 파악해서 필요한 행동을 하게 작성하는데 switch 문(다수의 if)이 사용되었다 [라인 22, 25].

이러한 코드는 일반적인 코드 작성법이지만 15개의 버튼에 대해서 작성한다면 아주 많은 코드를 작성해야 한다.

이 부분에 정리는 뒤에 하고 여기서는 넘어간다.


여기서 id (getId())는 고유한 숫자 값이다 [라인 21].

앞서 xml에서 지정한 id는 문자열이지만,

이 값을 안드로이드가 숫자로 변환해서 가지고 있고, 개발자가 지정한 문자는 일종의 상수처럼 선언되어 사용된다.

그래서 id를 사용할 때 “button0”처럼 사용하지 않고, R.id.button0으로 사용한다 [라인 22, 25].


이 예제 코드는

15개의 findViewById() 함수와

15개의 case 문 (실제로는 if문)이 사용되었다.

더욱이 이 예제는 버튼을 클릭하면 해당 값만 출력되지만 다음 코드와 같이 누적되게 작성하면 더 복잡하게 보인다.

           editText.setText(editText.getText().toString()  + "0");

첫 예제에서 클릭된 변수를 인식하는 2번째 방식을 이용하여 [라인 24],

복잡한 코드를 다음과 같이 줄여서 구현할 수 있다.

    Button.OnClickListener mClickListener = new View.OnClickListener() {
        public void onClick(View view) {
            Button button= (Button) view;
            editText.setText(editText.getText().toString() + button.getText().toString());
        }
    };


개념은 아주 간단하다.

클릭된 버튼이 무엇인지 알아야 했던 이유는

해당 버튼이 (view.getId())

무엇인지 (button0, button1, button2...) 알아서 해당 값(0, 1, 2 ...)을 지정하기 위한 것이었다.

1번 버튼이 클릭되면 값 1을 지정하기 위한 것이다.

이 1 값은 버튼의 텍스트(text)로 지정되어 있으니,

버튼이 클릭되면 해당 버튼의 값(getText())을 editText의 값에 넣어준다 (setText()).


세 번째 예제로, 15번이 사용된 findViewById() 함수를 줄여본다

두 번째 예제의 코드를 살펴보면,

버튼의 id가 button0, button1, button2, button? … 으로 숫자만 바뀌는 것을 알 수 있다.

따라서, 반복문을 이용하여 0부터 14까지 15번을 반복하고,

해당 id를 문자열로 생성해서 가지고 올 수 있으면, 다음과 같이 몇 줄로 구현할 수 있다.

    for(int i=0; i<15; i++) {
        String buttonID = "button" + i;
        int resID = getResources().getIdentifier(buttonID, "id", getPackageName());
        findViewById(resID).setOnClickListener(mClickListener);
    }

전체 코드

"button" 문자열과 변수 i의 값을 이용하여, 15 버튼의 id를 문자열로 생성한다 (buttonID).

앞서서 몇 번 정리했지만,

안드로이드는 xml에서 문자열로 지정한 id를 고유 번호를 부여해서 따로 관리하기 때문에 변수buttonID의 값으로 찾을 수 없다.

따라서 리소스 정보에 접근할 수 있는 getResources()를 이용하여, 지정된 문자열에 맞는 id 값을 받고 (resID),

이 id를 findViewById()함수로 찾아서 구현한다.


안드로이드(Android) 프로그래밍은 간단한걸 구현해도 제법 많은 코드가 필요하다.

그래서 Kotlin 등이 각광 받을 테지만

이상의 방법과 같이 작성하면

Android를 사용하든 Kotlin을 사용하든, 쉽고 가독성 좋은 코드를 작성할 수 있을 것이다.


다음으로 실제 계산하는 기능을 구현한다.



부록

지금까지 Java(Android) 사용법과 코드를 줄이는 방법을 정리하였다.

이번에는 화면(XML)에서 코드를 줄이는 방법을 간단하게 정리한다.


다음의 activity_main.xml 파일을 자세히 보면,

모든 위젯은 너비(layout_width)와 높이(layout_height)가 있고,

Layout은 orientation가,

Button은 layout_weight가 동일하게 (“=” 버튼 예외) 사용된 것을 알 수 있다.

        ~~ 생략 ~~   
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <Button
            android:id="@+id/button7"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="7"
            android:textSize="14sp" />

        <Button
            android:id="@+id/button8"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="8" />
        ~~ 생략 ~~   

전체코드

이렇게 디자인과 관련된 코드는 웹 개발처럼 스타일(Style)로 미리 지정해서 사용하면 쉽게 사용할 수 있다.

app/src/main/res/values/styles.xml 파일을 열어서 다음과 같이 작성한다.

<resources>
    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
    </style>

    <style name="layoutStyle">
        <item name="android:layout_width">match_parent</item>
        <item name="android:layout_height">wrap_content</item>
        <item name="android:orientation">horizontal</item>
    </style>
    <style name="buttonStyle">
        <item name="android:layout_width">wrap_content</item>
        <item name="android:layout_height">wrap_content</item>
        <item name="android:layout_weight">1</item>
    </style>
</resources>

전체코드

layoutStyle은 LinearLayout에 공통으로 사용할 스타일이고

buttonStyle은 Button에 공통으로 사용할 스타일이다.

공통으로 사용할 스타일을 styles.xml 파일에 미리 등록해서 사용한다.


그리고, 다음과 같이 activity_main.xml 파일을 수정한다.

        ~~ 생략 ~~   
    <LinearLayout  style="@style/layoutStyle">
        <Button
            android:id="@+id/button7"
            android:text="7"
            android:onClick="mClickListener"
            style="@style/buttonStyle"/>

        <Button
            android:id="@+id/button8"
            android:text="8"
            android:onClick="mClickListener"
            style="@style/buttonStyle"/>

        <Button
            android:id="@+id/button9"
            android:text="9"
            android:onClick="mClickListener"
            style="@style/buttonStyle"/>
        ~~ 생략 ~~   
       

전체코드

기존의 반복 코드를 지우고,

style 속성에 layoutStyle, buttonStyle을 지정해서 사용한다.

HTML의 CSS 클래스처럼 사용하는 것이다.


activity_main.xml 파일을 보면,

모든 버튼에 다음 코드가 추가 된 것을 볼 수 있다.

         android:onClick="mClickListener"

이벤트 리스너를 Java에서 지정한 것이 아니고,

HTML처럼 디자인에서 지정하였다.

버튼을 클릭하면(onClick) mClickListener함수를 호출한다.


MainActivity.java 파일을 다음과 같이 수정해야 한다.

    public void mClickListener (View view) {
            Button button= (Button) view;
            editText.setText(editText.getText().toString() + button.getText().toString());
    };

전체코드

기존에는 mClickListener를 OnClickListener 클래스의 변수로 선언해서 사용했는데,

이 클래스 변수를 디자인(xml)에서는 알 수가 없기 때문에

전역(public) 함수로 선연해서 디자인에서 호출할 수 있도록 해준다.




그림과 같이 입력한 값을 보여주는 입력상자(EditText) 하나와 15개의 버튼으로 구성된 간단한 계산기를 만들면서 안드로이드 프로그래밍 기초를 정리한다.

정리 방법은 다음과 같이 단계별로 예제를 구현하고, 예제 소스는 GitHub에서 받을 수 있다.

  1. 계산기 디자인과 기본 코드 (branch: master)
  2. 생성한 클래스를 이벤트 리스너로 사용 (branch: step1)
  3. 코드 줄이기  (branch: step2)
  4. 실제 계산 기능 구현 (branch: step3)
  5. 후위 표기법으로 계산 기능 구현 (branch: step4)
  6. 동적 생성 (branch: step5)

각각의 단계별 예제는 GitHub의 branch에서 받을 수 있다.


앞서의 예제에서는 계산기 화면을 쉽고 간단하게 만드는 방법을 정리했다.


네번째 예제로, 이번에는 실제 계산이 가능하도록 구현한다.

실제 계산기로 계산하는 과정을 정리하면,

     ① 사용자가 숫자를 입력하고

     ② 사칙 연산자를 선택한 뒤

     ③ 다시 숫자를 입력하고,

     ④ “=” 버튼을 클릭하여 계산을 하게 된다.

이처럼 계산기의 사용법 자체는 단순하다


이것을 조금 더 세분화해서 정리하면

     ① 사용자가 숫자를 입력하고

     ② 사칙 연산자를 선택하면

          - 현재까지 입력한 숫자는 첫 입력 값으로 저장하여 보관하고

          - 두 번째 값의 입력을 위해 기존 숫자는 지운다.

          - 선택된 연산자는 연산을 위해 보관한다.

     ③ 다시 숫자를 입력하고

     ④ “=” 버튼을 클릭하면 다시 입력한 숫자(③)를 두 번째 값으로 처리하여 두 값에 대하여 사칙 연산을 처리하고 결과 값을 출력한다.


여기까지는 쉬우니 (?) 직접 해보길 바라고, 조금 더 기능을 부여해서 구현해 본다.


① ,②, ③번의 과정은 동일하지만

④ 번에서 “=” 대신에 사칙 연산자를 선택할 수 있다.

④ 번을 세부화하여,

두 번째 값을 입력하고 사용자가 사칙 연산을 입력하면 앞서 입력한 두 값과 앞서 입력한 사칙 연산을 이용하여 계산한다.

“=” 버튼을 클릭한 것과 동일하다.

사칙 연산자를 선택한 것은 계산을 계속하겠다는 의미이므로, 계산 결과를 출력한 뒤 ⑤ 계산 결과를 첫 번째 값으로 보관한다.

사용자가 다시 숫자를 입력하고 사칙연산자나 “=”를 누르면 연산을 실행한다.

사칙 연산자를 누르면 ⑤번 과정을 반복하게 된다.

사칙 연산자가 아닌 “=”를 선택했을 경우, 그냥 계산 결과를 출력하고 사용자가 입력한 값들과 연산자를 초기화 해주면 된다.

다만, 다시 계산하기 위해 값을 입력하면 출력한 결과 값을 지우고, 사용자 입력 값이 출력되도록 하면 된다.

이 외에도 필요한 기능은 더 많지만 이 정도 수준에서 구현해 본다.

이 정도 수준이면 제법 복잡하고, 잡아야 할 버그도 제법 많다.


다시 정리하면

     ① 사용자가 숫자를 입력하고

     ② 사칙 연산자를 선택하면

          - 현재까지 입력한 숫자는 첫 입력 값으로 저장하여 보관하고

          - 두 번째 값의 입력을 위해 기존 숫자는 지운다.

          - 선택된 연산자는 연산을 위해 보관한다.

     ③ 다시 숫자를 입력하고

     ④ “=” 나 사칙 연산자 버튼을 클릭하면

          - 다시 입력한 숫자(③)를 두 번째 값으로 처리하여, 두 값에 대하여 사칙 연산을 처리하고 결과 값을 출력한다.

          - 클릭한 것이 사칙 연산자이면 계산 결과를 첫 번째 값으로 보관하고, 클릭한 사칙 연산자를 보관한다.

          - 사칙 연산자가 아닌 “=”를 선택했을 경우, 그냥 계산 결과를 출력하고 사용자가 입력한 값들과 연산자를 초기화 해주면 된다.


전체 코드

기존 예제의 OnClickListener 리스너를 다음과 같이 수정하면 된다.

코드가 복잡해 보이지만

앞서 정리한 문장을 이해하면 쉽게 이해할 수 있는 코드이다.

따라서 다음과 같이 문장과 코드를 같이 보면 된다.


① 사용자가 숫자를 입력하고 [라인 51]

② 사칙 연산자를 선택하면 [라인 18~44]

  - 현재까지 입력한 숫자는 첫 입력 값으로 저장하여 보관하고 [라인 18]

  - 두 번째 값의 입력을 위해 기존 숫자는 지운다 [라인 20].

  - 선택된 연산자는 연산을 위해 보관한다 [라인 42].

③ 다시 숫자를 입력하고 [라인 51]

④ “=” 나 사칙 연산자 버튼을 클릭하면 [라인 18~44]

  - 다시 입력한 숫자(③)를 두 번째 값으로 처리하여 [라인 23],

    두 값에 대하여 사칙 연산을 처리하고[라인 24~30] 결과 값을 출력한다 [라인 32].

  - 클릭한 것이 사칙 연산자이면 계산 결과를 첫 번째 값으로 보관하고[라인 40],

    클릭한 사칙 연산자를 보관한다 [라인 42].

  - 사칙 연산자가 아닌 “=”를 선택했을 경우[라인 36],

    그냥 계산 결과를 출력하고[라인 32]

   사용자가 입력한 값들과 연산자를 초기화 해주면 된다 [라인 46~48].


[주의] 이상의 예제를 실행하면 제법 많은 버그가 나온다. 찾아서 수정해 보길 바란다.

     - 숫자를 입력하고 사칙연산 선택, 또 사칙연산 선택시 오류

     - 숫자를 입력하고 “=” 입력시 오류 등 다양한 예외에 대한 처리가 필요하다.

다섯 번째 예제는 후위 표기법 방식으로 구현하는 것이다.

후위 표기법에 대한 개념은 검색 해보길 바라고 여기서는 간단하게 정리한다.

후위 표기법에 대한 개념이 없으면 다음 내용을 제대로 이해하기 어려울 수 있다.


앞서의 계산기는 연산자나 “=”을 입력하면 연산이 되도록 한 것이고,

이번에는 사용자가 입력한 모든 값과 연산자를 가지고 있다가 “=” 버튼을 클릭하면 계산하는 방식이다.

즉, 1 + 2 + 3 + 4 - 5 (수식) 를 입력하면 입력 박스에 그대로 출력되고,

“=” 버튼을 클릭하면 이 값을 분석해서 계산하게 된다.


위 수식을 중위표기법이라 부르고, 이것을 후위 표기법으로 변환해서 처리하게 된다.

후위 표기법으로 변환하는 이유는 쉽게 코딩 할 수 있기 때문으로,

개인적인 개념으로 정리하면 그냥 숫자와 연산자를 분리 / 보관해서 처리하는 것이다.


따라서 기본 개념을 정리하면

1. 사용자가 입력한 수식을 숫자와 연산자(+-*/)로 분리한다.

2. 첫 번째 숫자와 두 번째 숫자를 첫 번째 연산자로 계산해서 결과를 저장한다.

3. 첫 번째 결과와 세 번째 숫자를 두 번째 연산자로 계산해서 결과를 저장한다.

4. 숫자가 없을 때까지 이전 결과와 다음 숫자를 다음 연산자로 계산해서 결과에 저장한다.

★ 연산시 곱하기(*)과 나누기(/)가 먼저 실행되어야 한다.


이 문장을 자세히 보면 2~4번은 같은 의미를 나타낸다.

2~4번 문장은 이전 결과와 다음 숫자를 다음 연산자로 계산해서 결과에 저장하는 것이다.

이 경우 2번 문장의 첫 번째 숫자가 문제가 된다.

첫 번째 숫자를 계산 전에 결과에 넣고, 결과와 다음 숫자를 연산하면 된다.


따라서, 다음과 같이 정리된다.

1. 사용자가 입력한 수식을 숫자와 연산자(+-*/)로 분리한다.

2. 첫 번째 숫자를 결과에 저장한다.

3. 숫자가 없을 때까지 이전 결과와 다음 숫자를 다음 연산자로 계산해서 결과에 저장한다.

★ 연산시 곱하기(*)과 나누기(/)가 먼저 실행되어야 한다.


마지막으로 ★로 표기한 곱하기(*)과 나누기(/)가 수식에서 어디에 있던 먼저 실행되도록 해야 한다.

먼저 실행되게 하는 방법은,

연산자가 더하기(+)와 빼기(-)일 때는 그냥 넘어가고, 곱하기(*)과 나누기(/)일때만 처리한다.

그리고, 곱하기(*)과 나누기(/)가 끝나면 다시 더하기(+)와 빼기(-)를 처리한다.


따라서, 두 번의 반복문(While)이 필요한데, 약간의 트릭을 사용한다.

곱하기(*)과 나누기(/) 처리시 더하기(+)일 때는 결과 값을 그대로 저장하고, 빼기(-)일 때는 결과값을 음수화( * -1)해서 저장한다.

이렇게 하면 곱하기(*), 나누기(/), 더하기(+)의 결과는 그대로 저장될 것이고, 빼기는 결과값이 음수화되어 저장되니,

결과 값들을 모두 더하기 하면, 연산자 확인 없이 더하기(+)와 빼기(-) 처리가 된다.


다시 정리하면

1. 사용자가 입력한 수식을 숫자와 연산자(+-*/)로 분리한다.

2. 첫 번째 숫자를 결과에 저장한다.

3. 숫자가 없을 때까지 이전 결과와 다음 숫자를 다음 연산자로 계산해서 결과에 저장한다.

4. 이때, 곱하기(*)과 나누기(/)이면 연산해서 결과를 저장하고, 더하기(+)면 그대로 결과에 저장하고, 빼기면 음수화해서 저장한다.

5. 모든 결과들을 더해서 계산을 마친다.


전체코드

먼저, 사용자가 입력한 값이

"=" 이 아니면 (숫자나 연산자) 그대로 값을 추가해서 수식을 만들고 [라인 9],

"="이면 Calc() 함수를 호출해서 입력한 수식을 계산한다 [라인 7].


실제 계산을 하는 Calc() 함수는 다른 사람이 작성한 코드를 정리한 것으로

설명과 코드를 일치시켜서 정리하면,

1. 사용자가 입력한 수식(formulaStr)을 숫자[라인 16]와 연산자(+-*/)로 분리한다 [라인 15].

2. 첫 번째 숫자를 결과에 저장한다 [라인 19].

3. 숫자가 없을 때까지[라인 20] 이전 결과와 다음 숫자를 다음 연산자로 계산해서 결과에 저장한다 [라인 27, 31].

4. 이때, 곱하기(*)과 나누기(/)이면 연산해서 결과를 저장하고 [라인 25~32],

   더하기(+)면 그대로 결과에 저장하고, 빼기면 음수화해서 저장한다 [라인 33~38].

    5. 모든 결과들을 더해서 계산을 마친다 [라인 41~44].


계산 결과들을 저장하는 변수를 스택(Stack) 클래스를 사용한다 [라인 18].

스택(Stack)은 한 쪽 끝에서만 자료를 넣거나 뺄 수 있는 선형 구조(LIFO - Last In First Out)로,

왜 Stack을 사용한 것인지 스스로 생각해보길 바란다 (hint: LIFO).


학습을 위해 이상의 두 예제에 대한 개념을

파워포인트 등을 이용하여 플로우차트(Flowchart) 로 작성해 보면 많은 도움이 될 것이니 시도해 보길 바란다.


여섯 번째 예제는 동적 생성이다.

이전 예제는 XML 파일에 위젯들을 선언하고, Java 파일에서 이 위젯을 찾아서 사용했다.

이번에는 XML 파일이 아닌 Java파일에서 위젯들을 생성하는 방법을 정리한다.


개념을 익히기 위한 것이라 모든 위젯을 생성하지 않고, 4개의 레이아웃과 15개의 버튼만 동적으로 생성한다.

이 문제를 구현하기 위해서는 알아야 할 것은

동적으로 위젯(클래스)를 생성하는 방법 (new 클래스)과 하나의 레이아웃에 4개씩 버튼을 추가하는 방법이다.



그리고, 4개의 레이아웃과 15개의 버튼을 각각 생성하면 아주 많은 코드를 작성해야 한다.

따라서 다소 간단하게 생성하는 방법을 찾아야 한다.


위 그림을 토대로 구현 방법을 정리하면

    1. 생성할 버튼의 개수만큼 반복해서 버튼을 생성한다.

    2. 생성한 버튼에 필요한 속성을 지정하고,

    3. Layout에 추가한다.

    4. Layout에 버튼이 4개가 추가 되면, 새로운 Layout을 생성한다.

15개 버튼이니 4개의 Layout이 생성됨.


이 개념을 가지고 다음 코드를 살펴 본다.


    1. 생성할 버튼[라인 7]의 개수만큼 반복해서[라인 13] 버튼을 생성한다 [라인 20].

    2. 생성한 버튼에 필요한 속성을 지정하고 [라인 21~23],

    3. Layout에 추가한다 [라인 24].

    4. Layout에 버튼이 4개가 추가 되면 [라인 14], 새로운 Layout을 생성한다 [라인 15].

추가한 Layout과 버튼의 속성은 XML 파일에서 지정한 것과 동일하게 지정하면 된다.

“=” 버튼은 좀더 크게 만들어야 하는데 (예외), 생략했으니 직접 구현해 보길 바란다.


위 개념에는 없는 클래스(위젯) 생성과 부모에 추가하는 방법을 정리하면,

클래스는 new로 생성한다 [라인 15, 20].

생성된 클래스는 추가할 부모에 addView() 함수로 추가한다 [라인 18, 24].

XML에서는 태그 사이에 작성하는 것과 같다.


이상의 코드에서 버튼의 Layout 속성을 버튼을 생성할 때마다 지정하지 않고,

속성 클래스(LayoutParams) 하나를 생성해서, 생성하는 버튼에 지정하는 방식을 사용했다 [라인 10],

LinearLayout은 LinearLayout를 생성할 때 마다 매번 생성해서(new) 지정했다 [라인 16].

어느 방법이 더 좋을지 판단해 보길 바라고,

14라인에서 사용된 IF 문의 의미를 이해하고 넘어가길 바란다.


코드를 실행하기 전에 activity_main.xml에서

최상위 LinearLayout과 EditText를 제외하고는 모두 삭제한다.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
tools:context="com.gujc.mycalc1.MainActivity">

<EditText
android:id="@+id/editText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:inputType="text"
android:gravity="right" />
</LinearLayout>



+ Recent posts