본문 바로가기
자격증·공부 아카이브/Java 기초

Java 메서드와 객체지향: 클래스·생성자·상속 한 번에 정리

by 쑥쑥자라나라 2026. 7. 16.
728x90

Java를 처음 배울 때 메서드와 객체지향을 따로 외우면 둘의 연결이 잘 보이지 않습니다. 메서드는 클래스 안에 들어가는 단순한 코드 묶음이 아니라, 객체가 맡은 일을 수행하는 동작입니다. 클래스가 설계도를 만들고 생성자가 객체를 준비하면, 메서드가 그 객체를 실제로 움직입니다.

카페 주문 프로그램 하나를 끝까지 따라가며 메서드, 클래스, 객체, 생성자, 캡슐화, 상속, 추상화, 다형성이 어떤 순서로 이어지는지 살펴보겠습니다.

카페 주문서 양식은 클래스, 손님별 주문은 객체, 계산과 제조는 메서드임을 보여주는 일러스트

주문서 양식(클래스)으로 손님별 주문(객체)을 만들고 계산·제조 동작(메서드)을 실행하는 관계

1. 클래스·객체·메서드는 어떻게 연결될까?

카페에서 사용하는 주문서 양식을 클래스라고 생각하면 쉽습니다. 양식에는 음료 이름과 수량을 적을 칸이 있고, 금액을 계산하거나 영수증을 출력하는 규칙도 들어 있습니다.

손님이 주문할 때마다 양식을 바탕으로 실제 주문서가 만들어집니다. 손님 A의 아메리카노 2잔 주문과 손님 B의 라테 1잔 주문은 서로 다른 값을 가진 객체입니다. 각 주문 객체가 금액을 계산하고 영수증을 출력할 때 사용하는 동작이 메서드입니다.

클래스 = 데이터(필드) + 동작(메서드)의 설계도
객체 = 클래스를 바탕으로 메모리에 만들어진 실제 값
생성자 = 객체의 첫 상태를 설정하는 특별한 코드
메서드 = 객체가 수행할 수 있는 동작

메서드는 반복 코드를 줄이는 데도 유용하지만, 객체지향에서는 더 중요한 역할을 합니다. 객체 내부 데이터를 아무 코드에서나 직접 바꾸지 못하게 하고, 정해진 메서드를 통해서만 안전하게 다루도록 경계를 만듭니다.

2. 메서드 구조와 접근제어자

메서드 선언은 보통 다음 순서로 읽습니다.

public int calculatePrice(int unitPrice, int quantity) {
    return unitPrice * quantity;
}
구성예시의미
접근제어자public어디에서 메서드를 호출할 수 있는지 정합니다.
반환 타입int실행 후 돌려줄 값의 자료형입니다. 값이 없으면 void를 씁니다.
메서드 이름calculatePrice동작이 드러나는 이름을 사용합니다.
매개변수int unitPrice, int quantity메서드가 작업에 사용할 입력값입니다.
메서드 본문{ ... }실제로 실행할 명령문입니다.

private과 public은 반대가 아니다

기존 글에는 privatepublic 설명이 뒤바뀌어 있었습니다. 정확한 범위는 아래와 같습니다.

접근제어자같은 클래스같은 패키지다른 패키지의 자식 클래스전체
private가능불가불가불가
없음(package-private)가능가능불가불가
protected가능가능상속을 통해 가능불가
public가능가능가능가능
접근 범위를 외우는 기준
  • private은 현재 클래스 내부에서만 접근할 수 있습니다.
  • public은 어디에서나 접근할 수 있습니다.
  • protected는 같은 패키지에서는 상속 여부와 무관하게 접근할 수 있고, 다른 패키지에서는 자식 클래스가 상속 관계를 통해 접근할 수 있습니다.

3. 생성자부터 메서드 호출까지 실제 흐름

이제 주문 객체를 직접 만들어 보겠습니다. 필드는 외부에서 마음대로 바꾸지 못하도록 private으로 숨기고, 생성자와 공개 메서드를 통해서만 값을 다룹니다.

class CafeOrder {
    private String menu;
    private int quantity;

    public CafeOrder(String menu, int quantity) {
        if (quantity <= 0) {
            throw new IllegalArgumentException("수량은 1개 이상이어야 합니다.");
        }
        this.menu = menu;
        this.quantity = quantity;
    }

    public int calculatePrice() {
        return getUnitPrice() * quantity;
    }

    public void printReceipt() {
        System.out.println(menu + " " + quantity + "잔");
        System.out.println("결제 금액: " + calculatePrice() + "원");
    }

    private int getUnitPrice() {
        if (menu.equals("아메리카노")) {
            return 4_500;
        }
        if (menu.equals("라테")) {
            return 5_000;
        }
        throw new IllegalArgumentException("등록되지 않은 메뉴입니다.");
    }
}

public class CafeApp {
    public static void main(String[] args) {
        CafeOrder order = new CafeOrder("아메리카노", 2);
        order.printReceipt();
    }
}
아메리카노 2잔
결제 금액: 9000원
  1. new CafeOrder(...)가 객체를 만들 메모리 공간을 확보합니다.
  2. 생성자가 메뉴와 수량을 검사하고 필드의 첫 값을 설정합니다.
  3. 완성된 객체의 참조값이 order 변수에 저장됩니다.
  4. order.printReceipt()가 객체의 메서드를 호출합니다.
  5. printReceipt() 내부에서 calculatePrice()와 비공개 메서드 getUnitPrice()가 차례로 실행됩니다.

생성자는 메서드와 비슷해 보이지만 반환 타입이 없고 이름이 클래스와 같습니다. 객체를 만들 때 자동으로 호출된다는 점도 일반 메서드와 다릅니다.

4. 캡슐화·상속·추상화·다형성을 하나의 코드로 보기

객체지향의 네 특징은 서로 떨어진 암기 항목이 아닙니다. 주문 프로그램에 음료 제조 기능을 추가하면 한 흐름에서 모두 확인할 수 있습니다.

abstract class Beverage {
    private final String name;

    protected Beverage(String name) {
        this.name = name;
    }

    public final void serve() {
        make();
        System.out.println(name + " 준비 완료");
    }

    protected abstract void make();
}

class Americano extends Beverage {
    public Americano() {
        super("아메리카노");
    }

    @Override
    protected void make() {
        System.out.println("에스프레소에 물을 넣습니다.");
    }
}

class Latte extends Beverage {
    public Latte() {
        super("라테");
    }

    @Override
    protected void make() {
        System.out.println("에스프레소에 우유를 넣습니다.");
    }
}

public class CafeMachine {
    public static void prepare(Beverage beverage) {
        beverage.serve();
    }

    public static void main(String[] args) {
        Beverage first = new Americano();
        Beverage second = new Latte();

        prepare(first);
        prepare(second);
    }
}

캡슐화: 데이터와 동작을 묶고 내부를 보호한다

name 필드는 private이므로 외부에서 직접 변경할 수 없습니다. 외부에는 serve()라는 필요한 동작만 공개합니다. 필드와 메서드를 한 클래스에 묶는 것뿐 아니라, 객체의 유효한 상태를 지키는 것이 캡슐화의 핵심입니다.

상속: 공통 코드를 물려받고 차이를 확장한다

AmericanoLatteBeverageextends합니다. 이름 저장과 제공 순서는 부모 클래스가 담당하고, 음료마다 다른 제조 방법만 자식 클래스가 작성합니다. 상속은 단순 복사가 아니라 공통 규칙을 재사용하면서 차이를 확장하는 관계입니다.

추상화: 중요한 규칙만 드러낸다

Beverage는 모든 음료가 make() 동작을 가져야 한다는 규칙만 선언합니다. 실제 제조법은 자식 클래스에 맡깁니다. 복잡한 세부 구현보다 공통 역할과 필요한 인터페이스를 먼저 드러내는 것이 추상화입니다.

다형성: 같은 호출이 객체에 따라 다르게 동작한다

prepare(Beverage beverage)는 구체적인 음료 종류를 몰라도 됩니다. 전달된 객체가 아메리카노면 물을 넣고, 라테면 우유를 넣습니다. 변수와 매개변수의 타입은 부모인 Beverage지만 실행되는 메서드는 실제 객체가 재정의한 버전입니다.

에스프레소에 물을 넣습니다.
아메리카노 준비 완료
에스프레소에 우유를 넣습니다.
라테 준비 완료

5. 오버로딩과 오버라이딩 차이

이름이 비슷해 자주 헷갈리지만 적용 시점과 조건이 다릅니다.

구분오버로딩오버라이딩
의미같은 이름의 메서드를 여러 형태로 선언상속받은 메서드를 자식 클래스에서 재정의
조건매개변수 개수·타입·순서 중 하나가 달라야 함메서드 이름과 매개변수 목록이 같아야 함
결정 시점호출에 맞는 선언을 컴파일 시점에 선택실제 객체의 메서드를 실행 시점에 선택
대표 목적비슷한 동작에 일관된 이름 제공상속 구조에서 객체별 동작 구현
// 오버로딩: 매개변수가 다르다.
public int calculatePrice(int quantity) {
    return 4_500 * quantity;
}

public int calculatePrice(int quantity, int discount) {
    return 4_500 * quantity - discount;
}

// 반환 타입만 바꾸는 것은 오버로딩이 아니다.
// public double calculatePrice(int quantity) { ... }  // 컴파일 오류

6. 실수하기 쉬운 부분

  • privatepublic을 반대로 이해하지 않기: private은 현재 클래스만, public은 전체 접근입니다.
  • 생성자에 반환 타입을 쓰지 않기: void CafeOrder()는 생성자가 아니라 일반 메서드입니다.
  • 반환 타입만 바꿔 오버로딩하지 않기: 매개변수 목록이 달라야 합니다.
  • 필드를 무조건 public으로 열지 않기: 잘못된 값이 들어가지 않도록 생성자와 메서드에서 검사합니다.
  • 상속을 코드 재사용만 보고 선택하지 않기: 자식이 부모의 한 종류인 is-a 관계일 때 적합합니다.
  • @Override 붙이기: 철자나 매개변수 실수로 재정의에 실패하면 컴파일러가 바로 알려줍니다.
  • 인스턴스 메서드와 static 메서드 구분하기: 객체의 필드를 사용한다면 보통 인스턴스 메서드가 자연스럽습니다.

7. 전체 흐름 요약

코드를 읽을 때 이 순서만 기억하면 됩니다.
  • 클래스가 객체의 데이터와 동작을 설계합니다.
  • new와 생성자가 객체를 만들고 첫 상태를 설정합니다.
  • 객체는 공개된 메서드를 통해 동작합니다.
  • 캡슐화가 내부 상태를 보호합니다.
  • 추상화가 공통 규칙을 뽑고, 상속이 그 규칙을 확장합니다.
  • 다형성이 같은 메서드 호출을 실제 객체에 맞게 실행합니다.

메서드는 객체지향과 별개의 문법이 아닙니다. 클래스가 설계한 역할을 객체가 수행하도록 만드는 핵심 수단입니다. 작은 클래스 하나를 직접 만들고 생성자 호출부터 메서드 실행까지 따라가 보면, 캡슐화·상속·추상화·다형성도 하나의 흐름으로 연결됩니다.

728x90