어제는 태풍 ‘힌남노’가 지나갔다.

인천에 살고 있는 나는 태풍의 영향권이아니라서 크게 못 느꼈지만…
아랫지방은 타격이 꽤나 무시무시한가보다.

image

태풍이 지나가고 나니 날씨가 시원해져서
이제 선풍기랑 에어컨 없이도 집에서 공부가 가능할 것 같다!!
선선하니 커피먹기 좋은 날씨구만!

오늘도 열심히 공부시작!! 📖


어제는 객체지향의 기초의 마지막인 생성자와 이너클래스에 대해 공부했다.
이제부터 본격적인 게임 시작인 객체지향의 심화 과정을 들어가게된다.
조금 걱정이 앞서지만 노력을 이기는 것은 없다고 생각하기에 !!

객체지향 프로그래밍 핵심요소

상속 - 오늘 배울 것!!
캡슐화 - 오늘 배울 것!!
추상화
다형성

에 대서 공부해 보려고한다.
오늘은 그 중에서 상속과 캡슐화부터 시작


상속 (Inheritance)

자바의 상속이란? 기존 클래스를 재활용하여 새로운 클래스를 작성하는
자바의 문법 요소를 의미한다.

즉, 상위 클래스의 멤버를 하위 클래스와 공유하는 것을 의미한다.
이를 ~클래스로부터 상속받았다하는데 이보단 ~클래스로부터 확장되었다 라는
표현이 적절하고, extends 키워드를 사용한다.

image

예를 들자면, 위와 같이 로미오의 역할과 줄리엣의 역할은 이미 정해져있다.
대본이라든가 행동이라든가. 여기서 로미오와 줄리엣역할을 상위클래스라고 가정해보자.
반면, 로미오의 역할은 고정이고 그 역할을 수행할 수 있는 배우들은
얼마든지 변경이되고 교체가 가능하다.

그 기능을 수행할 수 있는 배우들을 하위 클래스라고 가정했을때
로미오라는 역할을 원빈이 확장받게되면, 즉 상위클래스로부터
특정한 속성과 기능을 내려받는 하위 클래스가 된다.
이렇게 보면 하나의 객채(위에서는 로미오역할)가 여러 모양으로
표현될 수 있다는 것을 다형성이라고 말한다.


코드예제

위에 로미오와 줄리엣 연극으로 예를 들어보자

public class Romio {
    String who = "로미오";
    String sex = "남자";
    String droughty = "몬테규 가문";
    void love(){
        System.out.println("줄리엣을 사랑한다고 말합니다.");
    }
    void hide(){
        System.out.println("담장을 넘어가 캐퓰렛 저택에 숨습니다.");
    }
}
public class Actor extends Romio {
    String name;
    int age;
}

로미오라는 배역은 하나이다. 정해져있고 그안에서의 역할이 구분되어있다.
간단하게 위에 로미오의 성별과 가문 이름, 그리고 기능들을 추가해놓고
하위클래스인 Actor을 만들어 Romio를 상속 시켜주었다.
Actor는 장동건이 될수도 원빈이될수도 그누구도 될 수도 있다.

이제 로미오의 역활도 정해졌고 배우를 정해주는 코드를 아래 짜보자

public class Test {
    public static void main(String[] args) {
        Actor onebin = new Actor();
        onebin.name = "원빈";
        onebin.age = 44;
        System.out.println(onebin.name); // 원빈
        System.out.println(onebin.age); // 44
        System.out.println(onebin.who); // 로미오
        System.out.println(onebin.sex); // 남자
        System.out.println(onebin.droughty); // 몬테규 가문
        onebin.love(); // 줄리엣을 사랑한다고 말합니다.
        onebin.hide(); // 담장을 넘어가 캐퓰렛 저택에 숨습니다.

        Actor Jangdonggun = new Actor();
        Jangdonggun.name = "장동건";
        Jangdonggun.age = 50;
        System.out.println(Jangdonggun.name); // 장동건
        System.out.println(Jangdonggun.age); // 50
        System.out.println(Jangdonggun.who); // 로미오
        System.out.println(Jangdonggun.sex); // 남자
        System.out.println(Jangdonggun.droughty); // 몬테규 가문
        Jangdonggun.love(); // 줄리엣을 사랑한다고 말합니다.
        Jangdonggun.hide(); // 담장을 넘어가 캐퓰렛 저택에 숨습니다.
    }
}

원빈이라는 배우와 장동건이라는 배우를 인스턴스로 만들었다.
Actor 클래스안에는 name,age밖에 없는데
Romio를 상속받았기 때문에 그 안에 있는 메서드와 필드값을 사용할 수 있다.
배역을 받은 원빈과 장동건을 순서대로 출력해보자.

이렇게 한가지 역할인 로미오를 두고
감독은 어떤 배우를 캐스팅할지 마음대로 정할 수 있다.
이걸 위에서 말한 다형성이라고한다.

그리고 자바의 객체지향 프로그래밍에서는 단일 상속만허용한다.
다른 말로, 다중 상속은 허용되지 않는다.
즉,public class Actor extends Romio, Juliet 이런식으로 쓰는 것은 불가능!
Actor라는 클라스에 상위클래스로는 하나밖에 두지 못한다.


포함관계

포함은 상속처럼 클래스를 재사용할 수 있는 방법으로
클래스의 멤버로 다른 클래스 타입의 참조변수를 선언하는 것이다.

public class Employee {
    int id;
    String name;
    Address address; // Address클래스 타입로 address참조변수 선언
    
    public Employee(int id, String name, Address address) {
        this.id = id;
        this.name = name;
        this.address = address;
    }
}
class Address {
    String city, country;

    public Address(String city, String country) {
        this.city = city;
        this.country = country;
    }
}

이렇게 Employee에서 Address라는 클래스 타입으로 address라는 참조변수를 선언했다.
Address라는 클래스의 멤버는 city,country로 두개를 포함시킨 타입의 참조변수이다.

   public static void main(String[] args) {
        Address address1 = new Address("리마", "페루");
        Address address2 = new Address("마닐라", "필리핀");
        Employee e1 = new Employee(1, "이재혁", address1);
        Employee e2 = new Employee(2, "염승호", address2);
 }

Address 생성자를통해 리마와 페루라는 값을
인스턴스 변수인 city와 country로 할당해주고
Employee 클래스 생성자를 통해 id와 name, 그리고 참조변수인
address1,2를 같이 넣어서 인스턴스를 만들 수 있다.


매서드 오버라이딩

메서드의 오버라이딩은 상위 클래스로부터 상속받은
메서드와 동일한 이름의 메서드를 재정의 하는 것이다.

class Vehicle {
    void run() {
        System.out.println("Vehicle is running");
    }
}
public class Bike extends Vehicle {
    void run() {
        System.out.println("Bike is running"); // 메서드 오버라이딩
    }

    public static void main(String[] args) {
        Bike bike = new Bike();
        bike.run();
    }
}

// 출력값
"Bike is running"

위와 같이 보면 void run() 이라는 메서드는 동일하다 Vehicle클래스나 Bike 클래스나.
Vehicle 클래스를 상속받은 Bike 클래스는 run(); 메서드를 호출할 경우 상위클래스(Vehicle)의
run()이 호출되는 것이아니라 오버라이드된 하위클래스(Bike)의 run()이 호출되어지는 것을 볼 수 있다.


super. super()

이전에 배운 this, this(); 와 상당히 유사하다.
this();생성자를 호출하고 this.자신의 인스턴스를 가르켰다면
super(); 상속된 상위클래스를 호출하고 super.은 상위 클래스의 객체를 가르킨다.

코드로 예제를 들어보자

class Upper {
    int count = 20; // super.count
}

상위 클래스의 count 값을 20으로 지정

class Lower extends Upper {
    int count = 15; // this.count

    void callNum() {
        System.out.println("count = " + count);
        System.out.println("this.count = " + this.count);
        System.out.println("super.count = " + super.count);
    }
}

// 출력값
count = 15
count = 15
count = 20

Upper 클래스를 상속한 Lower 클래스의 count 값을 15로 할당

public class Super {
    public static void main(String[] args) {
        Lower l = new Lower();
        l.callNum();
    }
}

여기서 callNum(); 호출하게 될 경우, 15,15,20 으로 표시가 되어지고
보듯이 super. 라는 문법으로 상위의 Upper 클래스의 count값을 가르키는걸 볼 수 있다.


Object 클래스

이렇게 생각하다보면 우리는 여러가지 클래스들을 상속도 받고
import도 하면서 서로 사용을 하고 있다.

여기서 상속계층에서도 최상위에 위치한 클래스가 ‘Object 클래스’라고 생각하면된다.
그말은 즉슨, 모든 클래스는 Object 클래스로부터 확장된다라는 뜻과 같다.

class example {  //  컴파일러가 "extends Object" 자동 추가 

}

class test extends example {

}

위에서 보면 다른 클래스로부터 상속받지 않는 클래스에는
자동적으로 extends Object가 추가되어 상속받게 된다.
Object 클래스의 대표적인 몇 가지 메서드들을 아래에 적어본다.

toString() String 객체 정보를 문자열로 출력
equals(Object obj) boolean 등가 비교 연산(==)과 동일하게 스택 메모리값을 비교
hashCode() int 객체의 위치정보 관련. Hashtable 또는 HashMap에서 동일 객체여부 판단
wait() void 현재 쓰레드 일시정지
notify() void 일시정지 중인 쓰레드 재동작

간단한 예제로 한가지 클래스를 만들어보고 테스트해보자

class HashTest {
    public static void main(String[] args) {
        Vehicle abc = new Vehicle();
        Vehicle abcd = new Vehicle();
        System.out.println(abc.hashCode()); // 2060468723 해쉬코드값
        System.out.println(abcd.hashCode()); // 622488023 해쉬코드값
    }

    @Override
    public int hashCode() {
        return super.hashCode();
    }
}

hashCode를 상속받아 상위클래스의 super.hasCode(); 값을 리턴 받는 모습니다.


캡슐화 (Encapsulation)

캡슐화란? 특정 객체 안에 관련된 속성과 기능을 하나의 캡슐로 만들어
데이터를 외부로부터 보호하는 것을 말한다.

  1. 데이터 보호목적
  2. 내부적으로만 사용되는 데이터에 대한 불필요한 외부 노출을 방지

즉, 정보 은닉이 가장 큰 장점이다.
캡슐화를 할 수 있는 자바의 문법인 접근제어자와 getter,setter를 알아보자
알아보기전에 패키지에 대한 지식을 먼저 공부해보자

패키지

특정한 목적을 공유한는 클래스와 인터페이스의 묶음이다.
만드는 방법은 클래스 만들듯 패키지를 생성하고
그 안에 클래스나 인터페이스를 생성하면된다.

package test;

public class Main {
    public static void main(String[] args) {
        System.out.println("나는 패키지안에 클래스");
    }
}

test라는 패키지 안에 Main이라는 클래스를 만들었다.
자바의 대표적인 패키지는 기본클래스들을 모아놓은 java.lang이다.
확장 클래스를 묶어 놓은 java.util도 있고 입출력관 관련된 클래스를 묶어놓은
java.io / java.nio 등이 있다.

예를들어 String이라는 클래스를 자주쓰는데 실제이름은 java.lang.String이다.
(.)은 계측구조를 나타낼때 사용한다.

image

예제로 패키지를 만들어 안에 Main이라는 이름의 파일을 만들어 보았다.
여기서 중요한점은 패키지가 다를 경우 동일한 이름의 Class도 생성이 가능하다.
위에 빨간색 네모부터 경로를 얘기하면

java.test.test 안에 Main클래스가 있고
java.test 안에 Main 클래스
java 파일 안에 Main 클래스가 있는 순서이다.


import

다른 패키지 내의 클래스를 사용하기 위해 사용한다.
예를들어 Scanner를 이용할때 위에 import되듯이 본인 패키지 외에
클래스를 사용하려고 쓰는 문법이다.

import문은 컴파일 시에 처리가되어 프로그램의 성능에는 영향을 주지 않는다.

작성방법은 아래와 같다.

import 패키지명.클래스명; 또는 import 패키지명.*;

IDE를 이용하다보면 필요한 import문을 자동으로 입력해준다.
여기서 패키지명.*의 내용은 패키지의 모든 클래스를 패키지명 없이
사용이 가능하다는 뜻이다.

간단한 예제로 import 사용방법을 알아보자

package test;

public class Import {
    int a = 3;
    int b = 5;
    public void sumNumber(){
        System.out.println(this.a+this.b);
    }
}

test라는 패키지안에 Import라는 클래스를 만들 었다.
안에 멤버는 a,b 그리고 sumNumber(); 이 있다.

import test.Import;

public class Main {
    public static void main(String[] args) {
        Import test = new Import();
        test.sumNumber();
    }
}

// 출력
8

여기서 Main이라는 클래스에서 외부에 있는
Import 클래스를 호출하려면….
맨위에 보는 것 처럼 import test.Import;
라는 구문으로 정의를 해주어야 아래와 같이 Import 클래스를 호출하여
가져다 쓸 수 있다. 위의 예제는 test 인스턴스를만들어
test.sumNumber();를 호출해 출력값으로 8을 확인해 볼 수 있다.


접근 제어자

드디어 접근제어자를 배워볼 시간이다.
앞선 시간에서 public, private 이 자리를 접근제어자라고 우리는 들었다.
제어자라는게 무슨 말인지 부터 배워보자

제어자란?

클래스,필드,메서드,생성자 등에 부가적인 의미를 부여하는 키워드이다.
마치 형용사의 역활과 같다. (빠른 자동차, 맛있는 탕수육 etc..)
각 대상에 대해서 접근 제어자는 단 한번만 사용할 수 있다.

제어자의 종류는 크게 2가지다.

  1. 접근 제어자 (public,protected,(default),private)
  2. 기타 제어자 (static,final,abstract,native,transient,synchronized etc..)
접근 제어자 접근 제한 범위
private 동일 클래스에서만 접근 가능
default 동일 패키지 내에서만 접근 가능
protected 동일 패키지 + 다른 패키지의 하위 클래스에서 접근 가능
public 접근 제한 없음

제한 범위를 표현하자면 아래에서부터 범위가 넓다.
참고로 default의 경우는 아무런 접근 제어자가 붙이지 않는 경우 이다.

ex) void sumNumber(){}

대상 사용가능한 접근 제어자
클래스 public, (default), final, abstract
메서드 모든 접근제어자, final, abstract, static
멤버변수 모든 접근제어자, final, static
지역변수 final

위에 사진은 접근제어자가 사용가능한 내용이다.
유용한정보! 클래스는 private, protected가 사용 불가!
불가능한 이유는 추후에 알아보자…

이제 접근제어자에 대한 코드 예제를 통해 알아보자

  • 같은 패키지 + 동일 클래스
package package1;

public class Parent {
    private int num1 = 1;
    int num2 = 2;
    protected int num3 = 3;
    public int num4 = 4;
    
    //같은 클래스이기 때문에 에러 발생하지 않음
    public void printNum(){
        System.out.println(num1);
        System.out.println(num2);
        System.out.println(num3);
        System.out.println(num4);
    }

}

package1 이라는 패키지 파일안에
Print라는 클래스와 Parent라는 클래스(아래에 있음) 두개를 만들었다.
Parent 클래스 안에는 num1~num4까지 접근제어자를 앞에 붙여주었고 테스트를 진행해보자.

먼저 Parent 클래스안에 printNum();이라는 함수를 만들었는데
여기서 에러가 발생하지 않는다.

왜냐하면 Parnet라는 클래스 안에서 이루어지는 일들이라
어떠한 접근제어자도 포함시키기 때문이다.

즉, 간단하게 생각하면 동일 클래스내에서는 접근제어자를 신경쓸필요없다!
(이 외에 밖에 클래스에 따라 필요한 범위만큼 접근제어자를 쓰는느낌)

  • 같은 패키지 + 다른 클래스
package package1;

public class Print {
    public static void main(String[] args) {
        Parent test = new Parent();
        System.out.println(test.num1); //num1 has private access in Parent 에러 발생 
        System.out.println(test.num2);
        System.out.println(test.num3);
        System.out.println(test.num4);
    }
}

연장 선상으로 Print라는 같은 패키지안에있는 다른 클래스이다.
여기서 만약 num1~num4까지 접근하려고한다면
private로 선언된 num1은 에러가 발생할 것이다.

  • 다른 패키지 + 하위 클래스
package package2;

import package1.Parent;

public class Child extends Parent {
    public void printParent(){
        System.out.println(num1); // private 에러발생
        System.out.println(num2); // default 에러발생
        System.out.println(num3);
        System.out.println(num4);
    }
}

package2라는 패키지의 Child라는 클래스를 만들었다.
여기서 특이점은 Child 클래스는 하위클래스, Parent 클래슨느 상위클래스로
상속받기 extends 구문을 추가해주었다.

위에서 알 수 있듯이 protected , public 접근제어자로 설정된 num3,num4는
에러가 발생하지 않는다.

  • 다른 패키지 + 다른 클래스
package package2;

import package1.Parent;

public class Test {
    public static void main(String[] args) {
        Parent test = new Parent();
        System.out.println(test.num1); // private 에러 발생
        System.out.println(test.num2); // default 에러 발생
        System.out.println(test.num3); // protected 에러 발생
        System.out.println(test.num4);
    }
}

package2라는 패키지안에 Test라는 클래스다
package1에 있는 Parent 클래스를 import시켜준후
test라는 인스턴스를 만들고 각각 출력해보았다.

이 경우에는 public을 제외한 접근제어자가 붙은 모든 인스턴스 변수들은
접근제어자에 의해 알람이 발생하는 모습을 확인할 수 있다.


getter와 setter

여기서 위에 객체지향의 캡슐화의 따라 데이터의 보호와 은닉하는
접근 제어자를 배웠다. 하지만 캡슐화의 목적을 달성하면서도 데이터가 변경이
필요한 경우가 있다. 이때 우리는 getter, setter 메서드를 사용할 수 있다.

예를들어 private로 다른클래스에선 데이터를 읽지도 쓰지도 못하는데
이것을 쓸 수 있게하는 방법은 아래의 예제와 같다.

public class Test {
    public static void main(String[] args) {
        Person man = new Person(); // 다른클래스인 Person으로 man이라는 인스턴스 생성
        man.setName("이재혁"); // private name 인스턴스변수에 값을 넣는 메서드 호출
        man.setAge(28);
        man.setId(950203);

        System.out.println("이 사람의 이름은 " + man.getName());
        System.out.println("이 사람의 나이는 " + man.getAge());
        System.out.println("이 사람의 주민번호는 " + man.getId());
    }
}
public class Person {
    private String name;
    private int age;
    private int id;

    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        if(age < 1) return;
        this.age = age;
    }
    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
}

private 접근제어자는 동일 클래스 내에서 밖에 접근을 못한다.
하지만 다른클래스에서 점근을하려면 getter, setter라는 메서드를 만들어 접근이 가능하다.

현재 Person이라는 클래스의 setName 메서드는 public이다.
이말은 즉슨 다른 클래스에서 접근이 가능하고
다른 클래스인 Test라는 클래스에서 man이라는 인스턴스를 만들어
setName을 호출중인 모습이다. setName 인자에다가
“이재혁”이라는 이름을 넣고 호출할 경우 this.name = name;

즉, Person 인스턴스변수 name에다가 setName한 “이재혁”이란 값을 할당하는 샘이다.
이렇게 될 경우 값은 정상적으로 설정되게 되어지며
이 설정값을 불러오기 위해서 아래에 getName();이라는 메서드로
return 값을 인스턴스 변수인 name을 리턴하게되면 다른클래스에서도 이값을 볼 수 있게되는 것이다.

즉, 이렇게 값을 효과적으로 보호하면서도 의도된 값으로 값을 변경하여
캡슐화를 보다 효과적으로 달성할 수 있다.



마치며…

오늘은 내가 정말 알지 못했던 내용인 상속과 캡슐화에대해 공부했다.
상속과 캡슐화라는 객체지향의 룰을 지키면서 사용하는 문법에 대해 공부했고
객체 지향 기초보다 확실히 난해한 느낌이 강하다.

공부하면서 연습도하고 어느정도 느낌은 알았는데
이것들을 실전에서 어떤식으로 적용할까? 라는 궁금증도 생기고
그럼 public로 설정하는 속성들은 없는건가? 의문도들고
아직 모르는 것들 투성이지만..! 오늘도 객체지향에 한걸음
나아간 느낌이들어 만족스럽고 앞으로도 계속 만날 개념들이니
포스트잇에 붙여놓고 항상 까먹을때마다 눈앞에 보이는 곳에 두고
기억을 해둬야겠다!!

이상 오늘 공부 끝!!


오늘의 커피량: ☕️ ☕️ ☕️ ☕️️️️
오늘의 점심: 뼈해장국, 옛날소세지, 공기밥