상세 컨텐츠

본문 제목

객체지향 언어란? 클래스와 객체 개념부터 자바 예제까지 쉽게 정리

Programming/JAVA

by 추천캐릭터 2022. 10. 27. 20:31

본문

728x90

프로그래밍을 공부하다 보면 반드시 만나게 되는 개념이 있습니다. 바로 객체지향 프로그래밍입니다.

특히 Java를 공부한다면 객체지향을 피할 수 없습니다. Java는 대표적인 객체지향 언어이며, 클래스, 객체, 상속, 캡슐화, 다형성 같은 개념을 기반으로 프로그램을 구성합니다.

처음에는 용어가 어렵게 느껴질 수 있습니다.
“클래스와 객체는 뭐가 다를까?”
“인스턴스는 또 무엇일까?”
“상속과 다형성은 왜 필요한 걸까?”

이번 글에서는 객체지향 언어의 기본 개념을 Java 예제와 함께 쉽게 정리해 보겠습니다.

객체지향 언어란?

객체지향 언어(Object-Oriented Language)는 프로그램을 객체 중심으로 구성하는 프로그래밍 언어입니다.

여기서 객체란 현실 세계에 존재하는 사물이나 개념을 프로그램 안에 표현한 것입니다. 예를 들어 자동차, 사람, 회원, 게시글, 주문, 상품 같은 것들이 객체가 될 수 있습니다.

객체는 보통 두 가지 요소를 가집니다.

구성 요소설명

속성 객체가 가지고 있는 데이터
기능 객체가 수행할 수 있는 동작

예를 들어 자동차 객체를 생각해 보겠습니다.

자동차의 속성은 색상, 모델명, 속도, 연료량 등이 될 수 있습니다.
자동차의 기능은 주행하기, 멈추기, 속도 올리기, 속도 줄이기 등이 될 수 있습니다.

프로그래밍에서는 이런 현실 세계의 대상을 코드로 표현합니다.

public class Car {
    String color;
    int speed;

    void drive() {
        System.out.println("자동차가 주행합니다.");
    }

    void stop() {
        System.out.println("자동차가 멈춥니다.");
    }
}

이처럼 객체지향은 데이터를 따로 두고 함수만 호출하는 방식이 아니라, 데이터와 동작을 하나의 객체로 묶어서 프로그램을 구성하는 방식입니다.

클래스란?

객체를 만들기 위해서는 먼저 설계도가 필요합니다.
이 설계도를 프로그래밍에서는 클래스(Class)라고 합니다.

클래스는 객체가 가져야 할 속성과 기능을 정의합니다.

public class Member {
    String name;
    int age;

    void introduce() {
        System.out.println("안녕하세요. 제 이름은 " + name + "입니다.");
    }
}

위 코드는 Member라는 클래스입니다.

이 클래스에는 다음 내용이 정의되어 있습니다.

  • 회원의 이름을 저장하는 name
  • 회원의 나이를 저장하는 age
  • 자기소개를 하는 introduce() 메서드

하지만 클래스 자체는 실제 데이터가 아닙니다.
설계도만 있다고 해서 자동차가 실제로 존재하는 것은 아닌 것처럼, 클래스만 작성했다고 해서 객체가 메모리에 생성되는 것은 아닙니다.

객체란?

객체(Object)는 클래스를 바탕으로 만들어진 실제 사용 대상입니다.

예를 들어 Member 클래스를 이용해 실제 회원 객체를 만들 수 있습니다.

public class Main {
    public static void main(String[] args) {
        Member member = new Member();

        member.name = "Kim";
        member.age = 25;

        member.introduce();
    }
}

여기서 new Member()를 통해 실제 객체가 생성됩니다.
그리고 member라는 변수를 통해 그 객체를 사용할 수 있습니다.

정리하면 다음과 같습니다.

용어의미

클래스 객체를 만들기 위한 설계도
객체 클래스를 기반으로 만들어진 실제 대상
인스턴스 메모리에 생성된 객체

객체와 인스턴스의 차이

객체와 인스턴스는 비슷한 의미로 사용됩니다.
하지만 관점에 따라 약간의 차이가 있습니다.

예를 들어 다음 코드를 보겠습니다.

Member member = new Member();

new Member()로 생성된 것은 객체입니다.
동시에 Member 클래스의 인스턴스라고도 말할 수 있습니다.

즉, 객체는 넓은 의미이고, 인스턴스는 “어떤 클래스로부터 만들어졌는가”를 강조할 때 사용하는 표현입니다.

예를 들어 다음과 같이 말할 수 있습니다.

  • member는 객체입니다.
  • member는 Member 클래스의 인스턴스입니다.

실무에서는 객체와 인스턴스를 거의 비슷하게 사용하지만, 개념을 정확히 이해하려면 이 차이를 알고 있는 것이 좋습니다.

객체지향의 핵심 특징

객체지향 프로그래밍에는 대표적인 특징이 있습니다.

보통 다음 네 가지를 많이 이야기합니다.

  1. 캡슐화
  2. 상속
  3. 다형성
  4. 추상화

각 개념을 하나씩 살펴보겠습니다.

1. 캡슐화

캡슐화(Encapsulation)는 객체의 데이터와 기능을 하나로 묶고, 외부에서 내부 데이터를 함부로 접근하지 못하게 보호하는 개념입니다.

Java에서는 접근 제어자를 사용해 캡슐화를 구현합니다.

대표적인 접근 제어자는 다음과 같습니다.

접근 제어자설명

private 같은 클래스 안에서만 접근 가능
default 같은 패키지 안에서 접근 가능
protected 같은 패키지 또는 자식 클래스에서 접근 가능
public 어디서든 접근 가능

예를 들어 회원의 나이는 음수가 될 수 없습니다.
그런데 필드를 public으로 열어두면 외부에서 잘못된 값을 직접 넣을 수 있습니다.

public class Member {
    public int age;
}

이 경우 다음과 같은 코드가 가능해집니다.

Member member = new Member();
member.age = -10;

나이가 -10이 되는 것은 논리적으로 잘못된 상태입니다.

이 문제를 막기 위해 필드는 private으로 숨기고, 메서드를 통해 값을 변경하게 만들 수 있습니다.

public class Member {
    private int age;

    public void setAge(int age) {
        if (age < 0) {
            throw new IllegalArgumentException("나이는 음수가 될 수 없습니다.");
        }
        this.age = age;
    }

    public int getAge() {
        return age;
    }
}

이렇게 하면 객체 내부 데이터를 보호하면서도 필요한 기능은 외부에 제공할 수 있습니다.

캡슐화의 핵심은 단순히 필드에 private을 붙이는 것이 아닙니다.
객체가 잘못된 상태가 되지 않도록 데이터 변경 규칙을 객체 내부에 숨기고 관리하는 것입니다.

2. 상속

상속(Inheritance)은 기존 클래스의 속성과 기능을 다른 클래스가 물려받는 개념입니다.

공통 기능을 부모 클래스에 정의하고, 자식 클래스가 이를 재사용할 수 있습니다.

예를 들어 동물이라는 공통 개념을 만들 수 있습니다.

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

그리고 강아지 클래스가 Animal을 상속받을 수 있습니다.

public class Dog extends Animal {
    void bark() {
        System.out.println("멍멍 짖습니다.");
    }
}

이제 Dog 객체는 bark()뿐 아니라 부모 클래스의 eat()도 사용할 수 있습니다.

public class Main {
    public static void main(String[] args) {
        Dog dog = new Dog();

        dog.eat();
        dog.bark();
    }
}

상속을 사용하면 공통 코드를 재사용할 수 있습니다.
하지만 상속을 무조건 많이 쓰는 것이 좋은 설계는 아닙니다.

부모 클래스와 자식 클래스가 강하게 연결되기 때문에, 부모 클래스의 변경이 자식 클래스에 영향을 줄 수 있습니다. 그래서 실무에서는 상속보다 조합을 선호하는 경우도 많습니다.

상속은 “코드를 줄이기 위한 도구”라기보다, is-a 관계가 명확할 때 사용하는 설계 방법으로 이해하는 것이 좋습니다.

예를 들어 다음 관계는 자연스럽습니다.

  • Dog is an Animal
  • Cat is an Animal
  • Car is a Vehicle

반대로 다음과 같은 관계는 상속으로 표현하기 어색할 수 있습니다.

  • User is a Database
  • Order is a Payment
  • Controller is a Service

상속은 관계가 명확할 때만 신중하게 사용하는 것이 좋습니다.

3. 다형성

다형성(Polymorphism)은 하나의 타입이나 메서드가 여러 형태로 동작할 수 있는 성질입니다.

객체지향에서 다형성은 매우 중요한 개념입니다.
다형성을 사용하면 코드를 더 유연하게 만들 수 있습니다.

예를 들어 Animal 타입으로 Dog와 Cat 객체를 함께 다룰 수 있습니다.

public class Animal {
    void sound() {
        System.out.println("동물이 소리를 냅니다.");
    }
}
public class Dog extends Animal {
    @Override
    void sound() {
        System.out.println("멍멍");
    }
}
public class Cat extends Animal {
    @Override
    void sound() {
        System.out.println("야옹");
    }
}

이제 다음과 같이 사용할 수 있습니다.

public class Main {
    public static void main(String[] args) {
        Animal animal1 = new Dog();
        Animal animal2 = new Cat();

        animal1.sound();
        animal2.sound();
    }
}

결과는 다음과 같습니다.

멍멍
야옹

변수 타입은 둘 다 Animal이지만, 실제 실행되는 메서드는 객체의 실제 타입에 따라 달라집니다.
이것이 다형성입니다.

오버라이딩과 오버로딩

다형성을 이해할 때 자주 나오는 개념이 오버라이딩오버로딩입니다.

두 용어는 이름이 비슷하지만 의미가 다릅니다.

오버라이딩

오버라이딩(Overriding)은 부모 클래스의 메서드를 자식 클래스에서 다시 정의하는 것입니다.

public class Parent {
    void print() {
        System.out.println("부모 클래스");
    }
}
public class Child extends Parent {
    @Override
    void print() {
        System.out.println("자식 클래스");
    }
}

오버라이딩은 상속 관계에서 사용됩니다.
메서드 이름, 매개변수, 반환 타입이 부모 메서드와 같아야 합니다.

오버로딩

오버로딩(Overloading)은 같은 이름의 메서드를 여러 개 정의하는 것입니다.
단, 매개변수의 개수나 타입이 달라야 합니다.

public class Calculator {

    int add(int a, int b) {
        return a + b;
    }

    int add(int a, int b, int c) {
        return a + b + c;
    }

    double add(double a, double b) {
        return a + b;
    }
}

오버로딩은 상속과 직접적인 관련이 없습니다.
같은 클래스 안에서도 사용할 수 있습니다.

정리하면 다음과 같습니다.

구분오버라이딩오버로딩

의미 부모 메서드를 자식이 재정의 같은 이름의 메서드를 여러 개 정의
관계 상속 관계 필요 상속 관계 필요 없음
기준 메서드 이름, 매개변수, 반환 타입 유지 매개변수 개수 또는 타입 변경
목적 동작 재정의 다양한 입력 처리

4. 추상화

추상화(Abstraction)는 복잡한 내부 구현은 숨기고, 중요한 기능만 외부에 드러내는 개념입니다.

예를 들어 자동차를 운전할 때 운전자는 엔진 내부 구조를 몰라도 됩니다.
핸들, 브레이크, 엑셀만 알면 자동차를 사용할 수 있습니다.

프로그래밍에서도 마찬가지입니다.
객체가 내부적으로 어떻게 동작하는지는 숨기고, 외부에는 필요한 기능만 제공할 수 있습니다.

Java에서는 추상 클래스와 인터페이스를 사용해 추상화를 구현합니다.

public interface Payment {
    void pay(int amount);
}
public class CardPayment implements Payment {
    @Override
    public void pay(int amount) {
        System.out.println(amount + "원을 카드로 결제합니다.");
    }
}
public class KakaoPayPayment implements Payment {
    @Override
    public void pay(int amount) {
        System.out.println(amount + "원을 카카오페이로 결제합니다.");
    }
}

결제 기능을 사용하는 쪽에서는 구체적인 결제 방식보다 Payment라는 공통 규칙에 의존할 수 있습니다.

public class OrderService {

    private final Payment payment;

    public OrderService(Payment payment) {
        this.payment = payment;
    }

    public void order(int amount) {
        payment.pay(amount);
    }
}

이 구조는 유지보수에 유리합니다.
나중에 네이버페이, 토스페이 같은 새로운 결제 방식이 추가되어도 기존 주문 로직을 크게 바꾸지 않아도 됩니다.

객체지향 프로그래밍의 장점

객체지향 프로그래밍은 다음과 같은 장점이 있습니다.

1. 코드 재사용성이 높다

공통 기능을 클래스, 상속, 인터페이스, 조합 등을 통해 재사용할 수 있습니다.
중복 코드를 줄이고, 기능을 구조적으로 관리할 수 있습니다.

2. 유지보수에 유리하다

객체는 자신의 역할과 책임을 가집니다.
코드가 역할별로 나뉘어 있으면 문제가 생겼을 때 수정 범위를 좁히기 쉽습니다.

3. 현실 세계 모델링이 쉽다

회원, 주문, 상품, 게시글처럼 현실 세계의 개념을 객체로 표현할 수 있습니다.
도메인 중심으로 프로그램을 설계하기 좋습니다.

4. 확장성이 좋다

인터페이스와 다형성을 활용하면 새로운 기능을 추가할 때 기존 코드를 덜 수정할 수 있습니다.
이는 실무 프로젝트에서 매우 중요한 장점입니다.

객체지향 프로그래밍의 단점

객체지향이 항상 좋은 것만은 아닙니다.
다음과 같은 단점도 있습니다.

1. 설계가 어렵다

객체를 어떻게 나눌지, 책임을 어디에 둘지, 어떤 관계를 만들지 결정해야 합니다.
초보자에게는 이 설계 과정이 어렵게 느껴질 수 있습니다.

2. 코드 구조가 복잡해질 수 있다

클래스가 많아지고 관계가 복잡해지면 오히려 이해하기 어려운 코드가 될 수 있습니다.
특히 상속을 과도하게 사용하면 변경에 취약한 구조가 될 수 있습니다.

3. 절차형 코드보다 장황할 수 있다

간단한 프로그램에서는 객체지향 구조가 오히려 불필요하게 길어 보일 수 있습니다.
작은 기능까지 모두 클래스로 나누면 코드가 복잡해질 수 있습니다.

대표적인 객체지향 언어

객체지향 개념을 지원하는 대표적인 언어는 다음과 같습니다.

언어특징

Java 대표적인 객체지향 언어, 플랫폼 독립성 제공
C++ C 언어를 확장한 언어, 객체지향과 절차형 모두 지원
C# .NET 생태계에서 많이 사용되는 객체지향 언어
Python 동적 타입 언어이며 객체지향을 지원
Kotlin JVM 기반 언어로 Java와 높은 호환성 제공
Ruby 객체지향 철학이 강한 동적 언어

이 중 Java는 클래스 기반 객체지향 언어의 대표적인 예입니다.
Spring Framework, Android, 서버 개발, 기업용 시스템 등 다양한 분야에서 사용됩니다.

Java에서 객체지향이 중요한 이유

Java는 모든 코드를 클래스 중심으로 작성합니다.

간단한 실행 코드도 클래스 안에 들어갑니다.

public class Main {
    public static void main(String[] args) {
        System.out.println("Hello Java");
    }
}

또한 Spring Framework를 사용하면 객체지향 개념이 더 중요해집니다.

예를 들어 Spring에서는 Service, Repository, Controller 같은 객체들이 각자 역할을 나누어 협력합니다.

Controller → Service → Repository

각 계층의 역할은 다음과 같습니다.

계층역할

Controller HTTP 요청을 받음
Service 비즈니스 로직 처리
Repository 데이터베이스 접근

이 구조도 객체지향적인 역할 분리의 한 예입니다.
각 객체가 자신의 책임을 가지고 협력하면서 하나의 애플리케이션을 구성합니다.

객체지향을 공부할 때 중요한 관점

객체지향을 처음 배울 때는 문법만 외우면 어렵습니다.

중요한 것은 다음 질문을 계속 던지는 것입니다.

  • 이 객체의 책임은 무엇인가?
  • 이 객체가 가져야 할 데이터는 무엇인가?
  • 이 객체가 제공해야 할 기능은 무엇인가?
  • 다른 객체와 어떤 관계를 가져야 하는가?
  • 내부 구현을 얼마나 숨겨야 하는가?
  • 변경에 강한 구조인가?

객체지향은 단순히 class를 쓰는 문법이 아닙니다.
역할과 책임을 기준으로 코드를 나누고, 객체들이 협력하도록 만드는 설계 방식입니다.

마무리

객체지향 언어는 프로그램을 객체 중심으로 구성하는 언어입니다.
객체는 속성과 기능을 가지고, 여러 객체가 서로 협력하면서 프로그램이 동작합니다.

핵심 개념은 다음과 같습니다.

  1. 클래스는 객체를 만들기 위한 설계도입니다.
  2. 객체는 클래스를 기반으로 생성된 실제 대상입니다.
  3. 인스턴스는 특정 클래스로부터 메모리에 생성된 객체입니다.
  4. 캡슐화는 내부 데이터를 보호하고 필요한 기능만 공개하는 개념입니다.
  5. 상속은 부모 클래스의 기능을 자식 클래스가 물려받는 개념입니다.
  6. 다형성은 하나의 타입이 여러 형태로 동작할 수 있는 성질입니다.
  7. 추상화는 복잡한 구현을 숨기고 중요한 기능만 드러내는 개념입니다.

Java를 제대로 이해하려면 객체지향 개념을 반드시 알아야 합니다.
문법을 외우는 것보다 중요한 것은 객체가 어떤 책임을 가지고, 어떻게 협력해야 하는지 이해하는 것입니다.

객체지향은 처음에는 어렵지만, 한 번 구조가 잡히면 유지보수하기 좋은 코드를 작성하는 데 큰 도움이 됩니다.

728x90

관련글 더보기