Java 필드와 메서드 — 객체의 상태와 행동 정의

Java 클래스를 구성하는 핵심 요소인 필드와 메서드를 완전 정복한다. 타입별 기본값, 인스턴스 vs 정적 멤버, 값 전달 방식(pass by value), return 문까지 상세히 다룬다

· 14 min read · PALDYN Team

지난 글에서 클래스가 설계도이고 객체가 그 실체임을 배웠다. 클래스 선언 안에는 세 가지 구성 요소(필드·생성자·메서드)가 있다고 소개했는데, 이번 글에서는 필드(Field)메서드(Method) 를 집중 해부한다. 이 두 요소를 완전히 이해하면 Java 클래스를 자유롭게 설계하고 읽을 수 있다.

필드(Field) — 객체의 상태를 담는 그릇

필드는 클래스 블록 최상단에 선언하는 변수다. 각 객체마다 독립적으로 존재하며, 객체가 “지금 어떤 상태인지”를 나타낸다.

public class Car {
    String brand;   // 브랜드 이름
    int speed;      // 현재 속도 (km/h)
    boolean running; // 시동 여부
}

지역변수(메서드 안에 선언)와 필드의 가장 큰 차이는 수명이다. 지역변수는 메서드가 끝나면 사라지지만, 필드는 객체가 살아 있는 동안 유지된다.

필드 선언 문법

[접근 제어자] [static] [final] 타입 필드명 [= 초기값];

// 예시
private String name;
public  int    age   = 0;
static  int    count = 0;
final   double PI    = 3.14159;
  • 접근 제어자: private, protected, public, 생략(패키지 접근). 좋은 설계는 private으로 감춘다.
  • static: 객체마다 따로 존재하지 않고 클래스 전체가 공유. 이후 글에서 자세히 다룬다.
  • final: 한 번 초기화하면 값을 변경할 수 없다. 상수 선언에 사용.
  • 초기값: 생략하면 타입별 기본값이 자동으로 들어간다.

타입별 기본값

필드는 선언만 해도 JVM이 자동으로 기본값을 채워준다. 반면 지역변수는 초기화를 안 하면 컴파일 에러가 발생한다. 이 차이를 반드시 기억해야 한다.

필드의 기본값 — 초기화 없이 선언만 하면?

기본값 규칙:

  • 정수형 (byte, short, int, long): 0, 0, 0, 0L
  • 실수형 (float, double): 0.0f, 0.0d
  • 문자형 (char): '�' (null 문자, 화면에 보이지 않는 공백)
  • 논리형 (boolean): false
  • 참조형 (클래스, 배열, 인터페이스): null
public class StatusChecker {
    int    count;     // 자동으로 0
    String message;   // 자동으로 null
    boolean active;   // 자동으로 false

    void check() {
        System.out.println(count);    // 0
        System.out.println(message);  // null
        System.out.println(active);   // false

        // ⚠ 참조형 null 사용 주의
        System.out.println(message.length()); // NullPointerException!
    }
}

참조형 필드가 null인 상태에서 .으로 멤버에 접근하면 NullPointerException이 발생한다. 참조형 필드를 사용하기 전에 null 여부를 확인하거나, 생성자에서 반드시 초기화하는 습관을 들여야 한다.

인스턴스 필드 vs 정적 필드

필드는 인스턴스 필드(instance field)정적 필드(static field) 로 나뉜다.

public class Counter {
    static int total = 0;   // 정적 필드 — 모든 객체가 공유
    int id;                 // 인스턴스 필드 — 각 객체가 독립 보유

    Counter() {
        total++;            // 생성될 때마다 공유 카운터 증가
        id = total;         // 이 객체의 고유 번호
    }
}

Counter c1 = new Counter();
Counter c2 = new Counter();
Counter c3 = new Counter();

System.out.println(Counter.total); // 3 (정적 → 클래스명으로 접근)
System.out.println(c1.id);         // 1
System.out.println(c2.id);         // 2
System.out.println(c3.id);         // 3

정적 필드는 클래스명.필드명으로 접근하는 것이 관례다. 객체 참조(c1.total)로도 접근은 되지만, 보는 사람이 “이게 인스턴스 필드인가?”라고 헷갈리게 하므로 피한다.

메서드(Method) — 객체의 행동을 정의한다

메서드는 클래스 안에 선언된 함수다. 필드에 저장된 상태를 읽거나 변경하는 것이 주된 역할이다.

public class Thermometer {
    double celsius;

    // 섭씨를 화씨로 변환해 반환
    double toFahrenheit() {
        return celsius * 9.0 / 5.0 + 32.0;
    }

    // 값을 설정하는 메서드 (반환값 없음)
    void setCelsius(double value) {
        this.celsius = value;
    }
}

메서드 선언 문법

[접근 제어자] [static] 반환타입 메서드명(매개변수목록) {
    // 실행 코드
    [return 값;]
}

메서드 구조 분해

각 구성 요소의 역할:

요소설명예시
접근 제어자외부 접근 가능 범위public, private
반환 타입돌려줄 값의 타입int, String, void
메서드 이름소문자 camelCase 관례toFahrenheit, setName
매개변수 목록입력값. 없으면 빈 괄호(int a, String b)
메서드 본문{ } 안의 실행 로직
return 문반환 타입이 void가 아니면 필수return result;

return 문 완전 이해

return은 두 가지 역할을 한다.

  1. 값을 반환한다: return 식; 형태로 반환값 지정.
  2. 메서드를 즉시 종료한다: return 이후 코드는 실행되지 않는다.
int max(int a, int b) {
    if (a > b) {
        return a;   // 이 줄에서 메서드 종료
    }
    return b;       // a <= b 일 때 여기까지 도달
}

반환 타입이 void인 메서드도 return; (값 없이) 을 써서 일찍 종료할 수 있다.

void printPositive(int num) {
    if (num <= 0) return;   // 음수면 아무것도 출력하지 않고 종료
    System.out.println(num);
}

컴파일러는 모든 실행 경로에서 값을 반환하는지 검사한다. 하나라도 반환하지 않는 분기가 있으면 컴파일 에러다.

int sign(int n) {
    if (n > 0) return 1;
    if (n < 0) return -1;
    // ❌ n == 0 일 때 반환 없음 → 컴파일 에러
}

int sign2(int n) {
    if (n > 0) return 1;
    if (n < 0) return -1;
    return 0;   // ✓ 모든 경로 커버
}

매개변수 전달 방식 — Pass by Value

Java는 항상 값에 의한 전달(pass by value) 을 사용한다. 메서드에 값을 넘기면 그 복사본이 매개변수로 전달된다.

기본형은 복사본이 전달된다

void increment(int x) {
    x++;   // 복사본을 증가. 원본에 영향 없음
}

int n = 10;
increment(n);
System.out.println(n);   // 10 (변하지 않음)

메서드 안에서 x를 바꿔도 원본 n은 변하지 않는다. xn의 값을 복사한 별도의 변수이기 때문이다.

참조형은 “참조값(주소)“의 복사본이 전달된다

void grow(int[] arr) {
    arr[0] = 999;   // arr이 가리키는 배열 객체를 수정
}

int[] data = {1, 2, 3};
grow(data);
System.out.println(data[0]);   // 999 (변경됨!)

여기서 data(배열 참조)의 복사본인 arr이 전달된다. 두 변수는 같은 배열 객체를 가리키므로, arr[0]을 바꾸면 data[0]도 바뀐다. 참조값의 복사본이 전달되는 것이지, 객체 자체가 전달되는 게 아님을 주의하자.

반면, 매개변수 자체를 다른 객체로 바꾸면 원본에 영향이 없다.

void reassign(int[] arr) {
    arr = new int[]{10, 20, 30};  // 매개변수 arr을 새 배열로 교체
    // 원본 data는 영향받지 않음
}

int[] data = {1, 2, 3};
reassign(data);
System.out.println(data[0]);   // 1 (그대로)

arr를 새 배열로 교체해도 data가 가리키는 원래 객체는 그대로다. arr라는 복사본 참조만 새 객체를 가리키게 된 것이다.

메서드와 스택 프레임

메서드가 호출될 때마다 JVM은 스택(Stack) 에 스택 프레임(Stack Frame) 을 하나 생성한다. 스택 프레임에는 매개변수, 지역변수, 반환 주소가 담긴다. 메서드가 반환하면 해당 스택 프레임은 즉시 해제된다.

int add(int a, int b) {
    int sum = a + b;   // sum: 스택 프레임에 저장
    return sum;
}   // 반환 → 스택 프레임 해제, sum 사라짐

이 구조가 재귀 호출에서 “스택 오버플로(Stack Overflow)” 가 발생하는 이유다. 재귀가 너무 깊어지면 스택 프레임이 쌓이다가 스택 공간을 초과한다.

메서드 오버로딩 맛보기

같은 이름의 메서드를 매개변수 목록이 다르게 여러 번 선언할 수 있다. 이를 메서드 오버로딩(Method Overloading) 이라 한다. 상세 내용은 다음 글에서 다룬다.

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

컴파일러는 호출 시 전달한 인자의 타입과 개수를 보고 어느 메서드를 실행할지 결정한다. 반환 타입만 다른 경우는 오버로딩으로 인정하지 않는다는 점에 주의하자.

필드와 메서드 실전 예제

지금까지 배운 개념을 종합한 Rectangle 클래스다.

public class Rectangle {
    double width;
    double height;

    Rectangle(double width, double height) {
        this.width  = width;
        this.height = height;
    }

    double area() {
        return width * height;
    }

    double perimeter() {
        return 2 * (width + height);
    }

    boolean isSquare() {
        return width == height;
    }

    void scale(double factor) {
        width  *= factor;
        height *= factor;
    }

    String describe() {
        return String.format("Rectangle(%.1f × %.1f)", width, height);
    }
}
Rectangle r = new Rectangle(4.0, 3.0);

System.out.println(r.area());        // 12.0
System.out.println(r.perimeter());   // 14.0
System.out.println(r.isSquare());    // false
System.out.println(r.describe());    // Rectangle(4.0 × 3.0)

r.scale(2.0);
System.out.println(r.describe());    // Rectangle(8.0 × 6.0)

area(), perimeter(), isSquare()는 값을 반환하고, scale()은 필드를 변경하며 반환값이 없다. describe()String을 반환한다. 이런 다양한 패턴을 익히면 클래스 설계가 자연스러워진다.

필드 vs 지역변수 — 헷갈리기 쉬운 비교

필드지역변수
선언 위치클래스 블록 최상단메서드 / 블록 안
기본값자동 초기화없음 (컴파일 에러)
수명객체 수명과 동일블록 종료까지
저장 위치힙(Heap) 객체 안스택(Stack)
접근 제어접근 제어자 적용적용 없음

정리

필드는 객체의 상태(State) 를 저장하고, 메서드는 객체의 행동(Behavior) 을 정의한다. 핵심 개념을 정리하면:

  • 필드는 선언만 해도 기본값이 자동으로 채워진다 (참조형은 null).
  • 메서드는 반환타입 이름(매개변수) { 본문 } 구조로 선언한다.
  • 반환 타입이 void가 아니면 모든 경로에서 return 이 있어야 한다.
  • Java는 항상 pass by value: 기본형은 값 복사, 참조형은 주소 복사.
  • 정적 멤버(static)는 객체가 아닌 클래스에 속하며, 모든 객체가 공유한다.

다음 글에서는 객체 생성의 핵심인 생성자(Constructor) 를 자세히 다룬다. 기본 생성자, 매개변수 생성자, 생성자 오버로딩, this() 호출까지 빠짐없이 살펴본다.


지난 글: Java 클래스와 객체 — 설계도와 실체의 세계


읽어주셔서 감사합니다. 😊