[JAVA] 객체 지향 - 캡슐화, 상속, 추상화, 다형성

이연우·2025년 7월 8일

TIL

목록 보기
2/100

객체 지향의 4가지 특징

  • 캡슐화: 데이터 보호
  • 상속: 재사용성과 확장
  • 추상화: 데이터의 계층적 표현
  • 다형성: 동일한 인터페이스로 다양한 동작 수행

> Part 1. 캡슐화(접근 제어자)

🔒 캡슐화란?

  • 객체의 데이터(속성)을 외부에서 직접 접근하지 못하게 감추고,
    정해진 방법(메서드)으로만 접근하게 만드는 것

🛡️ 접근 제어자란?

  • 클래스, 변수, 메서드 등이 어디에서 접근 가능한지를 지정하는 키워드

접근 제어자접근 범위 설명사용 예시
private같은 클래스 내에서만 접근 가능데이터 보호, 캡슐화 핵심
protected같은 패키지 + 상속받은 클래스에서 접근 가능상속 구조에서 자식에게 열어 줄 때
default (아무것도 안 쓰면)같은 패키지 내에서만 접근 가능패키지 단위로 관리할 때
public어디서든 접근 가능외부에서도 자유롭게 써야 할 때

  • 캡슐화는 데이터를 감추고(public 메서드로만 접근), 접근 제어자는 그 감추는 "정도"를 조절하는 도구
public class Account {
    private int balance; // 외부에서 직접 접근 불가

    public void deposit(int amount) {
        balance += amount;
    }

    public int getBalance() {
        return balance;
    }
}

-> balanceprivate 라 외부에서 account.balance = 1000; 사용 불가능
    -> 대신 deposit() 이나 getBalance() 처럼 정해진 방식만 허용


🔍 게터(getter)란?

  • 객체의 private 변수 값을 가져오는 메서드
  • 보통 get변수명() 형태로 이름을 지음
public String getName() {
    return name;
}

➡️ 이 메서드를 호출하면 name 변수의 값을 돌려줌

🔍 세터(setter)란?

  • 객체의 private 변수 값을 설정하는 메서드
  • 보통 set변수명(값) 형태로 이름을 지음
public void setName(String newName) {
    name = newName;
}

➡️ 이 메서드를 호출하면 name 변수에 새로운 값이 저장됨

📦 왜 필요할까?
1. 캡슐화 유지
private 변수는 외부에서 직접 접근할 수 없기 때문에 getter/setter 를 통해 안전하게 접근

2. 데이터 유효성 검사
→ 세터 안에 조건을 넣어서 잘못된 값이 들어오는 것을 방지

public void setAge(int age) {
    if (age >= 0) {
        this.age = age;
    }
}

3. 유지보수에 유리
→ 직접 변수에 접근하는 것보다 메소드를 통해 조작하면 기능 수정과 추가에 용이

🧾 전체 예시:

public class Person {
    private String name;

    public String getName() {
        return name;         // getter
    }

    public void setName(String name) {
        this.name = name;    // setter
    }
}

Person 객체의 name 변수는 외부에서 getName()setName() 을 통해서만 접근 가능
캡슐화 + 안전한 데이터 처리의 대표적인 형태


⚠️ 무분별한 세터 사용이란?

  • 모든 필드에 대해 아무 조건 없이 public setter 를 열어 두는 것
  • 즉, 외부에서 객체의 내부 상태를 마음대로 바꿀 수 있게 해 버리는 것

왜 문제일까?
1. 캡슐화 파괴
→ 세터를 열어 두면 객체의 상태를 외부가 마음대로 변경할 수 있어서 보호가 안 됨
→ 결국 캡슐화의 의미가 사라짐

2. 객체의 무결성 훼손
setAge(-5) 와 같은 잘못된 값이 들어와도 조건 없이 반영돼 버릴 수 있음

3. 설계가 취약해짐
→ 외부 코드가 객체의 내부 상태를 자주 바꾸면 코드 흐름 예측이 불가능해지고, 디버깅도 힘들어짐

🧾 예시 코드:

public class User {
    private String role;

    public void setRole(String role) {
        this.role = role;  // 위험! 아무 역할이나 막 설정 가능
    }
}
User user = new User();
user.setRole("admin");
user.setRole("guest");

문제점 요약

  • setRole()아무 조건 없이 public이기 때문에
    외부에서 User 객체의 핵심 속성을 제약 없이 조작 가능

🔧 안전한 데이터 설정 로직 추가

public class User {
    private String role;

    // 허용된 역할만 저장
    public void setRole(String role) {
        if (role.equals("admin") || role.equals("user") || role.equals("guest")) {
            this.role = role;
        } else {
            throw new IllegalArgumentException("허용되지 않은 역할입니다: " + role);
        }
    }

    public String getRole() {
        return role;
    }
}
  • "admin", "user", "guest" 처럼 허용된 값만 저장할 수 있음
  • 이외의 값을 넣으려고 하면 예외(Exception)를 발생시켜 차단

> Part 2. 상속

🧬 상속이란?

  • 기존 클래스(부모, 상위 클래스)의 속성과 동작을 새로운 클래스(자식, 하위 클래스)가 물려받는 것

    → 즉, 코드를 재사용하면서 확장할 수 있는 기능

🧾 예시 코드:

// 부모 클래스
public class Animal {
    public void eat() {
        System.out.println("먹는다.");
    }
}

// 자식 클래스
public class Dog extends Animal {
    public void bark() {
        System.out.println("멍멍!");
    }
}
public class Main {
    public static void main(String[] args) {
        Dog myDog = new Dog();
        myDog.eat();   // 부모 클래스 메서드
        myDog.bark();  // 자식 클래스 메서드
    }
}

➡️ Dog 클래스는 Animal 클래스를 상속받았기 때문에 eat() 이라는 메소드 그대로 사용 가능

🔍 상속의 장점

장점설명
코드 재사용이미 만든 클래스를 다시 써서 새로운 클래스를 쉽게 만들 수 있음
확장성기존 코드를 바꾸지 않고 기능을 덧붙일 수 있음
유지 보수 편리공통 기능을 부모에 모아 두면 변경할 때 한 곳만 수정하면 됨


🔍 super - 부모 인스턴스란?

  • 자식 클래스에서 부모 클래스의 필드(변수), 메서드, 생성자에 접근할 때 사용하는 키워드
  • 즉, "부모 클래스의 것"을 가리키는 참조

super의 주요 용도
1. 부모의 메서드 호출

  • 자식 클래스에서 오버라이딩한 메서드 대신 부모 클래스의 메서드를 호출하고 싶을 때 사용
class Animal {
    public void sound() {
        System.out.println("동물이 소리를 낸다.");
    }
}

class Dog extends Animal {
    @Override
    public void sound() {
        super.sound(); // 부모 메서드 호출
        System.out.println("멍멍!");
    }
}

출력 결과

동물이 소리를 낸다
멍멍!

2. 부모의 생성자 호출

  • 자식 클래스의 생성자 안에서 부모 클래스의 생성자를 명시적으로 호출할 수 있음
  • 항상 첫 줄에서만 사용 가능
class Animal {
    public Animal(String name) {
        System.out.println("동물 이름: " + name);
    }
}

class Dog extends Animal {
    public Dog() {
        super("댕댕이");  // 부모 생성자 호출
        System.out.println("강아지 생성됨.");
    }
}

출력 결과

동물 이름: 댕댕이
강아지 생성됨

3. 부모 클래스의 필드 참조

  • 자식 클래스에 같은 이름의 필드가 있을 경우, 부모의 필드에 접근하려면 super 를 사용해야 됨
class Animal {
    String type = "동물";
}

class Dog extends Animal {
    String type = "강아지";

    public void printType() {
        System.out.println(type);       // 강아지
        System.out.println(super.type); // 동물
    }
}

⚠️ 주의할 점

  • super()생성자 안에서만, 그리고 첫 줄에서만 사용 가능
  • 자식 클래스가 생성될 때는 항상 먼저 부모 클래스 생성자가 호출되므로, super() 는 자식 생성자의 첫 줄에 오는 게 원칙

🔁 메서드 오버라이딩(Overriding)이란?

  • 부모 클래스에서 정의한 메서드를 자식 클래스에서 똑같은 이름, 매개변수, 반환형으로 다시 정의하는 것
  • 즉, 부모의 행동을 자식이 자신에 맞게 "덮어쓰는" 것

⚙️ 오버라이딩의 조건

조건설명
메서드 이름부모와 완전히 같아야
매개변수개수, 순서, 타입이 같아야 함
반환형부모와 같거나 부모보다 더 구체적인 타입(covariant return type)
접근 제어자부모보다 같거나 더 넓은 범위(public > protected > default > private)
예외부모가 던지는 예외보다 더 넓거나 같아야 함

🧾 예시 코드:

class Animal {
    public void sound() {
        System.out.println("동물이 소리를 낸다.");
    }
}

class Dog extends Animal {
    @Override
    public void sound() {
        System.out.println("멍멍!");
    }
}
public class Main {
    public static void main(String[] args) {
        Animal a = new Dog();
        a.sound();  // "멍멍!" 출력됨
    }
}

➡️ sound()Animal 에도 있지만, Dog 에서 오버라이딩 되었기 때문에 Dog 버전이 실행됨

오버라이딩의 목적

  • 다형성 구현 : 부모 타입으로 자식 객체를 다룰 수 있음
  • 클래스 맞춤 행동 : 자식 클래스가 상황에 맞게 동작을 변경
  • 유연한 코드 구성 : 메서드 호출 결과를 객체에 따라 다르게 함

🚫 오버로딩과 차이점

개념설명
오버로딩(Overloading)같은 이름, 다른 매개변수 (→ 메서드 중복 정의)
오버라이딩(Overriding)같은 이름, 같은 매개변수 (→ 부모 메서드 재정의)

> Part 3. 추상화

🎯 추상화란?

  • 복잡한 시스템에서 핵심적인 특징만 추려 내어 표현하고, 불필요한 세부 정보는 감추는 것
  • 즉, 필요한 것만 보여 주고, 나머지는 숨긴다는 개념

왜 추상화가 중요한가?

이유설명
복잡성 감소사용자는 내부 구현을 몰라도 객체를 사용할 수 있음
유연한 설계전체 시스템을 더 쉽게 설계하고 구조화할 수 있음
재사용성 증가공통된 구조를 기반으로 다양한 객체를 만들 수 있음
캡슐화와 연결내부 구현을 감추기 때문에 보호도 자연스럽게 따라옴

🧩 객체 지향에서의 추상화

  • 객체 지향에서 추상화는 주로 클래스, 인터페이스, 추상클래스를 통해 구현됨
    → 공통적인 속성과 동작을 묶어 내고(일반화),
    → 불완전하거나 미정의된 부분은 추상 메서드로 남겨서
    → 자식 클래스가 나머지를 구현하게 함

🐾 예시: 고양이 → 동물 → 생명체

1. 고양이(Cat)

  • 구체적인 객체: 품종, 색깔, 야옹 소리 등 세부 특징이 존재
  • Cat 클래스 → 개별 동물을 나타내는 구체적인 클래스

2. 동물(Animal)

  • 고양이, 개, 호랑이, 새 등이 공통적으로 갖는 특징: 움직인다, 먹는다, 소리 낸다
  • Animal 클래스 → 고양이보다 더 추상적이며, 공통된 속성과 기능만 표현

3. 생명체(LivingThing)

  • 동물, 식물, 곰팡이 등이 공통적으로 갖는 특징: 성장한다, 번식한다, 죽는다
  • LivingThing 클래스 → 훨씬 더 추상적이고, 모든 생명체가 공유하는 개념만 포함

🧾 예시 코드:

abstract class LivingThing {
    public abstract void grow();
}

abstract class Animal extends LivingThing {
    public abstract void makeSound();
    public void eat() {
        System.out.println("먹는다.");
    }
}

class Cat extends Animal {
    @Override
    public void makeSound() {
        System.out.println("야옹");
    }

    @Override
    public void grow() {
        System.out.println("고양이가 자란다.");
    }
}

🎯 추상 클래스란?

  • 일부만 구현되어 있고 나머지는 자식 클래스가 반드시 완성해야 하는 클래스
  • 즉, 공통적인 구조와 기능은 정의해 두고, 구체적인 동작은 자식 클래스에게 맡기는 클래스

📌 특징 정리

  • abstract 키워드로 정의 → abstract class 클래스명 { ... }
  • 직접 객체 생성 불가 → new 로 인스턴스 생성 가능
  • 추상 메서드 포함 가능 → 몸체 없이 선언만 된 메서드(abstract void method();)
  • 일반 메서드도 포함 가능 → 구현된 메서드도 함께 가질 수 있음
  • 상속받은 자식 클래스는 반드시 추상 메서드를 구현해야 됨 → 구현하지 않으면 그 자식 클래스도 추상 클래스가 되어야 함

추상 클래스를 사용하는 이유

이유설명
공통 구조 제공여러 자식 클래스가 공유하는 속성과 기능을 한곳에 모아 둠
틀(template) 제공구현해야 하는 메서드를 명시적으로 강제 가능
코드 재사용기본 동작은 추상 클래스에, 변화되는 부분은 자식 클래스에 분리 가능
캡슐화 + 추상화내부 구현을 감추고 필요한 구조만 제공 가능

🧾 예시 코드:

abstract class Animal {
    public void eat() {
        System.out.println("먹는다.");
    }

    public abstract void sound(); // 추상 메서드
}
class Dog extends Animal {
    @Override
    public void sound() {
        System.out.println("멍멍!");
    }
}
public class Main {
    public static void main(String[] args) {
        // Animal a = new Animal(); // ❌ 오류: 추상 클래스는 인스턴스화 불가
        Animal a = new Dog();        // ✅ 자식 클래스는 인스턴스화 가능
        a.eat();                     // "먹는다."
        a.sound();                   // "멍멍!"
    }
}

🚫 추상 클래스와 인터페이스의 차이점

구분추상 클래스인터페이스
키워드abstract classinterface
다중 상속불가가능(자바 8 이후 디폴트 메서드도 허용)
생성자있음없음
필드일반 변수 가능상수(public static final)만 가능
메서드구현된 메서드 포함 가능(자바 8 이전엔) 모든 메서드는 추상 메서드만 가능

> Part 4. 다형성

🔍 다형성이란?

  • 하나의 이름(메서드, 클래스, 인터페이스 등)으로 여러 가지 형태의 동작을 수행할 수 있는 성질
  • 즉, 같은 메서드 이름을 써도 객체에 따라 다르게 동작하는 것

🧾 예시 코드:

class Animal {
    public void sound() {
        System.out.println("동물이 소리를 낸다");
    }
}

class Dog extends Animal {
    @Override
    public void sound() {
        System.out.println("멍멍!");
    }
}

class Cat extends Animal {
    @Override
    public void sound() {
        System.out.println("야옹~");
    }
}
public class Main {
    public static void main(String[] args) {
        Animal a1 = new Dog();
        Animal a2 = new Cat();

        a1.sound(); // 멍멍!
        a2.sound(); // 야옹~
    }
}

➡️ sound() 라는 하나의 메서드 이름으로 실제 객체에 따라 다르게 실행되는 것

왜 중요한가?

장점설명
코드 유연성같은 메서드를 다양한 객체에 사용할 수 있음
유지 보수 쉬움코드 변경 없이 기능 확장 가능
모듈화 & 재사용공통 인터페이스로 다양한 구현체를 만들 수 있음

🔁 형변환이란?

  • 객체의 자료형(참조형)을 다른 자료형으로 바꾸는 것
    → 주로 상속/다형성과 관련해서 부모 ↔ 자식 타입 간에 변환할 때 사용됨

🔼 업캐스팅(Upcasting)

  • 자식 타입 → 부모 타입으로 형변환
  • 자동 형변환(명시적 캐스팅 불필요)

🧾 예시 코드:

class Animal {
    public void sound() {
        System.out.println("동물이 소리를 낸다.");
    }
}

class Dog extends Animal {
    public void sound() {
        System.out.println("멍멍!");
    }

    public void wagTail() {
        System.out.println("꼬리를 흔든다.");
    }
}

public class Main {
    public static void main(String[] args) {
        Dog dog = new Dog();
        Animal a = dog;  // 업캐스팅(자동)
        a.sound();       // 멍멍! ← 다형성
        // a.wagTail();  // ❌ 사용 불가(Animal 타입엔 없음)
    }
}

→ 자동으로 변환됨
→ 자식 객체를 부모 타입으로 다룰 수 있음 ⇢ 다형성 구현 가능
→ 부모가 가진 기능만 사용할 수 있음

🔽 다운캐스팅(Downcasting)

  • 부모 타입 → 자식 타입으로 형변환
  • 명시적 형변환 필요

🧾 예시 코드:

Animal a = new Dog();   // 업캐스팅
Dog d = (Dog) a;        // 다운캐스팅(명시적)

d.wagTail();            // 가능해짐

⚠️ 주의:

  • 다운캐스팅은 형태는 부모지만 실제 객체가 자식일 때만 가능함
Animal a = new Animal();
Dog d = (Dog) a; // ❌ 오류: 실제로는 Dog가 아님

➡ 이럴 땐 런타임 에러(ClassCastException) 발생

💡 instanceof 연산자

  • 다운캐스팅 전에 안전하게 확인하기 위한 연산자
if (a instanceof Dog) {
    Dog d = (Dog) a;
    d.wagTail();
}

📌 요약 정리

구분업캐스팅다운캐스팅
방향자식 → 부모부모 → 자식
명시적 여부필요 없음필요함
안전성안전함위험함(런타임 오류 가능)
사용 목적다형성 구현, 공통 인터페이스 사용자식 고유 기능 사용 시 필요

0개의 댓글