[TIL] 11. Java OOP, Casting과 추상화

MinseoKim·2026년 8월 14일
post-thumbnail

1. 오늘의 한 줄 요약

오늘은 지난 시간에 배운 객체와 상속 개념을 바탕으로 Casting, 다형성, 오버라이딩, 캡슐화, 추상 클래스, 인터페이스, 싱글톤까지 Java OOP의 핵심 개념을 배웠다.

개념 하나하나도 어렵지만 서로 따로 떨어져 있는 내용은 아니었다.

상속으로 공통 구조를 만들고, 다형성과 오버라이딩으로 객체마다 다른 동작을 만들며, 캡슐화와 추상화로 객체를 안전하고 일관되게 사용하도록 만드는 것

이라고 큰 흐름을 잡아보려고 한다.


2. 배운 내용

1) Casting

Casting은 변수의 타입을 다른 타입으로 바꿔서 보는 것이다.

Java에서는 상속 관계에 있는 타입끼리 Casting할 수 있다.

업캐스팅

업캐스팅은 자식 객체를 부모 타입으로 다루는 것이다.

PersonDTO manager
    = new ManagerDTO("또치", 20, "서울", "교육사무국");

실제로 생성된 객체는:

new ManagerDTO(...)

이기 때문에 ManagerDTO 객체다.

하지만 객체를 담는 변수의 타입은:

PersonDTO manager

이다.

ManagerDTOPersonDTO를 상속받은 자식이기 때문에 부모 타입 변수에 담을 수 있다.

이 방식의 장점은 서로 다른 자식 객체를 하나의 부모 타입으로 묶을 수 있다는 것이다.

PersonDTO[] ary = new PersonDTO[3];

ary[0] = new TeacherDTO("홍길동", 20, "서울", "react");
ary[1] = new ManagerDTO("짱구", 20, "서울", "교육팀");
ary[2] = new StudentDTO("뽀로로", 20, "서울", "2026");

TeacherDTO, ManagerDTO, StudentDTO는 서로 다른 클래스다.

하지만 모두 PersonDTO를 상속받고 있기 때문에 하나의 PersonDTO[] 배열에 담을 수 있다.

즉 업캐스팅은:

서로 다른 자식 객체를 부모라는 공통 타입으로 묶어 다루기 위해 사용한다.


다운캐스팅

다운캐스팅은 부모 타입으로 보고 있던 객체를 다시 자식 타입으로 보는 것이다.

System.out.println(manager.getName());
System.out.println(manager.getAge());
System.out.println(manager.getAddress());

현재 manager의 변수 타입은 PersonDTO이기 때문에 PersonDTO가 가지고 있는 메서드는 바로 사용할 수 있다.

하지만:

getDept()

ManagerDTO에만 존재한다.

이때는:

System.out.println(
    ((ManagerDTO)manager).getDept()
);

처럼 ManagerDTO로 다운캐스팅해야 한다.

정리하면:

업캐스팅
자식 → 부모
공통 기능을 사용하기 위해

다운캐스팅
부모 → 자식
자식만의 기능을 사용하기 위해

instanceof

배열 안에 여러 자식 타입이 섞여 있다면 무작정 다운캐스팅하면 안 된다.

이때 실제 객체의 타입을 확인하는 연산자가:

instanceof

다.

for(int idx = 0; idx < ary.length; idx++) {

    PersonDTO per = ary[idx];

    if (per instanceof TeacherDTO) {

        System.out.println(
            ((TeacherDTO)per).getSubject()
        );
    }

    if (per instanceof ManagerDTO) {

        System.out.println(
            ((ManagerDTO)per).getDept()
        );
    }

    if (per instanceof StudentDTO) {

        System.out.println(
            ((StudentDTO)per).getSsn()
        );
    }
}

흐름은 다음과 같다.

PersonDTO 타입으로 객체를 꺼냄
↓
instanceof로 실제 객체 확인
↓
알맞은 자식 타입으로 다운캐스팅
↓
자식만의 기능 사용

2) OOP와 상속

상속은 한 클래스가 다른 클래스가 가지고 있는 내용을 물려받는 것이다.

부모
↓
공통 필드와 메서드 제공

자식
↓
부모의 내용을 물려받고
자신만의 내용 추가

자식 클래스는 부모가 가지고 있는 것을 물려받은 상태에서 시작한다.

그리고 자신만의 필드나 메서드를 추가할 수 있다.

상속 관계를 만들 때는 기본적으로:

자식 is a 부모

관계가 자연스러워야 한다.

예를 들어:

Teacher is a Person
Student is a Person
Manager is a Person

은 자연스럽다.


3) 다형성

오늘 배운 다형성은 크게 세 가지 관점으로 볼 수 있었다.

변수 타입의 다형성

부모 타입 하나로 여러 자식 객체를 다룰 수 있다.

PersonDTO person1 = new TeacherDTO(...);
PersonDTO person2 = new StudentDTO(...);
PersonDTO person3 = new ManagerDTO(...);

매개변수의 다형성

메서드의 매개변수를 부모 타입으로 정의하면 여러 자식 타입을 받을 수 있다.

public void printInfo(PersonDTO person) {
}

이 메서드에는:

TeacherDTO
StudentDTO
ManagerDTO

객체를 모두 전달할 수 있다.


메서드의 다형성 — Overriding

자식은 부모에게 물려받은 메서드를 자신의 방식으로 다시 정의할 수 있다.

이를 오버라이딩(Overriding)이라고 한다.

메서드 이름과 구조는 유지하면서 실제 동작을 자식에 맞게 바꾸는 것

이라고 이해했다.


4) Overriding

TV 실습에서 인터페이스와 오버라이딩을 같이 확인했다.

TV.java

package features.oop.tv;

public interface TV {

    public void turnOn();
}

인터페이스에서는:

public void turnOn();

처럼 어떤 기능이 있어야 하는지만 정의한다.

실제 동작 내용은 없다.


SamsungTV.java

package features.oop.tv;

public class SamsungTV implements TV {

    public SamsungTV() {
    }

    public void powerOn() {

        System.out.println(
            "samsung tv poweron"
        );
    }

    @Override
    public void turnOn() {

        throw new UnsupportedOperationException(
            "Unimplemented method 'turnOn'"
        );
    }

    public static TV getInstance() {

        throw new UnsupportedOperationException(
            "Unimplemented method 'getInstance'"
        );
    }
}

여기서:

implements TV

SamsungTVTV 인터페이스에서 정한 규칙을 구현한다는 의미다.


extendsimplements

오늘 둘의 차이도 다시 정리했다.

extends
→ 클래스가 클래스를 상속

implements
→ 클래스가 인터페이스를 구현

조금 더 쉽게 이해하면:

extends

부모가 이미 가지고 있는
필드와 기능을 물려받음
implements

"이 기능은 반드시 있어야 한다"
라는 규칙을 전달받고

실제 내용은 구현 클래스가 작성

인터페이스를 구현하면 왜 메서드를 만들어야 할까?

인터페이스가:

public interface TV {

    public void turnOn();
}

이라고 정의했다면 TV를 구현하는 클래스는:

turnOn()

을 구현해야 한다.

즉:

TV라고 부르려면 최소한 turnOn()이라는 기능은 반드시 가지고 있어야 한다.

라는 하나의 규칙을 만든 것이다.


5) 추상화

추상화에는 오늘 크게 두 가지를 배웠다.

추상 클래스
인터페이스

둘 다 직접 객체를 만드는 것이 목적이 아니라 자식 클래스에 공통 구조나 규칙을 제공하기 위한 것이다.


추상 클래스

추상 클래스는 abstract를 사용한다.

public abstract class Animal {
}

추상 클래스는 직접 인스턴스를 만들 수 없다.

Animal animal = new Animal();

위 코드는 사용할 수 없다.

하지만 타입으로는 사용할 수 있다.

Animal animal = new SuperMan();

추상 메서드

일반 메서드는 구현부가 있다.

public void a() {

    // 구현부
}

추상 메서드는 구현부가 없다.

public abstract void a();

추상 메서드를 하나라도 가진 클래스는 추상 클래스가 되어야 한다.


추상 클래스 실습

package features.oop.abstraction;

public class SuperMan extends Animal {

    @Override
    public void fly() {

        throw new UnsupportedOperationException(
            "Unimplemented method 'fly'"
        );
    }

    @Override
    public void takeOff() {

        throw new UnsupportedOperationException(
            "Unimplemented method 'takeOff'"
        );
    }

    @Override
    public void landing() {

        throw new UnsupportedOperationException(
            "Unimplemented method 'landing'"
        );
    }
}

SuperManAnimal의 추상 메서드를 실제로 구현해야 한다.


AbstractApp.java

import features.oop.abstraction.Animal;
import features.oop.abstraction.SuperMan;

public class AbstractApp {

    public static void main(String[] args) {

        // 추상 클래스는 직접 객체 생성 불가
        // Animal animal = new Animal();

        // 추상 클래스도 타입으로는 사용 가능
        // Animal animal = new SuperMan();

        // Animal[] ary = new Animal[10];
    }
}

여기서 기억할 부분은:

추상 클래스는 객체 생성은 못하지만 변수 타입이나 배열 타입으로는 사용할 수 있다.


6) Interface

인터페이스는 하나의 설계 표준이라고 배웠다.

public interface TV {

    public void turnOn();
}

인터페이스는 직접 인스턴스를 생성할 수 없다.

그리고 오늘 배운 기본 구성요소는:

추상 메서드
상수

였다.

이를 통해 실제 구현 클래스들이 같은 이름과 구조의 기능을 가지도록 만들 수 있다.


7) 기본 생성자는 항상 작성해야 할까?

아니다.

예를 들어:

public class SamsungTV {
}

처럼 생성자를 하나도 만들지 않았다면 Java 컴파일러가 매개변수가 없는 기본 생성자를 만들어준다.

그래서:

new SamsungTV();

를 사용할 수 있다.

하지만 직접 생성자를 하나라도 만들면 이야기가 달라진다.

public SamsungTV(String model) {

    this.model = model;
}

이제 Java는 기본 생성자를 자동으로 만들어주지 않는다.

따라서:

new SamsungTV();

도 사용해야 한다면 직접:

public SamsungTV() {
}

를 추가해야 한다.

정리하면:

생성자를 하나도 작성하지 않음
→ 기본 생성자 자동 생성

생성자를 하나라도 직접 작성
→ 기본 생성자 자동 생성 X

8) 캡슐화

캡슐화는 객체 내부의 데이터를 외부에서 마음대로 수정하지 못하게 하고 정해진 방법을 통해 접근하도록 만드는 것이다.

private String name;

처럼 필드를 private으로 숨긴다.

외부에서 값을 확인하거나 변경해야 한다면:

getName()
setName()

같은 메서드를 통해 접근한다.

외부 코드
↓
Getter / Setter
↓
private 필드

이렇게 하면 값이 어디서 어떻게 변경되는지 관리하기 쉬워진다.

또 내부 구현이 바뀌더라도 외부 코드에 미치는 영향을 줄일 수 있다.


9) Singleton

싱글톤은 프로그램 전체에서 객체를 하나만 만들어서 계속 재사용하는 방식이다.

public class Singleton {

    private static Singleton instance
        = new Singleton();

    private Singleton() {
    }

    public static Singleton getInstance() {

        return instance;
    }
}

핵심 구조를 보면:

1. 객체를 하나만 만들어둔다.

private static Singleton instance
    = new Singleton();

2. 생성자를 private으로 만든다.

private Singleton() {
}

외부에서는:

new Singleton();

을 사용할 수 없게 된다.

3. 객체를 꺼낼 수 있는 메서드를 제공한다.

public static Singleton getInstance() {

    return instance;
}

그래서 여러 곳에서:

Singleton.getInstance();

를 호출해도 같은 객체를 사용할 수 있다.


10) Enum

enum정해진 상수의 집합을 표현하기 위한 특수한 타입이다.

오늘 작성한 코드는 다음과 같다.

package features.oop.util;

public enum Flag {

    STUDENT(1),
    TEACHER(2),
    MANAGER(3);

    private final int flag;

    private Flag(int flag){

        this.flag = flag;
    }

    public int getFlag(){

        return this.flag;
    }
}

STUDENT, TEACHER, MANAGER처럼 허용되는 값을 정해놓을 수 있다.

각 상수에:

STUDENT(1)
TEACHER(2)
MANAGER(3)

처럼 값을 연결할 수도 있다.

그리고 연결된 값은:

getFlag()

를 통해 가져온다.

이런 데이터는 정해진 값으로 사용되는 경우가 많기 때문에 Setter를 두기보다 읽기 전용으로 사용하는 경우가 많다고 배웠다.

아직 enum은 수업 시간에 충분히 따라가지 못해서 추가로 다시 정리할 예정이다.


3. 실습 중 확인한 부분

@SuperBuilder

상속 구조에서 Lombok의 @SuperBuilder를 사용해봤다.

처음에는 자식 클래스인:

TeacherDTO
ManagerDTO
StudentDTO

에만 @SuperBuilder를 붙였다.

하지만 이 경우 정상적으로 사용할 수 없었다.

상속 관계에서 @SuperBuilder를 사용하려면 부모 클래스도 같이 @SuperBuilder 구조에 참여해야 했다.

예를 들어:

@SuperBuilder
public class PersonDTO {
}

그리고:

@SuperBuilder
public class TeacherDTO
    extends PersonDTO {
}

처럼 부모와 자식 모두 적용하는 구조가 필요했다.

내 코드에서는 부모인 PersonDTO에는 적용하지 않고 계속 자식 클래스만 수정하고 있어서 문제의 원인을 자식 쪽에서만 찾고 있었다.

이번 실습을 통해 상속 구조에서는 자식 클래스 코드만 보는 것이 아니라 부모 클래스까지 같이 확인해야 한다는 점도 알게 됐다.


4. 개념 요약

오늘 배운 내용을 크게 묶으면 다음과 같다.

상속

부모의 필드와 기능을 자식이 물려받는다.

Teacher is a Person
Student is a Person

업캐스팅

PersonDTO person
    = new TeacherDTO();

자식 객체를 부모 타입으로 다룬다.

여러 자식 객체를 하나의 공통 타입으로 관리할 수 있다.


다운캐스팅

((TeacherDTO)person).getSubject();

부모 타입으로 보고 있던 객체를 다시 자식 타입으로 본다.

자식만의 기능을 사용할 때 필요하다.


instanceof

person instanceof TeacherDTO

실제 객체가 어떤 타입인지 확인한다.


Overriding

@Override
public void turnOn() {
}

부모나 인터페이스에서 정의된 메서드를 자식이 자신의 방식으로 다시 구현한다.


extends

class Child extends Parent

클래스가 클래스를 상속받는다.


implements

class SamsungTV implements TV

클래스가 인터페이스의 규칙을 구현한다.


추상 클래스

abstract class Animal

직접 객체를 만들 수 없으며 자식 클래스에 공통 구조를 제공한다.


인터페이스

interface TV

구현 클래스들이 따라야 할 기능의 형태를 정의한다.


캡슐화

private String name;

객체의 데이터를 숨기고 정해진 메서드를 통해 접근하게 한다.


Singleton

Singleton.getInstance();

프로그램에서 하나의 객체만 만들어 공유한다.


Enum

enum Flag {
    STUDENT,
    TEACHER,
    MANAGER
}

사용할 수 있는 값을 정해진 상수 집합으로 관리한다.


5. 회고

오늘은 개발자 기술 면접에서도 자주 등장할 것 같은 OOP의 핵심 개념을 한꺼번에 다뤘다.

상속, 다형성, 오버라이딩, 추상화처럼 객체지향 언어를 사용한다면 한 번은 제대로 이해하고 넘어가야 할 개념들인데 아직은 조금 어렵게 느껴진다.

특히 처음에는:

상속
다형성
오버라이딩
추상 클래스
인터페이스

가 각각 별개의 개념처럼 느껴졌다.

그래도 오늘 코드를 다시 정리하면서 조금씩 관계가 보이기 시작했다.

상속
↓
부모와 자식 관계 생성
↓
업캐스팅으로 부모 타입에서 자식 객체 관리
↓
오버라이딩으로 실제 자식마다 다른 동작 실행
↓
추상 클래스 / 인터페이스로 공통 구조와 규칙 정의

결국 객체지향은 클래스를 많이 만드는 것이 목적이라기보다 공통된 것은 묶고 다른 부분은 각 객체에게 맡기는 방식이라는 생각이 들었다.

아직은 내가 직접 설명하라고 하면 막히는 부분이 많다.

다시 한 번 정리하면서 단순히 용어를 외우는 수준이 아니라:

"왜 이 기능이 필요한가?"

를 설명할 수 있는 수준까지 익혀야겠다.


6. 다음 해야 할 일

오늘 수업에서 enum 부분을 충분히 따라가지 못했다.

그래서 다음 TIL에서는 enum을 따로 보충해서 정리해보려고 한다.

특히 아래 내용을 확인할 예정이다.

  • enum을 사용하는 이유
  • 일반 상수와 enum의 차이
  • enum에 값을 넣는 방법
  • enum 생성자의 역할
  • values()를 이용한 반복
  • switch에서 enum 사용하기

그리고 OOP 개념도 다시 복습하면서 아래 질문에 직접 답할 수 있는지 확인해보고 싶다.

  • 왜 상속을 사용하는가?
  • 왜 부모 타입으로 자식 객체를 받는가?
  • 업캐스팅과 다운캐스팅은 왜 필요한가?
  • 왜 오버라이딩을 사용하는가?
  • 추상 클래스와 인터페이스는 왜 필요한가?
  • extendsimplements의 차이는 무엇인가?

오늘 배운 내용은 단순히 코드를 작성할 줄 아는 것보다 말로 설명할 수 있어야 제대로 이해한 내용에 가까운 것 같다.

0개의 댓글