Java (import, 접근제한자, 캡슐화, getter/setter, 상속, override, 다형성)

yihyun·2024년 7월 29일

Java

목록 보기
6/12

import

class를 객체화 할 때 다른 폴더(package)에 있는 class를 불러올 수 있는데 이 경우 import문을 사용한다.

Java에서는 다른 폴더에 있는 class를 가져와 사용할 수 있지만 너무 쉽게 가져오는 것은 안되기 때문에 다른 package에 있는 class를 가져와 사용하기 위해선 import 문을 사용해주어야 한다.

Java에서는 친절하게도 자동완성을 하면 자동으로 import를 작성해준다.

import문을 사용해서 불러올 메소드(method)는 public 키워드가 있어야 사용할 수 있다.

public int plus(int a, int b){
	return a + b;
}

그렇다면 public는 무엇이길래 사용 여부를 결정하는가?

접근제한자

접근제한자를 사용하는 이유는 보호를 하기 위해서이다.

다른 사람이 사용할 수 있는 코드도 있지만 내부적으로만 사용할 코드도 있기 때문에 보호와 보안을 위해 사용해주게 된다.

public : 어디서든 사용 가능 (All) +
protected : 부모, 자식 관계만 사용 가능(extends) #
default : 패키지 밖에서 사용 불가능(Package) ~ (생략 가능)
private : 파일 즉 클래스 밖에서 사용 불가능 (Class) -

이렇게 특정 부분의 접근을 제한하는 것을 캡슐화(Encapsulation) 라고 한다. (공개하는 소스코드의 보호를 위해)

하지만 캡슐화가 되어 있어도 소스를 보거나 수정하는 것이 가능한데 어떻게 보안을 위해라고 말할 수 있을까?

캡슐화의 보호는 소스를 보지 못하게 하는게 아니라 잘못 사용할까봐 보호하는 것이기 때문이다.

예를들어 컴퓨터 본체를 우리는 내부를 분해할 수 있지만 케이스를 씌워 중요한 내부 기기를 보호하고 우리가 자주 사용하는 전원코드만 밖으로 노출해두는 것처럼 Java에서도 사용자가 소스를 잘못 건드리는 것을 방지하지 위해 막아두는 것이다.

그렇다면 private 처럼 내부에서만 사용 가능한 접근제한자를 설정해두었지만 꼭 사용해야 할 경우에는 어떻게 해야할까?

바로 getter, setter을 사용해주면 된다.


getter, setter

gettersetter는 개구멍 🐾을 파주는 것이다.

정문을 통해서 들어오는 것이 아닌 개구멍을 통해서 일부만 사용할 수 있도록 해주는 것으로 gettersetter를 선택적으로 사용하는 것도 가능하다.

즉 내가 원하는 것만 사용하게 할 수 있다. (원하는 것만 열어주고, 감추고 싶은 것은 감추고) = 캡슐화

getter는 private를 외부로 꺼내는 메서드, setter는 private에 값을 넣는 메서드이다.

Getter

  • 내부의 멤버변수에 저장된 값을 외부로 리턴.
  • 메개변수는 없고, 리턴값만 있는 메서드로 정의한다.
  • 메서드명은 주로 getXXX() 메서드 형식으로 지정
  • XXX은 해당 멤버변수의 변수명을 사용.

Setter

  • 외부로부터 데이터를 전달받아 멤버변수에 저장.
  • 매개변수만 있고, 리턴값은 없는 메서드로 정의.
public class Computer{

	private boolean power = false;
    private int panSpeed;
    
//  boolean은 상태(true/false)만 있기 때문에 is가 들어간다.
    public bolean isPower(){
    	return power;
    }
    public void setPower(boolean power) {
	    this.power = power;
		if(this.power == true) {
		this.panSpeed = 50;
		this.temp = 60;
		}
	}
    public int getPanSpeed() {
		return panSpeed;
	}

	public void setPanSpeed(int panSpeed) {
		this.panSpeed = panSpeed;
//		110 - 팬속도 = 현재온도 (로 설정하기)
		this.temp = 110 - this.panSpeed;
	}
}

✔ 정리
1. 불러올 member가 다른 package에 있을 경우 import 문을 사용한다.
2. ptivate는 getter, setter을 사용한다.

상속(inheritance)

상속은 나에게 없는 것을 부모에게 물려 받아 원래 내 것 처럼 사용 하는 것을 의미한다.
즉 나만 객체화 하면 부모 것까지 다 사용할 수 있다.

부모님에게 상속받은 돈을 허락받지 않고 내 마음대로 사용할 수 있듯이 Java에서도 동일하다.

상속은 일반화(Generalization) 이라고도 하는데, 위에서 아래로 내려가면 상속이지만 아래에서 위로 올라가면 일반화이기 때문이다.

상속을 하는 이유는 Code의 중복을 피할 수 있고, 내 능력으로 만들 수 없는 기술을 내 것 처럼 사용할 수 있다는 장점이 있기 때문이다.

객체화를 해서 사용하는 것도 큰 차이는 없지만 Java는 사용하는 사람을 편하게 해주는 것에 기반을 둔 언어이기 때문에 상속이라는 더 편리한 방법을 제공한다.

부모님께 상속을 받을 때 부모님이 주시는 것만 받을 수 있고 개인 금고에 있는 것은 상속받을 수 없는 것처럼 Java에서도 private 접근필드를 갖는 멤버는 상속이 불가능하다.

상속을 위해서는 extends 라는 키워드를 사용한다.
(이유 : 기능의 확장, 또는 분류의 확장 (포함)의 의미가 있기 때문)

또한, java에서는 c++과 달리 다중 상속을 허용하지 않는다.
class는 분류 체계이기 때문에 다중 상속이 아닌 조부모를 만들어주는 형태로 만들어줘야 한다.

예를들어 사람이 포유류이면서 조류가 아닌 것처럼, 만약 조류이기도 하고 포유류이기도 하고 싶을 경우 포유류가 조류를 상속받고 그 상속받은 포유류를 사람이 상속받아야 한다.

public calss Person extens Mamal{  // Mamal 클래스를 상속 받은 Person 클래스
}

부모가 자식에게 상속을 해주려면 당연히 부모가 있어야 한다.

그렇다면 자식을 객체화 했을 때 실행 순서는 어떻게 될까?

만약 Main()에서 Child(자식)을 호출하면 Parent(부모)가 먼저 생성(객체화)된 후 자식이 생성(객체화)된다.

Child 호출Child 생성 전에Parent 먼저 생성된 후Child 생성

superclass = 부모 클래스
this = 이 클래스에 있는 것

그렇다면 왜 상속을 사용하면 남의 코드를 편하게 사용할 수 있다는 것일까?

만약 우리가 4개의 클래스를 만들어 클래스에 있는 각 메소드를 사용해주길 원한다면 총 사용자는 4번의 객체화를 진행해야 한다.
하지만 4개의 클래스를 상속한다면 한번의 객체화만으로도 많은 메소드를 사용할 수 있을 것이다.

하지만 부모에게 받은 것이 마음에 들지 않을 경우도 있다.
이럴 때에는 @override를 사용해줄 수 있다.

Override

부모와 자식 간에 같은 메소드를 가지고 있으면 override한 메소드라고 판단한다.

override는 전체를 바꾸는 것도 가능하지만 부분만 변경하는 것도 가능하다.

@override
public int run() { // superclass의 run() 메소드를 override 하여 일부 변경
	if(turbo == true) {
		return 200;
	} else {
		return super.run();
	}
}

여기서 주의할 점은 override를 할 경우 껍데기는 부모의 원형을 그대로 사용하고 {내용} 만 수정해줘야 한다.

overrideoverload와 이름이 비슷해서 면접 등에서 종종 둘의 차이점을 물어보는데, 이 둘은 이름만 비슷하고 내용은 완전히 다르다!

override 는 부모, 자식 사이(상속 관계)에만 만드는 것이고,
overload 는 같은 이름의 생성자를 여러개 만들 수 있는 것이므로 헷갈리는 일이 없도록 하자!

✔ 정리
1. Java에서 상속은 하나의 클래스만 가능하다.
2. 부모를 상속받은 자식을 객체화 할 경우 부모가 먼저 객체화 된다.
3. 상속받은 메소드는 내 맘대로 바꿔 쓸 수 있다. (override)
4. 단 읽기 전용인 final 키워드가 있는 메소드는 불가능하다.

다형성(Polymorphism)

(많을 다多) 형성 : 형태가 다양하다.

다형성은 부모를 상속받은 class는 같은 Type 으로 들어갈 수 있다는 뜻이다.
(그로인해 자원을 아낄 수 있다. / 하나의 변수로 여러개를 사용할 수 있다.)

예를들어 4명의 형제가 각각 부모의 집에서 독립해 자취를 시작한다면 월세부터 시작해서 청소와 요리 등등 형제들은 각각 자원들을 추가적으로 사용해줘야한다.
하지만 만약 부모집으로 다시 들어간다면 각각의 자원이 아닌 한번의 자원만 사용할 수 있기 때문에 효율성이 높아진다.

위에 예시를 코드로 만들어보자

// 1. ParentHouse 를 상속받는 Child1~4가 있다고 가정
// 2. uesRoom() 을 override 하여 정의

	Child child = new Child();
	child.useRoom();
	
	ChildOne childOne = new ChildOne();
	childOne.useRoom();
	
	ChildTwo childTwo = new ChildTwo();
	childTwo.useRoom();
	
	ChildThree childThree = new ChildThree();
	childThree.useRoom();

위 예시와 달리 만약 객체가 4개가 아니라 30~40개 라면 30~40개의 변수를 선언해 주어야 한다.

하지만 다형성을 사용하면 하나의 변수만 선언하고 여러 객체를 사용할 수 있다.
(필드 다형성) *다형성은 주로 변수에 사용한다.

ParentHouse house; // 부모형태의 변수 하나 선언
		
house = new Child();
house.useRoom();
		
house = new ChildOne();
house.useRoom();
		
house = new ChildTwo();
house.useRoom();
		
house = new ChildThree();
house.useRoom();

위 코드에서 확인할 수 있듯이 공통된 부모가 있는 객체들은 부모의 타입으로 들어갈 수 있고, 부모 객체의 형태로 사용할 수 있다.

정의+실제활용+코드활용 방법을 이야기하는게 가장 좋다.

묵시적 타입 변환(Promotion)과 명시적 타입 변환(Casting) 은 다형성에서도 적용된다.

예를들어 결혼을 한 자식이 부모님 집에 갈 때에는 연락 없이 그냥 갈 수 있지만 시어머니가 갑자기 우리 집에 비밀번호를 누르고 들어오는 것은 당황스럽듯이
자식 → 부모는 그냥 들어갈 수 있지만 부모 → 자식은 별도의 작업이 필요하다.

묵시적 형변환 : 자식이 부모집에 들어가는 것

명시적 형변환 : 부모가 자식의 집에 들어가는 것

즉 정리하자면 더 큰 개념이 작은 개념으로 들어갈 때에는 Casting이 필요하다.

인간이 포유류로 들어가는 것은 묵시적 형변환(promotion), 포유류가 인간으로 들어가는 것은 명시적 형변환(Casting)

묵시적 형변환 (promotion) 예시

/*
	     Vertebrate
	   /            \
     Birds	        Mamal 
     /   \           /  \
   Duck	 Chicken   Dog  Cat
*/

class Vertbrate{}
class Birds extends Vertbrate{} // 조류
class Mamal extends Vertbrate{} // 포유류

class Duck extends Birds{} 
class Chicken extends Birds{}

class Dog extends Mamal{}
class Cat extends Mamal{}
  • Duck은 Birds 와 Vertbrate에는 들어갈 수 있지만 Mamal에는 들어갈 수 없다.
  • Dog은 Birds에는 들어갈 수 없지만 Mamal과 Vertbrate에 들어갈 수 있다.

이렇게 부모의 집에 들어가는 것은 장점도 있지만 단점도 있다.

자취를 할 때에는 늦게도 들어가고 술도 마시고 하지만 부모님 집에 들어가면 예전처럼 살 수 없듯이 나의 고유의 특징을 잃어버리게 된다.

포유류의 특징인 새끼를 낳는다, 젖을 먹인다 라는 특성을 상속받은 강아지는 "멍멍" 짖는다는 자신만의 고유의 특성이 있다.

하지만 부모에게 돌아가게 된다면 자신의 특성을 버리고 부모의 특성만을 가지게 된다.
이런 특징을 다시 얻고 싶으면 부모에게서 빠져나가야 하고 이를 명시적 형변환(Casting) 이라고 한다.

즉, casting은 넓은 개념에서 좁은 개념으로 좁혀가는 것이며 이 경우 정확히 자신에게 돌아가야 한다는 주의사항이 있다.

public class Mammal { // 부모 클래스
	public void birth() {
		System.out.println("새끼를 낳다");
	}
	public void eat() {
		System.out.println("젖을 먹이다.");
	}
}

public class Cat extends Mammal { // 자식 클래스
//	Mammal 의 특성 - 나만의 것으로 개조
	@Override
	public void birth() {
		System.out.println("새끼를 4마리 낳는다.");
	}
//	고양이 고유 특성
	public void mew() {
		System.out.println("야옹 하고 울다.");
	}
}

public class Dog extends Mammal { // 자식 클래스
//	Mammal 의 특성 - 나만의 것으로 개조
	@Override
	public void birth() {
		System.out.println("새끼를 5마리 낳는다.");
	}
//	강아지 고유 특성
	public void bark() {
		System.out.println("멍멍 하고 짖는다.");
	}
}

이렇게 정의한 부모, 자식 클래스를 명시적 형변환을 통해 사용하는 예시는 다음과 같다.

public static void main(String[] args){
	Mammal mal = null;
    mal = new Dog();
    
    mal.birth();
    mal.eat();
    mal.bark(); // 사용 불가 
    
   // 사용을 원할 경우 명시적 형변환(casting) 필요
   Dog dog = (Dog) mal;
   dog.bark();
   
   // ※ 주의 :  다른 자식 클래스는 사용 불가능하다.
   Cat cat = (Cat) mal; // mal은 Dog으로 정의했기 때문에 Cat 사용 불가
   cat.mew(); // ClassCastException 발생
}

✔ 정리
1. 다형성은 자식 객체가 부모 객체 형태의 변수에 들어갈 수 있으므로, 자원을 아낄 수 있다.
2. 하지만 부모 형태의 변수에 들어가면 자식 고유의 기능(고유메소드)를 쓸 수 없다.
3. 그렇기 때문에 자신의 고유의 기능을 사용하고 싶을 경우 명시적 형변환(casting)을 통해 자식 형태로 돌아가 줘야 한다.


만약에 우리가 게임에서 마법사 캐릭터를 키울경우 레벨이 올라갈 수록 마법사가 사용할 수 있는 스킬의 수는 점점 늘어날 것이다.

그런데 이런 스킬을 각 객체로 선언하는 것이 아닌 다형성을 활용하여 하나의 "마법" 이라는 객체를 만들어 둔다면 하나의 변수로 여러 마법을 대응할 수 있고, 마법 추가시에도 상대적으로 적은 코드를 사용할 수 있다.

이렇게 공통된 부모가 있다면 그 부모 타입으로 변수 타입을 선언해 각각의 클래스를 배열에 넣을 수도 있다.

Spell[] spell = new Spell[3]; // 마법 스킬을 시전하는 부모 클래스

spell[0] = new Fire();  // 화염 마법 클래스
spell[1] = new Ice();   // 얼음 마법 클래스
spell[2] = new Light(); // 번개 마법 클래스

이렇게 배열로 넣으면 당연하게 for문으로 더욱 편리하게 사용할 수도 있다.

for (int i = 0; i < spell.length; i++) {
	System.out.println(spell[i].casting());		
}

필드 다형성 / 매개변수 다형성

위에서 계속 살펴보면 알 수 있듯이 다형성을 변수에 사용하고 있다.
이 변수는 또 다른 이름으로 필드 라고 부르기 때문에 이러한 다형성을 필드 다형성 이라고 부른다.

필드 다형성(일반 변수) 외에 만약 매개변수에 다형성이 적용된다면 우리는 매개변수 다형성이라고 말한다.

만약 다형성을 사용하지 않으면 무언가 추가될 때마다 코드를 추가하고 수정하는 작업이 반복되고 이를 '결합도가 높다' 고 표현한다.

만약에 차를 운전하는 운전기사가 있다.
이 운전기사는 BMW, Benz, Audi를 운전할 수 있다고 가정해보자

그렇다면 우리는 기본적으로 차 종류에 대한 class를 만들고 그 차량 별로 운전하는 메소드를 만들 것이고, 그렇게 된다면 각각의 변수를 선언해 줘야 할 것이다.

하지만 매개변수 다형성을 사용한다면 조금 더 편리하고 효율적이게 Java를 사용할 수 있다.

public class Car{ // 부모 클래스생성
	public String run(){
    	return "";
    }
}

public class Benz extends Car{ // Car(부모 클래스)를 상속받는 자식 클래스
	@Override
	public String run() {
		return "벤츠가 달립니다.";
	}
}

public class Racer{
	// 부모 클래스의 객체를 매개변수로 주면서 해당 부모를 상속받은 자식객체는 모두 해당 메소드를 사용할 수 있다.
 	public String drive(Car car) { 
		return car.run();
	}
}

public class DriveController{
	public static void main(String[] args){
    	Racer racer = new Racer; // drive 실행을 위한 객체 생성
        
        Car car = null; // 부모 타입 변수 선언
        
        car = new Benz; // 부모 타입 변수에 자식 타입 넣기
        System.out.println(racer.drive(car)); // 메소드 실행
    }
}
profile
개발자가 되어보자

0개의 댓글