Information Security Study

240108 this(), super(), 접근 제한자, 캡슐화 본문

네트워크 캠퍼스/JAVA

240108 this(), super(), 접근 제한자, 캡슐화

gayeon_ 2024. 1. 11. 16:21

this

  • this는 자기 자신 객체를 지정할 때 사용하는 키워드이다.
  • this. 을 사용하면 동일 클래스 내의 멤버(멤버변수, 메서드)를 참조할 수 있다.
  • this()를 사용하면 생성자 내부에서 자신의 다른 생성자를 호출할 수 있다.

 

this() 예제

package this_;

public class Human {

	public String name;
	public int age;
	
	// 생성자 선언을 해주시되 name, age를 입력받아 대입해 주세요.
	// 변수 이름은 name, age로 해주세요.
	public Human(String name, int age) {
		this.name = name;
		this.age = age;
	}
	
	// 메서드 오버로딩으로 이름만 입력받는 생성자 정의
	public Human(String name) {
		this(name, 0); // 다른 생성자 호출
		// this.name = name;
		// age = 0;
	}
	
	// 아무것도 입력하지 않았을 때 name에는 "이름없음", age에는 -1이 대입되는
	// void 생성자를 정의해 주세요.
	// 모든 필드값을 대입하는 구문과 this()를 사용하는 경우 모두 작성하세요.
	public Human() {
		this("이름 없음", -1); // 다른 생성자 호출
		// name = "이름 없음";
		// age = -1;
	}
	
	public void showInfo() {
		System.out.println("이름: " + name + ", 나이: " + age);
	}
}
package this_;

public class HumanMian {

	public static void main(String[] args) {
		Human kim = new Human("김자바", 20);
		kim.showInfo();
		
		Human park = new Human("박코딩");
		park.showInfo();
		
		Human naname = new Human();
		naname.showInfo();
	}

}

필드값에 직접 일일이 값을 넣는 것과 this()를 사용해 다른 생성자를 호출하는 것에는 기능적으로 차이는 없으나

코드의 유지, 보수적 측면에서는 this()를 사용하는 것이 이점을 갖고 있다.

 

 

예제의 메모리 구조는 다음과 같다.


super

  • 생성자는 상속이 되지 않기에 자식 생성자에서 부모 생성자를 먼저 초기화 해야 한다.
  • super는 한단계 위 부모클래스의 객체를 지정할 때 사용하는 키워드다.
  • super. 을 사용하면 부모클래스의 멤버를 참조할 수 있다.
  • super()는 생성자 내부에서만 사용이 가능하며 부모클래스의 생성자를 호출하는데 사용한다.
  • 생성자의 첫 라인에는 반드시 this(), super()가 있어야 하는데 이를 기술하지 않으면 묵시적으로 super()가 삽입되어 부모클래스의 기본 생성자를 자동으로 호출한다.
  • 생성자 내부에서 또다른 생성자를 호출할 때는 반드시 생성자 블록 내부의 첫 라인에 기술해야 한다.

 

super 예제

package super_;

public class Airplane {
	public String planeCode; 
	protected int speed;
	public int gas;
	
	// 속도 0, 연료 100으로 고정, 편명만 입력받는 생성자 정의
	public Airplane(String planeCode) {
		this.planeCode = planeCode;
		speed = 0;
		gas = 100;
	}
	
	// 최대속도를 900으로 설정하는 메서드 fly 정의
	// 호출시 연료는 3씩 차감, 속도는 100씩 증가한다.
	public void fly() {
		if(this.gas - 3 < 0) {
			System.out.println("연료 부족으로 가속 불가능");
			return;
		}
		
		if(this.speed + 100 >= 900) {
			speed = 900;
			gas -= 3;
			return;
		}
		
		this.speed += 100;
		this.gas -= 3;
	}
	
	// 계기판 보는 메서드
	public void showStatus() {
		System.out.println("편명: " + this.planeCode);
		System.out.println("속력: " + this.speed);
		System.out.println("연료: " + this.gas);
	}
}
package super_;

public class SupersonicAirplane extends Airplane{
	
	// 상속받는 멤버변수 없음
	
	public SupersonicAirplane(String planeCode) {
		// 생성자는 상속이 되지 않는다.
		// 따라서 자식 생성자에서 부모 생성자를 먼저 초기화하고
		// 나머지 코드를 실행해야 한다.
		super(planeCode); // super()가 가장 먼저 호출되어야 한다.
	}
	
	// 초음속 비행기도 fly()를 사용하지만 일반 비행기보다 확장된 개념을 사용하므로
	// 오버라이딩을 통해 추가적인 정의를 해야 한다.
	// 시속 900km 이하에서는 일반 비행을, 그 이후에는 초음속 비행을 하므로
	// 시속 900km 이하 구간에서는 오버라이딩 되기 전의 fly()를
	// 이후에는 재정의된 fly()를 호출하게 해야 한다.
	@Override
	public void fly() {
		if(this.speed < 900) {
			super.fly();
		} else if(this.speed + 300 <= 2200 && this.gas >= 5) {
			this.speed += 300;
			this.gas -= 5;
		} else if(this.speed + 300 >= 2200 && this.gas >= 5) {
			this.speed = 2200;
			this.gas -= 5;
		} else {
			System.out.println("연료가 부족해 비행이 불가능합니다.");
		}
	}
}
package super__;

import super_.SupersonicAirplane;

public class AirplaneMain {

	public static void main(String[] args) {
		SupersonicAirplane sa1 = new SupersonicAirplane("KE1010");
		
		for(int i = 0; i < 8; i++) {
			sa1.fly();
		} // 속도 800, 연료 76
		
		sa1.showStatus();
		System.out.println("----------------");
		for(int i = 0; i < 4; i++) {
			sa1.fly();
		}
		sa1.showStatus(); // 속도 1800, 연료 58
	}

}

 

 

 

 

호출 관계 메모리 구조는 위와 같다.

fly()는 부모, 자식 클래스 모두에 있다.


접근 제한자(Access Modifier)

접근 제한자

: 멤버변수나 멤버메서드에 접근할 수 있는 영역을 제한하도록 설정하는 키워드

아래와 같이 선언된 클래스의 인스턴스를 생성한 다음 호출하려 하면

사람 {
	private String name;
	private int age;
}

 

 

에러가 발생게 된다. 그 이유는 name이 private으로 처리되어서 외부 멤버로는 접근이 불가능하기 때문이다.


위 이미지와 같이 public으로 이름을 출력하는 메서드를 선언한 다음

메인에서 public인 조회 메서드를 호출할 경우 메서드 자체는 public이므로 main에서 호출이 가능하다.

그리고 해당 메서드는 “이름” 멤버변수와 같은 소속이므로, 같은 소속끼리는 조회가 가능하다.


// 사람 클래스 선언
class 사람 {
	private String name;
	private int age

	public void 이름대기(){
		System.out.println("학생의 이름" + name);
	}
}

// 사람 클래스를 상속받은 학생 클래스 선언
class 학생 extends 사람 {
	private String major;

	public void 자기소개(){
		System.out.println("학생의 이름" + name);//에러발생
		System.out.println("학생의 나이" + age);//에러발생
		System.out.println("학생의 전공" + major);
	}
}

 

또한 상속관계에서는 자식에서 부모의 private 요소 조회가 불가능하다.

 

 

 

따라서 이를 방지하기 위해 protected를 사용할 수 있다.

상속 관계에서 자식이 부모의 요소를 조회하고 싶다면 public, protected 접근 제한자를 사용해야 한다. 


// 사람 클래스 선언
class 사람 {
	protected String name;
	protected int age

	public void 이름대기(){
		System.out.println("학생의 이름" + name);
	}
}

// 사람 클래스를 상속받은 학생 클래스 선언
class 학생 extends 사람 {
	private String major;

	public void 자기소개(){
		System.out.println("학생의 이름" + name);//상속관계 접근 가능
		System.out.println("학생의 나이" + age);//상속관계 접근 가능
		System.out.println("학생의 전공" + major);
	}
}

main에서 조회하기 위해서는 접근 제한자가 public이어야 한다.

 

 

 

위와 같이 protected로 선언된 요소는 main과 같은 외부지역에서는 직접적으로 호출할 수 없으나

상속관계에 있는 자식클래스의 메서드에서도 부모쪽 요소에 접근을 허용한다.

 

  • 접근 제한자는 클래스와 멤버변수, 메서드, 생성자의 접근을 제어할 수 있는 제한자이다.
  • main() 메서드가 없는 클래스는 외부 클래스에서 이용할 목적으로 설계된 라이브러리 클래스다.
  • 라이브러리 클래스를 설계할 때는 외부 클래스에서 접근할 수 있는 멤버와 접근할 수 없는 멤버로 구분해서 변수, 생성자, 메서드를 설계하는 것이 바람직하다.
  • 외부에서 객체 생성을 막기 위해 생성자를 호출하지 못하게 하거나 객체의 특정 데이터를 보호하기 위해 해당 멤버변수에 접근하지 못하도록 막는 것이 접근 제한자의 역할이다.
  • 클래스에는 접근 제한자를 public package friendly(default)만 붙일 수 있다.

 

접근 제한자의 종류

  1. public: 같은 클래스, 같은 패키지, 다른 패키지 접근 가능
  2. protected: 같은 클래스, 같은 패키지는 접근이 가능하지만 다른 패키지에 속해있는 클래스인 경우 상속관계가 없으면 접근이 불가능 (상속관계이면서 패키지가 다른 경우 직접 접근 불가, super키워드를 활용해 접근 가능)
  3. package friendly(접근 제한자를 붙이지 않는 형태): 같은 클래스, 같은 패키지에서만 접근이 가능하며 패키지가 다를 경우 접근이 불가능 (파일은 달라도 되지만 패키지가 다르면 접근 안됨!)
  4. private: 같은 클래스 내부가 아니면 접근이 불가능 (파일이 다르면 접근 안됨!)

 

 

접근 제한자 예제

package accessmodifier.member.pack1;

public class A {

	// 멤버변수 선언
	public int a;
	int b; // default는 같은 패키지 내 요소에 접근 가능
	private int c;
	
	// 메서드 선언
	public void method1() {}
	void method2() {}
	private void method3() {}
	
	// 생성자 선언
	public A() {
		a = 1;
		b = 1;
		c = 1;
		
		method1();
		method2();
		method3();
	}
}
// A와 같은 패키지, 다른 파일
package accessmodifier.member.pack1;

public class B {

	// 생성자 내부에 코드 작성
	public B() {
		A a = new A();
		
		a.a = 1; // public 접근가능
		a.b = 2; // 같은 패키지 내에 있는 요소에 대해 default 접근 가능
		// a.c = 3; // private은 접근 불가
		
		a.method1(); // public 접근 가능
		a.method2(); // default 접근 가능
		// a.method3(); // private 접근 불가
	}
}
// A와 다른 패키지
package accessmodifier.member.pack2;

import accessmodifier.member.pack1.A;

public class C {
	
	public C() {
	
	A a = new A(); // 서로 다른 패키지 요소 호출시에는 import 필수
	
	a.a = 1; // public 어디서나 접근 가능
	a.b = 2; // default 패키지가 다르면 접근 불가
	a.c = 3; // private 같은 클래스 소속이 아니면 접근 불가
	
	a.method1(); // public
	a.method2(); // default
	a.method3(); // private
	}
	
}

위 예제의 패키지 구조이다.

 

 

 

접근 제한자 protected 예제

package accessmodifier.protec.pack1;

public class A {
	
	// protected는 다른 패키지일지라도 양 클래스가 부모, 자식 관계라면
	// 제한적으로 접근을 허용한다.
	protected String s;
	
	protected A() {} // 생성자
	
	protected void method() {}
}
// 같은 패키지는 호출 가능
package accessmodifier.protec.pack1;

public class B {

	public B () {
		A a = new A(); // 같은 패키지 내에 있으므로 호출 가능
		a.s = "hi";
		a.method();
		
	}
}
// A와 패키지가 달라 호출 불가능
package accessmodifier.protec.pack2;

import accessmodifier.protec.pack1.A;

public class C {
	
	public C() {
//		A a = new A(); // protected 요소기 때문에 패키지가 다르므로 호출 불가능
//		a.s = "hihihi";
//		a.method();
	}

}
// 상속 관계일 때는 super() 사용
package accessmodifier.protec.pack2;

import accessmodifier.protec.pack1.A;

public class D extends A {
	
	public D() {
//		A a = new A();
//		a.s = "hi";
//		a.method();
		
		// protected는 패키지가 다를 경우
		// 두 클래스 사이에 상속 관계가 있다면
		// super 키워드를 이용해 부모쪽 참조를 허용한다.
		super(); // super() 생성자는 첫 줄에 위치해야 한다.
		super.s = "hi";
		super.method();
	}

}

 

 

 

접근 제한자 protected 예제의 패키지 구조이다.

 

 

 

 

생성자의 접근 제한자 예제

// A 클래스 파일
package accessmodifier.constructor.pack1;

public class A {
	
	// 멤버 변수 선언
	A a1 = new A(true); // public 생성자 호출
	
	A a2 = new A(3); // default 생성자 호출
	
	A a3 = new A("hi"); // private 생성자 호출
	
	// 생성자 선언
	public A(boolean a) {} // public 생성자는 boolean 파라미터
	
	A(int i) {} // default 생성자는 int 파라미터
	
	private A(String s) {} // private 생성자는 String 파라미터
}
// 같은 패키지의 다른 파일인 B 클래스
package accessmodifier.constructor.pack1;

public class B {
	
	A a1 = new A(false); // public 호출 문제 없음
	 
	A a2 = new A(55); // default 호출 문제 없음
	
	// A a3 = new A("bye"); // private은 A 내부에서만 호출 가능
	

}

default는 다른 파일이어도 같은 패키지인 경우 접근 가능하다.

 

 

// 다른 패키지인 C 파일
package accessmodifier.constructor.pack2;

import accessmodifier.constructor.pack1.A;

public class C {

	// A a1, a2, a3를 선언하고 각각 다른 생성자로 호출해서
	// 접근 제한자별 차이를 생각해보자.
	A a1 = new A(true); // public 생성자 호출 가능
	// A a2 = new A(20); // default 생성자 호출 불가능
	// A a3 = new A("안녕"); // private 생성자 호출 불가능
}

패키지가 다른 경우 default 생성자는 호출하지 못한다.

 

 

접근 제한자 - 생성자 예제의 패키지 구조다.

 

 

 

클래스의 접근 제한자 예제

private은 클래스의 접근 제한자로 사용할 수 없다.

 

// A 클래스는 default 접근 제한자
package accessmodifier.class_.pack1;


// default 제한자는 접근제한자를 적지 않으면 적용되며
// 같은 패키지 내부끼리만 참조할 수 있다.
class A {

}
// A와 같은 패키지 다른 파일인 B 클래스
package accessmodifier.class_.pack1;

// public 접근제한자는 어디서나 접근 가능
public class B {

	// 클래스 A의 접근제한자가 default이기 때문에
	// 같은 패키지 내부인 B에서 A를 선언할 수 있다.
	A a = new A();
}
// A와 다른 패키지인 C 클래스
package accessmodifier.class_.pack2;

import accessmodifier.class_.pack1.B;

public class C {
	
	// 클래스 B는 public 클래스이므로 패키지가 달라도 생성 가능
	// 패키지가 다른 경우에는 import 구문을 선언해야 한다.
	B b = new B();
	
	// 클래스 A는 default 접근제한자를 적용받은 클래스이므로 패키지가 다르면 호출 불가능
	// 참조도 불가능하다.
	// A a = new A();
}

클래스의 접근 제한자가 default인 경우 다른 패키지에서 호출이 불가능하다.

import 또한 불가능하다.

 

 

접근 제한자 - 클래스 예제의 패키지 구조다.


은닉(Encapsulation)캡슐화

  • 은닉은 사용자에게 상세한 내부 구현을 숨기고 필요한 부분만 보이게 하는 것이다.
  • 은닉을 사용하기 위해서는 클래스의 멤버변수의 접근제한자를 private으로 설정한다.
  • 은닉된 멤버변수에 접근하기 위해서는 공개된(public) 메서드를 통해서 접근할 수 있다.
  • 변수의 값을 변경시키는데 사용되는 메서드는 setter메서드라고 부르고 변수의 값을 얻어오는데 사용하는 메서드를 getter메서드라고 부른다.
  • 공개 메서드를 이용하여 데이터를 변경시킬 경우 메서드 내에 데이터 유효성을 검증할 수 있는 루틴을 넣을 수 있다.
  • 경우에 따라 접근 권한을 체크할 수 있는 로직을 포함시키면 인가되지 않은 사용자에게 중요한 데이터나 로직을 숨길 수도 있고 제어할 수도 있다.
  • 멤버변수만 private 제한자를 가지는 것은 아니다. 외부에 공개하고 싶지 않은 메서드들도 private으로 선언할 수 있다.

 

캡슐화 예제

// bad case
package encapsulation.bad;

public class MyBirthday {
	
	int year;
	int month;
	int day;
	
	void showDateInfo() {
		System.out.println("내 생일은");
		System.out.println(year + "년");
		System.out.println(month + "월");
		System.out.println(day + "일");
		System.out.println("이니까 선물 주세요.");
		
	}

}
package encapsulation.bad;

// fool proof가 되지 않아 bad case
public class MainClass {
	
	public static void main(String[] args) {
		
		// 같은 패키지 내부 클래스 파일을 가져다 쓸 때는 import 구문이 필요 없다.
		MyBirthday b = new MyBirthday();
		
		b.year = 2024;
		b.month = 13; // 잘못된 정보를 기입할 수 있다.
		b.day = 32; // 잘못된 정보를 기입할 수 있다.
		
		b.showDateInfo();
	}
	

}

 

 

 

// good case
package encapsulation.good;

public class MyBirthday {
	// 캡슐화(은닉)시 변수는 무조건 private로 처리
	private int year;
	private int month;
	private int day;
	
	// alt + shift + s 혹은 마우스 우클릭 -> source
	// >> generate constructor using fields 선택
	public MyBirthday(int year, int month, int day) {
		this.year = year;

//		다음과 같이 작성할 시 값 대입, 검증이 가능하지만
//		로직에 문제가 발생할 경우 어느 부분이 문제인지 확인하기 비교적 어렵다.
//		리팩토링: 기능은 유지하면서 코드의 구조를 유지보수하기 좋게 개선하는 것
//		if(month < 1) {
//			this.month = 1;
//		} else if(month > 12) {
//			this.month = 12;
//		} else {
//			this.month = month;
//		}
		
		setMonth(month);
		setDay(day);
		
	}
	
	// 은닉된 변수에 접근하기 위해서는
	// 클래스 설계시 미리 설정해둔 setter/getter 메서드를 이용해
	// 데이터에 접근해야 한다.
	
	// setter 메서드 선언
	// 1. setter 메서드는 은닉변수에 값을 저장(세팅)하기 위해 선언한다.
	// 2. 메서드의 접근 제한자는 public으로 설정한다.
	// 3. 이름은 일반적으로 set+변수명으로 지정한다.
	private void setDay(int day) {
		if(day < 1 || day > 31) {
			this.day = 1; // 범위를 벗어나는 값이 들어오면 1로 고정
		} else {
			this.day = day; // 범위 내의 값은 그대로 적용
		}
	}
	
	// setMonth를 설계해서 1~12중 하나만 받아서 저장하게 하기.
	private void setMonth(int month) {
		if(month < 1) {
			this.month = 1;
		} else if(month > 12) {
			this.month = 12;
		} else {
			this.month = month;
		}
	}
	
	void showDateInfo() {
		System.out.println("내 생일은");
		System.out.println(year + "년");
		System.out.println(month + "월");
		System.out.println(day + "일");
		System.out.println("이니까 선물 주세요.");
		
	}
}
// good case
package encapsulation.good;

public class MainClass {
	public static void main(String[] args) {
		// 12월 32일 같은 없는 날짜를 걸러주는지 체크하기
		MyBirthday b = new MyBirthday(2024, 120, 50);
		// b.day = 50; // private이므로 외부인 Main에서 직접 주입 불가능
		b.showDateInfo();
	}
}

은닉된 변수에 접근하기 위해서는 클래스 설계시 미리 설정해둔 setter/getter 메서드를 이용해 데이터에 접근해야 한다.

 

setter 메서드 선언

1. setter 메서드는 은닉변수에 값을 저장(세팅)하기 위해 선언한다.

2. 메서드의 접근 제한자는 private으로 설정한다.

3. 이름은 일반적으로 set+변수명으로 지정한다.

 

의도한 범위 이외의 값이 들어가지 않기 위해서(fool proof) setter/getter를 사용한다.

 

 

 

refactoring

: 기능은 유지하면서 코드의 구조를 유지보수하기 좋게 개선하는 것

	public MyBirthday(int year, int month, int day) {
		this.year = year;

//		다음과 같이 작성할 시 값 대입, 검증이 가능하지만
//		로직에 문제가 발생할 경우 어느 부분이 문제인지 확인하기 비교적 어렵다.
//		리팩토링: 기능은 유지하면서 코드의 구조를 유지보수하기 좋게 개선하는 것
		if(month < 1) {
			this.month = 1;
		} else if(month > 12) {
			this.month = 12;
		} else {
			this.month = month;
		}
				
		setDay(day);
		
	}

month값 검증 로직을 위와 같이 작성한다면 유지보수적 측면에서 좋지 않다.

 

 

 

	public void setMonth(int month) {
		if(month < 1) {
			this.month = 1;
		} else if(month > 12) {
			this.month = 12;
		} else {
			this.month = month;
		}
	}

setMonth를 생성해 메서드를 분리하고

리팩토링하여 유지보수하기 좋게 수정한다.

 

1. 메서드 분리

2. 메서드 분리 + 값 수정 불가

 

데이터를 수정하지 못하게 하기 위해서는 setter/getter의 접근 제한자를 private으로 변경한다.

값 수정이 가능하게 하고 싶다면 public으로 사용해도 된다.