캡슐화


우리는 이제까지 객체나 클래스의 변수나 메소드를 직접 접근하는 프로그래밍을 해왔습니다. 우리가 저질렀던 이런 방법의 프로그래밍은 조금 위험하다는 것을 알아야합니다. 


왜요?

모두가 접근 가능한 변수나 메소드는 제약 조건없이 쉽게 데이터가 변경 가능하기 때문입니다.

우리는 이 데이터가 안전하게 변경되기 위해서 포장, 또는 알맹이처럼 쌓아야하는 의무가 있습니다. 그게바로 객체지향에서 말하는 캡슐화라고 하는 것이죠.




어떻게 캡슐화가 이루어지는 지 상황을 통해서 알아보도록 합시다.


우리는 계좌의 5만원의 잔액을 갖고 있는 클래스 BankAccount가 있다고 칩시다. 간단하게 정의해보겠습니다.

 

class BankAccount{
	int balance=50000;
}


우리는 이 클래스의 객체로부터 인출하는 동작을 하고 싶다는 것입니다.

그럴때 외부에서 직접적으로 balance 변수에 접근하게 된다면 balance가 음수가 될 수도 있다는 겁니다. 인출은 절대 음수가 될 수 없다는 원칙을 깨고 원치않는 프로그램의 오류가 생기게 됩니다. 실제 이렇게 된다면 피해가 막심할 것입니다.


그러니, 우리는 데이터를 제어해야합니다. 

우선 balance라는 변수는 그 클래스 외부에서 절대 접근을 불가하게 만들고 알맞은 로직을 갖고 있는 멤버함수를 두어, 그 balance를 변경하게 만들면 되지 않을까요??


접근 제어자가 우리의 작은 소망을 들어 줄 수 있습니다.


접근지정자


그 전에 우리는 접근 지정자, 또는 접근 제한자라고 하는 녀석들부터 알아야합니다.


자바의 접근 지정자에는 4개가 있습니다. public, protected, default, private라는 녀석들이지요. 이 4개의 접근 지정자들은 멤버 변수나 멤버 메소드를 어떤 범위 내에서 접근하게 허락할 것인지를 정의하게 해줍니다.




다음의 표와 그림이 범위를 보여주고 있습니다. 


 접근 지정자

 오직 클래스

같은 패키지

 자식 클래스

 외부 어디서나

 public

 O

 O

 O

 O

 protected

 O

 O

 O

 

 default

 O

 O

 

 

 private

 O

 

 

 




public > protected > default > private 순으로 범위가 점점 좁아지는 것을 알 수 있습니다.


이 접근 제어자를 통해서 위의 코드의 문제점을 잡아보도록 하지요.


class BankAccount{
	private int balance=50000;
	
	public int withdraw(int m){
		if(balance<m)
			return 0;
		balance-=m;
		return m;
	}
}

변수는 private로 지정해서 BankAccount내에서만 제어가 가능하게 만들어 줍니다.


그리고 메소드를 통해서 m만큼의 돈을 인출하는 withdraw를 정의하는 겁니다. public 지정자로 withdraw메소드를 정의했으니, 누가나 withdraw를 호출할 수 있습니다. 또한, withdraw메소드는 BankAccount 멤버이기 때문에 balance라는 변수에 접근이 가능하며 이 메소드에서 제어를 하고 있습니다. 만일 인출하려는 금액 m이 지금 잔액(balance)보다 크다고 하면 0을 반환하는 것입니다. 그 외에는 그 금액을 인출하는 것이죠. 물론 잔액은 줄어들게 됩니다.


이렇게 원치않는 변경을 막기 위해 접근 지정자를 쓰게 된다면 balance를 보호할 수가 있습니다.


캡슐화가 이해되셨나요?




상속 관계만 데이터 접근 protected

우리는 상속관계에서 부모클래스의 데이터나 메소드를 자식 클래스만 접근을 허락해야 할 때도 있습니다. 이때 사용하는 지정자가 protected입니다.


example이라는 패키지에 두개의 클래스를 A, A를 상속받은 클래스 B를 정의합니다.


package example;
public class A{
	protected int a;
	public A(){}
}


package example;
public class B extends A{
	public B(){
		a=30;
	}
}


B는 A를 상속했으니 a에 대한 변수에 접근이 가능합니다.



같은 패키지의 main함수와 다른 패키지의 main에서 실험해보세요. 같은 패키지에서는 a에 접근이 가능하고, 다른 패키지에는 아래 그림에서 처럼 a가 proposals조차에서도 보이지 않습니다.




왜냐면 protected지정자로 지정된 변수나 메소드는 같은 패키지에서 모두 접근이 가능하기 때문이지요. 


우리는 다른 패키지에 있는 클래스 역시 상속을 할 수 있기 때문에 상속관계에서만 데이터 접근을 허락할 때는 protected 접근 지정자를 사용해야합니다.

반응형
블로그 이미지

REAKWON

와나진짜

,

상속(Inheritance)


클래스라는 것을 배우면서 Object 클래스에 대해서 잠깐 소개한 적이 있었습니다. 그 이야기를 하면서 상속에 대해 언급한 적이 있었습니다. 자바에서는 이 상속의 사전적인 뜻을 그대로 반영합니다.


inheritance

1. 상속받은 재산, 유산; 상속   2. (과거·선대로부터 물려받는) 유산, 유전(되는 것)


내가 500억을 상속받았으면 이 포스팅을 하지 않았다.


사전을 보니 물려받는 것으로 간단하게 정의할 수 있겠군요.

우리는 이제부토 상속이란 클래스 관점으로 보면 됩니다. 클래스에는 변수와 메소드가 있죠. 클래스 입장에서는 이것이 재산이라고 생각하면 됩니다. 그래서 자식이 되는 클래스는 이런 재산을 코드의 추가없이 그대로 사용하거나, 변경해서 사용할 수 있습니다.




여기서 왜 우리는 상속을 해야하는 지 알 수 있습니다. 바로 코드의 재사용성을 더욱 확대하는 것이지요. 그렇다면 그냥 복사, 붙여넣기를 하면 되는 것이 아닌가? 라고 생각할 수도 있지만, 코드의 변경 역시 신경써야합니다. 그리고 관리도 복잡하게 되구요.


가장 중요한 것은 상속을 통해서 다형성을 만족시킬 수 있다는 점입니다. 이 부분에 대해서는 이 다음에 알아보도록 하지요.


extends 

상속을 하는 방법 아래와 같습니다.


class 자식클래스 extends 부모클래스


어떤 클래스를 정의함과 동시에 extends 예약어로 다른 클래스를 포함하게 된다면 extends 이후에 나오는 클래스를 상속하게 되는 것입니다.


어떤 클래스는 자동적으로 Object클래스를 상속했다고 했는데, 그럼 이런 형태일까요?


class 자식클래스 extends 부모클래스 extends Object


아닙니다. 자바에서는 클래스의 다중상속(여러 부모클래스를 두는 것)을 허용하지 않습니다. 이런 경우에는 이미 부모클래스가 Object클래스를 이미 상속받고 있기 때문에 자식클래스는 부모클래스를 상속받음으로써 Object클래스의 메소드를 사용할 수 있는 것입니다.


이렇게 되는 것이죠.


class 부모클래스 extends Object { ... }

class 자식클래스 extends 부모클래스 { ... }





코드를 통해 알아보자.

이쯤되면 코드를 볼 타이밍이겠군요.



class Parent{
	int age=45;
	String name="Parent";
	public Parent(){
		System.out.println("Parent Default Constructor");
	}
	public Parent(int _age,String _name){
		age=_age;
		name=_name;
		System.out.println("Parent Constructor");
	}
	public void showInfo(){
		System.out.println("Name:"+name+", age:"+age);
	}
}

class Child extends Parent{
	public Child(int _age, String _name){
		System.out.println("Child Constructor");
	}
}
public class Main {

	public static void main(String[] args){
		Child a=new Child(25,"REAKWON");
		a.showInfo();
	}
}


우선 Child 클래스는 Parent 클래스를 상속받고 있군요. 


코드에 대해서 간단하게 요약하면 Parent 클래스는 아래와 같은 특징이 있군요.


Default 생성자를 갖고 있다.

매개변수를 갖는 생성자도 있다.

showInfo라는 메소드가 있다.


Child클래스는 그저 매개변수를 받는 생성자가 다에요.


main메소드에서는 Child 객체를 초기화값을 넣어 생성자를 호출합니다. 어떤 결과가 나올까요? 결과를 봅시다.


Parent Default Constructor

Child Constructor

Name:Parent, age:45


어? 부모클래스의 생성자를 우선 호출하는 군요. Default 생성자를 호출하네요.

아아, 우리가 아무 클래스를 상속하게 된다면 부모클래스의 디폴트 생성자를 우선 호출하는 군요.


그리고 우리는 Child클래스에서 showInfo라는 메소드를 만들지 않았음에도 불구하고 main함수에서는 사용하고 있습니다. Parent 클래스를 상속 했기 때문이죠.


근데 좀 부족해요. 뭔가 2%만족이 되지 않는군요.


Child a=new Child(25,"REAKWON");


우리는 생성자를 통해서 age와 name을 바꾸고 싶다는 거죠. 어떻게 해야될까요?




super, this

우리는 위의 코드를 조금 변경해서 age와 name 변수의 데이터를 바꿀 수 있습니다. 바로 부모클래스의 데이터를 직접 변경하는 것이죠.


class Child extends Parent{
	public Child(int _age, String _name){
                age=_age;
                name=_name;
		System.out.println("Child Constructor");
	}
}

우리는 이미 Parent클래스를 상속받은 상태입니다. 그렇기 때문에 Parent클래스의 변수를 접근할 수도 있습니다(물론 부모클래스가 허용한다면).


그런데 우리는 이미 정의된 부모 클래스의 생성자를 다시 활용해보고 싶습니다. 


자식클래스에서 부모클래스를 super클래스라고 부릅니다. 그렇기 때문에 부모클래스의 변수나 메소드, 생성자에 접근할땐 super라는 키워드를 사용합니다.


class Child extends Parent{
	public Child(int _age, String _name){
                super(_age,_name);
		System.out.println("Child Constructor");
	}
}

이런식으로 부모클래스의 생성자(super( args))를 호출함으로써 구현해낼 수도 있습니다.

super. 을 눌러 어떤 변수와 메소드들이 있는지 확인해보세요. 부모클래스가 허락한다면 super클래스의 변수와 메소드를 직접 사용할 수도 있습니다.


이제 코드를 조금 더 변형시켜보도록 해서 실험해 봅시다.


class Child extends Parent{
	public Child(int age, String name){
                age=age;
                name=name;
		System.out.println("Child Constructor");
	}
}


이제 매개변수와 클래스 멤버 변수의 이름이 같습니다. 이런 경우에는 어떤 일이 벌어질까요?


결론은 클래스의 멤버 변수는 변하지 않습니다. 메소드가 호출할때는 매개변수나 그 안의 지역변수가 항상 우선순위를 갖기 때문입니다.


클래스에서 자기 자신을 가리킬 때는 this라는 키워드를 사용합니다. 그렇기 때문에 이런 코딩을 해야합니다.

class Child extends Parent{
	public Child(int age, String name){
                this.age=age;
                this.name=name;
		System.out.println("Child Constructor");
	}
}


this.age는 자기 자신의 변수 age를 명시적으로 나타냅니다. 어? 우리는 Child에는 age와 name이라는 변수가 없는데요?


아까 말했다시피 상속을 받았기 때문에 슈퍼클래스의 변수도 이제 Child 클래스의 것입니다. 그래서 super.age이건 this.age이건 age접근하는 것은 결론적으로 같습니다.




생성자는 어떨까요?

생성자 역시 다르지 않습니다.

this(args)를 통해서 자기 자신의 생성자를 호출 할 수 있습니다.


class Child extends Parent{
        public Child(){
                System.out.println("Child Default Constructor");
        }
	public Child(int age, String name){
                this();
                this.age=age;
                this.name=name;
		System.out.println("Child Constructor");
	}
}


this()를 보세요. 자신의 Default 생성자를 호출하고 있습니다.


다만, 한가지 주의해야할 점은 자기 자신의 생성자나 슈퍼클래스의 생성자를 다른 생성자 메소드에서 호출해야 될 때에는 호출할 생성자 메소드의 최상위에 코딩을 해야합니다. 


overriding

우리는 슈퍼클래스의 메소드를 우리 나름대로 다시 정의해보고 싶습니다. 그것도 역시 가능합니다.

우리는 부모가 정의한 메소드를 우리 입맛에 맞게 바꾸는 것을 Overriding이라고 합니다.




코드와 함께 보도록 합시다.


class Parent{
	int age=45;
	String name="Parent";
	public Parent(){
		System.out.println("Parent Default Constructor");
	}
	public Parent(int _age,String _name){
		age=_age;
		name=_name;
		System.out.println("Parent Constructor");
	}
	public void showInfo(){
		System.out.println("Name:"+name+", age:"+age);
	}
}

class Child extends Parent{
	public Child(){}
	public Child(int _age, String _name){
		super(_age,_name);
		System.out.println("Child Constructor");
	}
	@Override
	public void showInfo(){
		System.out.println("Child showInfo");
		super.showInfo();
	}
}
public class Main {

	public static void main(String[] args){
		Child a=new Child(25,"REAKWON");
		a.showInfo();
	}
}

Child클래스의 showInfo()를 보세요. 부모 클래스의 메소드를 오버라이딩한다는 것을 알리기 위해 @Override annotation으로 명시하고 있습니다. @Overriding이라는 annotation을 써주지 않아도 오류는 없지만, 우리는 컴파일러에게 부모클래스의 메소드를 오버라이딩한다는 것을 알려주어 원치않는 코딩(오타)을 막을 수 있습니다.


또한 Child클래스의 showInfo()는 super클래스의 showInfo() 역시 호출하며 코드를 줄이고 있습니다.


그 결과입니다.


Parent Constructor

Child Constructor

Child showInfo

Name:REAKWON, age:25



여기까지 상속에 대해 주저리 주저리 떠들어봤습니다.

아 힘들었다...

반응형
블로그 이미지

REAKWON

와나진짜

,

클래스(Class)


객체지향언어에서는 현실세계를 반영하기 위해 객체(Object)라는 개념을 도입하게 됩니다. 현실세계에서 보는 사람들, 자동차, TV 등이 객체지향언어에서는 객체로 표현이 됩니다. 


클래스란 객체를 생성하기 위해 그 객체가 어떤 데이터를 갖고 어떤 연산을 하는지에 대해 정의합니다.




클래스 정의

사람(Human)이라는 클래스가 있다고 칩시다. 사람은 눈, 코, 입이 있고, 손과 다리가 있겠죠. 이런 것이 클래스에서는 데이터입니다. 그리고 눈으로는 사물을 보고, 코로는 냄새를 맏고, 입으로는 말을 하거나 음식을 먹겠죠. 이것이 클래스 관점으로 보면 연산입니다.


간단히 이야기하면 멤버 변수와 멤버 메소드라고 기억하시기 바랍니다. 


코드로 정의를 해본다면 이렇게 될겁니다.


class Human{ String eyes="눈"; String ears="귀"; String nose="코"; String mouth="입"; void useEyes(){ System.out.println(eyes+"으로 봄"); } void useEars(){ System.out.println(ears+"로 소리를 들음"); } void useNose(){ System.out.println(nose+"로 냄새를 맡음"); } void useMouth(){ System.out.println(mouth+"으로 욕을 함"); } }


class라는 키워드로 Human이라는 클래스를 정의하고 있습니다. 클래스 안에는 데이터가 있고, 이 데이터들을 활용하여 행동을 정의하고 있습니다. 하지만 이렇게 클래스를 정의했다고 해서 바로 써먹을 수 있는 것은 아닙니다.




객체생성

자바에서는 모든 데이터를 객체로 취급합니다. 클래스를 정의한 이후에 클래스를 통해서 객체를 생성해야합니다. 객체를 생성하는 방법은 new라는 키워드로 메모리에 할당을 해야하는데요. 간단합니다.


Human human=new Human();


클래스를 통해서 메모리에 생성을 했습니다. 이제 human이라는 객체!가 생성이 된것이죠. 

C++을 배웠다면 new라는 키워드가 익숙할 겁니다. 네, C++에서의 new와 거의 동일한 역할을 합니다. 하지만 우리는 객체를 쓰고 닫을때 C++에서와 같이 메모리를 해제하지 않아도 됩니다. 왜냐하면 Garbage Collector가 알아서 메모리를 해제해 주거든요. C++에 대해 배우지 않았어도 상관없습니다. 


우리는 앞서 클래스를 정의할때 메소드 들을 정의했었습니다. 그렇기 때문에 이 메소드들을 사용할 수 있는 것입니다. 물론 그 객체의 변수들도 바꾸어 줄 수 있습니다. 


아래처럼요.


public class Main { public static void main(String[] args){ Human human=new Human(); human.eyes="쌍꺼풀 수술한 눈"; human.useEyes(); human.useEars(); human.useNose(); } }


물론 데이터가 바뀌었으니, 메소드의 연산도 바뀔 수 있습니다.




생성자

우리는 new라는 키워드를 통해서 객체를 생성할 때 


Human human=new Human();


라는 표현으로 객체를 생성했습니다. 하지만 눈 여겨 보면 괄호가 있는 것을 확인 할 수 있지요.

분명 괄호를 쓰는 이유가 있을 겁니다. 메소드처럼 그 안에 데이터를 매개변수로 전달하거나 하는 이유가 있지 않을까요?


우리는 클래스로부터 객체를 생성할때 초기 데이터를 전달해줄 수 있습니다. 그것이 바로 생성자라고 합니다.


생성자는 객체가 생성될때 가장 처음 호출하는 메소드라고 보면 됩니다. 이 생성자는 꼭 호출이 됩니다. 그리고 객체가 생성되지요. 하지만 우리는 생성자를 선언하지 않았습니다. 하지만 객체를 만들 수는 있었죠.


왜 일까요? 우리가 명시적으로 생성자를 선언하지 않아도 자동으로 default 생성자를 알아서 자바가 정의해줍니다. 물론 아무 기능을 하지 않는 생성자로요.


생성자의 정의는 일반 메소드를 정의하는 것과 같지만


1. 반환값이 없습니다.

2. 생성자의 이름은 클래스의 이름과 정확히 같아야합니다.

3. 생성자는 매개변수에 따라 여러개가 정의 될 수 있습니다.

4. public이라는 접근제어자여야 합니다.


위의 Human은 아무 매개변수가 없는 Default 생성자가 자동으로 호출이 된 것입니다. 




아래 코드는 생성자를 통해서 객체를 초기화 시키는 방법을 보여줍니다. 

class Human{
	String eyes="눈";
	String ears="귀";
	String nose="코";
	String mouth="입";
	public Human(){ 
		//default 생성자
	}
	public Human(String _eyes, String _ears, String _nose, String _mouth){
		eyes=_eyes;
		ears=_ears;
		nose=_nose;
		mouth=_mouth;
	}
	public void useEyes(){
		System.out.println(eyes+"으로 봄");
	}
	public void useEars(){
		System.out.println(ears+"로 소리를 들음");
	}
	public void useNose(){
		System.out.println(nose+"로 냄새를 맡음");
	}
	public void useMouth(){
		System.out.println(mouth+"로 욕을 함");
	}
}


public class Main {

	public static void main(String[] args){
		Human human=new Human("라식한 눈","귀","코털 삐져나온 코","험악한 입");
		human.useEars();
		human.useEyes();
		human.useMouth();
		human.useNose();
	}
}


위에서 생성자를 정의한 규칙대로 생성자를 정의해주었습니다. 그리고 default 생성자를 명시적으로 정의해주었습니다. 


그래서 생성자가 2개인 것이군요.


객체를 생성할때 2번째있는 생성자로 객체를 초기화시켰습니다. 그렇다면 초기화된 데이터로 객체가 생성됐을까요?

결과를 보고 확인해보죠.

 


제가 설명한 대로 군요.

생성자 어렵지 않지요?


Object 클래스

우리는 단지 위 4개의 메소드만을 정의했습니다. 하지만 우리가 정의하지 않은 메소드들도 리스트에 나옵니다. 우리는 분명이 아래와 같은 hashCode, notify, wait 과 같은 메소드들은 정의하지 않았습니다. 이것들은 무엇일까요? 우리는 이 정체를 밝혀내기 위해서 우선 Object 클래스를 알아야합니다.



사실 위의 메소드들은 Object클래스의 메소드들입니다. 위의 메소드들이 Object클래스에 그대로 존재하고 있다는 것을 확인하고 있지요.





허나 왜 Object클래스의 메소드가 왜 내가 만든 클래스에 딸려 나오는 것이냐?

모든 클래스는 Object클래스를 자동으로 상속받게 됩니다.




상속(Inheritance)?

아직 이야기하지는 않았습니다. 간단히 말해서 어떤 클래스의 변수와 메소드들을 물려받는다고 지금은 그렇게 이해하시기 바랍니다. 상속을 쓰게 되면 코드의 재활용이나 유지보수가 더욱 더 쉬워집니다. 상속을 사용하는 방법은 클래스를 정의하기 전 중괄호( { )에서 extends 클래스명 을 통해 이루어 집니다.


그렇기 때문에 extends Object를 명시해주지 않아도, 우리는 Object클래스의 메소드를 사용할 수 있는 겁니다.


모든 클래스입니다. 모든 클래스는 Object 클래스를 상속받는다는 것을 기억하세요.


아직은 Object 클래스의 메소드들을 일일이 설명하는 것은 좀 오바인것 같습니다.

포스팅을 하면서 중간중간에 설명하는 것으로 하거나, 정 궁금하다면 자바 Doc를 보아주세요.



반응형
블로그 이미지

REAKWON

와나진짜

,