[Section 1] 상속, 캡슐화

Kim·2022년 9월 6일
0

Boot Camp

목록 보기
13/64
post-thumbnail

지난 이틀간 객체지향 프로그래밍의 기본이되는 클래스와 객체를 배웠고 이를 정의하기 위한 변수와 메서드, 그리고 변수를 초기화하기 위한 생성자에 대해 배웠다.

객체지향 프로그래밍의 핵심 기둥이 되는 4가지는 상속, 캡슐화, 다형성, 추상화다. 그 중에서 오늘은 상속과 캡슐화에 대해 배운다.
이 4가지는 객체지향 프로그래밍 설계를 지향하는 모든 언어라면 공통적으로 적용된다.


상속

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

상위 클래스와 하위 클래스가 있을 때, 상위 클래스의 멤버(필드, 메서드, 이너 클래스)를 하위 클래스와 공유하는 것을 의미한다. 하위 클래스는 상위 클래스가 가진 모든 멤버를 상속받는다. 이때 두 클래스가 서로 상속 관계라고 부른다.

하위 클래스의 멤버 개수는 항상 상위 클래스와 비교했을 때 같거나 많다. 또한, "~클래스에서 상속받았다." 보다는 "~클래스에서 확장되었다." 라는 표현이 적절하다.

위 그림에는 3개의 클래스가 정의되어 있다.
People 클래스의 속성은 이름과 나이, 기능은 먹기, 자기가 정의되어 있다.
Student 클래스의 속성은 이름과 나이,고 기능은 먹기, 자기, 배우기가 정의되어 있다.
teacher 클래스의 속성은 이름과 나이, 기능은 먹기, 자기, 가르치기가 정의되어 있다.
이 세 개의 클래스에 공통적인 속성과 기능이 정의되어 있는 것을 알 수 있다.

위 그림을 상속의 맥락에서 설명하자면, People 클래스가 상위 클래스이며 Student, teacher 클래스가 상위 클래스로부터 확장된 속성과 기능을 가진 하위 클래스가 되는 것이다.

상속을 사용하는 이유

상속을 통해 클래스를 작성하면 코드를 재사용하여 비교적 적은 양의 코드로 새로운 클래스를 작성할 수 있어 코드 중복을 제거할 수 있다.
또한, 다형적 표현이 가능하다는 장점도 있다.

class People {
    String name;
    int age;

    void eat(){
        System.out.println("밥을 먹는다.");
    };
    void sleep(){
        System.out.println("잠을 잔다.");
    };
}

class Student extends People { //People 클래스로부터 상속 (extends 키워드 사용)
    String className;

    void learn(){
        System.out.println("수업을 배룬다.");
    };
}

class Teacher extends People { 
    String schoolName;

    void teach(){
		    System.out.println("수업을 가르친다.");
		};
}

public class HelloJava {
    public static void main(String[] args){

        //People 객체 생성
        People p = new People();
        p.name = "홍길동";
        p.age = 24;
        p.eat();
        p.sleep();
        System.out.println(p.name);

        //Student 객체 생성
        Student st = new Student();
        st.name = "신짱구";
        st.age = 26;
        st.sleep(); //People 에서 상속받아 사용 가능
        st.learn(); // People 의 개별 기능
        System.out.println(st.name);

    }
}

앞서 그림의 예시를 코드로 표현한 것이다.
People 클래스로부터 Student, Teacher 클래스가 확장되어 People 클래스 내에 속성과 기능을 사용할 수 있다.

중요한 점은 자바의 객체지향 프로그래밍에서는 단일 상속만을 허용한다.

포함 관계

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

public class Employee {
    int id;
    String name;
    Address address;

    public Employee(int id, String name, Address address) {
        this.id = id;
        this.name = name;
        this.address = address;
    }

    void showInfo() {
        System.out.println(id + " " + name);
        System.out.println(address.city+ " " + address.country);
    }

    public static void main(String[] args) {
        Address address1 = new Address("서울", "한국");
        Address address2 = new Address("도쿄", "일본");

        Employee e = new Employee(1, "홍길동", address1);
        Employee e2 = new Employee(2, "신짱구", address2);

        e.showInfo();
        e2.showInfo();
    }
}

class Address {
    String city, country;

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

위 코드는 근로자를 표현하기 위한 Employee 클래스의 멤버 변수다. 근로자의 주소를 나타내는 Address 클래스가 정의되어 있다.

Address 클래스에 포함된 인스턴스 변수들(city, country)을 묶어주고 Employee 클래스 안에 참조변수를 선언했다.

객체지향 프로그래밍에서는 상속보다 포함 관계를 사용하는 경우가 더 많다.
클래스 간의 관계를 설정할 때, 상속 관계를 맺어줄 것인지 포함 관계를 맺어 줄 것인지를 어떤 기준으로 선택해야 할까?

가장 쉬운 방법은 클래스 간의 관계가 ~은 ~이다.(IS-A) 인지 ~은 ~을 가지고 있다.(HAS-A) 인지 생각해보는 것이다.
"Employee는 Address이다." 라는 문장은 성립하지 않지만, "Employee는 Address를 가지고 있다."는 어색하지 않다. 이 경우에는 포함 관계가 적합하다..

메서드 오버라이딩

메서드 오버라이딩은 상위 클래스로부터 상속받은 메서드와 동일한 이름의 메서드를 재정의하는 것이다.
Override의 의미가 "~위에 덮어쓰다."를 의미한다는 것을 생각하면 이해하기 쉽다. 우리가 컴퓨터에 동일한 위치에서 같은 이름의 파일을 저장하고자 할 때 덮어쓰기를 사용하는데, 그 덮어쓰기를 연상해보면 쉽게 이해할 수 있다.

메서드 오버라이딩을 사용할 때 반드시 세 가지의 조건을 만족해야 한다.

1. 메서드의 선언부(메서드명, 매개변수, 반환타입)가 상위 클래스의 그것과 일치해야 한다.
2. 접근 제어자의 범위가 상위 클래스의 메서드보다 같거나 넓어야 한다.
3. 예외는 상위 클래스의 메서드보다 많이 선언할 수 없다.

모든 객체를 상위 클래스 타입 하나로 선언하게 되면, 간편하게 배열로 선언해 관리할 수 있다는 편리함이 있다.

super vs super()

super

지난 시간에 thisthis()에 대해 배웠는데 super는 기본적적으로 this와 같은 것이라고 할 수 있다. 다만, super상위 클래스의 멤버와 자신의 멤버를 구별하는데 사용된다는 점이 this와의 차이점이다.

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

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

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);
    }
}

위 코드에서 Lower 클래스는 Upper 클래스로부터 변수를 상속받는데, 자신의 인스턴스 변수와 이름이 같아 둘을 구분해야 한다.
이런 경우, 두 개의 같은 이름의 변수를 구분하기 위해 super 키워드를 사용한다.

super 를 붙이지 않으면 자바 컴파일러는 자신이 속한 인스턴스 객체의 멤버를 먼저 참조하게 된다. 종종 상위 클래스의 변수를 참조해야 할 경우가 있는데, 그 경우 super를 사용해 부모 객체의 멤버 값을 참고할 수 있다.

super()

super()this()처럼 생성자를 호출할 때 사용한다. this()와의 차이점은 this()는 같은 클래스의 다른 생성자를 호출하지만, super()0는 상위 클래스의 생성자를 호출하는데 사용된다.

public class Test {
    public static void main(String[] args) {
        Student s = new Student();
    }
}

class Human {
    Human() {
        System.out.println("휴먼 클래스 생성자");
    }
}

class Student extends Human { // Human 클래스로부터 상속
    Student() {    
        super(); // Human 클래스의 생성자 호출
        System.out.println("학생 클래스 생성자");
    }
}

Human 클래스를 확장해서 Student 클래스를 생성했다.
Student 생성자를 통해 상위 클래스인 Human 클래스의 생성자를 호출하고 있다.

super() 또한 this()와 마찬가지로 생성자 내에서만 사용이 가능하고 반드시 첫 줄에 작성되어야 한다.

가장 중요한 점은 모든 생성자의 첫 줄에는 반드시 super() 또는 this()가 선언되어야 한다는 것이다.

Object Class

Object 클래스란 자바의 클래스 상속 계층도에서 최상위에 위치한 상위 클래스다. 이 말은 즉, 자바의 모든 클래스는 Object 클래스로부터 확장된다는 것이다.
자바 컴파일러는 다른 클래스로부터 아무런 상속을 받지 않는 클래스에 자동으로 extends Object를 추가해서 Object 클래스를 상속받게 한다.

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

}

class ChildEx extends ParentEx {

}

ParentEx 클래스를 상속받아 ChildEx 클래스를 만들었을 때, 상위 클래스인 ParentEx 클래스는 아무것도 상속하고 있지 않아 extends Object를 추가하는 것이다.

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

캡슐화

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

캡슐화를 하는 목적은 크게 두 가지다.

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

정리하자면, 캡슐화의 장점은 정보 은닉에 있다.
외부로부터 객체의 속성과 기능이 함부로 변경되는 것을 막고, 데이터가 변경되더라도 다른 객체에 영향을 주지 않아 독립성을 확보할 수 있다.
유지보수와 코드 확장 시에도 오류의 범위를 최소화할 수 있다.

패키지

패키지란 특정 목적을 공유하는 클래스와 인터페이스의 묶음을 의미한다. 클래스들을 그룹으로 묶어 효과적으로 관리하기 위한 목적을 갖고 있다.
컴퓨터를 사용할 때 폴더를 만들고 그 안에 관련된 파일들을 넣어 관리하는 것과 비슷하다고 볼 수 있다.

자바에서의 패키지란 하나의 디렉토리다. 하나의 패키지에 속한 클래스나 인터페이스 파일은 모두 해당 패키지에 속해있다.
디렉토리는 하나의 계층구조를 가졌다. 계층구조 간의 구분은 .으로 표현한다.

//패키지를 생성했을 때
package practicepack.test; //패키지 구문 포함 (패키지가 없다면 구문 필요없음)

public class PackageEx {

}

Import문

import문은 다른 패키지 내의 클래스를 사용하기 위해 사용한다. 일반적으로 패키지의 구문과 클래스문 사이에서 작성한다.

import문을 사용하지 않고 다른 패키지의 클래스를 사용하려면 매번 패키지 명을 붙여줘야 한다.
반면에 import문을 사용하면 사전에 컴파일러에게 소스파일에 사용된 클래스에 대한 정보를 제공하기 때문에 번거로움을 덜어준다.

//import문을 사용하지 않는 경우
package practicepack.test2;

public class PackageImp {
		public static void main(String[] args) {
			practicepack.test.ExampleImport example = new practicepack.test.ExampleImport();
		}
}
//import문을 사용하는 경우
package practicepack.test2; 

import practicepack.test.ExampleImp //import문 작성

public class PackageImp {
		public static void main(String[] args) {
			ExampleImp x = new ExampleImp(); //패키지명 생략 가능
		}
}

import문은 아래와 같이 작성할 수 있다.

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

접근 제어자

Modifier : 제어자

자바 프로그래밍에서의 제어자는 클래스, 필드, 메서드, 생성자 등에 부가적인 의미를 부여하는 키워드다.
예를 들면 파란 하늘, 붉은 노을 에서의 파란붉은처럼 명사를 꾸며주는 형용사의 역할이다.

접근 제어자public, protected, (default), private
기타 제어자static, final, abstract, native, transient, synchronized · · ·

높고 파란 하늘에서 높고, 파란이란 형용사가 두 번 사용된 것처럼 하나의 대상에 대해 여러 제어자를 사용할 수 있다.
하지만, 각 대상에 대해서 접근 제어자는 한 번만 사용할 수 있다.

Access Modifier : 접근 제어자

접근 제어자를 사용하면 클래스의 불필요한 데이터 노출을 방지할 수 있고, 외부로부터 데이터가 임의로 변경되지 않게 막을 수 있다.
자바의 접근 제어자는 아래와 같이 4가지다.

접근 제어자접근 가능 범위
private동일 클래스 내에서만 접근 가능
default동일 패키지 내에서만 접근 가능
protectes동일 패키지 & 다른 패키지의 하위 클래스에서 접근 가능
public접근 제한 없음

public(접근 제한 없음) > protected(동일 패키지 + 하위클래스) > default(동일 패키지) > private(동일 클래스) 로 표현할 수 있다.

defult의 경우 기본적인 설정을 의미한다. 변수명 앞에 아무런 접근 제어자가 없는 경우 자동으로 해당 변수의 접근 제어자는 default가 된다.

접근 제어자클래스 내패키지 내다른 패키지의 하위 클래스패키지 외
privateOXXX
defaultOOXX
protectesOOOX
publicOOOO

getter vs setter

객체지향의 캡슐화의 목적을 달성하면서 데이터의 변경이 필요한 경우엔 어떻게 할까?
private 접근 제어자가 포함되어 있는 객체의 변수에 데이터값을 추가하거나 수정하고 싶을 때를 생각해보자.
이런 경우 gettersetter 메서드를 사용할 수 있다.

public class GetterSetterTest {
    public static void main(String[] args) {
        Worker w = new Worker();
        w.setName("홍길동");
        w.setAge(24);
        w.setId(5);

        String name = w.getName();
        System.out.println("근로자의 이름은 " + name);
        int age = w.getAge();
        System.out.println("근로자의 나이는 " + age);
        int id = w.getId();
        System.out.println("근로자의 ID는 " + id);
    }
}

class Worker {
    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;
    }
}

setter는 외부에서 메서드에 접근해 조건에 맞는 경우 데이터 값을 변경할 수 있게 한다. 일반적으로 메서드명에 set-을 붙여서 정의한다.

getter는 이렇게 설정한 변수의 값을 읽어오는데 사용한다. 객체 외부에서 필드값을 사용하기에 부적절한 경우가 발생할 수 있는데, 이런 경우 그 값을 가공한 후 외부로 전달하는 역할을 한다. 일반적으로 메서드명 앞에 get-을 붙여서 사용한다.

참고자료

상속(프로그래밍)
상속(객체 지향 프로그래밍)
상속의 개념

캡슐화(1)
캡슐화(2)
Java - Encapsulation

0개의 댓글