개발하는 햄팡이

[JAVA] OOP란? - 객체 지향 프로그래밍의 개념과 특징 본문

Back-End/Java

[JAVA] OOP란? - 객체 지향 프로그래밍의 개념과 특징

hampangee 2025. 4. 16. 17:09

1. 객체 지향 프로그래밍(OOP)란?

객체 지향 프로그래밍(Object-Oriented Programming, OOP)은 데이터와 그 데이터를 처리하는 코드를 하나의 객체(Object)로 묶어 설계하는 방법이다.

전통적인 프로그래밍 방법은 절차적 프로그래밍인데, 절차적 프로그래밍함수(절차)를 먼저 만들고, 데이터는 그 함수를 계산하기 위해 존재하는 형태라면, OOP데이터가 먼저 있고 그 데이터를 다루기 위한 함수(메서드)가 객체 안에 존재하는 형태이다.

 

객체 지향 프로그래밍을 설명하기 전에 알아야한 용어들은 다음과 같다.

  • 객체(Object)
    사물이나 개념을 소프트웨어로 모델링한 단위
  • 클래스(Class)
    객체를 찍어내기 위한 설계도
  • 인스턴스(Instance)
    클래스로부터 생성된 실제 객체

 

Java는 클래스와 객체를 기본 단위로 설계된 언어로 대표적인 객체 지향 언어이다.

맨 처음 IDE를 사용하여 자바 프로젝트를 만들면 main함수도 클래스 안에 있는 것을 알 수 있다. Java는 모든 코드가 클래스 내부에 작성된다.

 

객체 지향 프로그래밍엔 4가지 핵심 개념이 있다.

  • 캡슐화 (Encapsulation)
  • 상속 (Inheritance)
  • 다형성 (Polymorphism)
  • 추상화 (Abstraction)

Java로 코드를 작성할땐 위의 4가지를 꼭 지켜야하는데 해당 개념들이 무엇인지 살펴보자.


2. 객체 지향 프로그래밍의 4가지 핵심 개념

2.1. 캡슐화 ( Encapsulation )

캡슐화라는 것은 객체의 정보를 감싸 외부에서 직접 접근하지 못하게 데이터를 보호하는 개념이다.

캡슐 처럼 감싸 내부를 보호하고 외부로부터 내용물을 숨긴다는 뜻에서 캡슐화라고 한다.

 

캡슐화는 접근제어자(public, protected, private)를 사용하여 접근 법위를 제한하여 구현할 수 있다.

아래는 예시 코드이다.

public class BankAccount {
    private String accountNumber;  
    private double balance;

    public BankAccount(String accountNumber) {
        this.accountNumber = accountNumber;  // 생성 시 계좌 번호만 설정
        this.balance = 0;
    }

    public void deposit(double amount) {
        if (amount <= 0) {
            throw new IllegalArgumentException("입금액은 0보다 커야 합니다.");
        }
        balance += amount;  
    }

    public boolean withdraw(double amount) {
        if (amount <= 0) {
            throw new IllegalArgumentException("출금액은 0보다 커야 합니다.");
        }
        if (amount > balance) {
            return false;  // 잔액 부족
        }
        balance -= amount;  
        return true;
    }

    public double getBalance() {
        return balance;  // 잔액만 읽기 전용으로 제공
    }
}

 

예시를 보면 필드 값을 private으로 설정하고

메소드를 이용하여 정해져있는 로직대로 필드값에 접근할 수 있고,

내 마음대로 값을 0으로 만든다던가 그런 행동은 할 수 없다.

 

오직 열려있는 메소드인 입금하기, 출금하기, 잔액 조회하기 메소드를 이용해서만 접근할 수 있다.

우리는 메소드를 이용하여 올바른 데이터를 저장할 수 있도록 제한하는 것이다.

 

캡슐화의 장점

  • 잘못된 상태 접근 방지
  • 내부 구현 변경 시 외부 영향 최소화

 


 

2.2. 상속 ( Inheritance )

상속은 기존 클래스(부모)의 속성과 기능을 물려받아 새로운 클래스(자식)를 만드는 것을 의미한다.

extends 키워드를 이용하여 부모 클래스의 모든  public/protected 멤버를 물려 받는다.

그렇게 물려받은 자식 클래스는 

부모 메서드를 재사용하거나 오버라이드(재정의) 할 수 있다.

 

이해하기 쉽도록 코드를 먼저 보자

public class Animal {
    protected String name;

    public Animal(String name) {
        this.name = name;
    }

    public void eat() {
        System.out.println(name + "가 냠냠 먹습니다.");
    }

    public void sleep() {
        System.out.println(name + "가 잠을 잡니다.");
    }
}

public class Dog extends Animal {
    public Dog(String name) {
        super(name);  // 부모 생성자 호출
    }

    // 부모의 eat() 재사용 + 추가 행동
    @Override
    public void eat() {
        super.eat();
        System.out.println("꼬리를 흔들며 맛있게 먹습니다.");
    }

    public void bark() {
        System.out.println(name + "가 멍멍 짖습니다.");
    }
}

// 사용 예
Animal a = new Animal("동물");
a.eat();   // 동물가 냠냠 먹습니다.

Dog d = new Dog("바둑이");
d.eat();   // Animal.eat + 추가 메시지
d.bark();  // 바둑이가 멍멍 짖습니다.

 

코드를 살펴보면 Dog 클래스는 Animal 클래스를 상속하고 있다.

Dog 생성자를 보면 super(name) 코드를 이용하여 부모 생성자를 호출한다.

 

상속을 이해하려면 각각의 클래스의 특징과 계층 구조에 대해서 이해를 해야한다.

모든 Animal은 음식을 먹고 잠을 잔다는 가정하에 Animal 클래스는 eat과 sleep이라는 메소드가 포함되어있다.

 

Animal객체는 eat()호출 시 " name가 냠냠 먹습니다." 한줄만 출력되지만

Animal을 상속한 Dog객체의 eat은 override(재정의)되어

 

name가 냠냠 먹습니다.

꼬리를 흔들며 맛있게 먹습니다.

 

두 줄이 출력된다. 

이는 모든 동물은 음식을 먹지만 Dog는 추가로 꼬리를 흔들면서 먹기 때문에 메소드를 재정의한 모습이다.

 

 

부모에게 없는 bark() 메소드는 기능을 확장했다 라고 말한다.

 

상속의 장점

  • 코드 재사용성 증가
    : 부모 클래스에 이미 작성된 내용을 그대로 사용할 수 있음
  • 계층적(계층구조) 설계
    : 시스템의 도메인 모델을 현실 세계의 계층 구조처럼 설계
  • 유지보수성 향상
    : 공통 로직을 부모 클래스에 모아 두면, 기능 변경 시 부모 클래스만 수정해도 자식 클래스 전체에 일괄 적용
  • 확장성(유연성)
    : 부모 클래스에 정의된 기능을 바로 물려받아 최소한의 코드만 작성
  • 표준화된 인터페이스
    : 부모 클래스 또는 인터페이스에 공통 메서드(계약)를 정의해 두면, 자식 클래스들은 이 계약을 따르게 되어 일관된 API를 제공

**상속은 "is-a" 관계가 아니라면 공통된 코드가 있다고 해도 적용하지 않는다.

 


 

2.3. 추상화 ( Abstraction )

추상화추상클래스나 인터페이스를 이용하여 '무엇을 할 것인가?'만 선언하고 구체적인 구현은 서브클래스에 위임하는 것을 말한다.

추상화를 할 때엔 핵심적인 속성과 행위만 뽑아내어 설계한다.

세부 구현을 감추고 사용자(or 다른 개발자)에게 무엇을 할 수 있는지에 대해서만 명확히 한다.

이를 통해 코드 이해의 복잡도를 줄이고 관심사를 분리할 수 있다.

 

추상화를 하는 방법엔 추상클래스를 사용하는 방법과 인터페이스를 사용하는 방법이 있다.

각각 용도에 따라서 사용한다.

 

  • 추상 클래스(Abstract Class)
    • 일부 구현(Concrete Method)과 추상 메서드(Abstract Method)를 모두 가질 수 있음
    • 공통 필드와 메서드를 정의하고, 세부 행위는 서브클래스가 강제 구현
  • 인터페이스(Interface)
    • Java 8 이전에는 오직 추상 메서드만 선언 가능
    • Java 8 이후 default 메서드(구현 포함)와 static 메서드도 정의 가능
    • 다중 구현을 통해 다양한 타입 계층 구성

 

 

 

추상 클래스 vs 인터페이스

  • 추상 클래스: 상태(필드) 공유 + 일부 구현 가능 
    “무엇을 할 수 있고, 어떻게 일부는 구현되어 있다”
  • 인터페이스: 구현 없이 ‘행위’만 선언 (Java 8 이후 default 메서드 제공)
    “무엇을 할 수 있는지”
  • "is-a"관계에서 공통 상태(필드)와 기본 구현이 필요할 땐 추상클래스, 순수 추상화만 필요하거나 다중 구현이 필요할 땐 인터페이스
    ** 다중 구현 : 상속은 단 하나만 상속할 수 있다(부모가 여럿이면 안됨). but, 클래스가 여러 역할을 수행해야 될 때가 있는데 그땐 implements를 사용하여 구현한다.

 

추상화의 장점

  • 복잡도 감소
    : 불필요한 세부 구현을 숨김으로써, 상위 설계에서는 핵심 로직만 이해
  • 유연한 확장
  • 계약 기반 설계(Contract‑First)
    : “어떤 기능을 제공할 것인가”를 먼저 정의하고, 세부 구현을 따르게 함으로써 모듈 간 결합도↓
  • 테스트 용이
    : 추상 타입을 목(Mock) 객체로 대체해 단위 테스트를 쉽게 작성

 


 

2.4. 다형성 ( Polymorphism )

“같은 인터페이스(메서드 호출)가 다양한 객체에서 서로 다른 방식으로 동작하는 능력”

 

다형성의 종류

  1. 컴파일 시 다형성(Compile‑time Polymorphism)
    • 메서드 오버로딩(Method Overloading)
    • 연산자 오버로딩(Operator Overloading) (Java는 지원 안 함)
  2. 실행 시 다형성(Runtime Polymorphism)
    • 메서드 오버라이딩(Method Overriding)
    • 업캐스팅(Up‑casting) + 동적 바인딩(Dynamic Binding)

 

2.4.1. 메서드 오버로딩 (Method Overloading)

같은 이름의 메서드를 매개변수 시그니처(타입·개수·순서)에 따라 여러 개 정의하는 것

다양한 입력 타입을 하나의 메서드로 처리하는 방법이다.

 

예시 코드를 보면 매개변수로 int형이 들어왔을 때, double이 들어왔을 때, 값이 세개가 들어왔을 때 모두 add 메소드이지만 구현은 다르다.

의미가 동일한 기능을 여러 형태에 맞추어 정의한 것이다.

 

**메서드 오버로딩을 할 때엔 매개변수 목록이 반드시 달라야 한다.

public class Calculator {
    public int add(int a, int b) {
        return a + b;
    }
    public double add(double a, double b) {
        return a + b;
    }
    public int add(int a, int b, int c) {
        return a + b + c;
    }
}

// 호출 예
Calculator calc = new Calculator();
calc.add(1, 2);        // add(int, int)
calc.add(1.5, 2.3);    // add(double, double)
calc.add(1, 2, 3);     // add(int, int, int)

 

 

2.4.2. 메서드 오버라이딩 (Method Overriding)

자식 클래스가 부모 클래스(또는 인터페이스)에 정의된 메서드를 같은 시그니처로 재정의하는 것

 

public class Animal {
    public void sound() {
        System.out.println("어떤 소리를 내는지 모른다.");
    }
}

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

// 사용 예
Animal a = new Dog();  
a.sound();  // Dog.sound() 실행 → “멍멍”

 

2.4.3 업캐스팅(Up‑casting) + 동적 바인딩(Dynamic Binding)

 

  • 업캐스팅(Up‑casting)
    자식 클래스 객체를 부모 클래스(또는 인터페이스) 타입으로 참조하는 것
Dog dog = new Dog();
Animal a = dog;  // 업캐스팅

 

  • 동적 바인딩(Dynamic Binding)
    메서드 호출 시점에 JVM이 실제 객체 타입을 확인해 오버라이드된 메서드를 실행하는 것
a.sound();  // 컴파일 시 a는 Animal, 런타임에 Dog.sound() 호출

 

 

다형성의 장점

  • 유연성: 클라이언트 코드는 상위 타입만 알면 되므로, 구현체 교체·추가가 자유롭다.
  • 확장성: 새로운 서브클래스를 추가할 때 기존 코드를 전혀 수정하지 않아도 된다.
  • 유지보수성: 공통 인터페이스를 통해 일관된 호출 구조를 유지하므로, 버그 수정·기능 추가 시 영향 범위를 최소화할 수 있다.

 

언제 다형성을 사용할까?

  • 플러그인 구조: 기능별 모듈을 인터페이스로 정의하고, 런타임에 구현체를 주입
  • 전략 패턴: 알고리즘(전략)을 인터페이스로 추상화해 실행 시점에 선택
  • 팩토리 패턴: 객체 생성 로직을 캡슐화하고, 다형성으로 구체 클래스를 결정

다형성은 사실 실제로 코드를 작성하고 기능을 구현해 봐야 이해가 되는 부분이다.

 

다른 개념들은 예시 코드를 보면 이해가 되지만

다형성은 이론만 보면 정말 장점이 뭔지도 모르겠고 업캐스팅이랑 다운캐스팅을 왜 하는지도 이해가 잘 가지 않는 부분이다.

그나마 메서드 오버로딩은 이해가 되지만

메서드 오버라이딩과 업캐스팅과 다운캐스팅이 중요한 이유는 다들 이해가 잘 안될 것이라고 생각한다.

 

Spring을 배우고 코드를 구현한다면 다들 다형성에 대해서도 잘 이해할 수 있을 것이라고 생각한다.

 

따라서 다음 시간에는 내가 공부할때

인터페이스의 활용과 다형성에 대해서 가장 이해하기 쉬웠던 SSO로그인 구현 예시를 이용하여

설명하는 글을 써봐야겠다.