🔖Java

Java 기본 (객체 지향 프로그래밍)

Jerry_K 2025. 3. 16. 22:43

해당 포스트는 김영한님의 Java 기본을 듣고,

나 스스로 복습을 하기 위해 정리해 놓았다.

 

시작하기에 앞서 인프런의 김영한님께서 항상 강조 하시는 말을 기억하며...

"자바에서 대입은 항상 변수에 들어 있는 값을 복사해서 전달한다."

 

 

또한 객체지향의 4가지 특징을 기억하자.

"캡슐화, 상속, 다형성, 추상화"

여기에서 가장 중요한 것은 다형성이다 !

 

 

클래스와 데이터 / 기본형과 참조형 / 객체 지향 프로그래밍 / 생성자 / 패키지 /

접근 제어자 / 자바 메모리 구조와 static / final / 상속 / 다형성 


클래스 구성 요소

https://kephilab.tistory.com/46

 

클래스

  • 사용자 정의 타입을 만들수 있도록 하고, 이것에 대한 설계도
  • 설계도인 클래스를 사용해서 실제 메모리에 만들어진 객체 또는 인스턴스
    • 객체인스턴스 용어는 자주 혼용 됨 (대부분 잘 구분하지 않고 사용)
  • 클래스를 통해 사용자가 원하는 종류의 데이터 타입 정의  

 

멤버 변수 (필드) 

  • 멤버 변수필드는 똑같은 의미
  • 특정 클래스에 소속된 멤버 
  • 참고는 클래스는 관례상 대문자로 시작

 

클래스가 필요한 이유에 대해 알아보자 ! 

  • 데이터와 기능을 한 곳 묶음
  • 코드 재사용성 증가
  • 유지보수성 향상
  • 캡슐화, 추상화, 상속, 다형성 

 

객체와 인스턴스 / 멤버 변수와 필드 / 인자와 매게 변수 / 메서드

이런 것들은 정말 자주 틀린다.

개념을 잘 잡지 못하면 나중에 진짜 헷갈리니 꼭 기억하자. 


기본형과 참조형

https://inpa.tistory.com/entry

기본형

  • Primitive Type
  • int, long, double, boolean 처럼 변수에 사용할 값을 직접 넣은수 있는 타임
  • 메서드로 기본형 전달이 되면 값이 복사 됨
    • 메서드 내부 매개변수의 값을 변경해도 호출자의 변수 값에는 영향 없음
    • 참고로 매개변수도 지역 변수이다.

 

참조형

  • Reference Type
  • 기본형을 제외한 나머지 
  • 객체 또는 배열처럼 데이터에 접근하기 위한 참조(주소)를 저장하는 타입
  • 메서드로 참조형이 전달되면 참조값이 복사됨
    • 파라미터로 전달된 객체의 멤버 변수를 변경하면, 호출자의 객체도 변경 

 

참고로  사실 String도 클래스로 참조형이다. 


변수의 값 초기화

멤버 변수

  • 자동 초기화 
  • 인스턴스의 멤버 변수는 인스턴스 생성할 때 자동 초기화
  • 숫자 = 0 / boolean = false / 참조형 =null

 

지연 변수

  • 수동 초기화 
  • 지역 변수는 항상 직접 초기화 해야 함

 

자동으로 초기화 되는 멤버변수 / 수동으로 초기화되는 지역변수 

 

NullPointException 

Student student2 = createStudent("jerry",10,100);
student2 = null;
System.out.println(student2.age);
  • 위와 같은 경우 NullPoinException 에러가 발생한다. 
  • null 값에 . (dot)을 찍었다 생각하면 문제를 쉽게 찾는다. 

프로그래밍 패러다임

절차 지향 프로그래밍

  • 이름 그대로 절차를 지향 
  • 프로그램의 흐름을 순차적으로 따르며 처리
  • "어떻게"를 중심으로 프로그래밍

 

객체 지향 프로그래밍

  • 객체를 중요하게 생각하는 방식
  • 실제 세계의 사물이나 사건을 객체로 봄
  • 객체들 간의 상호작용을 중심으로 프로그래밍 
  • "무엇을" 을 중심으로 프로그래밍

 

절차 지향은 데이터와 해당 데이터 처리 방식이 분리되어 있지만.

객체 지향은 데이터와 그 데이터에 대한 행동(메서드)가 하나의 객체 안에 포함

 

원래 객체 지향 프로그래밍이 나오기 전까지는 데이터와 기능이 분리되었다. 

객체 지향 프로그래밍 이후에는 데이터와 기능을 온전히 하나로 묶여서 사용할 수 있게 되엇다.

 

 

객체 지향을 정말 잘하면, 객체가 살아있는 느낌이 난다.


this와 생성자

this 

  • this는 인스턴스 자신의 참조값을 가르킴
  • 멤버 변수에 접근하려면 this 사용
  • 일반적으로 변수 찾을 때, 가장 가까운 지역 변수를(매개변수도 지역 변수) 찾음
  • 그 다음 멤버 변수에서 찾고, 멤버 변수에도 없으면 오류 발생
  • 요즘은 IDE가 멤버 변수와 지역 변수 색상을 구분해줘서 잘 쓰지는 않음

 

생성자

  • 보통 객체를 생성하고 이후 바로 초기값을 할당해야 하는 경우가 많음
  • 대부분의 객체 지향 언어는 생성자 기능 제공
  • 생성자의 이름은 클래스 이름과 같아야함 (첫 글자도 대문자로 시작)
  • 생성자의 반환 타입을 비워두어야 함 

 

생성자의 장점 

MemberInit member1 = new MemberInit("jerry", 10, 90);
  • 인스턴스를 생성하고 바로 생성자 호출
  • 생성 직후 필요한 작업을 한번에 처리 가능
  • 생성자를 만들어두면, 직접 정의한 생성자를 반드시 호출해야 에러가 나지 않음
    • 생성자를 만들어두지 않으면, 초기값 설정 안해도 에러가 발생하지 않음
    • 생성자를 사용하여 필수값 입력을 보장

 

좋은 프로그램은 무한한 자유도가 주어지는 프로그램이 아니라

적절한 제약이 있는 프로그램

 

this( ) 

public class User {
    String name;
    int age;

    public User() {
        this("Guest");         // (1)
    }

    public User(String name) {
        this(name, 0);         // (2)
    }

    public User(String name, int age) {
        this.name = name;      // (3)
        this.age = age;
    }
}
User u = new User();
System.out.println(u.name); // "Guest"
System.out.println(u.age);  // 0
  • new User() → (1) → (2) → (3) 순으로 호출
  • this( ) 의 기능으로 생성자 내부에서 자신의 생성자를 호출할 수 있음 
  • this는 인스턴스 자신의 참조값을 가르키기 때문에, 자신의 생성자를 호출
  • 단, 제한이 있는데 this( ) 는 생성자 코드의 첫줄에만 작성 가능 

접근 제어자

https://kadosholy.tistory.com/96

접근 제어자

OOP에서 데이터 보호와 코드의 캡슐화를 강화시키기 위해 접근 제어가자 필요하다. 

접근 제어자로 데이터 보호, 캡슐화, 결합도 감소, 코드 안정성을 가져 올 수 있다.

주로 접근 제어자는 필드와 메서드, 생성자에 사용되고, 클래스 레벨에도 일부 접근 제어자 사용 가능하다.

 

접근 제어자  종류 

  • private :  모든 외부 호출을 막음
  • default : 같은 패키지안에서 호출은 허용
  • protected : 같은 패키지안에서 호출 허용 + 패키지가 달라도 상속 관계의 호출 허용
  • public : 모든 외부 호출 허용

 

private → default → protected → public  

순서대로 private가 가장 많이 차단, public이 가장 많이 허용 

 

 

접근 제어자 사용 (클래스 레벨)

  • 클래스 레벨의 접근 제어자는 public과 default만 사용 가능
  • public 클래스는 반드시 파일명과 이름이 같아야 함
    • 하나의 자바 파일에 public 클래스는 하나만 등장 가능
    • 하나의 자바 파일에 default 접근 제어자를 사용하는 클래스 무한정 가능


캡슐화

 package access;
 public class BankAccount {
    private int balance;
    
    public BankAccount() {
        balance = 0;
    }
     
    // public 메서드: deposit
    public void deposit(int amount) {
         if (isAmountValid(amount)) {
            balance += amount;
        } 
        else {
         System.out.println("유효하지 않은 금액입니다.");
        }
    }
    
     // public 메서드: withdraw
    public void withdraw(int amount) {
         if (isAmountValid(amount) && balance - amount >= 0) {
            balance -= amount;
        } 
        else {
         System.out.println("유효하지 않은 금액이거나 잔액이 부족합니다.");
        }
    }
    
    // public 메서드: getBalance
    public int getBalance() {
         return balance;
    }
        
     // private 메서드: isAmountValid
    private boolean isAmountValid(int amount) {
         // 금액이 0보다 커야함
        return amount > 0;
    }
}
  • 속성(데이터)과 기능(메서드)을 하나로 묶고, 외부에 꼭 필요한 기능만 노출
  • 그 외 나머지는 모두 내부로 숨김
  • 캡슐화를 안전하게 완성할 수 있게 하는 장치가 접근 제어자
  • 숨겨야 할 것들
    • 속성(데이터) 
      • 객체 내부의 데이터를 외부에서 접근 못하게 막음
      • 객체의 데이터는 객체가 제공하는 기능인 메서드를 통해 접근
    • 기능(메서드)
      • 기능은 꼭 필요한 기능만 노출하는 것이 좋은 캡슐화
      • 사용자에게 꼭 필요한 기능만 외부에 노출

 

Protected가 뭐하는 애인지 기억이 안날 때가 종종 있음


자바 메모리 구조

 

 

메서드 영역

  • 프로그램이 실행하는데 필요한 공통 데이터 관리 
    • 클래스 정보 (클래스 실행 코드, 필드, 메서드와 생성자 등의 코드)
    • static 영역 (static 변수 보관)
    • 런타임 상수 풀 (프로그램 실행에 필요한 공통 리터럴 상수 보관)
  • 객체가 생성될 때, 인스턴스 변수에는 메모리가 할당되지만, 메서드에 대한 새로운 메모리 할당은 없음
  • 메서드는 메서드 영역에서 공통으로 관리되고 실행

 

스택 영역

  • 자바 실행 시, 하나의 실행 스택 생성
  • 각 스택 프레임은 지역 변수, 메서드 호출 정보, 중간 연산 결과 포함
  • 메서드 호출할 때 마다 하나의 스택 프레임이 쌓이고, 메서드 종료되면 스택 프레임 제거
  • 스레드별로 하나의 실행 스택 생성 (스레드가 여러개면 여래개의 스택 영역 생성)
    • 한 스레드에 여러개의 메서드(함수) 호출 가능
    • 각 함수가 호출될 때마다 새로운 스택 프레임 생성

 

힙 영역

  • 객체와 배열이 생성되는 영역
  • GC가 이뤄지는 주요 영역
  • 참조되지 않는 객체는 GC에 의해 제거 

 

메서스 영역 / 스택 영역 / 힙 영역 

특히 메서드 영역이 낯선데,  꼭 기억해두자


Static

 

static 변수

  • 객체가 생성될 때 마다, 필드 값들이 초기화 됨
  • 같은 클래스를 쓰고있는 객체들이 특정 변수를 공용으로 사용하면 편리
  • 이거를 가능하게 하는게 staic 키워드
  • static 변수를 정적 변수 또는 클래스 변수라고 함
  • static 정적 변수에 접근하는 법은 클래스명에 .(dot)을 사용
    • ex) MovieReview.count  (객체가 아니라 클래스명으로 접근 )
  • 인스턴스 영역에 생성되지 않고 메서드 영역에서 관리
  • 간단하게 말해서, 공용 변수를 사용해서 문제 해결

 

Static 같은 경우 클래스 변수이기 때문에, 객체 생성이 필요 없다 !!

따라서 클래스명으로 바로 접근한 점 꼭 기억하자.

 

변수와 생명주기

1. 지역 변수(매개변수 포함)

  • 지역 변수는 스택 영역에 있는 스택 프레임 안에 보관
  • 메서드가 종료되면 스택 프레임도 제거되고 그안에 지역 변수도 제거
  • 지역 변수는 생존 주기가 짧음

 

2. 인스턴스 변수 

  • 인스턴스에 있는 멤버 변수가 인스턴스 변수
  • 인스턴스 변수는 힙 영역 사용
  • 힙 영역은 GC 발생 전까지 생존


3. 클래스 변수

  • 매서드 영역의 static 영역에 보관되는 변수
  • 프로그램 전체에서 사용하는 공용 공간
  •  JVM에 로딩 되는 순간 생성되고, 종료될 때 까지 생명주기 이어짐

 

static이 정적인 이유는 프로그램 실행 시점에 만들어지고

종료시점에서 제거되니 말 그대로 정적 ! 

반면 힙 영역에서 생성되는 인스턴스 변수는 동적으로 생성 됨

 

 

static 메서드

public class MathUtil {
    public static int add(int a, int b) {
        return a + b;
    }
}
public class Main {
    public static void main(String[] args) {
        int result = MathUtil.add(3, 5);  // 클래스 이름으로 직접 호출
        System.out.println("결과: " + result);  // 결과: 8
    }
}
  • 메서드 앞에도 static을 붙일 수 있음
  • static 메서드를 정적 메서드 또는 클래스 메서드라고 함
  • 정적 메서드는 정적 변수처럼 인스턴스 생성 없이 클래스 명을 통해 바로 호출 가능

 

static 메서드 사용법

  • static 메서드는 static만 사용 가능
  • 클래스 내부 기능을 사용할 때, 정적 메서드는 인스턴스 변수나, 인스턴스 메서드 사용할 수 없음
    • 정적 변수나 정적 메서드는 JVM 로딩 되자마자 생성
    • 하지만 인스턴스 변수나 인스턴스 메서드는 객체 생성되고 만들어지기 때문에 사용 할 수 없음
    • 정적 메서드 같은 경우 인스턴스 변수나 메서드의 참조값을 알 수 없음
  • 반면 모든 곳에서 static을 호출 할 수 있음 (접근 제어자만 허락한다면)

 

Static이 뒤로가면 좀 헷갈린다. 

예를들어 정적 메서드가 인스턴스 변수나 메서드 사용할 수 없다는 것들이다 ... 

또한 객체 생성없이 바로 Class로 접근 가능하다는 점도 때때로 헷갈린다.

 

main( ) 메서드

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 메서드는 객체를 생성하지 않아도 static 메서드이기 때문에 작동
  • main이 static이기 때문에 main이 있는 클래스의 다른 메서드 또한 static이 되어야 함
    • static 메서드는 정적 메서드만 사용 가능하기 때문에

final 

 

    • 키워드 이름 그대로 최초의 한번만 값 변경 가능 
    • final이 붙으면 매서드 내부에서 매개변수의 값 변경 불가능
    • 메서드에 final이 붙으면 오버라이딩이 불가능
    • 클래스에 final이 붙으면 상속이 불가능 
final int value = 10 ;

  • 클래스에서 이렇게 인스턴스의 변수로 선언을 할 경우, 값이 고정 값임에도 불구하고 모든 객체들이 해당 값에 대해 각각의 인스턴스 변수를 가지게 됨 → 중복 문제 발생
 //수학 상수
public static final double PI = 3.14;
 //시간 상수
public static final int HOURS_IN_DAY = 24;
 public static final int MINUTES_IN_HOUR = 60;
 public static final int SECONDS_IN_MINUTE = 60;
 //애플리케이션 설정 상수
public static final int MAX_USERS = 1000
  • 이렇기때문에 static final 을 사용 → static 같은 경우 클래스 변수로 객체들끼리 공동으로 사용
  • static final 같은 경우 상수로 대문자로 써야 함  (ex) VALUE
  • 필드에 직접 접근해도 데이터가 변하지 않음   
  • 상수는 중앙에서 값을 하나로 관리 가능
  • 하지만 상수는 런타임에 변경 불가능  

 

final과 static이 종종 헷갈린다 ... 


상속 관계 

  • 상속은 객체 지향 프로그래밍의 핵심 요소 중 하나
  • 기존 클래스의 필드와 메서드를 새로운 클래스에 재사용 가능 
  • 상속을 사용하려면 extends 키워드 사용
  • extends 대상은 하나만 선택 가능 (다중 상속 불가능)
  • 부모 클래스 (슈퍼 클래스) / 자식 클래스 (서브 클래스)
  • 자식은 부모를 아는데, 부모 입장에서는 자식이 누구인지 모름
    • 부모 클래스는 자식에게 접근하지 못한다.
  • 접근 제어자 protected는 다른 패키지여도 상속 관계 호출을 허용 

 

상속과 메모리 구조

  • 이 개념은 다형성 이해에 매우 중요하다 ! 
  • 상속 관계의 객체를 생성하면 그 내부에는 부모와 자식이 모두 생성
  • 상속 관계의 객체를 호출할 때, 대상 타입을 정해야 함
  • 호출자의 타입을 통해 대상 타입을 찾음
  • 현재 타입에서 기능을 찾지 못하면 상위 부모 타입으로 기능을 찾음
  • 기능을 찾지 못하면 컴파일 오류 발생

 

메서드 오버라이딩

 

  • 부모에게 상속 받은 기능을 자식이 재정의 
  • 일단 호출자의 타입에서 기능을 찾음
  • 실행할 메서드를 이미 찾으면 부모 타입을 찾지 않음

 

 

오버로딩과 오버라이딩

오버로딩

  • 메서드 이름이 같고 매개변수(파라미터)가 다른 메서드를 여러개 정의
  • Overloading (과적)
  • 과하게 담았다는 뜻으로 매서드 여러개 정의했다고 이해

메서드 오버라이딩

  • 하위 클래스에서 상위 클래스의 메서드를 재정의하는 과정
  • 부모의 기능을 자식이 다시 정의 
  • Overriding (위에 올라타다 → 덮어쓰다)
  • 자식의 새로운 기능이 부모의 기존 기능을 넘어 새로운 기능으로 덮어버림

 

super - 부모 참조

System.out.println("this value = " + this.value); // 자식 필드(본인)
System.out.println("super value = " + super.value); // 부모 필드
this.hello(); //자식 메소드(본인)
super.hello() // 부모 메소드
  • 부모와 자식의 필드명이 같거나 메서드가 오버라이딩 되어 있는 경우-
  • 자식에서 부모의 필드나 메서드를 호출 할 때 super 키워드를 사용해서 부모를 참조
  • super는 부모 클래스에 대한 참조를 나타냄

 

super - 생성자

  • 상속 관계의 인스턴스를 생성하면 결국 메모리 내부에 자식과 부모 클래스가 각각 만들어짐
  • 따라서 각각의 생성자도 모두 호출되어야 함
    • 기본 생성자가 파라미터가 없는 생성자 인 경우 super( ) 생략 가능 
  • 상속 관계를 사용하면 자식 클래스의 생성자 첫줄에 부모 클래스의 생성자 반드시 호출
  • 결국에는 부모 클래스의 생성자가 가장 먼저 호출 됨

 

package super1;

public class ClassA {
    public ClassA() {
        System.out.println("생상자 A 호출");
    }
}
package super1;

public class ClassB extends ClassA{
    public ClassB(int a) {
        System.out.println("생성자 B 호출 " + a);
    }
}
  • ClassA는 매개변수가 없는 기본 생성자여서 ClassB에서 super 키워드 생략 가능
package super1;

public class ClassC extends ClassB{

    public ClassC() {
        super(10);
        System.out.println("생성자 C 호출");
    }
}
  • 첫 줄에 부모 생성자를 super로 생성
  • ClassB에서는 매개변수가 필요하여 ClassC에서 super 생략 불가능

 


다형성

  • 같은 타입이지만 실행 결과는 다르게 동작 
    • 보통 부모 타입의 참조변수로 자식 객체를 다룸
  • 객체는 하나의 타입으로 고정되어 있는데, 다형성은 한 객체가 여러 타입의 객체로 취급될 수 있음
  • 객체지향 프로그래밍의 대표적인 특징으로 캡슐화, 상속, 다형성이 있음
  • 그 중에서 다형성은 객체지향 프로그래밍의 꽃 
  • 좋은 개발자가 되기 위해서는 다형성에 대한 이해가 필수 ! 
  • 프로그래밍에서 다형성은 한 객체가 여러 타입의 객체로 취급 될 수 있음을 의미

 

 

다형적 참조

Parent poly = new Child();
  • 부모 타입의 변수가 자식 인스턴스 참조
    • 타입은 부모 타입이라는 점을 기억
    • 때문에 Parent 클래스를 탐색 (자식 메서드 호출 불가능)
    • 하지만 오버라이딩된 자식 메서드가 있는 경우 자식 메서드가 우선순위를 가짐
  • 이 경우 자식 타입인 Child를 생성했기 때문에 메모리 상에 Child와 Parent 모두 생성
  • 생성된 참조값을 Parent 타입의 변수인 poly에 담아둠
  • 부모는 자식을 담을 수 있지만, 자식은 부모를 담을 수 없음
    • Child child = new Parent() ;  → 이런 경우 컴파일 에러
Parent poly = new Parent() ;
Parent poly = new Child() ;
Parent poly = new Grandson() ;
  • 부모 타입은 자신은 물론이고, 자신을 기준으로 모든 자식 타입 참조 가능 

 

다형적 참조의 한계

  • Parent poly = new Child() 이렇게 자식을 참조한 상황에서 poly는 자식 메서드 childMethid() 호출 불가능
  • poly는 Parent 타입으로 Parent 클래스부터 시작해서 필요한 기능을 탐색
  • 상속 관계는 부모 방향으로 찾아 올라갈 수는 있지만, 자식 방향으로는 찾아 내려가기 불가능

 

 

부모는 자식을 담을 수 있자만, 자식을 부모를 담을 수 없다.

 

자식을 부모 방향으로 찾아 올라갈 수 있지만, 부모는 자식 방향으로 내려갈 수 없다 

즉, 부모는 자식 메서드를 호출 할 수 없다는 것이다. 

하지만...!!  오버라이딩 된 경우에는 자식의 메서드를 먼저 호출한다.

부모가 자식 메서드를 쓰려면 캐스팅이 필요한다. 

 

 

다형성과 캐스팅

Child child = (Child) poly;
  • 호출하는 타입을 자식인 Child 타입으로 변경 
  • 다운 캐스팅이라고 함 (업 캐스팅은 부모타입으로 변경)
    • 업캐스팅 같은 경우 생략이 가능
  • 참고로 이렇게 캐스팅 한다고 해서 Parent poly의 타입이 변하는 것이 아님
    • poly의 참조값을 꺼내고 꺼낸 참고값이 Child 타입이 되는 것
    • 따라서 poly의 타입은 Parent로 기존과 같이 유지 

 

일시적 다운 캐스팅

((Child) poly).childMethod();

  • 이렇게 일시적으로 다운캐스팅을 사용 할 수 있음 
  • 메서드 호출하는 순간만 다운캐스팅
  • 별도의 변수 없이 인스턴스의 자식 타입의 기능 사용 가능 

업캐스팅 같은 경우 굳이 쓰지않음 ...  자주 쓰이기 때문에 생략이 권장 ! 

 

 

다운 캐스팅 주의점 

// 런타임 에러 발생
Parent parent = new Parent() 
Child child = (Child) Parent;

  • 위와 같은 경우 ClassCastException 오류 발생
  • 메모리 상에 자식 타입이 존재하지 않음
Parent parent = new Child(); // 업캐스팅 (자동)
Child child = (Child) parent; // 다운캐스팅 (가능)
  • 위와 같이 할 경우 오류 발생하지 않음
  • 업캐스팅 경우 객체를 생성하면 해당 타입의 상위 부모 타입이 모두 생성되기때문에 이런 문제 발생하지 않음

 

instanceof

perent instanceof Child  //false
객체 instanceof 클래스
  • 참조하는 대상이 다양하기 때문에 어떤 인스턴스를 참조하는지 확인하는게 필요
  • 인스턴스의 타입을 확인하기 위해 instanceof 키워드 사용
  • 타입 변경 가능한지 확인 후 다운 캐스팅 수행하는 것이 안전

 

메서드 오버라이딩

  • 오버라이딩 된 메서드가 항상 우선권을 가짐
  • 기존 기능을 덮어 새로운 기능을 재정의 한다는 뜻
  • 앞서 배운 오버라이딩은 반쪽짜리이고 메서드 오버라이딩의 힘은 다형성과 함께 사용에서 나타남

 

// 자식의 method 메서드는 현재 Override 됨

Parent poly = new Child();
 System.out.println("Parent -> Child");
 System.out.println("value = " + poly.value); //변수는 오버라이딩 X
poly.method(); //메서드 오버라이딩
  • poly.value 값은 Parent 필드의 value 값이 됨
    • Parent 타입이기 때문에 Perent 클래스 필드에서 value를 먼저 탐색
    • 필드는 Overriding이 안됨
  • 하지만 poly.method 같은 경우 Override 된 상황으로, 우선권을 가짐
    • 오버라이딩 된 메서드는 항상 우선권을 가짐
    • 따라서 Parent.method() 가 아니라 Child.method( ) 가 실행
  • 만약 손자에서도 같은 메서드를 오버라이딩 하면 손자의 오버라이딩 메서드가 우선권을 가짐

 

다형적 참조 : 하나의 변수 타입으로 다양한 자식 인스턴스를 참조할 수 있는 기능

메서드 오버라이딩 : 기존 기능을 하위 타입에서 새로운 기능으로 재정의

 

(참고)

(Parent) parent = new Parent  |  (타입) 변수명 = new 클래스()


다형성 활용1

그냥 Child 타입 선언하면 되는데,

굳이 Parent 타입 선언하고 다운캐스팅까지 하는 이유가 뭘까 ? 

 

 

문제 상황

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("동물 소리 테스트 종료");
    }
}
  • Dog, Cat, Caw 클래스가 있다고 하자.
  • 여기에서 새로운 동물이 추가되면, 출력하는 부분은 계속 중복이 증가한다.
 private static void soundCaw(Caw caw) {
        System.out.println("동물 소리 테스트 시작");
        caw.sound();
        System.out.println("동물 소리 테스트 종료");
}
  • 이런식으로 메서드로 중복 제거를 시도하려 해도, 이 메서드는 Caw 전용 메서드여서 불가능
  • Dog, Cat, Caw 타입이 서로 다르기 때문에 메서드 함께 사용하는 것 불가능
 Caw[] cawArr = {cat, dog, caw}; //컴파일 오류 발생!
  • 배열을 통해 중복을 제거하려해도 배열 타입은 Dog, Cat, Caw 중 하나로 지정
  • 때문에 Dog, Cat, Caw를 하나의 배열에 담는 것 불가능 

 

문제 해결책

  • Dog, Cat, Caw 타입이 다 다른게 문제점
  • Dog, Cat, Caw 모두 같은 타입으로 사용 할 수 있는 방법

→ 다형성의 핵심인 다형적 참조메서드 오버 라이딩으로 모두 같은 타입 사용하게 하여 각각 자신의 메서드 호출 가능함 !! 

(자신만 있는 메서드는 다운캐스팅이 필요하지만, 오버 라이딩된 메서드는 다운캐스팅 없이 바로 호출 가능)

 

 

 

다형성 활용2

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

        soundAll(dog);
        soundAll(cat);
        soundAll(caw);
    }
    
    public static void soundAll(Animal animal){
        System.out.println("동물 소리 테스트 시작");
        animal.sound();
        System.out.println("동물 소리 테스트 종료");
    }
}

  • soundAll 매개변수에는 Dog, Cat, Caw의 인스턴스가 전달된다.
  • Animal은 Dog, Cat, Caw의 부모 타입이기 때문에 가능하다.
  • animal.sound()를 호출하면 Animal 타입이기 때문에 Animal을 확인해야하지만, 오버라이딩 메서드 우선권 때문에 자식 sound() 실행
  • 다형성 덕분에 새로운 동물 추가해도 재사용 가능
public class AnimalSoundMain {
    public static void main(String[] args) {
        Animal[] animals = {new Dog(),new Cat(),new Caw()};

        for (Animal animal : animals) {
            animalSound(animal);
        }
    }

    private static void animalSound(Animal animal) {
        System.out.println("동물 소리 테스트 시작");
        animal.sound();
        System.out.println("동물 소리 테스트 종료");
    }
}
  • 이런식으로 더 개선 할 수도 있음

 

남은 문제

  • 대부분의 문제는 해결되었지만, Animal 클래스를 생성할 수 있는 문제가 있다.
    • Animal 클래스는 직접 생성해서 사용할 일이 없음
    • Animal 클래스는 다형성을 위해 필요한 것이지 직접 인스턴스 생성해서 사용할 일이 없음
  • Animal 클래스를 상속 받는 곳에서 sound() 메서드 오버라이딩을 잊는 경우
    • Animla 클래스를 상속 받는 Pig 클래스를 만든다 했을때, sound() 메서드를 잊어버림을 가정
    • 이렇게 되는경우 Animal 클래스의 sound 매서드가 호출 됨 

추상 클래스추상 메서드로 해결 가능 

 

좋은 프로그램은 제약이 있는 프로그램이라는 것을 명심 !! 


추상 클래스와 추상 메서드

추상 클래스

 abstract class AbstractAnimal {...}
  • 부모 클래스는 제공하지만, 실제 생성되면 안되는 클래스
  • 여러 클래스가 공통된 메서드를 반드시 구현하게 만들기 위함
  • 추상적인 개념을 제공
  • abstract 키워드 사용
  • 실체인 인스턴스가 존재하면 안됨 
    • new AbstractAnimal() 와 같이 인스턴스 생성 못함


추상 메서드

 public abstract void sound();
  • 부모 클래스를 상속 받는 자식 클래스가 반드시 오버라이딩 해야 하는 메서드
  • 추상 메서드가 하나라도 있는 클래스는 추상 클래스로 선언해야함
  • 추상 메서드는 메서드 바디가 없음 
  • 추상 메서드는 상속 받는 자식 클래스가 반드시 오버라이딩 해서 사용해야 함

 

위의 제약을 제외하고 메모리 구조,

실행 결과 모두 일반적인 클래스와 동일하다 ! 

 

목적은 하위 클래스에 공통 메서드를 강제하고, 객

체 생성을 방지하는 템플릿 역할을 한다

 

 

순수 추상 클래스

public abstract class AbstractAnimal {
    public abstract void sound();
    public abstract void move();
}
  • 모든 메서드가 추상 메서드인 것을 순수 추상 클래스라고 한다.
  • 순수 추상 클래스는 실행 로직을 전혀 갖고 있지 않음
  • 단지 다형성을 위한 부모 타입으로써 껍데기 역할만 제공
  • 이런 특징은 규격을 지켜서 구현해햐 하는 것 처럼 느껴짐 → 인터페이스에 활용 !

자바에서는 순수 추상 클래스라는 말이 없고,

이런거를 만들기보다 그냥 인터페이스를 만듬

 

 

 

인터페이스

// interface 
public interface InterfaceAnimal {
    void sound();
}
  • 사실 public abstract void sound(); 인데, public abstract를 생략
  • class가 아니라 interface 키워드 사용
public class Dog implements InterfaceAnimal {
    @Override
    public void sound() {
        System.out.println("멍멍");
    }
}
  • 인터페이스는 extends 대신에 implements 키워드 사용
  • 인터페이스는 상속이라 하지 않고 구현이라고 함
public class AnimalMain {
    public static void main(String[] args) {
        Cat cat = new Cat();
        Dog dog = new Dog();

        soundAniaml(cat);
        soundAniaml(dog);
    }

    private static void soundAniaml(InterfaceAnimal animal) {
        animal.sound();
    }
}
  • 순수 추상 클래스를 더 편리하게 사용할 수 있는 인터페이스라는 기능 제공
public interface InterfaceAnimal {
	// public static final double MY_PI = 3.14;
	double MY_PI = 3.14;
 }
  • 상수를 선언 할 때는 public static final 생략 가능 

  • 인터페이스는 구현으로 UML 표현에서 점선을 사용

 

상속과 구현

  • 인터페이스 같은 경우 모든 메서드가 추상 메서드이다.
  • 물려받을 수 있는 기능이 없다.
  • 인터페이스는 매서드 이름만 있는 설계도로, 실제 작동하는 방법은 하위 클래스에서 구현한다.
  • 따라서 인터페이스는 상속이 아니라 구현이라고 표현한다. 

 

그냥 인터페이스 대신 추상 메서드인 순수 추상 클래스를 만들어쓰면 되는거 아닌가 ?

  • 제약
    • 인터페이스를 구현하는 곳에 강력한 제약을 줄 수 있음
    • 순수 추상 클래스 같은 경우, 미래의 누군가 그곳에 실행 가능한 메서드 끼워 넣을 수 있음
      • 인터페이스를 사용하면 위와 같은 경우 원천차단 ! 
  • 다중 구현
    • 자바에서 클래스 상속은 부모 하나만 지정 가능
    • 하지만 인터페이스는 부모를 여러명 두는 다중 구현 (다중 상속)이 가능


인터페이스 - 다중 구현

public class Child implements InterfaceA, InterfaceB {
    @Override
    public void methodA() {
        System.out.println("Child.methodA");
    }
    @Override
    public void methodB() {
        System.out.println("Child.methodB");
    }
    @Override
    public void methodCommon() {
        System.out.println("Child.methodCommon");
    }
}
  • 자바에서는 다중 상속을 지원하지 않음 (다이아몬드 문제 때문에)
  • 하지만 인터페이스는 다중 구현을 허용함
    • 인터페이스는 모두 추상 메서드로 이뤄줘 있기 떄문에, 무조건 자식의 메서드 실행
    • 굳이 부모의 methodCommon 메서드를 고려할 필요가 없음

  • 결국 methodCommon( )은 하위 타입인 Child에서 오버라이딩 됨

 

다형성을 위한 추상 클래스에서부터 인터페이스까지

필요한 모든 과정까지 이해해두자


객체 지향 프로그래밍

  • 협력
    • 여러개의 독립된 단위, 즉 "객체" 들의 모임으로 파악하고자 하는 것
    • 각각의 객체는 메시지를 주고받고, 데이터를 처리할 수 있음
  • 프로그램을 유연하고 변경이 용이하게 만듬
    • 레고 블럭 조립하듯이

 

역할과 구현 분리  

  • 클라이언트는 대상의 역할(인터페이스)만 알면 된다.
  • 클라이언트는 내부 구조를 모르고, 내부 구조가 변경되어도 영향 받지 않음
  • 역할 = 인터페이스
  • 구현 = 인테페이스를 구현한 클래스
  • 객체 설계에서 역할과 구현을 명확히 분리 ! 
    • 객체의 역할을 먼저 부여하고, 그 역할을 수행하는 구현 객체 만들기 

 

다형성의 본질

  • 인터페이스를 구현한 객체 인스턴스를 실행 시점에 유연하게 변경 가능
  • 클라이언트를 변경하지 않고, 서버의 구현 기능 유연하게 변경 가능 

 

다형성을 잘 이해하는게 OOP에 정말 정말 중요함

 

 

 

다형성 - 역할과 구현 예제

package car;

public interface Car {
    void startEngine();
    void offEngine();
    void pressAccelerater();
}
package car;

public class Driver {
    private Car car;

    public void setCar(Car car){
        System.out.println("자동차를 설정합니다." + car);
        this.car = car;
    }

    public void drive(){
        System.out.println("자동차를 운전합니다.");
        car.startEngine();
        car.pressAccelerater();
        car.offEngine();
    }
}
  • Driver (클라이언트) 클래스 
package car;

public class CarMain {
    public static void main(String[] args) {
        Driver driver = new Driver();

        K3Car k3Car = new K3Car();
        driver.setCar(k3Car);
        driver.drive();

        Model3Car model3Car = new Model3Car();
        driver.setCar(model3Car);
        driver.drive();
    }
}

  • 역할과 구현 분리
  • 클라이언트 코드의 변경 없이 구현 객체 변경 가능
  • Driver는 Car (역할)에만 의존하고, 구현인 K3, Model3 자동차에 의존하지 않음
    • 의존이란 클래스 의존 관계를 뜻하는 것으로, 클래스 상에서 어떤 클래스를 알고 있는가를 뜻함
    • Driver 클래스는 Car 인터페이스만 사용 (의존)

 

OCP (Open-Closed Principle)

  • 객체 지향 설계 원칙 중 하나 
  • Open for extension 
    • 새로운 기능의 추가나 변경 사항이 생겼을 때, 기존 코드는 확장 할 수 있어야 함
  • Closed for modification
    • 기존의 코드는 수정되지 않아야 함
  • 기존 코드 수정 없이 새로운 기능 추가 할 수 있어야 된다는 말 

  • NewCar를 만들었는데, Driver (클라이언트)의 코드 수정하지 않아도 됨
  • Car 인터페이스를 통해서 새로운 차량 자유롭게 추가 가능
  • 역할과 구현을 잘 분리하여 새로운 자동차를 추가해도 대부분의 핵심 코드들 그대로 유지

 

'🔖Java' 카테고리의 다른 글

Java 기초 정리  (0) 2025.03.09