인터페이스나 혹은 추상클래스에 있는 추상메소드의 목적

추상메소드에 대해 제대로 이해하기가 쉽지는 않는듯 하다.

추상메소드와 관련된 일반적인 '지식'에 해당되는 내용들은 얼마든지 있다.

그러나 이것이 어디에 쓰기위해 존재하는지에 대해서 명확한 정립이 되어있지 않으면 남들이 만들어 놓은 추상메소드를 코딩은 하지만 정작 내가 클래스를 만들면서 추상 메소드가 가진 목적을 발휘하는 그런 클래스를 디자인하고 만들어 쓰는데까지는 가지 못하는 것을 보게된다.

오늘은 추상 메소드(abstract method)가 존재하는 목적이 뭔지에 대해 뿌리를 캐보고자 한다.


가령 예를 들어서 우리가 어떤 디바이스를 켜는(power on) 행위(method)를 한번 생각해 보자.

power on에는 다양한 종류가 있을 것이다.

PC를 켜거나(Power On), 스마트폰을 켜거나, TV를 켜거나, ... 이럴 때 켤때마다 첫번째 초기 화면으로 그 제품을 만든 회사의 로고 이미지가 뜨도록 강제하고 싶을 경우가 있을 것이다.

PC를 켤때 삼성에서 만들었으면 삼성 로고가 뜨고 LG에서 만들었으면 LG 로그가 뜨고... 스마트폰도 마찬가지로...

이와같이 Power On 할때 초기 화면에 제조사 로고를 뜨도록 강제하고 싶을 경우 이때 이 목적을 위해서 존재하는 것이 추상 메소드(abstract method)이다.


이것을 클래스로 만들어 PowerOn이라는 클래스로 만들어보자. 

이때 PowerOn을 상속받는 하위 클래스들에서는 power on할 때(켤 때) 반드시 제조사의 로고 이미지를 뜨도록 강제하고자 하면 그에 해당하는 기능을 추상 메소드로 만들어 두면 하위 클래스에서는 반드시 로고 이미지가 뜨는 기능(method)을 만들어야만 되도록 되어 있다. 그렇지 않으면 컴파일 단계에서 에러를 발생시킨다. 따라서 그것을 상속 받는 하위 클래스에서는 무조건 로고 이미지 뜨는 기능을 구현해야만되고 이것은 하위 클래스들은 이 면에서 동일한 공통성을 띄게 강제하게 되는 결과가 된다.

말하자면 사용자들에게 동일한 인터페이스를 제공해 주게 되는 결과를 창출하게 된다.

이런 목적으로 존재하는 것인 추상 메소드이다.

예제 코드를 만들어 보자.


abstract class PowerOn

{

//하위 클래스들은 무조건 로고를 띄우는 기능을 만들도록 하고 싶다.

//그럴 경우 로고를 띄우는 메소드를 추상메소드로 만들면 된다.

abstract void showLogo(); 


public String getName() {

...

return name;

}

}



class SamsungPhone extends PowerOn

{

public void showLogo() {

//여기서 삼성 로그 띄우게

}

}


class LGPC extends PowerOn

{

public void showLogo() {

//여기서 LG 로그 띄우게

}

}


그래서 결론적으로 interface나 abstract class에 있는 abstract method의 목적은 어떤 특정 기능을(메소드를) 하위 클래스에서 반드시 만들도록 강제하고자 할때 사용하는 것이다.


그런 점에서 interface라는 것의 존재 목적이 보이는 것이다.

interface는 interface가 가지고 있는 모든 메소드는 전부 abstract 메소드이다.

추상 메소드(abstract 메소드)의 목적은 하위 클래스에 특정 기능을 반드시 만들도록 강제하고자 하는 것이 그 목적이다.

그로 인해 창출되는 결과(효과)는 하위 클래스들은 모두가 특정 기능(메소드)이 다 공통적으로 존재하게 된다는 점이다. 따라서 사용자는 어떤 하위 클래스를 사용하더라도 동일한 사용자 인터페이스를 경험하게 되는 것이다.


따라서 abstract 클래스와 유사하게 interface는 그 이름 그대로 이 interface를 상속받는(구현하는 implements하는) 하위 클래스들은 모두가 공통된 기능들을 갖추게 된다. 즉 interface에 있는 메소드들을 모든 하위 클래스들은 모두다 그 기능을(그 메소드를) 만들어야 하므로 사용자는 특정 interface를 상속받은(implements한) 모든 하위 클래스들에서는 동일한 사용자 인터페이스를 경험하게될 것이다.

이것이 interface가 노리는 목적이다.





Java의 Reference 타입 자료형에 대해


자바의 자료형은 크게 기본형과 레퍼런스 타입으로 분류할 수 있다. 

레퍼런스 타입은 new 연산자를 통해서 힙(heap) 영역에 생성되는 자료형들을 의미한다. 

레퍼런스 타입으로는 클래스, 배열, 인터페이스 들이 있다.


따라서 arr1, arr2라는 2개의 배열이 있을 때 

arr1 = arr2;

를 하면 arr2에 있는 데이터를 arr1에 대입하는 것이 아니라(call-by-value가 아니라)

arr2가 가리키는 메모리 주소 값을 대입한다(call-by-reference이다). 


따라서 arr1에서 값을 변경하면arr2에서도 값이 변경된다. 

반대로 arr2에서 값을 변경하면 arr1의 값도 동시에 바뀐다.


그러나 

int arr1, int arr2;


가 있다면 이때 

arr1 = arr2;


라고 하면 이건 값(데이터)을 대입한다. 즉 call-by-value이다. 

따라서 arr1의 값을 변경해도 arr2에는 아무런 영향이 없고

arr2의 값을 변경해도 arr1에는 아무런 영향이 없다.


그런데 배열은 근본적으로 call-by-reference이다. 

따라서 아래 코드에서 

mRGBData를 chageData(byte[] rgb)로 넘기면 

mRGBData와 rgb는 동일한 기억 장소를 가리키게 된다. 

따라서 changeData()에서 mRGBData에 값을 대입하는 것이 전혀

없을지라도 mRGBData를 출력해 보면 changeData()에서 수정한 값이 잘 출력된다.


public class MainActivity extends Activity {

private TextView txt;

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);

txt = (TextView)findViewById(R.id.txt);

byte[] mRGBData = null;

mRGBData = new byte[5];

changeData(mRGBData);

String str = "";

for (int i=0; i<mRGBData.length; i++) {

str += mRGBData[i] + ", ";

}

txt.setText(str);

} //onCreate

private void changeData(byte[] rgb) 

{

for(int j=0; j<rgb.length; j++) {

//여기서 mRGBData에 값을 대입하지 않고 rgb에만 값을 넣었는데도

//onCreate()에서 TextView에 mRGBData를 출력해 보면 0, 2, 4, 6, 8이 출력된다.

rgb[j] = (byte)(j*2);

}

}

}



아래와 같이 class도 reference type이다. class 참조변수(객체)에 대입을 하면 value가 대입되는 것이 아니라

reference가(해당 클래스의 기억장소 주소 값이) 대입 되게된다.

그래서 class의 두 참조 변수는 동일한 기억 장소를 가리키게 된다.


public class MainActivity extends Activity {

private TextView txt;

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);

txt = (TextView)findViewById(R.id.txt);

MyClass myClass = new MyClass();


//아래 로그를 출력해 보면 다음과 같이 My2클래스에서 myc와 mc를 출력했을 때와 같이

//동일한 기억장소를 가리키게 된다.

// myClass : com.example.test.MainActivity$MyClass@411e71c0

Log.e("$$$$$$$", "myClass : "+myClass.toString());

My2 my2 = new My2(myClass);

String str = myClass.name + "\n" + myClass.age;

txt.setText(str); 

} //onCreate

class MyClass

{

private String name;

private int age;

}

class My2

{

private long kkk;

MyClass myc;

public My2(MyClass mc)

{

myc = mc;

//아래 로그를 출력해 보면

//myc : com.example.test.MainActivity$MyClass@411e71c0

Log.e("$$$$$$$", "myc : "+myc.toString());


//아래 로그를 출력해 보면

//mc : com.example.test.MainActivity$MyClass@411e71c0

Log.e("$$$$$$$", "mc : "+mc.toString());

mc.name = "홍길동";

mc.age = 29;

}

}

}




Map과 List에 대한 간단한 이해


List는 원소들을 가지고 있는 데이터 집합이다. 순차 자료구조로 순차적으로 메모리에 저장되는 형태이다. 쉽게 배열로 바꾸어서 생각해도 된다. 

따라서 List의 원소에 접근할 때는 index(첨자)로 접근한다.


List는 Generic이라는 옵션이 존재하는데 Generic 옵션이 설정된 List는 Generic에 해당되는 데이터 형만을 담을 수 있다.


List<Integer> myList = new ArrayList<Integer>();


위와 같이 선언된 List는 Generic 옵션으로 Integer 형을 받았다.

myList에 add할 수 있는 자료들은 Integer형의 데이터만 담을 수 있게 된다.


myList.add("data")와 같이 문자열을 넣으려 한다면 에러가 발생하게 된다.


myList.get(i)과 myList.remove(i) 메소드를 사용해서 myList 안에 있는 원소들을 인텍스로 접근, 삭제 등을 할수 있다.


Map은 List와 같은 원소들을 모아 놓은 집합이다.

List와는 달리 index(첨자)로 접근하는 것이 아니라 key로써 데이터를 제어할수 있다.

Map 또한 Generic 옵션을 설정할 수 있다.


Map<String, Object> myMap = new HashMap<String, Object>();

와 같이 Generic을 key와 data형으로 지정한다.


myMap.put("elementA", "Data_1");

과 같은 형태로 String의 key로 myMap에 저장된 Object 데이타를 제어할 수 있게 되며 Generic 형식에 따라야 한다.


put으로 저장된 데이터는 put할 때 지정했던 key로 접근이 가능하며

myMap.get("elementA")을 호출하면 저장된 "Data_1"이 반환된다.

myMap.remove("elementA")을 호출하면 해당하는 key를 가진 원소가 삭제된다.





이클립스의 몇 가지 유용한 단축키


Ctrl-Shift-O

⇒ 이클립스에서 클래스 자동으로 import 시키기


Alt-Shift-O

⇒ 특정 변수가 사용된 위치를 손쉽게 파악하게 하는 토글 기능

특정 변수가 사용된 위치를 찾기 위해서 Ctrl-F로 검색할 변수명을 입력해서 찾을수도 있으나

이클립스는 해당 변수를 마우스롤 클릭하면 해당 변수가 사용된 위치를 아래 그림과 같이 표시해 준다.

그런데 이게 정상적으로 작동이 안될 때가 있다. 

이때 Alt-Shift-O를 누르면 정상적으로 작동이 된다.

아래 그림에서 화면 우측 하단의 작은 회색 사각형이 해당 변수가 사용된 위치이다.






+ Recent posts