이전 파트는 OOP의 기본 단위인 '클래스'를 알아봤다면,
이번 파트는 클래스의 세팅과 각 클래스 간의 관계를 만드는 법에 대해 배운다.
OOP의 꽃은 '다형성'이다. 이번 주제 중 가장 중요한 것으로 꼭 이해하고 넘어가야 한다.
다형성과 추상을 통해서 우리는 하나의 '규격'을 만들고, 이를 구현함으로서 OOP의 본질을 살려낼 수 있다.
따라서 인터페이스는 꼭 이해해야 된다.
상속은 기존 클래스를 재사용해, 새 클래스를 만드는 것
예) 설계도 재탕
코드 재사용성이 본질 같지만 이는 반절만 정답이다. 상속의 나머지 본질은 조상 클래스에 정의된 코드를 확장시키는 것이다.
기존 작성된 코드를 재사용하기 때문에, 적은 코드로 새 클래스를 만들 수 있다.
상속 관계 안의 코드는 공통적으로 관리할 수 있다.
프로그램의 '유지 보수', '확장'의 면에서 중요하다.
- 생성자와 초기화 블럭은 상속되지 않는다.
- 자손 클래스의 멤버수는 조상보다 같거나 많다.
- 자손 간의 연결은 되지 않는다.
상속 구현
class Parent {}
class Child extends Parent{
// 내부 구현...
}
상속은 extends
을 사용해서 상속 관계를 구현한다.
조상 클래스 : 상속하는 클래스(Parent)
자손 클래스 : 상속받는 클래스(Child)
조상 클래스의 멤버 상속
상속 관계 안에서 자손 클래스는 조상 클래스의 '모든 멤버(변수, 메서드)'를 상속받는다.
즉, 조상에 정의된 코드가 자동으로 따라온다.
class Parent {
int age;
}
class Child extends Parent{}
/*-----실행 코드-----*/
public static void main(String[] args) {
Child c = new Child();
c.age = 10;
System.out.println(c.age); // 10
}
Parent의 멤버변수 age
는 존재하고, 이를 상속받는 Child 내에도 존재한다.
물론 코드 상에서는 Child에 적혀있진 않다. 그 이유는 사진처럼 두 클래스의 관계에서 Parent가 부분집합이기 때문이다.
그래서 실제 Child인스턴스를 생성하면, 내부에 age
라는 인스턴스 변수를 제어할 수 있다.
자손 클래스의 멤버 추가
이번엔 반대로 자손 클래스에 멤버를 추가해보았다.
class Parent {
int age;
}
class Child extends Parent{
void play(){
System.out.println("play");
}
}
/*-----실행 코드-----*/
public static void main(String[] args) {
Parent p = new Parent();
p.play(); // 에러
}
play()
라는 메서드를 사용할 수 없는 것이다.따라서 자손 클래스는 조상 클래스보다 멤버가 같거나 더 많다. 상속을 받을 수록 멤버들이 늘어나기 때문이다. 그런 점에서 상속을 구현할 때, extends(확장)이라 쓰는 것이다.
자손 클래스 간의 관계
Parent를 상속받는 자손 클래스를 하나 더 만들어보자.
// 조상
class Parent { /*...이전 예제 참조*/ }
// 자손1
class Child1 extends Parent{
/*...이전 예제 참조*/
}
// 자손2
class Child2 extends Parent{
/*...생략*/
}
자손의 자손 클래스
그렇다면 자손을 상속받는 자손 클래스는 어떨까?
// 조상
class Parent { /*...이전 예제 참조*/ }
// 자손1
class Child extends Parent{
/*...이전 예제 참조*/
}
// 자손의 자손
class GrandChild extends Child{
/*...생략*/
}
클래스 간 중복된 코드를 최소화하여, 한 군데(조상)에서 관리할 수 있다.
이 경우 조상의 멤버를 변경하면 상속받는 자손도 모두 바뀌기 때문에 유지보수에 있어 안정적이다.
자손 클래스는 자기 배부에 정의된 멤버들만 관리하면 되서 코드가 줄어들고, 관리가 쉬워진다.
제일 중요한 건, 자손 인스턴스는 내부에 조상 인스턴스도 함께 생성되어 합쳐진 인스턴스이다..
그래서 조상 인스턴스를 따로 생성하지 않고, 멤버들을 사용할 수 있다.
포함관계는 상속을 사용하지 않고 클래스 간의 관계를 만들어주는 방법이다.
class Circle {
int x;
int y;
int raidus;
}
위와 같은 원을 표현하는 클래스가 있다고 가정하고, 저 안의 좌표값만 따로 클래스로 빼서 사용해보자.
class Circle {
Point p = new Point();
int raidus;
}
class Point{
int x;
int y
}
포함관계는 한 클래스의 멤버변수로 다른 클래스의 참조변수를 선언하는 것이다.
포함관계를 이용하면 좌표값처럼, 단위별로 묶어서 클래스를 만들어 멤버변수를 관리할 수 있다.
역으로 단위별로 묶인 클래스들은 다른 클래스에서 필요 시 포함관계로 재사용할 수 있다.
단, 참조되는 클래스의 변경 시 참조하는 클래스의 코드도 수정해야된다.
클래스 간 관계 결정 Tip
실제 코드 자체는 별반 다른 게 없다. 참조해서 쓸건지, 상속받아 쓸건지 그 차이일 뿐이다.
// 포함
class Circle {
Point c = new Point();
int r;
}
// 상속
class Circle extends Point {
int r;
}
class Point{
int x;
int y
}
그럼 어떻게 구분해야될까?
상속과 포함을 결정하는 방식은 '문장'으로 만들어 보면 쉽다.
// 도형
class Shape { /*생략*/}
// 점
class Point { /*생략*/}
// 원
class Circle extends Shape { // '원은 도형이다' ▷ 상속관계
// '원은 점이 있다' ▷ 포함관계
Point p = new Point();
}
C++에서는 여러 조상 클래스를 상속받는 '다중 상속'이 가능하다. 하지만 자바는 이를 막았다.
그래서 자바에선 '단일 상속'만 가능하다.
다중 상속이 가능한 건 인터페이스...
그럼 다중 상속이 뭐가 좋은걸까?
다중상속이 가능하면, 여러 가지 기능을 가진 클래스들을 상속받아서 복합적인 클래스를 쉽게 만들어 낼 수 있다.
근데 자바가 이를 막은 건, 그 단점인 '동명의 인스턴스 멤버'를 구별할 방법이 없다는 것이다.
따라서 자바는 중복된 이름에 의한 충돌을 막기 위해서 '단일 상속'만 허용한다.
그나마 가능하려면, 오버로딩이나 메서드명 변경이 필요할텐데 이를 사용하는 것도 비효율적이다. 그래서 자바는 코드의 신뢰성을 높이기 위해 '단일 상속'을 선택한다.
꼼수로 상속-포함 관계를 이용해 다중 상속을 구현하는 방법도 있다.
class Tv{
boolean power;
int channel;
void power(){ power = !power; };
void channelUp(){ ++channel; };
void channelDown(){ --channel; };
}
class VCR{
booelean power;
int counter = 0;
void power(){ power = !power; }
void play(){ /*구현*/ };
void stop(){ /*구현*/ };
void rew(){ /*구현*/ };
void ff() { /*구현*/ };
}
class TVCR extends Tv{ // 단일상속
VCR vcr = new VCR();
void play(){
vcr.play();
}
// ...
}
Tv의 멤버는 상속받아서 사용하고, VCR의 멤버 메서드들은 포함관계를 통해 TVCR의 실제 메서드로 실행되게 한다.
VCR의 메서드를 변경하면, TVCR의 메서드도 함께 변경되어 상속의 효과를 낼 수 있다.
Object 클래스는 모든 클래스의 조상 클래스다.
즉, 모든 클래스는 Object클래스를 자동 상속받는다.
Object의 멤버들을 모든 클래스들이 사용할 수 있다.
toString(), equals(Object o)
Object o의 다형성 때문에 equals()가 가능하다.
자손은 조상 클래스의 멤버만 상속받는다. (생성자, 초기화 X)
자손 인스턴스가 생성되면 내부에 조상 인스턴스도 생성된다.
그래서 자손 안에서 부모의 멤버를 사용할 수 있다.
상속이 늘어날 수록 '기능이 확장된다.' (extends)
포함관계는 다른 클래스의 참조변수를 멤버 변수로 사용해서 구현한다.
자손 클래스에서 부모 클래스의 메서드를 오버라이딩(재정의)할 수 있다.
그러면 부모 클래스의 메서드가 아닌, 자손 클래스의 멤버 메서드로 실행된다.
다중 상속은 관계 안에서 동명의 멤버를 구별하기 어렵기 때문에, 자바는 단일 상속만 허용한다.
다중 상속은 상속과 포함을 적절히 사용해서 자바에서 구현할 수는 있다.
모든 클래스의 조상은 'Object'클래스이다. 컴파일러가 자동으로 추가해준다.
물론 다른 클래스에서 상속받게 작성되면, 추가하지 않는다.
조상 클래스로부터 상속받은 메서드를 재정의하는 것
class Point{
int x;
int y;
String getLocation(){
return "x :" + x + ", y :" + y;
}
}
class Point3D extends Point{
int z;
// 오버라이딩
String getLocation(){
return "x :" + x + ", y :" + y + ", z :" + z;
}
}
Point클래스는 x,y 2차원 좌표만
getLocation()
으로 출력한다.
하지만 상속받은 Point3D클래스는 3차원이기 때문에 오버라이딩되야 한다.
오버라이딩은 메서드의 구현부만 자손 클래스의 새로 작성하는 것이다.
따라서 선언부(메서드명, 반환타입, 매개변수)는 조상 클래스와 일치해야 한다.
선언부 외에도 '접근 제어자, 예외'는 제한 조건 안에서 변경이 가능하다.
접근 제어자는 조상 클래스의 메서드보다 좁으면 안된다.
접근제어자의 순서는 public, protected, (default), private
로 뒤로 갈수록 좁아진다.
예를 들어, 조상이 public이면 자손도 public메서드여야 한다는 것이다.
반대로 조상이 private면 자손은 'public, protected, (default), private' 모두 가능하다.
조상 클래스의 메서드보다 예외의 범위, 개수가 크면 안 된다.
에러가 발생하면, main()에서 코드 진행이 튕겨나가고 프로그램이 멈춘다.
이 에러를 처리하는 것을 예외 처리라고 한다. 이 중 throws 예외처리 클래스명
를 사용하는 것을 '예외 전가'라 한다.
예외 전가는 특정 에러를 감지하고 처리하는 기능을 구현한 '클래스'들을 사용한다.
이 클래스의 수가 조상 보다 많으면 안된다.
주의해야될 건, 무조건 수가 적다고 끝나는 게 아니다. 예외마다 담당하는 범위가 있는데
클래스의 조상이 Object인 것처럼, 예외의 조상은 Exception 클래스이다.
Exception은 예외의 범위가 가장 넓음으로, 조상의 예외 범위가 이보다 작을 경우 자손에서 사용할 수 없다.
인스턴스 메서드, static 메서드 간의 상호 변경은 불가능하다.
정적 메서드는 자손 클래스에서 똑같은 이름으로 만들 순 있다.
다만 이 경우는 '오버라이딩'이 아니다. 왜냐면 정적 메서드는 정의된 클래스에 묶여 있기 때문이다.
이름이 비슷할 뿐, 목적이 다르다.
super
멤버변수와 지역변수의 이름이 같을 떄,
this
를 사용한 것처럼super
는 상속받은 멤버와 자신의 멤버를 구분하는 지역변수이다.
조상 클래스(슈퍼 클래스)와 자손 클래스의 멤버를 구분하는 용도.
super는 조상 클래스의 멤버를 가리킨다.
물론 내부적으로 자손의 멤버이기 때문에 this도 사용 가능하다.
그래서 상속 관계에서 중복 정의된 멤버는 super로 구분한다.
super
에는 조상 인스턴스의 주소가 저장되며, this
와 함께 스택 프레임에 자동 저장되는 참조변수이다.
super
역시, 인스턴스 메서드에서만 사용이 가능하다. (클래스 메서드에선 불가)
super
// 조상
class Parent {
int x = 10;
}
// 자손
class Child extends Parent {
int x = 20;
void method(){
System.out.println("x= " + x);
System.out.println("this.x= " + this.x);
System.out.println("super.x= " + super.x);
}
}
// 실행
class Test {
public static void main(String[] args){
Child c = new Child();
c.method();
}
}
/*결과
x= 10
this.x= 20
super.x= 10
*/
super
로 구분이 가능하기 때문에, 조상에 선언된 멤버 변수는 자손에서 중복 선언이 가능하다.
super
super
가 있으면, 구분이 가능해서 자손 메서드의 내부에서도 호출이 가능하다.// 조상
class Point {
int x;
int y;
String getLocation(){
return "x : " + x + ", y : " + y;
}
}
// 자손
class Point3D extends Point {
int z;
// 오버라이딩
String getLocation(){
return super.getLocation() + ", z : " + z;
}
}
// 실행
class Test {
public static void main(String[] args){
Child c = new Child();
c.method();
}
}
/*결과
x= 10
this.x= 20
super.x= 10
*/
조상 메서드의 변경은 자손의 메서드에 자동으로 반영되니, 유지보수에 유용하다.
super()
조상의 생성자
this()
: 같은 클래스의 다른 생성자를 호출super()
: 조상 클래스의 생성자를 호출1. 생성자를 쓰는 이유
자바의 객체는 초기화하지 않으면 생성할 수 없다.
그래서 생성자는 클래스에 한 개 이상 정의되야 한다.
또한, 생성자 없이는 인스턴스 생성 시, 외부에서 매개변수로 인스턴스 변수의 값을 넘겨줄 방법이 없다.
2. super()의 필요성
인스턴스 변수의 초기화는 해당 클래스가 처리해야 한다.
그래서 초기화블럭과 생성자는 상속되지 않는다.
상속에선 자손 인스턴스가 생성되면, 내부에 조상 인스턴스도 함께 생성된다.
따라서 자손에서 사용되는 조상의 멤버는 조상의 생성자로 처리해야 한다.
3. 생성자의 추가 조건
모든 클래스 생성자의 첫 줄에는 반드시 생성자를 호출해야 한다.
모든 클래스의 조상은 Object클래스이다. 즉, 모든 클래스는 숨겨진 상속관계가 있다.
이는 컴파일러가 자동 추가해준다.
따라서 모든 클래스는 super()
으로 자신의 조상 Object의 생성자를 호출한다.
이 super()
호출은 Object()까지 호출해야 끝난다.
super()
는 각 생성자의 첫 줄에서 호출된다. 크게 두 가지 이유가 있다.
중복되는 생성자 간에는 this()
를 사용해 한 생성자에서만 super()
를 호출하는 방법도 있다.
반대로 각 생성자가 중복되지 않을 땐, 생성자마다 super()
를 넣어줘야 한다.
다행히 super()
를 안 써도, 컴파일러가 생성자의 첫 줄에 super()
를 자동 삽입한다.
class Point {
int x;
int y;
Point(){
this(0, 10); // 다른 생성자 호출 → 조건 충족
}
Point(int x, int y){
this.x = x; // ← 첫 줄에 생성자가 없음으로 컴파일러가 super(); 자동 추가
this.y = y;
}
}
주의 : 조상에 기본 생성자가 없는 경우, 따로 추가하지 않으면 컴파일 에러가 발생한다.
4. 조상의 기본생성자가 없는 경우
// 조상
class Point { // 컴파일러가 extends Object를 추가한다.
int x = 10;
int y = 20;
// 따로 기본생성자 point()를 정의하지 않았다.
// 매개변수가 있는 생성자
Point(int x, int y){
// 컴파일러가 자동으로 super(); 추가한다. super()는 Object();을 의미
this.x = x;
this.y = y;
}
}
// 자손
class Point3D extends Point {
int z = 30;
Point3D() {
this(100, 200, 300); // 다른 생성자를 호출한다. super();가 없음
}
Point3D(int x, int y, int z) {
super(x, y); // 여기서 조상 클래스의 생성자를 호출한다.
this.z = z;
}
/*컴파일 에러 예시
Point3D(int x, int y, int z) {
super(); ◀ 따로 안적어도 컴파일러가 super();를 자동 삽입. 하지만 Point 클래스에 기본 생성자가 없기 때문에 컴파일에러가 난다.
this.z = z;
}
*/
}
// 실행
class Test {
Point3D p3 = new Point3D();
}
/* 생성자 호출 순서
Point3D(); → Point3D(int x, int y, int z); → Point(int x, int y) → Object();
*/
만약 조상 클래스에 매개변수가 있는 생성자만 정의해서, 기본 생성자가 없을 땐 super()
를 호출하면 컴파일 오류가 발생한다.
이 경우엔 조상 생성자 맞춰 매개변수를 넣어 호출하든지, 조상에 기본 생성자를 정의해서 해결한다.
오버라이딩은 상속받은 메서드를 재정의하는 것으로, 구현을 강제하거나 기능을 확장하는 용도로 쓰인다.
오버라이딩은 선언부가 일치해야 한다. (반환타입이 자손 클래스까지는 허용)
오버라이딩 시, 접근제어자와 예외 처리의 조건을 맞춰야 한다.
클래스 멤버는 자신이 정의된 클래스에 묶여 있다. 따라서 오버라이딩을 구현할 수 없다.
super
참조변수는 조상과 자손의 중복을 구분할 때 사용한다. (this와 같음)
인스턴스 메서드 안에서 인스턴스 멤버를 대상으로 사용이 가능하다.
자손에선 생성자의 첫 줄엔 부모의 생성자 super()
를 호출해야 한다.
컴파일러가 기본생성자를 컴파일 시, 추가한다.
컴파일러는 사용자 생성자가 하나라도 정의되면 기본 생성자를 추가하지 않는다.
그래서 조상 클래스에 기본 생성자가 없으면 컴파일 에러가 난다.
패키지는 클래스의 묶음이다.
패키지에는 클래스, 인터페이스를 포함할 수 있다.
서로 관련된 클래스끼리 그룹 단위로 묶어서 관리할 수 있다.
같은 이름의 클래스도 패키지가 다르면 존재할 수 있다.
그래서 클래스명의 풀네임에는 패키지가 포함된다. ex) java.lang.String
클래스(.class)가 물리적 파일인 것처럼 패키지도 물리적인 하나의 디렉토리다.
즉, 패키지는 클래스파일을 포함하는 '디렉토리'다.
패키지도 하위 패키지를 생성할 수 있다.
패키지와 패키지는 .
을 구분자로 하여 계층 구조를 구성할 수 있다.
하나의 소스파일에는 첫 문장으로 단 한 번의 패키지 선언만을 허용한다.
package 패키지명;
패키지명은 대소문자 모두 가능하지만, 클래스와 구분하기 위해 소문자가 원칙이다.
해당 소스파일에 포함된 모든 클래스나 인터페이스는 선언된 패키지에 속한다.
패키지 선언을 안하면 기본 제공인 '이름없는 패키지'에 속한다.
1. 패키지 생성
package pack1.pack2.pack3; // 패키지 선언
class PackageTest {
/*구현*/
}
예제는 pack1, pack2, pack3 총 3개의 패키지가 생성되고, pack3 안에 PackageTest 클래스가 생성되는 걸 의도한다.
이제 CMD에서 -d
옵션을 걸어, 해당 파일을 컴파일하면 선언된 패키지가 존재하지 않아도 만들어준다.
C:\OpenJDK\jdk-11.0.1\work>javac -d . PackageTest.java
-d
옵션 뒤에는 루트 디렉토리, 최상단 디렉토리의 위치를 선정해줄 수 있다.C:\OpenJDK\jdk-11.0.1\work
가 루트 디렉토리로 설정.2. 클래스패스
클래스패스(classpath)는 컴파일러, JVM 등이 클래스의 위치를 찾는데 사용되는 경로
위 예제에서 루트 디렉토리는 C:\OpenJDK\jdk-11.0.1\work
이다.
이걸 클래스패스에 포함시켜줘야, JVM이 PackageTest 클래스를 찾을 수 있다.
클래스패스 지정법 : 시스템 변수 설정
;
: 구분자 → 변수 값을 구별해서, 여러 개 넣어줄 수 있음.;
: 클래스패스를 따로 설정할 경우, 기본값이었던 현재 디렉토리가 사라짐..
도 추가해야됨..jar
파일은 클래스패스에 추가하려면, 파일명까지 적어줘야 된다.실행 시, 클래스의 패키지명(경로)까지 적어줘야 한다.
>java package1.package2.package3.PackageTest
-cp 사용할 클래스패스 패키지경로.클래스명
옵션을 사용하면 된다.컴파일러에게 소스파일에 사용된 클래스의 패키지 정보를 제공해서 패키지명을 생략
다른 패키지의 클래스를 사용하려면 패키지명까지 포함한 풀네임으로 클래스명을 적어야 된다.
import문으로 미리 패키지를 선언하면, 패키지명은 생략할 수 있다.
import문에 선언된 패키지를 컴파일러가 컴파일하면서 클래스명 앞에 붙여 준다.
같은 패키지 내의 클래스 간에는 생략이 가능하다.
선언 위치 : package문과 class 선언문 사이
package문
import문
public class 클래스명 {}
선언 방법
import 패키지명.클래스명;
import 패키지명.*;
*
는 지정된 패키지의 모든 클래스를 의미한다.import.java.*
◀ 이렇게 쓰는 거 아니다.static import문을 사용하면, static멤버를 호출할 때 클래스명을 생략할 수 있다.
import static java.lang.Integer.*; // Integer클래스의 모든 static 메서드
import static java.lang.Math.random; // Math.random()만. 괄호 안붙임.
import static java.lang.System.out; // System.out out 으로 참조가능
System.out.println(Math.random()); → out.println(random());
모든 클래스는 하나의 패키지 안에 속해있어야 한다.
패키지는 클래스 파일을 갖고 있는 디렉토리다.
동명의 클래스도 패키지가 다르면 가능하다.
클래스패스를 사용자 설정하면, 자동으로 현재 디렉토리를 사용하지 못한다.
기본 경로를 사용하면 지정 경로에 파일을 넣어주기만 하면 된다.
import문이 있으면, 다른 패키지의 클래스를 호출 시 패키지명을 생략할 수 있다.
클래스, 변수, 메서드 선언부에 함께 사용해서 부가적인 의미를 부여하는 예약어
종류
▶ 접근 제어자는 한개만 가능, 나머지는 하나의 대상에 대해 여러 제어자를 조합해서 사용 가능
'클래스의', '공통적인' → 인스턴스를 생성하지 않고 사용이 가능해진다.
앞에서 배운 것처럼, 클래스 멤버들은 인스턴스 간 공통된 값을 유지하는게 목적이다.
또는 인스턴스 멤버를 사용하지 않아서, 굳이 인스턴스 생성과정을 거칠 필요가 없다.
이러한 경우 static
을 멤버필드, 메서드, 초기화 블럭에 사용한다.
static
+ 멤버변수
static
+ 메서드
static
+ 초기화블럭{}
'마지막의', '변경될 수 없는' → 상수
final
+ 클래스
final
+ 메서드
final
+ 변수
생성자를 이용한 final멤버 변수 초기화
상수는 선언, 초기화를 같이 해서 변경을 막지만, 멤버변수인 경우 예외가 있다.
클래스 내부엔 선언, 생성자로 초기화하면 final 멤버변수도 초기화가 가능하다.
이렇게 하면 인스턴스마다 다른 값의 final 멤버변수를 가질 수 있다.
불가능하면 용도가 static 변수랑 별반 다를게 없다.
class Card{
final int NUMBER;
final String KIND;
Card(int num, String kind){
NUMBER = num;
KIND = kind;
}
}
'미완성' → 완성되지 않아서, 사용할 수가 없다.
abstract
를 붙인 메서드, 클래스는 사용할 수 없다.
정확히는 사용보단, 호출과 생성이 불가하다.
그래서 추상 메서드가 들어간 클래스는 '추상 클래스'가 되어야 한다.
abstract
+ 클래스
abstract
+ 메서드
일반 메서드의 방향은 기능의 '확장'이다.
이런 점에서 추상 메서드는 상속받는 쪽에서 해당 메서드를 '구현'하길 강요하는 용도로
사용할 수 있다.
멤버, 클래스에 사용해 외부(패키지, 클래스)로부터의 '접근'을 제한한다.
1. 종류
2. 사용 위치
클래스, 멤버변수, 메서드, 생성자
지역변수에는 못 쓴다.
대상 | 사용 가능한 접근제어자 |
---|---|
클래스 | public, (default) |
멤버 | public, protected, (default), private |
3. 접근 범위
제어자 | 같은 클래스 | 같은 패키지 | 자손 클래스 | 전체 | 설명 |
---|---|---|---|---|---|
public | O | O | O | O | 어느 곳에서든 접근할 수 있다. |
protected | O | O | O | X | 상속 관계 한정. 패키지 관계없이 접근 가능 |
default | O | O | X | X | 기본값. 같은 패키지 안에서만 접근 가능 |
private | O | X | X | X | 해당 클래스 내에서만 가능, 내부에 접근하려면 메서드를 사용해야됨. |
캡슐화
OOP에선 '데이터 보호'를 위해 캡슐화라는 방식을 사용하며, 이를 구현하기 위해 접근 제어자하를 사용한다.
캡슐화 자체는 데이터를 숨기는 것이다. 알약처럼 말이다.
캡슐화를 통해, 사용자는 클래스 내부 데이터에 맘대로 접근할 수 없다.
사용자에게 노출되는 정보 제한해, 과잉 정보로 인한 오판단을 방지한다.
리모컨 설명서에 리모컨의 모든 로직을 작성하면, 제대로 쓸 수 있는 사람이 몇명이나 될까?
즉, 외부에서 접근할 필요 없는 멤버를 숨김으로 복잡성을 줄일 수 있다.
위와 연결되어 내부에서만 쓰는 건 외부에서 접근 못하게 막는다.
유출하면 안되는 중요 로직들을 숨길 수 있다.
접근 범위에 따라서 오류 수정 시, 그 범위를 유추할 수 있다.
private
는 그 해당 멤버만 보면 된다. (자손 클래스에서도 접근이 불가함)
Getter&Setter
private로 제한된 멤버는 어떻게 외부로 어떻게 전달할까?
이를 위해 사용되는 것이 Getter, Setter형식의 메서드이다.
Getter : 외부로 값을 반환 / Setter : 내부의 값을 변경
class PvClass {
private int A;
// Getter
public int getA(){ return A; }
// Setter
public void setA(int A) {
this.A = A;
}
}
class PbClass {
public static void main(String[] args){
PvClass pv = new pvClass();
// Setter 호출
pv.setA(10);
// Getter 호출
System.out.println(pv.getA()); // 10;
}
}
생성자의 접근 제어자
Sigleton패턴이라 불리며, 인스턴스의 생성 개수를 접근 제어자를 통해서 제한할 수 있다.
외부에서 제한없이 인스턴스 생성이 가능하면, 같은 용도의 인스턴스가 동시다발적으로 생성되어 메모리가 낭비된다.
특히 DB관련 작업에서는 DB연결이 끊기는 상황도 발생한다.
그래서 싱글톤은 어떻게 구현하느냐?
1. 내부에서 인스턴스를 생성한다.
class Singleton{
private static Singleton instance = new Sigleton();
}
생성과 초기화를 외부에서 하면 애초에 싱글톤이 성립이 안된다.
따라서 내부에서 클래스가 로딩되면서 만들어지게 한다.
클래스가 로딩되면서, 힙 영역에 인스턴스가 생성되고 해당 인스턴스는 메서드 영역의 클래스 변수를 통해 접근이 가능하다.
인스턴스는 계속 연결되어 있기 때문에 사라지지 않는다.
2. 생성자는 private로 접근을 제한한다.
class Singleton{
private static Singleton instance = new Sigleton();
private Sigleton(){
/.../
}
}
3. Getter로 인스턴스의 참조변수를 외부로 반환한다.
class Singleton{
private static Singleton instance = new Sigleton();
private Sigleton(){
/.../
}
// Getter
public static Sigleton getInstance(){
return Sigleton
}
}
class SingletonTest{
public static void main(String[] args){
// Getter 호출
Singleton singleInstance = Singleton.getInstance();
}
}
대상 | 사용 가능한 접근제어자 |
---|---|
클래스 | public, (default), abstract, final |
메서드 | public, protected, (default), private, abstract, final, static |
멤버변수 | public, protected, (default), private, final, static |
지역변수 | final |
1. 클래스에 final, abstract은 함께 사용할 수 없다.
2. 메서드에 static, abstract은 함께 사용할 수 없다.
3. 추상메서드(abstract)의 접근 제어자는 private면 안된다.
4. 메서드의 private, final은 중복될 필요가 없다.
제어자는 조합해서 사용이 가능하다.
final이 붙은 인스턴스 변수는 생성자로 초기화가 가능하다.
생성자가 private
로 제한된 클래스는 상속할 수 없다.
부모 생성자가 외부에서 호출이 불가능하기 때문이다.
이 경우 클래스도 final
로 상속이 불가능하다는 걸 알려주는게 좋다.
접근 제한자는 OOP의 데이터를 보호하기 위해서 사용된다.
추상화된 메서드와 클래스는 상속관계에서 기능의 확장과 구현이 목적이다.
도움이 되셨다면 '좋아요' 부탁드립니다 :)