[Java] OOP

전두엽힘주기·2025년 9월 30일

Java

목록 보기
4/9

객체 지향 프로그래밍 OOP

객체들의 상호작용으로 만들어지는 프로그램
객체(Object) == 인스턴스(Instance)

객체 지향 프로그래밍 목표

수정이 쉬워야 한다: 버그를 잡거나 로직을 변경할 때.

확장이 쉬워야 한다: 새로운 기능을 추가할 때, 기존 코드를 최소한으로 고쳐야 함.

객체지향 특성

1. 캡슐화

  • 객체의 데이터(필드)와 기능(메소드)을 하나로 묶고, 내부 데이터를 외부로부터 보호하여 숨기는 것

  • 외부에 공개된 메소드(Public Method)를 통해서만 데이터의 접근 및 수정이 가능하도록 허용.

클래스

class Animal{
	String name;
    int age;
    void eat(){...};
    void speak(){...};
}

객체

//field
String name lion;
int age = 4;
//method
void eat();
void speak();

2. 상속

정의: 기존의 상위 클래스(부모 클래스)가 가진 필드와 메소드를 하위 클래스(자식 클래스)가 물려받아 그대로 사용하거나 확장하는 것.

목적: 코드의 재사용성을 높이고 확장을 용이하게 함. (예: Point 클래스를 만들어 두면, ColorPoint 클래스는 color 관련 기능만 추가하면 됨)

용어:

  • 자바에서는 부모/자식 클래스보다 슈퍼 클래스(Super Class) / 서브 클래스(Sub Class) 라는 용어를 더 많이 씀.
    C++에서는 Parent / Child라고 부름.

구현: extends 키워드를 사용. (예: class Student extends Person { ... })

객체 지향에서는 '자식(서브 클래스)'이 더 큼.

이유: 자식 클래스 = 부모의 모든 기능 + 자신만의 추가적인(확장된) 기능을 더 가지고 있기 때문.

// 상위 클래스 (부모 클래스, Superclass)
class Animal {
    String name;

    void eat() {
        System.out.println("먹이를 먹습니다.");
    }
}

// 하위 클래스 (자식 클래스, Subclass)
class Human extends Animal { // Animal 클래스를 상속
    String job;

    void work() {
        System.out.println("일을 합니다.");
    }

    // 상속받은 eat() 메소드를 그대로 사용 가능
}

public class Main {
    public static void main(String[] args) {
        Human person = new Human();
        person.name = "홍길동"; // Animal로부터 상속받은 필드
        person.job = "개발자";   // Human 클래스 고유의 필드

        person.eat();         // Animal로부터 상속받은 메소드
        person.work();        // Human 클래스 고유의 메소드
    }
}

자바(Java) 상속의 주요 특징

(1) 다중 상속을 지원하지 않는다

자바 클래스는 오직 하나의 슈퍼 클래스만 extends 할 수 있음. (class A extends B, C 형태 불가능)

상속의 깊이(A→B→C)는 무제한이지만, 횡으로 늘리는 것은 불가능.

대안: 다중 상속이 필요할 경우, 클래스가 아닌 인터페이스(Interface)를 사용. (인터페이스는 다중 상속 가능)

(2) 접근 지정자 (Access Specifier)

상속 시, 부모의 멤버에 자식이 접근할 수 있는지를 결정.

private: 오직 자기 클래스 내부에서만 접근 가능. (자식 클래스조차 접근 불가능)

default (아무것도 안 쓴 경우): 같은 패키지 안에서만 접근 가능.

public: 어디서든(모든 패키지) 접근 가능.

protected: default의 기능(같은 패키지) + 다른 패키지라도 상속받은 자식 클래스라면 접근을 허용.

상속과 생성자(Constructor)

자식 클래스(Sub Class)의 객체를 생성할 때(new) 생성자 호출 순서가 매우 중요.

(1) 생성자 호출 순서

결론: 부모와 자식 생성자 둘 다 실행됨.

순서: 슈퍼 클래스(부모) 생성자 실행 → 서브 클래스(자식) 생성자 실행

이유: 논리적으로 부모의 멤버(필드)가 먼저 초기화되어야, 자식이 그것을 기반으로 자신의 멤버를 초기화할 수 있기 때문.

연쇄 호출: C가 B를 상속하고 B가 A를 상속하면, new C() 호출 시 실행 순서는 A → B → C.

(2) super(): 부모 생성자 명시적 호출

기본 규칙: 자식 생성자에서 부모 생성자를 명시적으로 부르지 않으면, 컴파일러는 자동으로 부모의 '기본 생성자'(인자 없는 생성자)를 호출.

문제 상황 (컴파일 오류): 만약 부모 클래스에 기본 생성자가 없고, 인자가 있는 생성자만 정의되어 있다면, 자바는 자동으로 호출할 기본 생성자를 찾지 못해 컴파일 오류(Undefined constructor)가 발생.

해결책: 자식 생성자에서 super()를 사용해 부모 생성자를 직접 선택해야 함.

super(); // 부모의 기본 생성자 호출

super(10); // 부모의 인자(int 1개)가 있는 생성자 호출

super()의 핵심 규칙

super()는 "반드시 서브 클래스 생성자 코드의 첫 라인(first line)"에 와야 함.

이유: 자식 멤버가 초기화되기 전에 부모 멤버가 반드시 먼저 초기화되어야 하는 논리적 순서를 강제하기 위함.

3. 추상화

정의: 객체의 공통적이고 본질적인 특징만 추출하고, 복잡하고 불필요한 세부 사항은 숨기는 것.

구체적인 객체들(Student, Worker)을 생각했다면, 반드시 다시 추상화하는 생각(Person)을 먼저 해야 함.

설계: Person이라는 슈퍼 클래스에 공통 속성(이름, 나이)과 메서드(말하기)를 정의.

효과: 새로운 Visitor 클래스를 추가할 때 Person을 상속받으면 '말하기' 기능을 또 만들 필요가 없음. (확장 용이)

자바에서는 추상 클래스와 인터페이스를 사용해 추상화를 구현.

1) 추상 클래스 (Abstract Class)

정의: abstract 키워드가 붙은 클래스. "구현이 되어있지 않은", "불완전한" 클래스.

객체 생성 불가 (불완전하므로 new로 인스턴스를 만들 수 없음.)

추상 메서드: abstract가 붙고 몸체({})가 없는 메서드를 가질 수 있음.

목적:

"오버라이딩 강제": 부모(추상 클래스)가 draw()를 추상 메서드로 정의하면, 자식은 반드시 draw()를 오버라이딩하여 구현해야만 함. ("구현을 강제")

"설계와 구현의 분리": 부모는 "이런 기능이 있어야 한다"는 "틀(설계)"만 잡고, 실제 "어떻게 동작할지(구현)"는 자식에게 위임.

2) 인터페이스 (Interface)

정의: 추상 클래스보다 더 엄격한 규격(Specification) 또는 설계도

목적: 서로 다른 클래스들이 "이 규격(뼈대)에 맞춰서 구현해라"라고 기능의 뼈대를 강제하여 호환성을 맞추기 위함.

class 대신 interface 키워드를 씀.

변수(필드)를 가질 수 없고, 상수(Constant)만 가질 수 있음.

모든 메서드는 자동으로 public abstract (뼈대만 존재, 구현 불가).

상속 시 extends(확장)가 아닌 implements(구현) 키워드를 씀.

가장 큰 차이점 : 클래스는 단일 상속만 가능하지만, 인터페이스는 다중 상속(구현)이 가능.

// 클래스는 하나만 extends
// 인터페이스는 여러 개 implements 가능
class SmartPhone extends Phone implements MP3Interface, CameraInterface {
    // ... MP3Interface와 CameraInterface의 모든 추상 메서드를 구현해야 함
}

4. 다형성 Polymorphism

다형성(Polymorphism): 하나의 코드(인터페이스)가 여러 가지 다른 실제 동작(구현)을 가지는 특성을 말함.

업캐스팅(Upcasting), 메소드 오버라이딩(Method Overriding), 동적 바인딩(Dynamic Binding)


1) "is-a" 관계와 업캐스팅 (Upcasting)

다형성의 전제 조건

  • "is-a" 관계: 상속 관계에서 "자식은 부모의 한 종류이다"가 성립하는 관계를 의미함. (예: Student is a Person)
  • 업캐스팅 (Upcasting): "is-a" 관계가 성립하기 때문에, 자식 클래스 객체를 부모 클래스 타입의 변수에 할당할 수 있음.
    // Student(자식) 객체를 Person(부모) 타입 변수에 할당
    Person p = new Student(); 
  • 결과: 변수 pPerson 타입(그릇)이므로, Person 클래스에 정의된 멤버(메소드, 변수)에만 접근할 수 있음. Student가 가진 확장된 기능(예: study())은 p를 통해서는 바로 사용할 수 없음.

2) 메소드 오버라이딩 (Method Overriding)

다형성이 동작하는 방식

  • 정의: 상속 관계에서, 부모(슈퍼 클래스)로부터 물려받은 메소드를 자식(서브 클래스)이 재정의하는 것.
  • 핵심 조건: 부모 메서드와 이름, 매개변수, 리턴 타입이 완전히 동일해야 함. ("뼈대가 같다")

3) 동적 바인딩 (Dynamic Binding)

다형성이 실행되는 시점임.

  • 정의: 업캐스팅된 변수가 오버라이딩된 메소드를 호출할 때, 런타임(실행 시점) 에 실제 객체의 메소드가 호출되는 현상.
  • 예시:
    Shape s = new Line(); // 1. 업캐스팅 (s는 Shape 타입, 실제 객체는 Line)
    s.draw(); // 2. 메서드 호출
  • 결과: 컴파일 시점에는 sShape 타입이라 Shapedraw()를 부르는 것처럼 보임.
  • 하지만, 실행 시점에는 s가 실제 가리키는 객체인 Line의 오버라이딩된 draw()가 호출(바인딩)됨.
  • 자식에서 오버라이딩하면 자식 꺼 호출된다.

4) 다형성의 진정한 목적 (확장성)

이 모든 것을 하는 이유

  • 핵심 예시 (paint 메서드):
    • Shape(부모), Circle, Line, Rectangle(자식들)이 있고, 모든 자식이 draw()를 오버라이딩했다고 가정.
    • 이때, paint라는 메서드를 단 하나만 만들 수 있음.
    // 1. 매개변수를 부모 타입(Shape)으로 받음
    void paint(Shape p) { 
        // 2. p가 실제 가리키는 객체의 draw()를 호출 (동적 바인딩)
        p.draw();         
    }
  • 효과: 이 paint 메서드 하나로 모든 자식 객체를 처리할 수 있음.

    • paint(new Circle())Circledraw()가 실행됨
    • paint(new Line())Linedraw()가 실행됨
  • 나중에 Triangle(삼각형)이라는 새로운 클래스를 추가하더라도, TriangleShape을 상속하고 draw()를 오버라이딩하기만 하면, 기존의 paint 메서드는 단 한 줄도 고칠 필요가 없음. (paint(new Triangle())이 알아서 동작함)

  • 이것이 객체 지향의 목표인 수정 없이 확장을 가능하게 하는 핵심 원리


5) (참고) 메소드 오버로딩 (Method Overloading)

  • (오버라이딩과 다름)
  • 하나의 클래스 내에서 같은 이름의 메소드매개변수의 개수나 타입을 다르게 하여 여러 개 정의하는 것.

2개의 댓글

comment-user-thumbnail
2025년 9월 30일

java 공부 좀 하다 갈께요

1개의 답글