좋은 객체 지향 프로그래밍??

  1. 객체 지향 프로그래밍의 의미
    • 프로그램 명령을 목록으로 보는 시각에서 벗어나 여러개의 독립된 단위, “객체”들의 모임으로, 각각의 객체 끼리 데이터를 주고 받으며 처리 할 수 있는 프로그래밍을 말한다.
    • 객체 지향 프로그래밍은 “유연” 하고 “변경”이 용이 하여 대규모 소프트웨어 개발에 많이 사용된다.

⎈ 객체 지향의 유연, 변경 용이??

  • 레고 블럭 조립하듯이
  • 키보드, 마우스 갈아 끼우듯
  • 컴퓨터 부품 갈아 끼우듯
  • 컴포넌트를 쉽고 유연하게 변경하면서 개발할 수 있는 방법
  1. 객체 지향 특징
    • 추상화
    • 캡슐화
    • 상속
    • 다형성 ⇒ 객제지향 프로그래밍에서 가장 중요

다형성의 실세계 비유

  • 역할과 구현으로 세상을 구분
  • 역할 ⇒ 인터페이스
  • 구현 ⇒ 역할을 구현한 실제 객체

⇒ 자동차 역할 에 k3, 아반떼, 테슬라 무엇이 오든 상관 없이 운전자는 자동차를 사용할 수 있다.

 

⇒ 로미오, 줄리엣 역할을 누가 하든 로미오나 줄리엣에 영향을 끼치지 않고 변환이 가능하다.

 

 

역할(인터페이스)과 구현(객체)을 분리 의 장점

  • 사용자 (Client) 는 구현 까지 알 필요 없고 역할 (인터페이스) 만 알면 되고, 구현 (내부 구조) 는 몰라도 되며, 구현 (내부 구조) 가 변경이 되어도 영향을 받지 않는다. 그리고 구현 자체를 변경하여도 클라이언트는 영향을 받지 않는다.

역할 과 구현의 분리 한계 점

  • 인터페이스가 변경이 되면 클라이언트 와 서버 모두 큰 변경이 발생 한다.

다형성의 본질

  • 클라이언트를 변경하지 않고 서버의 구현 기능을 유연하게 변경할 수 있는 것

⇒ 클라이언트가 car 라는 인터페이스를 사용할 때 인터페이스와 클라이언트 변경 없이 그때그때 원하는 것을 사용할 수 있게 해주는 것


OCP 원칙 (Open-Closed Princilpe)

  • 좋은 객체 지향 설계 원칙 중 하나로 OCP 원칙이라는 것이 있다.
  • Open for extension : 새로운 기능의 추가나 변경 사항이 생겼을 때, 기존 코드는 확장할 수 있어야 한다. ⇒ 확장의 기능
  • Closed for modification : 기존의 코드는 수정되지 않아야 한다.

⇒ 결론 : 기존 코드의 수정 없이 새로운 기능을 추가할 수 있다는 의미, 다형성이 바로 이 OCP 원칙을 잘 키고 있는 코드이다.

⇒ NewCar 가 추가 되어도 기존 코드 Car 나 Driver 의 코드에 변경 없이 잘 사용 된다.

  • Open 의미 ⇒ Car 인터페이스를 사용해 새로운 차량을 자유롭게 추가할 수 있는 의미, 그리고 클라이언트인 Driver 오 Car 인터페이스를 통해 추가된 차량을 자유롭게 호출할 수 있다. 이것이 확장에 열려 있다는 의미이다.
  • Closed 의미 ⇒ 새로운 차가 추가 되었을 때 당연히 어딘가의 코드는 수정 되지만, 핵심 부분인 Car 나 Driver 부분이 수정 되지 않는 뜻 이다.

인터페이스의 다중 구현

  • 상속 에서 다중 상속이 불가능한 이유 ⇒ 부모의 메서드를 무조건 오버라이딩 하여 사용 하는 것이 아니므로 서로 다른 부모의 동일한 메서드가 있을때 어떤 것을 호출 해야 하는 지 몰라 에러가 발생하게 된다. ⇒ 이걸 다이아몬드 문제라고 한다.⇒어떤 부모의 move() 를 상속 받아 사용할지 모른다??

⇒어떤 부모의 move() 를 상속 받아 사용할지 모른다??

  • 인터페이스가 다중 구현이 가능한 이유 ⇒ 인터페이스의 메서드는 모두 추상 메서드 이기 때문에 자식 클래스에서 구현이 꼭 되어야 한다 ⇒ 모호한 경우가 발생하지 않기 때문에 사용 가능하다.

⇒ 어떤 부모 인터페이스 이든 상관없이 자식 클래스에서 구현 되어지는 모습

다중 상속을 받는 자식 class 의 메모리 구조

⇒ 자식 객체 내부에 2개의 인터페이스 영역도 같이 생기게 되고 어떤 타입 인지에 따라 호출 가능 대상이 달라진다.


인터페이스

  • 메서드가 모두 추상 메서드인 순수 추상 클래스를 더 편리하게 사용할 수 있도록 기능을 제공
  • class 가 아닌 interface 키워드를 사용한다.
  • interface 는 모두 추상 메서드 이기 때문에 메서드에 abstract 키워드 생략 가능하다.
public interface InterfaceAnimal {
	void aound();
	void move();
}

⇒ 위와 같이 사용한다.

인터페이스 특징

  • 인터페이스 메서드는 모두 public, abstract 이다. ⇒ 생략이 권장 된다.
  • 인터페이스는 다중 구현 (다중 상속) 을 지원한다.
  • 자기 자신의 객체 생성이 불가능 하다.
  • 구현 시 모든 메서드를 오버라이딩 해야 한다.
  • 주로 다형성을 위해 사용된다.
  • 인터페이스의 자식 클래스는 뭔가 부모 인터페이스의 모든 것을 사용해야 하기 때문에 상속이 아니라 구현 이라고 표현한다. (메모리 구조는 상속과 같다)
  • 구현 하는 자식 class 에서 implemets 라는 키워드를 사용 하여 구현 받는다.

 

⎈ 인터페이스의 메서드의 접근 제어자가 기본 public 인 이유?

  • 인터페이스는 보통 여러 군데에 사용한다는 전제 하에 만들어 지기 때문에 public 이 기본으로 되어있다.

 

인터페이스의 멤버 변수

  • 인터페이스의 멤버 변수는 public, static, final 태그가 모두 포함되어 있다고 간주하고 사용된다.
  • static final 태그를 붙여 정적이면서 고칠 수 없는 상수로 만든다.

 

인터페이스의 메소드를 오버라이딩 한 메소드 호출 방식

⇒ 추상 class 의 추상 method 를 오버랑이딩 호출 과 같은 방식이다.

 

상속 VS 구현

  • 상속 (클래스)
    • 이름 그대로 부모의 기능을 물려 받는 것을 목적으로 사용한다.
    • 부모의 기능 (메서드) 의 바디부위가 존재하고 그것을 자식이 가져다 사용하는 느낌
  • 구현 (인터페이스)
    • 모든 기능 (메서드)이 추상 메서드 이기 때문에 바디 부위가 없는 메서드를 자식이 전부 오버라이딩해 구현 해낸다는 느낌

⇒ 상속과 구현은 사람이 표현하는 단어만 다를 뿐 자바 입장에서 의 메모리 구조는 같고, 동일하게 작동한다.

 

인터페이스 사용 이유?

  • 제약 : 인터페이스의 메서드를 반드시 구현해 내야한다는 것을 알려주기 위해, 그리고 인터페이스 내에 추상 메서드가 아닌 구현 메서드를 작성하는 실수를 막기 위해 사용한다.
  • 다중 구현 : 상속은 자식이 1개의 부모만 받을 수 있지만 구현은 여러개의 인터페이스를 부모로 구현 할 수 있다.

순수 추상 클래스

  • 해당 클래스의 모든 메서드가 추상 메서드일때를 순수 추상 클래스 라고 한다.
  • 그러면 상속 받는 자식 클래스에선 모든 메서드를 오버라이딩 해야 한다.

순수 추상 클래스의 특징

  • 순수 추상클래스는 기능을 구현하는 몸통이 없으므로, 단지 다형성만을 위한 부모 타입의 껍데기 역할만 제공 한다.
  1. 인스턴스를 생성할 수 없다.
  2. 상속시 자식은 모든 메서드를 오버라이딩 해야 한다.
  3. 주로 다형성을 위해 사용된다.

⇒ 이러한 특징으로 순수 추상 클래스를 상속 받는 자식 클래스는 상속 이란 개념보다 어떠한 규격을 지켜 구현 해야 하는 것 처럼 느껴지게 한다.

순수 추상 클래스 라는 용어는 없다 → 인터페이스가 이 기능을 제공한다.


추상클래스

  • 부모 클래스는 제공하지만, 실제 생성되면 안되는 클래스를 말한다.
  • 말그대로 추상적인 개념을 제공하는 클래스로, 객체(인스턴스)가 존재하지 않는다.
  • 상속의 목적으로 사용되고, 부모 클래스 역할을 담당한다.
  • 개발자가 실수로 오버라이딩을 하지 않고 생성하여 코드상 아무런 문제가 발생하지 않는 경우가 생기는것을 방지하기 위해 사용한다.

→ 사용 법 ) class 앞에 abstract 를 붙여주어 사용한다.

abstract class AbstractAnimal{ ... } // <- 추상 클래스 선언

⇒ 특징 )

  1. 추상 클래스는 기존 클래스와 완전 같다
  2. 다만 new AbstactAnimal 과 같은 자신으로 객체 (인스턴스) 생성을 못하도록 막는다.

추상메서드

  • 부모 클래스를 상속 받는 자식 클래스가 반드시 오버라이딩 해야 하는 메서드 이다.
  • 부모 클래스에 작성한 추상 메서드는 body 부분이 없어야 한다.

→ 사용 법 ) class 와 마찬가지로 class 의 메서드 앞에 abstract 를 붙여주어 사용한다.

abstract class AbstractAnimal{ // <- 추상 클래스 선언
		public abstract void sound () // <- 추상 메서드 선언
} 

⇒ 특징 )

  1. 추상 메서드가 1개라도 있는 클래스는 무조건 추상 class 로 만들어 주어야한다.
  2. 추상 메서드는 자식 클래스에서 반드시 오버라이딩 되어 사용되어야 한다. → 그렇지 않으면 컴파일 오류가 발생한다.
  3. 만약 추상 메서드가 존재한 클래스를 상속 받는 자식 클래스가 추상 메서드를 오버라이딩 하지 않는다면 해당 자식 클래스도 추상 클래스가 되어야 오류가 발생하지 않는다.

⇒ 결론 ) 추상 메서드는 기존 메서드와 같지만 바디가 없고, 자식 클래스에서 반드시 오버라이딩 되거나 자식을 추상 클래스로 만들어야하는 제약이 생긴다.

추상 클래스, 추상 메서드 결론

  • 추상 클래스 덕분에 실수로 부모 객체를 생성 하는 문제를 근본적으로 방지해준다.
  • 추상 메서드 덕분에 새로운 자식 클래스를 만들 때 필수 오버라이딩을 안 하는 문제를 방지해준다.

다형성 활용 - 문제 인식

package poly.ex1;

public class AnimalSoundMain {
    public static void main(String[] args) {
        Dog dog = new Dog();
        Cat cat = new Cat();
        Caw caw = new Caw();

        System.out.println("동물 소리 테스트 시작");
        dog.sound();
        System.out.println("동물 소리 테스트 종료");

        System.out.println("동물 소리 테스트 시작");
        cat.sound();
        System.out.println("동물 소리 테스트 종료");

        System.out.println("동물 소리 테스트 시작");
        caw.sound();
        System.out.println("동물 소리 테스트 종료");

    }
}

→ 다음과 같은 코드를 사용할때에 동물이 늘어나거나 줄어들때에 계속 코드를 수정 해주어야 한다.

System.out.println("동물 소리 테스트 시작");
caw.sound();
System.out.println("동물 소리 테스트 종료");

→ 코드를 보면 이런 형식이 반복 되는것을 볼 수 있다.

→ for문을 돌릴 수도 있지만 Dog, Cat, Caw 의 클래스가 다 다른 클래스 이어서 문제가 발생한다.

→ 이럴때 상속과 다형성, 메서드 오버라이딩을 사용하면 된다.


다형성 사용 - 다형성으로 문제 풀기

다형성을 사용하기 위해 위와 같은 상속 관계를 만들어 줄 것이다.

  1. animal 에서 sound 기능을 생성해주고
  2. 각각 dog, cat, caw 가 상속을 받고 sound 기능을 오버라이딩하여 사용할 것 이다.

⇒ 다형적 참조와 메서드 오버라이딩 덕분에 여러줄의 코드 추가 없이 간단하게 재사용하여 해결 할 수 있다.


다형성 활용3

→ 다형적 참조와 메서드 오버라이딩 한 코드를 배열과 for 문을 사용하여 중복을 제거해보자

package poly.ex2;

public class AnimalPolyMain2 {
    public static void main(String[] args) {
        Dog dog = new Dog();
        Cat cat = new Cat();
        Caw caw = new Caw();
        Animal[] animalArr = {dog, cat, caw};
        // Dog, Cat, Caw 모두 Animal 타입 이기때문에 받을수 있다
        for (Animal animal : animalArr) {
            System.out.println("동물 소리 테스트 시작");
            animal.sound();
            System.out.println("동물 소리 테스트 종료");
        }
        // 배열을 순회하면서 오버라이딩 메서드도 호출 된다.
    }
}

→ 더 간단하게 다음과 같이 만들 수 있다.

package poly.ex2;

public class AnimalPolyMain3 {
    public static void main(String[] args) {
        Animal[] animalArr = {new Dog(), new Cat(), new Caw()};

        for (Animal animal : animalArr) {
            soundAnimal(animal);
        }
    }

    // 변하지 않는 부분
    private static void soundAnimal(Animal animal) {
        System.out.println("동물 소리 테스트 시작");
        animal.sound();
        System.out.println("동물 소리 테스트 종료");
    }
}


다형성과 메서드 오버라이딩

  • 다형성을 이루는 중요한 요인중 위에서 공부한 다형적 참조와 이번에 배울 메서드 오버라이딩이 핵심이론이다.
  • 기억해야할점은 오버라이딩 된 메서드가 항상 우선권을 가진다.

→ 자식 타입 변수 가 자식 객체를 참조 할때에 🔽

package poly.overriding;

public class OverridingMain {
    public static void main(String[] args) {
        // 자식 변수가 자식 인스턴스 참조
        Child child = new Child();
        System.out.println("Child -> Child");
        System.out.println("value = " + child.value);
        child.method();
    }
}

→ 우리가 배운대로 찾는다.

 

→ 부모 타입 변수 가 부모 객체를 참조 할때에 🔽

// 부모 변수가 부모 인스턴스 참조
Parent parent = new Parent();
System.out.println("Parent -> Parent");
System.out.println("value = " + parent.value);
parent.method();

→ 부모타입 역시 자신의 객체 타입 내의 메서드와 변수를 호출 할때에 문제없이 호출 한다.

 

→ 부모 타입 변수 가 자식 객체를 참조 할때에 🔽 (하이라이트 - 다형적 참조 상태)

// 부모 변수가 자식 인스턴스 참조 (다형적 참조)
Parent poly = new Child();
System.out.println("Parent -> Child");
System.out.println("valud = " + poly.value); // 변수는 오버라이딩 x
poly.method(); // 메서드는 오버라이딩 O

// -----------------------------------------------------------------
// 실행 결과 
Parent -> Child
valud = parent // 부모 변수 그대로 
Child.method // 오버라이딩 된 자식 메서드 사용 

→ 중요한 부분이다 우리가 배운 다형적 참조 지식 이라면 변수는 부모 타입의 변수의 값과 기능을 호출할때에는 자식 객체 내의 부모 영역에서 찾는다고 알고 있었다.

→ 하지만 !! 자식 객체 내에 오버라이딩 된 메서드는 항상 우선권을 갖게 된다.

→ 따라서 Parent.method 가 아닌 오버라이딩 된 child.method 가 실행 된것 이다.

→ 만일 손자타입 객체 사용 시 손자 에서 메서드가 오버라이딩 된 것을 호출한다면 손자의 것을 호출하게 된다.


instanceof - 캐스팅 사용시 해당 객체에 무엇이 들어있는지 확인?

  • 다형성에서 참조형 변수는 다양한 자식을 대상으로 참조할 수 있다. → 하지만 어떤 객체를 참조 하는지 확인 하려 할때 사용 한다.

→ instanceof 사용 예 🔽

package poly.basic;

public class CastingMain5 {
    public static void main(String[] args) {
        Parent parent1 = new Parent();
        System.out.println("parent1 호출");
        call(parent1);

        System.out.println("--------");

        Parent parent2 = new Child();
        System.out.println("parent2 호출");
        call(parent2);

    }

    private static void call (Parent parent) {
        parent.parentMethod();

        if (parent instanceof Child) { 
		        // <- 객체의 타입을 instanceof 를 사용하여 비교 할 수 있다.
            System.out.println("Child 인스턴스 맞음");
            Child child = (Child) parent;
            child.childMethod();
        } else {
            System.out.println("Child 인스턴스 아님");
        }
    }
}

→ instanceof 실행 결과 🔽

parent1 호출
Parent.parentMethod
Child 인스턴스 아님
--------
parent2 호출
Parent.parentMethod
Child 인스턴스 맞음
Child.childMethod

instanceof 사용 이해 하기

  • instanceof 키워드는 오른쪽 대상타입이 왼쪽에 있는 타입에 포함되어 있는지 여부를 판단한다고 생각하면 된다.
new Parent() instanceof Parent // true -> 자기 자신 내에는 자신 영역이 존재
new Child() instanceof Parent // true -> 자식 객체 내에는 부모 영역도 존재

Java16 버전 부터 - Pattern Matching for instanceof

  • instancof 를 사용하면서 동시에 변수 선언 까지 하는 것

→ 이런식으로 사용가능하다.


 

다형성과 캐스팅

  • 다운캐스팅
    • 부모타입의 형태를 자식타입의 형태로 강제로 형변환을 시켜주는것
// 다움캐스팅 (부모타입 -> 자식타입)
Child child = (Child) poly; 
// <- 이렇게 사용하면 강제로 부모티입을 자식타입으로 변환 시킨다.
child.childMethod();

→ 이렇게 사용하면 자식 타입에서 호출한 주소를 사용하는 부모가 자식 타입의 기능을 찾게 해준다.

  • 실행 순서 🔽
Child child = (Child) poly // 다운 캐스팅을 통해 부모 타입을 자식 타입으로 변환한 다음 대입 시도
Child child = (Child) x001 // 참조 값을 읽은 다음 자식 타입으로 지정
Child child = x001 // 최종 결과

→ 주소 값을 복사하여 읽어온다는걸 기억하자

  • 업 캐스팅
    • 부모 타입으로 변경

캐스팅의 종류

  • 일시적 다운 캐스팅
  • → 자식 캐스팅을 일일히 선언하고 사용 하기 번거로워 해당 메서드를 호출하는 순간에만 다운 캐스팅을 하는것을 말한다.
// 일시적 다운캐스팅 - 해당 메서드를 호출하는 순간만 다운캐스팅
((Child) poly).childMethod();

→ Parent 타입의 poly 를 호출 할 때에 기존 참조에서는 Parent 에서 찾았지만 일시적 다운 캐스팅을 하면 Child 에 바로 찾으러 간다.

  • 업 캐스팅
  • → 다운 캐스팅과 반대로 자식 타입을 부모 티입으로 바꾸는것을 말한다.
package poly.basic;

// upCasting cs downCasting
public class CastingMain3 {

    public static void main(String[] args) {
        Child child = new Child();
        Parent parent1 = (Parent) child; // 업 캐스팅은 생략 가능, 생략 권장
        Parent parent2 = child; // 업 캐스팅 생략 -> 이렇게 사용해야 한다.

        parent1.parentMethod();
        parent2.parentMethod();
    }

}

→ 부모 타입은 자식 타입을 담을 수 있기 때문에 업 캐스팅 시에는 부모타입 형변환을 생략할 수 있다.


다운 캐스팅과 주의점

  • 다운 캐스팅을 잘못하면 심각한 런타임 오류가 발생할 수 있다.
package poly.basic;

// 다운 캐스팅을 자동으로 하지 않는 이유
public class CastingMain4 {
    public static void main(String[] args) {
        //---------------------------------------------------
        Parent parent1 = new Child();
        Child child1 = (Child) parent1;
        // 부모의 크기로 자식을 담은 모양은 자식에 바로 못 들어 간다.
        child1.childMethod(); // 문제 없이 실행 됨
        
        //---------------------------------------------------

        Parent parent2 = new Parent();
        Child child2 = (Child) parent2;
        // 부모 자체를 자식에 담게 되면  런타임 오류 생성
        child2.childMethod(); // 실행 불가
        //---------------------------------------------------
    }
}

→ 문제없이 실행된 경우 🔽

→ 자식 객체로 생성한 부모타입 변수에는 자식과 부모의 영역이 모두 생성 되어있기 때문에 다운 캐스팅 하여 호출이 가능하능하다.

 

 

→ 다운 캐스팅중 문제가 발생한 경우 🔽

→ 부모 객체로 생성한 부모 타입 변수 내부에는 자식 영역이 존재 하지 않기 때문에 런타임 오류가 발생 하게 된다.

다운 캐스팅 결론

  • 자바 에서는 사용할 수 없는 타입으로 다운캐스팅 하는 경우에 예외를 발생시키고 프로그램을 종료 시키기 때문에 다운 캐스팅 사용 할때는 주의를 기울여야 한다.

업캐스팅이 안전하고 다운 캐스팅이 위험한 이유

  • 업캐스팅의 경우는 다운캐스팅 처럼 호출 대상이 존재 하지 않는 경우가 없다. → 이유는 해당 타입의 상위 부모 타입은 모두 함깨 생성 되기 때문에 업 캐스팅의 경우 문제가 없다.
  • 하지만 다운 캐스팅의 경우 부모 타입의 객체를 생성 할때에 자식 타입은 생성 하지 않기 때문에 → 개발자 다운 캐스팅 사용시 항상 주의를 하여 사용해야 한다.

컴파일 오류 VS 런타임 오류

  • 컴파일 오류 : 변수명 오타, 잘못된 클래스등 자바 실행전 발생하는 오류로, IDE 에서 즉시 확인 하는 안전한 오류다.
  • 런타임 오류 : 말 그대로 프로글매 실행중 발생하는 오류, 고객이 프로그램 실행도중 발생하기 때문에 매우 안좋은 오류다.

+ Recent posts