정적(static)

정적 멤버(혹은 변수)와 메소드라는 것은 무엇일까요? 우선 아래와 같은 상황이 있다고 가정해봅시다. Counter라는 클래스가 있으며 Counter는 멤버로 cnt를 갖고 있습니다. 그리고 count라는 메소드가 있는데, 이 count 메소드는 단순히 cnt를 하나 증가하는 역할을 합니다.

그리고 메인에서는 Counter의 두 객체 counter1, counter2를 생성하고 둘은 counter() 메소드를 통해서 cnt를 증가시키고 있습니다. 여기서 저의 의도는 두 객체가 cnt를 같이 증가시키고 싶습니다.

class Counter{
	public int cnt;
	public void count() {
		cnt++;
	}
	
}

public class TestMain {
	
    public static void main(String[] args){
    	Counter counter1=new Counter();
    	Counter counter2=new Counter();
    	counter1.count();
    	counter2.count();
    	System.out.println("counter1:"+counter1.cnt+", counter2:"+counter2.cnt);
    	
    }
}

 

만약 프로그램이 저의 의도대로 동작한다면 counter2까지 왔을때 cnt는 2가 되어야합니다. (물론 그렇게 동작하지 않습니다.) 결과를 보면 아래와 같네요.

counter1:1, counter2:1

 

정적 변수(Static variable)

여기서 저의 희망은 cnt라는 맴버를 공통으로 사용하는 것입니다. 이때 static하나만 써주면 저의 목적이 달성됩니다. Counter 클래스에서 cnt를 선언할때 static만 붙여봅시다.

class Counter{
	public static int cnt;
	public void count() {
		cnt++;
	}
}

public class TestMain {
	
    public static void main(String[] args){
    	Counter counter1=new Counter();
    	Counter counter2=new Counter();
    	counter1.count();
    	counter2.count();
    	System.out.println("counter1:"+counter1.cnt+", counter2:"+counter2.cnt);
    	
    }
}

그리고 결과를 봐야겠죠.

counter1:2, counter2:2

 

제가 말한대로 동작하는 것을 알 수 있습니다.

어떻게 이런것이 가능할까요? static 변수나 메소드는 static 메모리 구역에 따로 고정적(정적)으로 할당되어 관리됩니다. 그래서 이 static 변수와 메소드는 모든 Counter 클래스가 공통적으로 사용할 수 있습니다. 이러한 특징 때문에 클래스 변수, 메소드라고 합니다.

 

static에 대해서 가볍게 아시려면 "공용 변수, 메소드" 라고 쉽게 기억하시면 됩니다.

 

그렇지만 static에 대해서 좀 더 자세히 알려면 여러분은 우선 프로그램이 실행될때의 메모리 구조에 대해서 아셔야합니다. 우리가 항상쓰던 new로 객체를 생성하는 것은 heap 영역에 저장되어 프로그램이 실행하는 중간에 메모리를 할당합니다. 그래서 메소드가 끝나서 쓸일이 없어질때 자바의 Garbage Collector에 의해서 메모리에서 수거가 됩니다.

하지만 static은 프로그램이 실행 전 먼저 메모리에 잡히게 됩니다. 이때 실행 전이라고 하는 것은 프로그램이 메모리에 적재되고 명령어를 수행하기 전을 말합니다. 그리고 프로그램이 종료될때 해제가 되지요. 즉, heap의 영역보다 나중에 정리된다는 뜻입니다.

메모리 구조

 

가장 먼저 메모리에 적재되어 정적(static)으로 존재하기 때문에 static 멤버나 메소드는 클래스의 객체 생성없이 클래스의 이름만가지고도 사용할 수 있습니다.

public static void main(String[] args){
	Counter.cnt++;
    Counter.cnt++;
    System.out.println("cnt:"+Counter.cnt);
}
cnt:2

 

정적 메소드(static method)

이제 정적 변수는 알았는데, 정적 메소드는 그렇다면 무엇일까요? 역시 변수와 개념은 다르지 않습니다. 메모리에 메소드하나를 고정적으로 두어서 사용이 가능합니다. Counter 클래스를 변형시켜서 보도록 합시다. 

class Counter{
	public static int count(int cnt) {
		return cnt+1;
	}
}

public class TestMain {
	
    public static void main(String[] args){
    	Counter counter=new Counter();
    	System.out.println("count 1:"+Counter.count(1));
    	System.out.println("count 2:"+Counter.count(2));
    	System.out.println("counter's count 5:"+counter.count(5));
    }
}

 

그러면 메모리에는 이런식으로 잡히게 됩니다. 마치 counter라는 메소드를 공용으로 사용하듯 말이죠.

 

실행을 시켜보면 아래와 같은 결과를 갖게 됩니다.

count 1:2
count 2:3
counter's count 5:6

 

이제 메모리에 정적변수나 메소드가 다른 동적으로 메모리를 할당(new 키워드로 할당하거나 일반 메소드)하는 시점보다 먼저 메모리에 잡히게 되었다는 것을 알았다면 다음과 같은 규칙을 이해하게 될 것입니다.

 - 정적 메소드에서는 정적 메소드나 정적 변수만 사용할 수 있다. 멤버 변수는 사용할 수 없다.

이러한 이유는 정적 메소드에서는 자신보다 나중에 메모리가 할당되는 멤버 변수나 메소드는 언제 할당되는지 알수가 없습니다. 만약 아래의 코드처럼 count가 먼저 호출되는 상황에서 cnt가 메모리에 할당되지 않았다면요. 그래서 이처럼 빨간줄이 쳐지면서 static 만이 올수가 있다고 메시지를 띄워줍니다.

 

main 메소드에서 static 메소드만 호출 가능했던 이유

main 메소드 역시 static 메소드이기 때문에 여러분들이 클래스를 배우기 전 메소드를 배울때 묻지도 따지지도 않고 우선 static을 붙여서 메소드를 구현해보셨을 것입니다. main 메소드는 프로그램 실행 전 먼저 메모리에 할당되어야하기 때문이고, 메인이 static이기 때문에 호출되는 메소드 역시 static이어야하기 때문입니다.

 

이상으로 자바에서 static 개념에 대한 설명이었습니다. 무엇보다 메모리가 할당되는 시점만 이해한다면 왜 이런 결과가 나오는지 이해하기가 쉬울 것입니다.

반응형
블로그 이미지

REAKWON

와나진짜

,

SwipeRefreshLayout

요즘 어플리케이션들을 보면 아래로 당겨서 화면을 새로 로딩하는 기능이 많죠. 이런 기능을 담당하는 것이 SwipeRefreshLayout입니다. 그래서 아래로 당기면 아래처럼 로딩하는 화면(빙글 빙글 돌아가는 로딩화면)이 나온 이후에 로딩이 완료되면 어플리케이션의 정보를 갱신해줍니다.

 

 

SwipreRefreshLayout을 사용하기 위해서는 build.gradle 파일에 아래처럼 종속 항목을 명시해줍니다. 

build.gradle

 

build.gradle

dependencies {
	//...
    implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
    //...
}

 

현재 안정화버전은 1.1.0버전이기 때문에 1.1.0버전을 사용하도록 하겠습니다. 추후에 안정화버전이 변경될 수 있으니 안드로이드 개발 문서를 참고해주시기 바랍니다.

https://developer.android.com/jetpack/androidx/releases/swiperefreshlayout?hl=ko#1.1.0 

 

Swiperefreshlayout  |  Android 개발자  |  Android Developers

Swiperefreshlayout 스와이프하여 새로고침 UI 패턴을 구현합니다. 최근 업데이트 현재 안정화 버전 다음 버전 후보 베타 버전 알파 버전 2020년 7월 22일 1.1.0 - - 1.2.0-alpha01 종속 항목 선언 SwipeRefreshLayout

developer.android.com

 

이제 sync를 하고 나면 벌써 사용할 준비는 끝이 났습니다. 이제 메인화면을 구성할 activity_main.xml을 만들어줍시다.

activity_main.xml

<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id='@+id/refresh_layout'>

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:id="@+id/text_view"
            android:text="아래로 당겨보세요."/>
    </RelativeLayout>

</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

 

SwipeRefreshLayout에 새로고침이 될 레이아웃을 추가하였습니다. 저는 간단히 RelativeLayout안의 TextView의 텍스트를 새로운 데이터로 고쳐보도록 하겠습니다. 이것을 프로그램한것이 MainActivity에 존재합니다.

MainActivity.java 

import androidx.appcompat.app.AppCompatActivity;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;

import android.os.Bundle;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

    private SwipeRefreshLayout swipeRefreshLayout;
    private TextView textView;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        swipeRefreshLayout=(SwipeRefreshLayout)findViewById(R.id.refresh_layout);
        textView=(TextView)findViewById(R.id.text_view);

        swipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
            @Override
            public void onRefresh() {
                textView.setText("새로 고침 완료");
                swipeRefreshLayout.setRefreshing(false);
            }
        });
    }
}

 

누가봐도 매우 간단한 코드네요. SwipeRefreshLayout에 OnRefreshListener를 추가해주면 새로고침이 시작되었을때 onRefresh 메소드가 호출이 됩니다. 이때 다시 로드하는 코드를 추가하여 주고 setRefreshing에 false를 전달하여 로딩중인 화면을 없애주면 됩니다.

 

실행하면 화면이 로딩된 이후 TextView에서 로딩이 완료되었다는 메시지와 함께 Refreshing을 중지합니다.

 

만약 쓰레드를 이용하면 onRefresh에 쓰레드를 실행하여 Handler를 통해 완료 메시지를 받으면 여기서 setRefreshing을 false로 전달해주면 됩니다. 

반응형
블로그 이미지

REAKWON

와나진짜

,

람다 표현식(Lambda Expression)

람다 표현식은 JAVA SE 8부터 추가된 기능입니다. 람다 표현식은 함수를 하나의 식으로 표현한 것인데요. 메소드의 이름을 갖다버린 익명 함수(Anonymous Function)로 볼 수 있습니다.

아주 간단히 이야기하자면 귀찮은 익명 메소드(자바에서는 익명 클래스, Anonymous Class)를 아주 간단하게 줄인 표현식이라고 생각하면 됩니다.

 

기존 방식

예를 들어 안드로이드에서 클릭 이벤트 발생시에 아래와 같이 View.OnClickListener를 정의해주고 onClick 메소드를 채워줘야 클릭 이벤트를 받을 수 있습니다.

button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.e(TAG,"클릭 발생");
            }
        });

 

구현해야할 메소드는 onClick 메소드 하나 뿐인것을 알 수 있습니다. onClick 메소드는 너무나 간단하게 로그를 찍는 코드밖에 없습니다. 하지만 꽤나 복잡한 과정을 거치게 되죠. 분명 로그만 찍는데 부가적으로 기록해야하는 코드가 길다는 것이죠. 

혹은 우리가 자주 사용하는 Thread를 Runnable을 통해서 객체 생성과 동시에 동작시키려면 이렇게 코드를 짜야합니다.

new Thread(new Runnable() {
    		@Override
    		public void run() {
    			System.out.println("Thread run");
    		}
    	}).start();

 

 

람다를 사용한 방식

만약 위의 코드들에 람다표현식을 적용한다면 이렇게 바뀝니다.

button.setOnClickListener(v->Log.e(TAG,"클릭 발생"));

 

그리고 Thread를 동작시키는 코드는 아래와 같게됩니다.

new Thread(()->System.out.println("Thread run")).start();

 

몇줄이 단지 한줄로 바뀌게 되지요. 이렇게 람다를 적용하면 이렇게 간단한 코드로 바뀌게 됩니다. 즉, 가독성이 정말 높아지게 됩니다. 여기서 우리는 단지 하나의 익명 클래스를 구현했지만, 그것이 여러개가 된다면 가독성이 매우 떨어지게 됩니다.

 

반면 익명 함수이기 때문에 재사용이 불가능하고, 디버깅이 어려운 단점이 있습니다. 

 

람다 표현식 문법

이제는 람다를 사용하는 방법에는 어떤 방법이 있는지 알아보도록 합시다. 이때 람다를 사용할 수 있는 경우는 Overriding해야할 메소드가 하나여야합니다. 두개 이상의 메소드를 구현해야할때에는 사용할 수가 없습니다.

1. 구현부가 한줄인 경우

() -> System.out.println(x)

 

2. 인자가 있는 경우

() -> {System.out.println("lambda")};

 

3. 인자가 있는 경우

인자가 2개 이상인 경우에는 괄호로 묶어야합니다. 그리고 타입을 명시적으로 지정해줄 수도 있습니다.

x -> {System.out.println(x)};
(x) -> {System.out.println(x)};
(x,y) -> {System.out.println(x+","+y)};
(int x, int y) -> {System.out.println(x+","+y)};

 

4. 반환값이 있을 경우

(int x) -> { return x*x };

 

 

함수형 프로그래밍

람다를 사용하는 가장 주된 이유는 함수형 프로그래밍을 해줄 수 있게 만들어주기 때문입니다. 여기서 함수형 프로그래밍이라는 것은 순수 함수, 메소드로 문제를 해결하는 프로그래밍 페러다임입니다. 자바 8 이전에는 어떻게(how) 문제를 해결하는 것에 초점을 두지만, 8 이후에는 무엇(what)을 할 것인가에 대해서도 지원하게 됩니다.

예를 들어 for을 우리가 구현하지 않고 미리 정의된 forEach를 사용하는 방식입니다. 이렇게 되면 코드를 짧게 유지하여 유지 보수하기가 매우 쉬워집니다. 

 

함수형 프로그래밍을 도와주는 몇몇 기법에는 지금 소개한 람다 표현식과 StreamAPI 등이 있습니다.

반응형
블로그 이미지

REAKWON

와나진짜

,

특정 시간에 Notification 발생 시키기

 

특정 시간이 되면 알림이 발생되는 코드를 android에서 구현해보도록 하겠습니다. 특정 시간을 설정해서 그 시간이 되면 알려주는 무엇인가가 있어야하는데 이를 가능하게 만들어주는 것이 AlarmManager입니다. 코드를 통해서 알아보도록 합시다.

 

먼저 레이아웃은 간단하게 아래처럼 구성되어있습니다.

activity_main.xml

<?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=".MainActivity">

    <TimePicker
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:timePickerMode="spinner"
        android:id="@+id/time_picker" />


    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="저장"
        android:id="@+id/save"/>

</LinearLayout>

 

이제 time_picker에서 시간을 설정하고 save 버튼을 누르게 되면 그 시간에 Notification이 발생하게 되는 코드를 설명합니다.

 

MainActivity

MainActivity의 onCreate 코드는 아래와 같습니다. 

package com.reak.alarmtest;

import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AppCompatActivity;

import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.widget.Button;
import android.widget.TimePicker;
import android.widget.Toast;

import java.util.Calendar;

public class MainActivity extends AppCompatActivity {

    private Button save;
    private TimePicker timePicker;

    @RequiresApi(api = Build.VERSION_CODES.M)
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        timePicker=(TimePicker)findViewById(R.id.time_picker);
        save=(Button)findViewById(R.id.save);

        save.setOnClickListener(v->{

            Calendar calendar = Calendar.getInstance();
            calendar.setTimeInMillis(System.currentTimeMillis());
            int hour=timePicker.getHour();
            int minute=timePicker.getMinute();
            calendar.set(Calendar.HOUR_OF_DAY,hour);
            calendar.set(Calendar.MINUTE,minute);

            if (calendar.before(Calendar.getInstance())) {
                calendar.add(Calendar.DATE, 1);
            }

            AlarmManager alarmManager=(AlarmManager)this.getSystemService(Context.ALARM_SERVICE);
            if (alarmManager != null) {
                Intent intent = new Intent(this, AlarmReceiver.class);
                PendingIntent alarmIntent = PendingIntent.getBroadcast(this, 1, intent, PendingIntent.FLAG_UPDATE_CURRENT);

                alarmManager.setRepeating(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(),
                        AlarmManager.INTERVAL_DAY, alarmIntent);

                Toast.makeText(MainActivity.this,"알람이 저장되었습니다.",Toast.LENGTH_LONG).show();
            }
        });
    }
}

 

여기서 Calendar 객체를 현재시간으로 미리 설정해두고, set 메소드로 timePicker에서 설정된 시간과 분으로 설정시키는 것입니다. 이때 시간과 분을 TimePicker에서 얻어오려면 @RequiresApi(api = Build.VERSION_CODES.M) 를 메소드위에 추가시켜야합니다.

이제 AlarmManager를 가져옵니다. Intent는 수신자 클래스를 전달하게 됩니다. 여기서는 AlarmReceiver라는 수신자이며 밑에서 이 Receiver를 구현하게 될겁니다. 그리고 PendingIntent를 얻어와서 setRepeating 메소드로 정확한 시간에 알람을 설정시켜줍니다.

 

여기서 setRepeating메소드는 알람을 반복시키는 메소드입니다. 여기서는 AlarmManager.INTERVAL_DAY를 사용하였고, 이것은 매일 알람이 울릴것을 명시해준것입니다. 

 

굳이 정확한 시간에 알람을 울리지 않을 경우에는 setInexactRepeating 메소드를 사용할 수도 있습니다. 실제로 안드로이드 개발문서에는 이 메소드를 사용하라고 권고하고 있네요.

 

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.reak.alarmtest">

    <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/Theme.AlarmTest">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

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

        <receiver android:name=".AlarmReceiver"/>

    </application>

</manifest>

 

수신자를 사용하려면 위에서처럼 메니페스트에 수신자를 명시해주어야합니다. 

 

AlarmReceiver

package com.reak.alarmtest;

import android.app.AlarmManager;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.TaskStackBuilder;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.media.RingtoneManager;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;

import androidx.core.app.NotificationCompat;

import java.util.Calendar;
import java.util.StringTokenizer;

import static android.app.Notification.EXTRA_NOTIFICATION_ID;

public class AlarmReceiver extends BroadcastReceiver {

    private Context context;
    private String channelId="alarm_channel";
    @Override
    public void onReceive(Context context, Intent intent) {
        this.context = context;


        Intent busRouteIntent = new Intent(context, MainActivity.class);

        TaskStackBuilder stackBuilder = TaskStackBuilder.create(context);
        stackBuilder.addNextIntentWithParentStack(busRouteIntent);
        PendingIntent busRoutePendingIntent =
                stackBuilder.getPendingIntent(1, PendingIntent.FLAG_UPDATE_CURRENT);

        final NotificationCompat.Builder notificationBuilder=new NotificationCompat.Builder(context,channelId)
                .setSmallIcon(R.mipmap.ic_launcher).setDefaults(Notification.DEFAULT_ALL)
                .setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION))
                .setAutoCancel(true)
                .setContentTitle("알람")
                .setContentText("울림")
                .setContentIntent(busRoutePendingIntent);


        final NotificationManager notificationManager=(NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE);

        if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.O){
            NotificationChannel channel=new NotificationChannel(channelId,"Channel human readable title",NotificationManager.IMPORTANCE_DEFAULT);
            notificationManager.createNotificationChannel(channel);
        }

        int id=(int)System.currentTimeMillis();

        notificationManager.notify(id,notificationBuilder.build());

    }
}

 

이제 알람이 발생되면 AlarmReceiver의 onReceive 메소드가 호출되게 됩니다. 이때 아까 우리가 MainActivity에서 넘긴 PendingIntent를 requestCode를 통해서 얻어올 수가 있습니다. 이 requestCode는 알람을 설정했던 것과 같은 코드를 사용해야합니다. 여기서는 1입니다.

 

오레오(Oreo)버전 이상부터는 NotificationChannel을 명시해주어야합니다. 그 코드도 적용되어 있습니다.

 

대충 완성하게 되면 아래와 같은 모습입니다. 앱 디자인은 개나 줘버린 모습이지만 구현이 제대로 되었나 확인만 하는 용도입니다.

alarm test 앱화면

 

그리고 원하는 시간에 알람을 저장시켜줍니다. 그래서 9시 15분에 알람을 설정합니다. 그리고 그 시간에 울리는 지 잠깐 기다려주겠습니다. 이때 9시 14분이었고, 1분만 기다리면 됩니다.

alarm 저장

 

다음은 알람이 발생되어 Notification이 발생된 화면입니다.

 

분명 9시 15분이었는데, 9시 16분에 알람이 발생되었군요. 정확한 시간에 알람이 딱 울리지는 않는 것 같습니다. 제 생각에는 setRepeating이 setInexactRepeating메소드보다 더 정확한 시간에 발생시켜주는 것으로 확인이 됩니다.

알람 취소

그리고 알람을 취소하려면 아래와 같이 코드를 추가하면 되는데요.  위의 코드는 없지만 만약 알람 취소버튼을 추가하고 그 버튼이 눌리면 alarmManager의 cancel 메소드를 이용하면 됩니다.

PendingIntent cancelIntent=PendingIntent.getBroadcast(MainActivity.this, 1, intent,PendingIntent.FLAG_UPDATE_CURRENT);
alarmManager.cancel(cancelIntent);

 

 

여기까지 Alarm과 Notification, Receiver를 조합하여 앱을 구현해보았습니다.

반응형
블로그 이미지

REAKWON

와나진짜

,