oslob.site
☕️
ITJAVA

자바 기초

2024.12.15

1. 클래스와 데이터

클래스

class

  • 객체를 생성하면 자바는 메모리 어딘가에 있는 이 객체에 접근할 수 있는 참조값(주소) x001 을 반환한다.
  • new 키워드를 통해 객체를 생성되고 나면 참조값을 반환한다. 앞서 선언한 변수인 Student studen1 에 생성된 객체의 참조값(x001)을 보관한다.
student1 = Student@5caf905d
student2 = Student@27716f4
  • @ 앞은 패키지 + 클래스 정보를 뜻한다. @ 뒤에 16진수는 참조값을 뜻한다.

배열

array

  • 배열 생성 시 배열 에는 아직 참조값을 대입하지 않기 때문에 참조값이 없다는 의미의 null 값으로 초기화 된다.

중요 : 자바에서 대입은 항상 변수에 들어 있는 값을 복사한다.

주의!

  • 변수에는 인스턴스 자체가 들어있는 것이 아니라 인스턴스의 위치를 가리키는 참조값이 들어있을 뿐.
  • 배열에 대입 시 참조값을 대입하기 때문에 기존 객체에 멤버 변수 값 변경 시 배열에 들어있는 해당 값 역시 변경된다.

2. 기본형과 참조형

  • 기본형(Primitive Type): int , long , double , boolean 처럼 변수에 사용할 값을 직접 넣을 수 있는 데이

터 타입을 기본형이라 한다.

  • 참조형(Reference Type): Student student1 , int[] students 와 같이 데이터에 접근하기 위한 참조

(주소)를 저장하는 데이터 타입을 참조형이라 한다. 참조형은 객체 또는 배열에 사용된다.

참조형 연산 시 오류 발생

Student student3 = student1 + student2;

// Operator '+' cannot be applied to 'Student', 'Student'

GC - 아무도 참조하지 않는 인스턴스의 최후

gc

datanull 을 할당했다. 따라서 앞서 생성한 x001 Data 인스턴스를 더는 아무도 참조하지 않는다. 이렇게 아무도 참조하지 않게되면 x001 이라는 참조값을 다시 구할 방법이 없다. 따라서 해당 인스턴스에 다시 접근할 방법이 없다. 이렇게 아무도 참조하지 않는 인스턴스는 사용되지 않고 메모리 용량만 차지할 뿐이다.

C와 같은 과거 프로그래밍 언어는 개발자가 직접 명령어를 사용해서 인스턴스를 메모리에서 제거해야 했다. 만약 실수로 인스턴스 삭제를 누락하면 메모리에 사용하지 않는 객체가 가득해져서 메모리 부족 오류가 발생하게 된다. 자바는 이런 과정을 자동으로 처리해준다. 아무도 참조하지 않는 인스턴스가 있으면 JVM의 GC(가비지 컬렉션)가 더이상 사용하지 않는 인스턴스라 판단하고 해당 인스턴스를 자동으로 메모리에서 제거해준다.

객체는 해당 객체를 참조하는 곳이 있으면, JVM이 종료할 때 까지 계속 생존한다. 그런데 중간에 해당 객체를 참조하는 곳이 모두 사라지면 그때 JVM은 필요 없는 객체로 판단다고 GC(가비지 컬렉션)를 사용해서 제거한다.

3. 객체 지향 프로그래밍

절차 지향 프로그래밍

  • 절차 지향 프로그래밍은 프로그램의 흐름을 순차적으로 따르며 처리하는 방식이다. 즉, “어떻게”를 중심으로 프로그래밍 한다.

객체 지향 프로그래밍

  • 객체 지향 프로그래밍은 실제 세계의 사물이나 사건을 객체로 보고, 이러한 객체들 간의 상호작용을 중심으로 프로그래밍하는 방식이다. 즉, “무엇을” 중심으로 프로그래밍 한다.

둘의 중요한 차이

  • 절차 지향은 데이터와 해당 데이터에 대한 처리 방식이 분리되어 있다. 반면 객체 지향에서는 데이터와 그 데이터에 대한 행동(메서드)이 하나의 ‘객체’ 안에 함께 포함되어 있다.

메서드 추출

각각의 기능을 메서드로 만든 덕분에 각각의 기능이 모듈화 되었다.

  • 중복 제거: 로직 중복이 제거되었다. 같은 로직이 필요하면 해당 메서드를 여러번 호출하면 된다.
  • 변경 영향 범위: 기능을 수정할 때 해당 메서드 내부만 변경하면 된다.
  • 메서드 이름 추가: 메서드 이름을 통해 코드를 더 쉽게 이해할 수 있다.

모듈화: 쉽게 이야기해서 레고 블럭을 생각하면 된다. 필요한 블럭을 가져다 꼽아서 사용할 수 있다.

인스턴스의 메서드 호출

  • 클래스는 속성(데이터, 멤버 변수)과 기능(메서드)을 정의할 수 있다.
  • 객체는 자신의 메서드를 통해 **자신의 멤버 변수에 접근**할 수 있다.
    • 객체의 메서드 내부에서 접근하는 멤버 변수는 객체 자신의 멤버 변수이다.

캡슐화

속성과 기능을 하나로 묶어서 필요한 기능을 메서드를 통해 외부에 제공하는 것을 캡슐화라 한다.

4. 생성자

생성자 장점

  • 생성자 호출 필수
    • 객체를 생성할 때 **직접 정의한 생성자가 있다면 직접 정의한 생성자를 반드시 호출**해야 한다.
    • 생성자를 사용하면 필수값 입력을 보장할 수 있다.

참고 : 좋은 프로그램은 무한한 자유도가 주어지는 프로그램이 아니라 적절한 제약이 있는 프로그램이다.

생성자는 메서드와 비슷하지만 다음과 같은 차이가 있다.

  • 생성자의 이름은 클래스 이름과 같아야 한다. 따라서 첫 글자도 대문자로 시작한다.
  • 생성자는 반환 타입이 없다. 비워두어야 한다.
  • 나머지는 메서드와 같다.

기본 생성자

  • 매개변수가 없는 생성자를 기본 생성자라 한다.
  • 클래스에 생성자가 하나라도 없으면 자바 컴파일러는 매개변수가 없고, 작동하는 코드가 없는 기본 생성자를 자동으로 만들어 준다.
  • 생성자가 하나라도 있으면 자바는 기본 생성자를 만들지 않는다.

오버로딩과 this()

public class MemberConstruct {
     String name;
     int age;
     int grade;
	MemberConstruct(String name, int age) {
	 this(name, age, 50); //변경
	}
	MemberConstruct(String name, int age, int grade) {
		System.out.println("생성자 호출 name=" + name + ",age=" + age + ",grade=" + grade);
         this.name = name;
         this.age = age;
         this.grade = grade;
     }
}

this() 규칙

  • this()는 생성자 코드의 첫줄에만 작성할 수 있다.

6. 접근 제어자

캡슐화

데이터와 해당 데이터를 처리하는 메서드를 하나로 묶어서 외부에서의 접근을 제한하는 것

  • 데이터를 숨겨라
    • 객체의 데이터는 객체가 제공하는 기능인 메서드를 통해서 접근해야 한다.
  • 기능을 숨겨라
    • 객체의 기능 중에서 외부에서 사용하지 않고 내부에서만 사용하는 기능들은 감추는 것이 좋다.

데이터는 모두 숨기고, 기능은 꼭 필요한 기능만 노출하는 것이 좋은 캡슐화 이다.

7. 자바 메모리 구조와 static

자바 메모리 구조

memory

  • 메서드 영역(Method Area) : 메서드 영역은 프로그램을 실행하는데 필요한 공통 데이터를 관리한다. 이 영역은 프로그램의 모든 영역에서 공유한다.
    • 클래스 정보 : 클래스의 실행 코드(바이트 코드), 필드, 메서드와 생성자 코드등 모든 실행 코드가 존재한다.
    • static 영역 : static 변수들을 보관한다. 뒤에서 자세히 설명한다.
    • 런타임 상수 풀 : 프로그램을 실행하는데 필요한 공통 리터럴 상수를 보관한다. 프로그램을 효율적으로 관리하기 위한 상수들을 관리한다.
  • 스택 영역(Stack Area) : 자바 실행시, 하나의 실행 스택이 생성된다. 각 스택 프레임은 지역 변수, 중간 연산 결과, 메서드 호출 정보 등을 포함한다.
    • 스택 프레임 : 스택 영역에 쌓이는 네모 박스가 하나의 스택 프레임이다. 메서드를 호출할 때 마다 하나의 스택 프레임이 쌓이고, 메서드가 종료되면 해당 스택 프레임이 제거된다.
  • 힙 영역(Heap Area) : 객체(인스턴스)와 배열이 생성되는 영역이다. 가비지 컬렉션(GC)이 이루어지는 주요 영역이며, 더 이상 참조되지 않는 객체는 GC에 의해 제거된다.

참고: 스택 영역은 더 정확히는 각 쓰레드별로 하나의 실행 스택이 생성된다. 따라서 쓰레드 수 만큼 스택 영역이 생성된다. 지금은 쓰레드를 1개만 사용하므로 스택 영역도 하나이다. 쓰레드에 대한 부분은 멀티 쓰레드를 학습해야 이해할 수 있다.

멤버 변수 (필드)의 종류

  • 인스턴스 변수: static 이 붙지 않은 멤버 변수, 예) name
    • static 이 붙지 않은 멤버 변수는 인스턴스를 생성해야 사용할 수 있고, 인스턴스에 소속되어 있다. 따라서 인스턴스 변수라 한다.
    • 인스턴스 변수는 인스턴스를 만들 때 마다 새로 만들어진다.
  • 클래스 변수: static 이 붙은 멤버 변수, 예) count
    • 클래스 변수, 정적 변수, static 변수등으로 부른다. 용어를 모두 사용하니 주의하자
    • static 이 붙은 멤버 변수는 인스턴스와 무관하게 클래스에 바로 접근해서 사용할 수 있고, 클래스 자체에소속되어 있다. 따라서 클래스 변수라 한다.
    • 클래스 변수는 자바 프로그램을 시작할 때 딱 1개가 만들어진다. 인스턴스와는 다르게 보통 여러곳에서 공유하는 목적으로 사용된다.

변수와 생명주기

  • 지역 변수(매개변수 포함) : 지역 변수는 스택 영역에 있는 스택 프레임 안에 보관된다. 메서드가 종료되면 스택 프레임도 제거 되는데 이때 해당 스택 프레임에 포함된 지역 변수도 함께 제거된다. 따라서 지역 변수는 생존 주기가 짧다.
  • 인스턴스 변수 : 인스턴스에 있는 멤버 변수를 인스턴스 변수라 한다. 인스턴스 변수는 힙 영역을 사용한다. 힙 영역은 GC(가비지 컬렉션)가 발생하기 전까지는 생존하기 때문에 보통 지역 변수보다 생존 주기가 길다.
  • 클래스 변수 : 클래스 변수는 메서드 영역의 static 영역에 보관되는 변수이다. 메서드 영역은 프로그램 전체에서사용하는 공용 공간이다. 클래스 변수는 해당 클래스가 JVM에 로딩 되는 순간 생성된다. 그리고 JVM이 종료될때 까지 생명주기가 이어진다. 따라서 가장 긴 생명주기를 가진다.

static 이 정적이라는 이유는 바로 여기에 있다. 힙 영역에 생성되는 인스턴스 변수는 동적으로 생성되고, 제거된다. 반면에 static 인 정적 변수는 거의 프로그램 실행 시점에 딱 만들어지고, 프로그램 종료 시점에 제거된다. 정적 변수 는 이름 그대로 정적이다.

정적 메서드 사용법

  • static 메서드는 static 만 사용할 수 있다.
    • 클래스 내부의 기능을 사용할 때, 정적 메서드는 static 이 붙은 정적 메서드나 정적 변수만 사용할 수 있다.
    • 클래스 내부의 기능을 사용할 때, 정적 메서드는 인스턴스 변수나, 인스턴스 메서드를 사용할 수 없다.
  • 반대로 모든 곳에서 static 을 호출할 수 있다.
    • 정적 메서드는 공용 기능이다. 따라서 접근 제어자만 허락한다면 클래스를 통해 모든 곳에서 static 을 호출할 수 있다.

main() 메서드는 정적 메서드

인스턴스 생성 없이 실행하는 가장 대표적인 메서드가 바로 main() 메서드이다. main() 메서드는 프로그램을 시작하는 시작점이 되는데, 생각해보면 객체를 생성하지 않아도 main() 메서드가 작 동했다. 이것은 main() 메서드가 static 이기 때문이다.

public class ValueDataMain {
    public static void main(String[] args) {
        ValueData valueData = new ValueData();
        add(valueData);
		}
		static void add(ValueData valueData) {
				valueData.value++;
				System.out.println("숫자 증가 value=" + valueData.value); 
		} 
}

정적 메서드는 정적 메서드만 호출할 수 있다. 따라서 정적 메서드인 main() 이 호출하는 메서드에는 정적 메서드를사용했다. 물론 더 정확히 말하자면 정적 메서드는 같은 클래스 내부에서 정적 메서드만 호출할 수 있다. 따라서 정적 메서드인 main() 메서드가 같은 클래스에서 호출하는 메서드도 정적 메서드로 선언해서 사용했다.

8. Final

//final 매개변수
static void method(final int parameter) {
	//parameter = 20; 컴파일 오류
}
  • final 을 지역 변수에 설정할 경우 최초 한번만 할당할 수 있다. 이후에 변수의 값을 변경하려면 컴파일 오류가 발생한다.
  • final 을 지역 변수 선언시 바로 초기화 한 경우 이미 값이 할당되었기 때문에 값을 할당할 수 없다.
  • 매개변수에 final 이 붙으면 메서드 내부에서 매개변수의 값을 변경할 수 없다. 따라서 메서드 호출 시점에 사용된 값이 끝까지 사용된다.

9. 상속

  • 부모 클래스 (슈퍼 클래스) : 상속을 통해 자신의 필드와 메서드를 다른 클래스에 제공하는 클래스
  • 자식 클래스 (서브 클래스) : 부모 클래스로부터 필드와 메서드를 상속받는 클래스

상속을 사용하려면 extends 키워드를 사용하고 대상은 하나만 선택할 수 있다.

상속과 메모리 구조

extendM

  • 상속 관계의 객체를 생성하면 그 내부에는 부모와 자식이 모두 생성된다.
  • 상속 관계의 객체를 호출할 때, 대상 타입을 정해야 한다. 이때 호출자의 타입을 통해 대상 타입을 찾는다.
  • 현재 타입에서 기능을 찾지 못하면 상위 부모 타입으로 기능을 찾아서 실행한다. 기능을 찾지 못하면 컴파일 오류가 발생한다.

오버라이딩과 메모리 구조

overrideM

  • ElectricCar 타입에 move() 메서드가 있다. 해당 메서드를 실행한다. 이때 실행할 메서드를 이미 찾았으므 로 부모 타입을 찾지 않는다.

오버로딩(Overloading)과 오버라이딩(Overriding)

  • 메서드 오버로딩 : 메서드 이름이 같고 매개변수(파라미터)가 다른 메서드를 여러개 정의하는 것을 메서드 오버로딩(Overloading)이라 한다. 같은 이름의 메서드를 여러개 정의했다고 이해하면 된다.
  • 메서드 오버라이딩 : 메서드 오버라이딩은 하위 클래스에서 상위 클래스의 메서드를 재정의하는 과정을 의미한다. 따라서 상속 관계에서 사용한다. 부모의 기능을 자식이 다시 정의하는 것이다. 자식의 새로운 기능이 부모의 기존 기능을 넘어 타서 기존 기능을 새로운 기능으로 덮어버린다고 이해하면 된다. 오버라이딩을 우리말로 번역하면 무언가를 다시 정의한다고 해서 재정의라 한다.상속 관계에서는 기존 기능을 다시 정의한다고 이해하면 된다. 실무에서는 메서드 오버라이딩, 메서드 재정의 둘다 사용한다.

메서드 오버라이딩 조건

  • 메서드 이름 : 메서드 이름이 같아야 한다.
  • 메서드 매개변수(파라미터) : 매개변수(파라미터) 타입, 순서, 개수가 같아야 한다.
  • 반환 타입 : 반환 타입이 같아야 한다. 단 반환 타입이 하위 클래스 타입일 수 있다.
  • 접근 제어자 : 오버라이딩 메서드의 접근 제어자는 상위 클래스의 메서드보다 더 제한적이어서는 안된다. 예를 들어, 상위 클래스의 메서드가 protected 로 선언되어 있으면 하위 클래스에서 이를 public 또는 protected 로 오버라이드할 수 있지만, private 또는 default 로 오버라이드 할 수 없다.
  • 예외 : 오버라이딩 메서드는 상위 클래스의 메서드보다 더 많은 체크 예외를 throws 로 선언할 수 없다. 하지만더 적거나 같은 수의 예외, 또는 하위 타입의 예외는 선언할 수 있다. 예외를 학습해야 이해할 수 있다. 예외는 뒤에서 다룬다.
  • static , final , private : 키워드가 붙은 메서드는 오버라이딩 될 수 없다.
    • static 은 클래스 레벨에서 작동하므로 인스턴스 레벨에서 사용하는 오버라이딩이 의미가 없다. 쉽게 이야기해서 그냥 클래스 이름을 통해 필요한 곳에 직접 접근하면 된다.
    • final 메서드는 재정의를 금지한다.
    • private 메서드는 해당 클래스에서만 접근 가능하기 때문에 하위 클래스에서 보이지 않는다. 따라서 오버라이딩 할 수 없다.
  • 생성자 오버라이딩 : 생성자는 오버라이딩 할 수 없다.

10. 다형성

다형적 참조 : 부모 타입의 변수가 자식 인스턴스 참조

Relation

부모는 자식을 담을 수 있다.

  • 부모 타입은 자식 타입을 담을 수 있다.
  • Parent poly 는 부모 타입이다. new Child() 를 통해 생성된 결과는 Child 타입이다. 자바에서 부모 타입은 자식 타입을 담을 수 있다!
    • Parent poly = new Child() : 성공
  • 반대로 자식 타입은 부모 타입을 담을 수 없다.
    • Child child1 = new Parent() : 컴파일 오류 발생

다형적 참조

지금까지 학습한 내용을 떠올려보면 항상 같은 타입에 참조를 대입했다. 그래서 보통 한 가지 형태만 참조할 수 있다.

  • Parent parent = new Parent()
  • Child child = new Child()

그런데 Parent 타입의 변수는 다음과 같이 자신인 Parent 는 물론이고, 자식 타입까지 참조할 수 있다. 만약 손자가있다면 손자도 그 하위 타입도 참조할 수 있다.

  • Parent poly = new Parent()
  • Parent poly = new Child()
  • Parent poly = new Grandson() : Child 하위에 손자가 있다면 가능

자바에서 부모 타입은 자신은 물론이고, 자신을 기준으로 모든 자식 타입을 참조할 수 있다. 이것이 바로 다양한 형태를참조할 수 있다고 해서 다형적 참조라 한다.

다형정 참조의 한계

limitP

Parent poly = new Child() 이렇게 자식을 참조한 상황에서 poly 가 자식 타입인 Child 에 있는 childMethod() 를 호출하면 어떻게 될까? poly.childMethod() 를 실행하면 먼저 참조값을 통해 인스턴스를 찾는다. 그리고 다음으로 인스턴스 안에서 실행할 타입을 찾아야 한다. 호출자인 polyParent 타입이다. 따라서 Parent 클래스부터 시작해서 필요한 기능을찾는다. 그런데 상속 관계는 부모 방향으로 찾아 올라갈 수는 있지만 자식 방향으로 찾아 내려갈 수는 없다. Parent 는부모 타입이고 상위에 부모가 없다. 따라서 childMethod() 를 찾을 수 없으므로 컴파일 오류가 발생한다.

이런 경우 childMethod() 를 호출하고 싶으면 어떻게 해야할까? 바로 캐스팅이 필요하다.

다형적 참조의 핵심은 부모는 자식을 품을 수 있다는 것이다.

그런데 이런 다형적 참조가 왜 필요하지? 라는 의문이 들 수 있다. 이 부분은 다형성의 다른 이론들도 함께 알아야 이해 할 수 있다. 지금은 우선 다형성의 문법과 이론을 익히는데 집중하자.

다형성과 캐스팅

캐스팅 실행순서

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

참고로 캐스팅을 한다고 해서 Parent poly 의 타입이 변하는 것은 아니다. 해당 참조값을 꺼내고 꺼낸 참조값이 Child 타입이 되는 것이다. 따라서 poly 의 타입은 Parent 로 기존과 같이 유지된다.

캐스팅

Child child1 = (Child) poly; // 다운캐스팅, 부모 타입으로 변경
Parent poly1 = (Parent) child; // 업캐스팅, 자식 타입으로 변경

업캐스팅

부모타입으로 변환하는 경우에는 다음과 같이 캐스팅 코드인 (타입) 를 생략할 수 있다.

 Parent parent2 = child
 Parent parent2 = new Child()

업캐스팅은 생략할 수 있다. 다운캐스팅은 생략할 수 없다. 참고로 업캐스팅은 매우 자주 사용하기 때문에 생략을 권장한다. 자바에서 부모는 자식을 담을 수 있다. 하지만 그 반대는 안된다. (꼭 필요하다면 다운캐스팅을 해야 한다.)

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

castinDanger

new B() 로 인스턴스를 생성하면 인스턴스 내부에 자신과 부모인 A,B가 생성된다. 따라서 B의 부모타입인 A,B모두 B 인스턴스를 참조할 수 있다. 상위로 올라가는 업케스팅은 인스턴스 내부에 부모가 모두 생성되기 때문에 문제가 발생하지 않는다. 하지만 객체를 생성할 때 하위 자식은 생성되지 않기 때문에 하위로 내려가는 다운케스팅은 인스턴스 내부에 없는 부분을 선택하는 문제가 발생할 수 있다.

  • A a = new B() :A로업케스팅
  • B b = new B() :자신과같은타입
  • C c = new B() :하위타입은대입할수없음,컴파일오류
  • C c = (C) new B() :하위타입으로 강제 다운캐스팅, 하지만 B인스턴스에 C와 관련된 부분이 없으므로 잘못된 캐스팅, ClassCastException 런타임 오류 발생

컴파일 오류 VS 런타임 오류

컴파일 오류는 변수명 오타, 잘못된 클래스 이름 사용등 자바 프로그램을 실행하기 전에 발생하는 오류이다. 이런 오류는 IDE에서 즉시 확인할 수 있기 때문에 안전하고 좋은 오류이다. 반면에 런타임 오류는 이름 그대로 프로그램이 실행되고 있는 시점에 발생하는 오류이다. 런타임 오류는 매우 안좋은 오류이다. 왜냐하면 보통 고객이 해당 프로그램을 실행하는 도중에 발생하기 때문이다.

instanceof

변수가 참조하는 인스턴스의 타입을 확인을 확인하고 싶다면 instanceof 키워드를 사용하면 된다.

new Parent() instanceof Parent
Parent p = new Parent() //같은 타입 true

new Child() instanceof Parent
Parent p = new Child() //부모는 자식을 담을 수 있다. true

new Parent() instanceof Child
Child c = new Parent() //자식은 부모를 담을 수 없다. false 

new Child() instanceof Child
Child c = new Child() //같은 타입 true

자바 16 - Pattern Matching for instanceof

private static void call(Parent parent) { 
		parent.parentMethod();
		//Child 인스턴스인 경우 childMethod() 실행
		if (parent instanceof Child child) {
				System.out.println("Child 인스턴스 맞음");
        child.childMethod();
    }
}

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

메서드 오버라이딩에서 꼭! 기억해야 할 점은 오버라이딩 된 메서드가 항상 우선권을 가진다는 점이다.

image

  • poly 변수는 Parent 타입이다. 따라서 poly.value , poly.method() 를 호출하면 인스턴스의

Parent 타입에서 기능을 찾아서 실행한다. - poly.value : Parent 타입에 있는 value 값을 읽는다. - poly.method() : Parent 타입에 있는 method() 를 실행하려고 한다. 그런데 하위 타입인 Child.method() 가 오버라이딩 되어 있다. 오버라이딩 된 메서드는 항상 우선권을 가진다. 따라서 Parent.method() 가 아니라 Child.method() 가 실행된다.

오버라이딩 된 메서드는 항상 우선권을 가진다. 오버라이딩은 부모 타입에서 정의한 기능을 자식 타입에서 재정의하는 것이다. 만약 자식에서도 오버라이딩 하고 손자에서도 같은 메서드를 오버라이딩을 하면 손자의 오버라이딩 메서드가우선권을 가진다. 더 하위 자식의 오버라이딩 된 메서드가 우선권을 가지는 것이다.

  • 다형적 참조 : 하나의 변수 타입으로 다양한 자식 인스턴스를 참조할 수 있는 기능
  • 메서드 오버라이딩 : 기존 기능을 하위 타입에서 새로운 기능으로 재정의

추상 클래스

abstract class AbstractAnimal {...}
  • 추상 클래스는 클래스를 선언할 때 앞에 추상이라는 의미의 abstract 키워드를 붙여주면 된다.
  • 추상 클래스는 기존 클래스와 완전히 같다. 다만 new AbstractAnimal() 와 같이 직접 인스턴스를 생성하지못하는 제약이 추가된 것이다.

추상 메서드

  • 추상 메서드가 하나라도 있는 클래스는 추상 클래스로 선언해야 한다.
    • 그렇지 않으면 컴파일 오류가 발생한다.
  • 추상 메서드는 상속 받는 자식 클래스가 반드시 오버라이딩 해서 사용해야 한다.

순수 추상 클래스 : 모든 메서드가 추상 메서드인 추상 클래스

public abstract class AbstractAnimal {
     public abstract void sound();
     public abstract void move();
}
  • 인스턴스를 생성할 수 없다.

  • 상속시 자식은 모든 메서드를 오버라이딩 해야 한다.

  • 주로 다형성을 위해 사용된다.

  • 상속하는 클래스는 모든 메서드를 구현해야 한다.

이런 특징을 잘 생각해보면 순수 추상 클래스는 마치 어떤 규격을 지켜서 구현해야 하는 것 처럼 느껴진다. AbstractAnimal 의 경우 sound() , move() 라는 규격에 맞추어 구현을 해야 한다.

인터페이스

인터페이스 - public abstract 키워드 생략 가능

public interface InterfaceAnimal {
     void sound();
     void move();
 }
  • 인터페이스의 메서드는 모두 public , abstract 이다.
  • 메서드에 public abstract 를 생략할 수 있다. 참고로 생략이 권장된다.
  • 인터페이스는 다중 구현(다중 상속)을 지원한다.

인터페이스와 멤버 변수

public interface InterfaceAnimal {
     // public static final double MY_PI = 3.14;
     double MY_PI = 3.14; // 생략 권장
}

클래스, 추상 클래스, 인터페이스는 모두 똑같다.

  • 클래스, 추상 클래스, 인터페이스는 프로그램 코드, 메모리 구조상 모두 똑같다. 모두 자바에서는 .class 로 다루어진다. 인터페이스를 작성할 때도 .java에 인터페이스를 정의한다.
  • 인터페이스는 순수 추상 클래스와 비슷하다고 생각하면 된다.

[중요] 인터페이스를 사용해야하는 이유

  • 제약 : 인터페이스를 만드는 이유는 인터페이스를 구현하는 곳에서 인터페이스의 메서드를 반드시 구현해라는 규약(제약)을 주는 것이다. USB 인터페이스를 생각해보자. USB 인터페이스에 맞추어 키보드, 마우스를 개발하고 연결해야 한다. 그렇지 않으면 작동하지 않는다. 인터페이스의 규약(제약)은 반드시 구현해야 하는 것이다. 그런데 순수 추상 클래스의 경우 미래에 누군가 그곳에 실행 가능한 메서드를 끼워 넣을 수 있다. 이렇게 되면 추가된 기능을 자식 클래스에서 구현하지 않을 수도 있고, 또 더는 순수 추상 클래스가 아니게 된다. 인터페이스는 모든 메서드가 추상 메서드이다. 따라서 이런 문제를 원천 차단할 수 있다.
  • 다중 구현 : 자바에서 클래스 상속은 부모를 하나만 지정할 수 있다. 반면에 인터페이스는 부모를 여러명 두는 다중 구현(다중 상속)이 가능하다.

인터페이스 - 다중 구현

자바는 다중 상속을 지원하지 않는다.

interface

  • 상속 관계의 경우 두 부모 중에 어떤 한 부모의 methodCommon() 을 사용해야 할지 결정해야 하는 다이아몬드 문제가 발생한다.
  • 하지만 인터페이스 자신은 구현을 가지지 않는다. 대신에 인터페이스를 구현하는 곳에서 해당 기능을 모두 구현해야 한다.
  • 그냥 인터페이스들을 구현한 Child 에 있는 methodCommon() 이 사용된다.

12. 다형성과 설계

다형성(Polymorphism)은 이름 그대로 “다양한 형태”, “여러 형태”를 를 뜻한다.

프로그래밍에서 다형성은 한 객체가 여러 타입의 객체로 취급될 수 있는 능력을 뜻한다. 보통 하나의 객체는 하나의 타입으로 고정되어 있다. 그런데 다형성을 사용하면 하나의 객체가 다른 타입으로 사용될 수 있다는 뜻이다.

객체 지향 특징

  • 추상화
  • 캡슐화
  • 상속
  • 다형성

객체 지향 프로그래밍

  • 객체 지향 프로그래밍은 컴퓨터 프로그램을 명령어의 목록으로 보는 시각에서 벗어나 여러 개의 독립된 단위, 즉 “객체”들의 모임으로 파악하고자 하는 것이다. 각각의 객체메세지를 주고받고, 데이터를 처리할 수 있다. (협력)
  • 객체 지향 프로그래밍은 프로그램을 유연하고 변경이 용이하게 만들기 때문에 대규모 소프트웨어 개발에 많이 사용된다.

역할과 구현을 분리(자바)

  • 자바 언어의 다형성을 활용
    • 역할 = 인터페이스
    • 구현 = 인터페이스를 구현한 클래스, 구현 객체
  • 객체를 설계할 때 역할과 구현을 명확히 분리
  • 객체 설계 시 역할(인터페이스)을 먼저 부여하고, 그 역할을 수행하는 구현 객체 만들기

OCP (Open-Closed Principle) 원칙

  • Open for extension : 새로운 기능의 추가나 변경 사항이 생겼을 때, 기존 코드는 확장할 수 있어야 한다.
  • Closed for modification : 기존의 코드는 수정되지 않아야 한다.
  • 확장에 열려있다는 의미

Car 인터페이스를 사용해서 새로운 차량을 자유롭게 추가할 수 있다. Car 인터페이스를 구현해서 기능을 추가할 수있다는 의미이다. 그리고 Car 인터페이스를 사용하는 클라이언트 코드인 DriverCar 인터페이스를 통해 새롭게 추가된 차량을 자유롭게 호출할 수 있다. 이것이 확장에 열려있다는 의미이다.

  • 코드 수정은 닫혀 있다는 의미

새로운 차를 추가하게 되면 기능이 추가되기 때문에 기존 코드의 수정은 불가피하다. 당연히 어딘가의 코드는 수정해야한다.

  • 변하지 않는 부분

새로운 자동차를 추가할 때 가장 영향을 받는 중요한 클라이언트는 바로 Car 의 기능을 사용하는 Driver 이다. 핵심은 Car 인터페이스를 사용하는 클라이언트인 Driver 의 코드를 수정하지 않아도 된다는 뜻이다.

  • 변하는 부분

main() 과 같이 새로운 차를 생성하고 Driver 에게 필요한 차를 전달해주는 역할은 당연히 코드 수정이 발생한다. main() 은 전체 프로그램을 설정하고 조율하는 역할을 한다. 이런 부분은 OCP를 지켜도 변경이 필요하다.

  • 정리
    • Car 를 사용하는 클라이언트 코드인 Driver 코드의 변경없이 새로운 자동차를 확장할 수 있다.
    • 다형성을 활용하고 역할과 구현을 잘 분리한 덕분에 새로운 자동차를 추가해도 대부분의 핵심 코드들을 그대로 유지할 수 있게 되었다.

전략 패턴(Strategy Pattern)

디자인 패턴 중에 가장 중요한 패턴을 하나 뽑으라고 하면 전략 패턴을 뽑을 수 있다. 전략 패턴은 알고리즘을 클라이언트 코드의 변경없이 쉽게 교체할 수 있다. 방금 설명한 코드가 바로 전략 패턴을 사용한 코드이다. Car 인터페이스가 바로 전략을 정의하는 인터페이스가 되고, 각각의 차량이 전략의 구체적인 구현이 된다. 그리고 전략을 클라이언트 코드( Driver )의 변경 없이 손쉽게 교체할 수 있다.

👇 도움이 되셨다면 👇

B

u

y

M

e

A

C

o

f

f

e

e

© Powered by oslob