지난 시간 객체지향적 프로그래밍(OOP)의 개요와 기본이 되는 클래스에 대해서 배워다.
잠깐 정리를 하면, 클래스는 최소단위로서 가장 기본적인 설계도이다.
이를 통해 사용자는 어디서든 원리를 완벽히 알지 못해도 해당 프로그램을 사용할 수 있게 된다.
이 원리를 몰라도 사용할 수 있는 것이 객체지향이 절차지향과 다른 방향성 중 하나다.
마치 우리는 Tv 설계도만 가지고 있으면, 언제든지 Tv를 만들어 사용하는 것처럼 말이다.
그래서 오늘은 객체지향의 방향성에서 나오는 “encapsulation(캡슐화)”라는 속성을 통해 기본문법을 배워보자.
- 정보은닉
- 접근제한자
Getters
&Setters
- 암묵적 문법
this
constructor
static
과 정적변수
우리가 지난 시간 배운 클래스의 기본적인 형태는 다음과 같다.
public class Tv {
// 속성
int price;
int size;
String brand;
String brand1;
String brand2;
// 기능
void powerOn() {}
void powerOff() {}
void channelUp() {}
void channelDown() {}
}
여기서 보면 속성은 변수의 형태를 가진다. 이를 우리는 멤버필드
또는 멤버변수
라고 부른다.
근데 우리가 배울 때는 {}
안의 있는 것은 지역변수라고 배웠다.
하지만 그건 OOP를 배우기 전엔 멤버변수를 다룰 일이 없기에 그랬을 뿐이다.
그래서 정확히 변수에 대해서 구분하면 메모리의 영역에 따른 변수의 “생성 주기”를 알아야 한다.
지역변수 : method
안에 생성된 변수로 자신이 속한 method
와 실행-종료
가 일치하는 변수
멤버 변수 : 특정 클래스를 구성하는 변수, 객체의 생성주기와 일치한다.
멤버변수는 객체가 new
를 통해 생성될 때, 함께 생성된다.
즉, 멤버변수는 heap
에 생성되고 지역변수는 stack
에 만들어진다.
그럼 클래스를 구성하는
method
는 어떻게 만들어질까?
이에 대해서는 static
을 짚고 넘어가야 되지만, 그건 오늘의 후반부에 다룰 것이다.
기본적으로 클래스 method
는 new
를 사용하면 RAM
의 Text
영역에 동작 정보를 적재된다.
그리고 이는 인스턴스 생성 시, 암묵적인 다른 변수와 가상 함수 테이블 통해 링크된다고 한다.
이때, 인스턴스를 몇 개를 만들어도 method
는 한 개를 공유한다.
이 연결을 통해 코드에 접근하고 Call
하는 순간 Stack
에 고유영역을 만들어 작업을 실행한 뒤, 반환 후 Stack
에서 사라지면서 종료된다.
따라서 일단 인스턴스가 생성되어 연결되어 있으면, 언제든 method
를 사용할 수 있다.
이렇게 우린 클래스를 사용하기 위해선 우리는 new
를 사용하여 heap
에 인스턴스를 생성하고 참조변수를 통해 접근하여 사용했다.
그러나 우리가 실상 접근하여 사용되는 클래스 파일을 보면, 오픈소스를 제외하고는 쉽사리 읽어낼 수 없다.
이는 클래스 파일은 소스코드가 적힌 것이 아닌 이진파일로 변환되어 사용자에게 배포되기 때문이다.
즉, 우리는 공개되지 않은 소스 코드에는 접근할 수 없다. 이는 앞서 설명한 것처럼 객체지향 프로그램의 특징 중 하나인 ‘캡슐화’ 때문이다.
그럼 캡슐화는 무엇일까?
비유하면 캡슐화는 우리가 약국에서 약을 사먹는 행위와 똑같다.
감기에 걸려서 약국에서 약을 사먹는다고 하자.
근데 그 과정에서 약이 어떤 성분을 포함하는지, 정확히 그 성분이 무슨 작용을 하는지
또 공정과정은 어떻게 되고, 돈을 얼마가 투입되는지, 이런 것을 고민하지 않는다.
즉, 그 원리 때문이 아닌 감기를 낫게 하는 기능이 있는 ‘감기약’을 복용하는 것이다.
이처럼 객체지향프로그램이 지향하는 것은 사용자가 그 원리보단 기능을 알고 이용하길 권장한다. 따라서 사용자에게 그 원리를 숨겨 기능에 집중하게 하는 속성이 ‘캡슐화’이다.
이로 인해 포인트는 ‘숨긴다’에 자연스럽게 맞춰진다. 그럼 어떻게 숨기는가?
바로 오늘 배울 객체지향 기법인 ‘정보은닉’을 사용해서 숨긴다.
캡슐화에서 정보은닉은 “외부에 노출될 필요가 없는 것과 노출할 것을 키워드로 숨기고, 보여줘라.” 라는 원리로 작동한다.
곧, 사용자가 .
을 통해 사용하고 싶은 객체의 기능을 숨기거나 노출 시키는 기능이다.
그리고 정의를 보면 이는 ‘키워드’를 통해서 가능하기 때문에 자연스럽게
접근 제한 키워드를 살펴보게 된다.
public
| protected
| package
| private
접근 제한 키워드는 위와 같이 네 가지가 있는데, 주로 public
private
를 사용한다.
이는 멤버 필드와 method
에 적용하는 키워드들로 사용될 때, 다음과 같은 의미를 가진다.
public
: 동일 프로젝트 파일 어디서나 접근이 가능.
private
: 소속 클래스 외에는 접근할 수 없다.
곧, public
이 붙은 멤버 변수와 method
들은 우리가 참조연산자를 통해서 언제든지 사용할 수 있게 된다.
하지만 private
이 붙은 것들은 외부에 노출되지 않고, 사용하고자 한다면 not visible
이라는 에러가 발생한다.
따라서 private
이 붙은 변수와 method
를 사용하려면 작성된 클래스 내부에서만 연산하고 처리할 수 있게 된다.
근데 여기서 한 가지 드는 의문은 “왜 숨기지?”라는 것이다.
이에 대해서 설계도를 작성하는 입장인 ‘개발자’와 사용하는 입장인 ‘사용자’의 측면에서
의미를 가진다.
첫째, 개발자 입장에선 클래스를 에러 없이 안전하게 사용하도록 하기 위해서이다.
예를 들어 Tv의 채널 정보를 생각해보자. 사용자 입장에서는 채널이 우리에게 노출된 정보로서 마음대로 쓸 수 있다고 생각하지만 실상은 개발자가 일정 채널 이상으로 접근하는 것을 막아뒀다.
즉, 사용자가 채널에 음수값을 집어넣을 경우 Tv에는 자연스럽게 에러가 발생할 것이다.
따라서 개발자가 의도치 않은 상황을 사용자가 할 경우, 클래스 자체에 문제가 발생할 수 있기 때문에 이를 위해서 ‘정보은닉’을 사용하는 것이다.
둘째, 사용자 입장에서 편의를 제공하기 위함이다.
하나의 클래스를 구성하는 method
는 크기나, 프로젝트 단위에 따라서 무수히 많아진다.
이 중에는 분명 사용자가 건드리면 안 되는 기능들과 정보가 있을 것이다.
그러나 이것이 자연스럽게 사용자에게 공개된다면?
만든 사람은 몰라도 사용하는 사람은 무엇을 만져야할지 고민하게 될 것이다.
따라서 개발자가 사용자의 편의를 위해, 사용할 수 있는 기능들을 노출하고 사용하면 안되는 기능을 숨기는 것이다.
추가적으로 package
는 기본값으로 설정되는 것으로 일반적이 폴더이다.
그래서 우리가 프로젝트 규모로 들어가면 파일이 매우 많아지게 되고 이를 관리하기 위해서 종류별로 나눠서 클래스를 모아놓을 때 사용되고, 자신과 같은 패키지에서만 접근을 허용하도록 할 수 있다.
그런데 막아 놓으면, 사용할 때 필요한 정보를 어떻게 입력하지?
다음과 같은 코드가 있다.
private int a;
public int b;
클래스 안에 있는 멤버필드 두 가지를 외부에서 사용하고자 한다면, 사용자에게 노출되는 것은 int b
만 있을 것이다.
그런데 어쩌다가 외부에서 int a
의 값을 수정하거나 입력해야 될 경우가 생길 수도 있다.
또는 그 값을 확인해야될 경우도 분명 존재할 것이다.
그럴 때, 객체지향프로그래밍은 이를 기능으로 구현하라고 한다.
바로 Setter
와 Getter
라는 기능으로 말이다.
Getters
& Setters
이는 하나의 관습으로 자바 개발자들 사이에서 통용되는 ‘이름 정하기’ 정도라고 할 수 있다.
하지만 너무 많이 그리고 중요하게 사용되기 때문에 이젠 하나의 문법이 되어 있다고 볼 수 있다.
먼저 관습적으로 값을 넣는 기능
을 만들 때는 ‘set’이라는 이름을 쓰고, 그 뒤에는 필드명을 써준다.
private String name;
public void setName(String name) {
this.name = name;
}
이런 기능을 구현할 때는 공통적인 부분이 있다.
void
이다. 이런 공통점이 생기는 이유는 첫째, 입력을 받기 때문에 입력받을 공간이 필요하기 때문에, 둘째, 입력만 받을 뿐 값을 돌려주지 않기 때문이다.
그리고 이것들을 묶어서 Setters
라고 부른다.
이와 반대로 값을 꺼내오는 기능
을 만들 때는 get필드명
을 사용한다.
public String getName() {
return name;
}
그리고 공통적인 특징은 setter
와 반대로 매개변수가 없고, 리턴값이 존재한다.
이런 Getters
Setters
가 좋은 클래스를 설계하기 위한 정보은닉이라는 기본 문법이라고 할 수 있다. 그리고 실 개발에 있어서 멤버필드는 의무적으로 private
이기 때문에, Setter
Getter
를 만들고 사용하는 습관을 가져야 한다.
그런데 한 가지 모순이 있다.
값을 입력받는 Setters
을 사용할 때, 웬만하면 매개변수는 멤버필드의 이름과 똑같이 설정한다. 그런데 int a = int a;
를 하게 된다면 값을 입력받아도 의미가 없게될 것이다.
물론 원리를 따지면 멤버 필드와 매개변수의 메모리 영역은 다르기에 문제는 없을 것이다.
그러나 막상 컴파일러에선 같은 값을 넣는 행위라고 주의를 주게 된다.
이런 입력의 입장에서 애매함을 해결한 키워드가 this
이다.
1) this.
앞선 문제를 해결하기 위해서 this
라는 변수 겸 키워드를 사용하는데, 이는 다음과 같은 의미를 가진다.
this.
: 멤버 필드로 연결한다.
만약 이게 없으면 자연스럽게 처리는 가까운 변수로 이어지게 될 것이다.
그럼 this
를 찍는 것이 왜 멤버 필드와 연결되는지 그 원리를 알아보자.
정확히 this
는 클래스 안에 숨겨져 있는 ‘자동 생성 변수’ 이다.
이는 자바 자체에 숨겨져 있는 ‘암묵적 문법’으로 작성자 입장에선 숨겨져 있는 것이다.
그래서 정확한 형태는 private final this;
이다.
근데 상수이기 때문에 자료형을 써야 하는데, 자료형이 보이지 않는다. 이는 this
가 자기 참조변수이기 때문이다.
흔히 heap
에 만들어진 인스턴스를 사용하려면 stack
의 참조변수에 객체의 주소를 저장한다. 이렇게 되면 인스턴스 자신은 본인이 어디 있는지 모르게 된다.
그러므로 자신의 주소값을 인스턴스 내부의 변수에 저장한 게 this
이다.
즉, this
의 입력된 값이 자신의 주소이다.
따라서 클래스 내부에서 private
로 설정된 것을 사용할 때 this.
을 하면 private
도 나타나게 된다. 결과적으로 this
자체의 자료형은 해당 클래스로서 생략된다.
이제 사용되는 매개변수는 지역변수와 같이 ‘자신이 속한 곳에서만 존재한다’라는 성질을 지닌다. 따라서 일시적으로 stack
에 생성되는 매개변수는 결코 heap
의 멤버필드와 겹치지 않기 때문에 이런 원리를 이용하여 this
를 사용한다.
2) constructor
의 명시적 사용
암묵적 문법을 배운 김에 또 다른 암묵적 문법인 Constructor (생성자)
에 대해서 알아보자.
기본 클래스를 구성하는 요소는 다음과 같다. 그런데 여기서 3, 4는 우리가 따로 본 적이 없는 것인데 우리는 이 중 3번째인 ‘생성자’만을 다루고자 한다.
1. Member Field
2. Member Method
3. Constructor
4. Nested Class
Constructor (생성자)
는 사실 좀 특이한 method
일 뿐 특별하진 않다. 그리고 기본적으로 클래스 내부에 숨겨져 있다. 우리는 이 숨겨져 있는 method
를 명시적으로 바꿔서 사용하고자 하는데, 다음과 같은 규칙이 적용된다.
1. `method` 의 이름은 클래스의 이름과 동일해야 한다.
2. 생성자 `method` 는 반환 값을 가지지 않는다.
3. 생성자 `method` 는 `Call` 되는 시점이 이미 정해져 있다.
4. 그 외 규칙은 일반 `method` 와 동일하다.
5. 생성자는 우리가 작성하지 않을 경우 기본 생성자가 탑재되고, 기본값이 우선된다.
6. 기본이 먼저 실행된 뒤에, 명시적 생성자가 실행된다.
그럼 생성자의 기능은 무엇일까?
우리가 인스턴스를 생성하면 기본 문법에 따라 멤버필드의 초기값이 0 (null)
로 되어 있다.
그래서 setter
를 사용해서 값을 원래 넣어줘야 한다.
하지만 생성자가 있으면, 객체 생성과 함께 값을 넣어줄 수 있다.
생성자는 어떻게 호출하는가?
돌아보면 이미 인스턴스가 생성될 때, new Monitor();
로 함께 호출된다.
그래서 다음과 같이, 매개변수를 설정해서 this
로 본인한테 넣어준다.
public Student(String name, int kor, int eng, int math) {
this.name = name;
this.kor = kor;
this.eng = eng;
this.math = math;
}
따라서 구분하면 생성자는 초기값을 설정할 때, Setter
는 값을 바꿀 때, 사용하는 것이다.
그래서 클래스의 값을 입력할 때는 다음과 같은 우선순위가 존재한다.
1. 개발자가 클래스의 멤버필드에 직접 입력한 기본값으로 설정되어 실행된다.
2. 명시적 생성자로 그 값을 덮어씌울 수 있다.
3. 생성 후엔 언제든지 `Setter` 을 이용해 값을 바꾼다.
물론 생성자는 필수적이지 않다. 다만 우리가 명시적으로 설정하지 않는다면 기본 생성자가 암묵적으로 탑재되어 있다. 그리고 명시적으로 사용하게 될 경우 기본 탑재는 사라진다.
하지만 이 경우에는 매개변수를 안 넣으면, 객체 생성이 불가하다. 따라서 매개변수도 안 넣고 싶을 땐, 일반 method
와 규칙이 같기에 오버로딩 하면 된다.
public Student() {
}
public Student(String name, int kor, int eng, int math) {
this.name = name;
this.kor = kor;
this.eng = eng;
this.math = math;
}
여기서 주의할 것은 우선순위이다. 곧, 명시적으로 입력받지 않은 값은 클래스 내부적으로 연산을 하였을 경우, 개발자가 먼저 지정한 기본값들을 통해서만 연산이 되게 된다.
private int sum = this.kor + this.eng + this.math;
static
과 정적변수우리가 method
를 작성할 때, 기본적으로 static
와 함께 입력하였다. 이는 static
이 붙은 method
들은 프로그램이 실행될 때, 가장 먼저 실행되기 때문이다.
그래서 main()
앞엔 static
이 붙어있을 수 밖에 없다. 물론 사건순서상 static
이 main()
보다 먼저 실행한다 할 수 있지만, 모든 프로그램은 main()
에서 실행되기 때문에 같이 실행된다고 이해하면 될 것이다.
이런 static
은 method
뿐만이 아니라 변수에도 사용할 수 있다. 물론 매개변수, 지역변수에는 사용할 수 없다. 그래서 static
은 클래스 내부의 멤버 필드와 함께 사용할 수 있는데, 이 경우의 변수는 class member field
라고 한다.
하지만 주의해야할 것은 클래스 안의 멤버지만, 동시에 멤버변수가 아니다.
그럼 다음 두 코드를 통해 비교해보자
public class Temp {
public int a; // instance member field
public static int b; // class member field
}
위의 a
는 인스턴스가 new
로 생성되어야만 만들어진다.
그러나 b
는 프로그램 실행시. 즉, main()
가 실행되면서 함께 생성되어 있다.
이 원리로 인해 설계도 안에서 static
이 있으면 인스턴스 생성 없이도 사용할 수 있다.
해당 인스턴스의 method
와 변수를 사용할 수 있다.
따라서 static
이 붙는 클래스(정적)변수나 method
를 이용하기 위해선 다음과 같이 사용해야 한다.
Temp.b;
Math.random();
결과적으로 클래스 변수는 생성주기가 더 길고, 인스턴스의 관계없는 Data 메모리에 생성된다.
단, 프로그램이 실행될 때 1번만 만들어진다. 따라서 우리가 같은 인스턴스를 여러개 생성해도 단, 1개의 정적 변수를 공유하여 사용하는 성질을 주의해야 한다.
public static int a = 0;
Calculator a1 = new Calculator();
Calculator a2 = new Calculator();
Calculator a3 = new Calculator();
Calculator.a = 10;
System.out.println(a1.a);
System.out.println(a2.a);
System.out.println(a3.a);
위의 경우는 접근제한자가 public
일 때 얘기다.
반대로 private static
을 하면 같은 객체 내부에서만 공유하는 변수가 된다.
private static int a = 0;
public int A() {
this.a = 10;
return a;
}
public int B() {
return a;
}
System.out.println(a1.A()); // 10
System.out.println(a1.B()); // 10
이렇게 메모리에 따른 변수의 생성 위치와 주기에 대해서 이해를 하고 코드를 작성해야 한다.
다시 한번 변수들 정리해보면 자바에는 총 4가지의 변수가 있다.
지역, 매개변수 (Stack)
method
내부에 포함되어 method
가 호출되면 만들어지고, 끝나면 사라진다.
그래서 중간에 남는 공간을 컴퓨터가 자유롭게 활용할 수 있다.
멤버필드 (Heap)
인스턴스 안에 포함된다.
그래서 인스턴스가 생성과 종료와 함께 하기 때문에 활용 공간이 좁다.
정적 변수 (Data)
메모리 반환 없이 프로그램 실행부터 끝까지 있기에 메모리 효율이 안 좋다. 이를 인지하고 사용해야한다.
마지막으로 Static
인 변수와 method
는 Static
안에서만 사용해야 한다. 다음의 코드를 보자.
public int a;
public static int b;
public static void funcA(){
a = 10;
b = 20;
}
public void funcB(){
a = 10;
b = 20;
}
위 코드를 보면, funcA
의 a
에서 에러가 발생한다.
앞서 말한 변수의 생성주기와 method
를 따지자면, a
는 인스턴스 생성시에만 사용할 수 있다. 그러나 funA
는 프로그램 실행과 함께 만들어진다.
따라서 컴파일러 입장에선 인스턴스를 만들 수도, 없을 수도 있기에 에러를 일으키는 것이다.
funA
안에서 a
는 확정적으로 사용될 수 없다.
반대로 funcB
멤버 method
로 객체 생성 시에만 작동을 하기에 정적변수 b
와 멤버변수 a
는 무조건 사용할 수 있다.
곧, 확실하게 fucB
가 실행되는 시점에 이미 b
는 존재하고, a
도 인스턴스의 생성과 함께 만들어진다.
결과적으로 Static
안에서는 Static
이 아닌 것들을 사용할 수 없다.
같은 원리로 동일 클래스 안에서 main()
에서 멤버 method
를 사용하고 싶으면 인스턴스 생성하여 사용하거나, static
method
로 만들면 된다.