F-lab Java 1주차 / Phase 2 / Unit 2.1 본격 학습 자료
9-섹션 마스터 프롬프트 형식으로 깊이 파헤친다.선수 지식: Unit 1.2 (클래스와 객체의 본질)
다음 Unit: 2.2 — 가변인자 (Varargs)
전자레인지를 떠올려보세요. 전자레인지에는 다양한 버튼이 있습니다:
각 버튼은 세 가지 정보를 가집니다:
→ 이게 메서드의 본질입니다. 객체가 가진 "버튼" 들이 메서드.
ATM의 메뉴를 보면:
| ATM 버튼 | 입력 | 결과 |
|---|---|---|
| "잔액 조회" | 비밀번호 | 잔액 |
| "출금" | 비밀번호, 금액 | 현금 + 영수증 |
| "입금" | 현금 | 영수증 |
| "화면 종료" | 없음 | 없음 |
각 버튼이 메서드, ATM이 객체 입니다.
당신은 ATM의 내부 동작(데이터베이스 조회, 카드 인증, 현금 카운터 작동...) 을 몰라도, 버튼만 누르면 됩니다.
→ 메서드는 객체에게 일을 시키는 명령 창구.
| 비유 요소 | 자바 메서드 |
|---|---|
| 버튼 이름 | 메서드명 |
| 버튼 입력 | 매개변수 |
| 버튼 결과 | 반환값 |
| 버튼 누르는 행위 | 메서드 호출 |
| 누가 누를 수 있나 | 접근 제어자 |
초기 프로그래밍 (어셈블리, 초기 BASIC) 에서는 함수(메서드) 라는 개념이 없거나 약했습니다.
같은 작업을 반복하려면:
[프로그램]
주문 1번 처리:
- 가격 계산 코드 100줄
- 세금 계산 코드 50줄
- 결제 코드 80줄
주문 2번 처리:
- 가격 계산 코드 100줄 ← 똑같은 코드 또 작성
- 세금 계산 코드 50줄 ← 또 작성
- 결제 코드 80줄 ← 또 작성
주문 3번 처리:
- ... 또 똑같이 ...
문제:
이 문제를 해결하려고 "함수" 가 등장:
함수 정의: 가격_계산하기 = (...코드 100줄...)
[프로그램]
주문 1번 처리:
- 가격_계산하기() 호출
- 세금_계산하기() 호출
- 결제하기() 호출
주문 2번 처리:
- 가격_계산하기() ← 같은 함수 재사용
- 세금_계산하기()
- 결제하기()
효과:
→ 함수는 "재사용 가능한 코드 단위" 라는 혁명적 발명.
자바 같은 객체지향 언어에서는 함수가 객체에 묶이면서 이름이 메서드(method) 로 바뀝니다.
| 함수 (Function) | 메서드 (Method) | |
|---|---|---|
| 소속 | 독립적 (전역) | 클래스에 속함 |
| 호출 | 함수명() | 객체.메서드명() |
| 데이터 접근 | 인자로 받음 | 자기 객체의 필드 직접 접근 |
| 예 | C의 printf() | 자바의 obj.toString() |
→ 메서드는 "객체의 행동을 정의하는 함수".
"메서드는 객체가 할 수 있는 일을 정의한 약속이다."
객체에게 메서드는:
객체가 데이터(필드)만 있고 메서드가 없다면 어떤 일이 벌어질까요?
// 메서드 없는 빈혈 객체 ❌
public class BankAccount {
public int balance; // 잔액 (public이 문제 더 키움)
}
이 객체를 사용하려면 외부에서 모든 일을 직접 해야 합니다:
BankAccount account = new BankAccount();
account.balance = 10000;
// 입금하려면?
account.balance = account.balance + 5000; // 외부에서 직접 계산
// 출금하려면?
if (account.balance >= 3000) {
account.balance = account.balance - 3000; // 외부에서 검증 + 계산
} else {
System.out.println("잔액 부족");
}
// 다른 곳에서도 같은 입금/출금이 필요할 때?
// → 같은 코드 또 작성 ❌
문제 1: 모든 사용자가 내부 동작을 알아야 함.
문제 2: 같은 로직이 여러 곳에 중복.
문제 3: 누구나 balance 를 직접 만짐 → 무결성 X.
문제 4: 음수 잔액도 가능 (account.balance = -99999).
public class BankAccount {
private int balance; // private — 직접 접근 X
public void deposit(int amount) {
if (amount <= 0) {
throw new IllegalArgumentException("입금액은 양수여야 합니다");
}
this.balance += amount;
}
public void withdraw(int amount) {
if (amount <= 0) {
throw new IllegalArgumentException("출금액은 양수여야 합니다");
}
if (this.balance < amount) {
throw new IllegalStateException("잔액 부족");
}
this.balance -= amount;
}
public int getBalance() {
return this.balance;
}
}
사용:
BankAccount account = new BankAccount();
account.deposit(10000); // 입금
account.withdraw(3000); // 출금
int current = account.getBalance();
// 안전성 ✅
account.withdraw(99999); // → 예외! 잔액 부족
account.deposit(-100); // → 예외! 양수만 가능
// account.balance = -1; // → 컴파일 에러! private
| 메서드 없음 | 메서드 있음 | |
|---|---|---|
| 코드 중복 | 매번 외부에서 작성 | 한 번 정의 후 재사용 |
| 데이터 보호 | 누구나 직접 변경 | 메서드를 통해서만 |
| 검증 로직 | 호출자가 알아서 | 메서드 안에 한 번만 |
| 변경 영향 | 모든 호출자 수정 | 메서드만 수정 |
| 가독성 | 낮음 | 높음 |
→ 메서드는 객체지향의 핵심 도구. 메서드 없이는 캡슐화도, 다형성도 불가능.
자바 메서드의 표준 형태:
[접근제어자] [기타제어자] 반환타입 메서드명(매개변수목록) [throws 예외] {
// 메서드 본문
return 반환값;
}
실제 예시:
public static int calculateTotal(int price, int quantity) throws IllegalArgumentException {
if (price < 0 || quantity < 0) {
throw new IllegalArgumentException("음수 불가");
}
return price * quantity;
}
각 부분을 분해해보면:
| 부분 | 예시 | 역할 |
|---|---|---|
| 접근 제어자 | public | 누가 호출할 수 있는가 |
| 기타 제어자 | static | 인스턴스 없이 호출 가능 |
| 반환 타입 | int | 어떤 결과를 주는가 |
| 메서드명 | calculateTotal | 무엇을 하는가 |
| 매개변수 | (int price, int quantity) | 무엇을 입력받는가 |
| 예외 선언 | throws IllegalArgumentException | 어떤 예외를 던질 수 있는가 |
| 본문 | { ... } | 실제 로직 |
| 반환문 | return price * quantity; | 결과를 돌려줌 |
| 제어자 | 같은 클래스 | 같은 패키지 | 자식 클래스 (다른 패키지) | 그 외 |
|---|---|---|---|---|
public | ✅ | ✅ | ✅ | ✅ |
protected | ✅ | ✅ | ✅ | ❌ |
| (default) | ✅ | ✅ | ❌ | ❌ |
private | ✅ | ❌ | ❌ | ❌ |
원칙: 가장 좁은 범위로 시작 → 필요할 때만 넓힘 (캡슐화 원칙)
실무 예시:
public class Fare {
private int amount; // 외부 차단 — 가장 안전
public int getAmount() { ... } // 외부 조회용 — 공개
protected void onAmountChanged() { ... } // 자식만 사용 가능
void internalUpdate() { ... } // 같은 패키지만 (default)
private void validate() { ... } // 클래스 내부만
}
암기 팁: public > protected > default > private 순으로 좁아짐.
// 1. 기본형
public int getAge() { return 25; }
public double getPrice() { return 9.99; }
public boolean isActive() { return true; }
// 2. 객체
public String getName() { return "Alice"; }
public Customer getCustomer() { return this.customer; }
// 3. 배열
public int[] getScores() { return new int[]{90, 85, 70}; }
// 4. 컬렉션
public List<String> getTags() { return List.of("a", "b"); }
// 5. 반환 없음
public void doSomething() {
System.out.println("작업 완료");
// return 없음 (자동 종료) 또는 return; 가능
}
void 와 return 의 미묘한 관계void 는 "값을 반환하지 않는다" 는 의미:
public void greet(String name) {
System.out.println("Hello, " + name);
// return 생략 가능 (자동 종료)
}
그러나 return; (값 없는 return) 은 사용 가능 — 메서드 즉시 종료용:
public void greet(String name) {
if (name == null) {
return; // ✅ 즉시 종료, 이후 코드 실행 X
}
System.out.println("Hello, " + name); // name이 null이면 실행 안 됨
}
return value; 는 불가:
public void doSomething() {
return 42; // ❌ 컴파일 에러: void 메서드는 값 반환 X
}
자주 헷갈리는 용어:
// 매개변수 (Parameter) — 메서드 정의 시
public int add(int a, int b) { // ← a, b가 매개변수
return a + b;
}
// 인자 (Argument) — 호출 시 전달하는 실제 값
int result = add(3, 5); // ← 3, 5가 인자
비유:
public class Calculator {
public int add(int a, int b) {
int sum = a + b;
return sum;
}
}
Calculator calc = new Calculator();
int result = calc.add(3, 5);
JVM 내부 흐름 ⭐ :
[Stack 영역] [Heap 영역]
main()
- calc (참조) ───────→ Calculator 인스턴스
- result ↑
│
add() 호출 시 │
┌──────────────────┐ │
│ add() ← 새 스택 프레임 │
│ - this (자기 객체) ──────────┘
│ - a = 3
│ - b = 5
│ - sum = 8
└──────────────────┘
↓ return 후
add() 스택 프레임 제거
result = 8
핵심 동작:
1. add(3, 5) 호출 시 Stack에 새 프레임 생성
2. 매개변수 a=3, b=5 가 새 프레임에 복사 (Pass by Value)
3. 메서드 안의 지역변수 sum 도 새 프레임에 생성
4. return 시 결과값을 반환받는 변수에 복사
5. 메서드 종료 → Stack 프레임 제거
→ 이게 4주차 JVM 메모리 모델의 기초.
this — 메서드 호출의 숨은 주인공public class Counter {
private int count = 0;
public void increment() {
this.count++; // this = 호출한 객체
}
}
Counter c1 = new Counter();
Counter c2 = new Counter();
c1.increment(); // this = c1, c1.count = 1
c1.increment(); // this = c1, c1.count = 2
c2.increment(); // this = c2, c2.count = 1
JVM의 비밀 ⭐ :
c1.increment() 는 사실 컴파일러가 Counter.increment(c1) 으로 변환this 로 전달됨→ Java가 객체와 메서드를 연결하는 핵심 메커니즘.
"같은 이름, 다른 시그니처"
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; }
public String add(String a, String b) { return a + b; }
}
calc.add(1, 2); // 첫 번째 호출
calc.add(1.5, 2.5); // 두 번째 호출
calc.add(1, 2, 3); // 세 번째 호출
calc.add("Hi", " Bob"); // 네 번째 호출
오버로딩 규칙 ⭐ :
public int foo() { ... }
public String foo() { ... } // ❌ 컴파일 에러
왜 반환 타입만 다르면 안 되나?
obj.foo(); 만으로는 int 메서드인지 String 메서드인지 구별 불가JVM 내부의 메서드 식별 ⭐ :
자바는 항상 값으로 전달(Pass by Value) 합니다. (4주차에서 깊이 다룸)
기본형 매개변수:
public void changeNumber(int x) {
x = 100; // 메서드 내부의 x만 변경
}
int num = 10;
changeNumber(num);
System.out.println(num); // 10 (변경 안 됨)
객체 매개변수 (참조의 값 전달):
public void changeName(Person p) {
p.setName("Bob"); // 객체 내부 변경 — 외부 영향 ✅
}
public void replacePerson(Person p) {
p = new Person("Bob"); // 메서드 내부의 참조만 변경
}
Person alice = new Person("Alice");
changeName(alice);
System.out.println(alice.getName()); // "Bob" (객체 내부 변경됨)
Person charlie = new Person("Charlie");
replacePerson(charlie);
System.out.println(charlie.getName()); // "Charlie" (참조 변경 안 됨)
→ 4주차에서 자세히 다룰 주제. 지금은 "자바는 항상 값 전달" 만 기억.
ILIC 운임 시스템으로 메서드를 다양한 형태로 보겠습니다.
public class FareService {
private final FareRepository repository;
// 1. 매개변수 X, 반환 X
public void initialize() {
System.out.println("FareService 초기화");
}
// 2. 매개변수 X, 반환 O
public int getCount() {
return repository.count();
}
// 3. 매개변수 O, 반환 X
public void delete(Long id) {
repository.deleteById(id);
}
// 4. 매개변수 O, 반환 O
public Fare findById(Long id) {
return repository.findById(id)
.orElseThrow(() -> new EntityNotFoundException("운임 없음"));
}
// 5. 여러 매개변수, 복잡한 반환
public List<Fare> search(Long customerId, FareStatus status, LocalDate from, LocalDate to) {
return repository.findByCriteria(customerId, status, from, to);
}
}
public class Fare {
private int amount; // 외부 접근 불가
private FareStatus status;
// public — 외부 API
public void changeAmount(int newAmount) {
validateAmount(newAmount); // 내부 호출
this.amount = newAmount;
notifyChange();
}
// protected — 자식 클래스에서 오버라이드 가능
protected void notifyChange() {
System.out.println("운임 변경됨");
}
// package-private (default) — 같은 패키지만
void internalReset() {
this.amount = 0;
this.status = FareStatus.DRAFT;
}
// private — 클래스 내부만
private void validateAmount(int amount) {
if (amount < 0) {
throw new IllegalArgumentException("음수 불가");
}
}
public int getAmount() { return amount; }
}
좋은 설계의 신호:
private 가 가장 많음 (내부 구현)public 은 명확한 외부 API만protected, default 는 의도적인 경우만public class FareCalculator {
// 기본 거리 계산
public int calculate(int distance) {
return distance * 500;
}
// 거리 + 무게
public int calculate(int distance, int weight) {
return distance * 500 + weight * 100;
}
// 거리 + 무게 + 긴급 여부
public int calculate(int distance, int weight, boolean urgent) {
int base = calculate(distance, weight);
return urgent ? base * 2 : base;
}
// FareRequest 객체로
public int calculate(FareRequest request) {
return calculate(
request.getDistance(),
request.getWeight(),
request.isUrgent()
);
}
}
// 사용 — 같은 이름으로 다양한 호출
calc.calculate(100); // 50000
calc.calculate(100, 50); // 55000
calc.calculate(100, 50, true); // 110000
calc.calculate(new FareRequest(100, 50, true)); // 110000
효과: 사용자는 calculate 라는 한 이름 만 기억하면 됨. 다양한 시나리오를 한 메서드명으로 처리.
public class FareValidator {
public void validate(Fare fare) {
// Early Return 패턴 ⭐
if (fare == null) {
return; // 즉시 종료
}
if (fare.getAmount() < 0) {
throw new IllegalArgumentException("음수 운임");
}
if (fare.getStatus() == null) {
throw new IllegalStateException("상태 없음");
}
// 모든 검증 통과
System.out.println("검증 OK");
}
}
Early Return 패턴:
Bad (중첩):
public void validate(Fare fare) {
if (fare != null) {
if (fare.getAmount() >= 0) {
if (fare.getStatus() != null) {
// 검증 OK — 들여쓰기 지옥 ❌
}
}
}
}
Good (Early Return):
public void validate(Fare fare) {
if (fare == null) return;
if (fare.getAmount() < 0) throw new IllegalArgumentException();
if (fare.getStatus() == null) throw new IllegalStateException();
// 메인 로직 — 평평한 구조 ✅
}
// ❌ 명사 — 메서드인지 변수인지 모호
public int total() { ... }
public String name() { ... }
// ✅ 동사 — 의도 명확
public int calculateTotal() { ... }
public String getName() { ... }
public boolean isActive() { ... } // 진위형은 is/has/can으로
규칙 ⭐ :
calculate, find, save)getName, getAge)isActive, hasPermission, canDelete)// ❌ 매개변수 7개
public Fare createFare(
Long customerId,
int amount,
String currency,
LocalDate departureDate,
String origin,
String destination,
boolean urgent
) { ... }
문제:
createFare(1L, 50000, "KRW", date, "서울", "부산", true);
createFare(1L, 50000, "KRW", date, "부산", "서울", true); // 출발-도착 바뀜! ❌
해결 — 객체로 묶기:
public Fare createFare(FareCreateRequest request) { ... }
public record FareCreateRequest(
Long customerId,
int amount,
String currency,
LocalDate departureDate,
Route route, // origin, destination 묶음
boolean urgent
) {}
원칙: 매개변수 4개 이상이면 객체로 묶기 검토.
// ❌ 매개변수를 변경 — 혼란 야기
public int processAmount(int amount) {
amount = amount * 2; // 매개변수 자체를 변경
if (amount > 10000) {
amount = 10000;
}
return amount;
}
문제:
해결 — 매개변수는 final, 새 변수 사용:
public int processAmount(final int amount) {
int result = amount * 2;
if (result > 10000) {
result = 10000;
}
return result;
}
public void doSomething() {
int result = ...;
return result; // ❌ 컴파일 에러
}
해결:
return; 만 (값 없이)// ❌ 메서드 하나가 200줄
public void processOrder(Order order) {
// 1. 검증 (50줄)
// 2. 계산 (50줄)
// 3. 저장 (30줄)
// 4. 알림 (40줄)
// 5. 로깅 (30줄)
}
문제:
해결 — 작은 메서드로 분리:
public void processOrder(Order order) {
validate(order);
int total = calculate(order);
save(order);
notify(order);
log(order);
}
private void validate(Order order) { ... }
private int calculate(Order order) { ... }
private void save(Order order) { ... }
private void notify(Order order) { ... }
private void log(Order order) { ... }
권장: 메서드는 한 화면에 들어오는 정도 (보통 20줄 이내).
public class Calculator {
public void process(int x) { ... }
public void process(Integer x) { ... } // ⚠️ Auto-boxing 모호함
}
calc.process(10); // int? Integer? 컴파일러가 우선순위 결정
원칙:
[Unit 2.1: 메서드의 구조] ← 지금 여기
↓
[Unit 2.2: 가변인자] — 다음 학습
↓
[Unit 2.3: 상속과 생성자 체이닝]
↓
[Unit 2.4: 다형성] ★ — OOP의 정점
1주차 내:
미래 주차:
<T> T foo(T input))@PreAuthorize 가 메서드 단위 권한 검증| 패턴 | 메서드 활용 |
|---|---|
| Strategy | 인터페이스 메서드를 다양하게 구현 |
| Template Method | 상위 메서드가 흐름, 하위 메서드가 구현 |
| Factory Method | 객체 생성을 메서드로 캡슐화 |
| Builder | 메서드 체이닝으로 객체 구성 |
→ 5주차에서 본격 학습.
| 질문 | 이 Unit에서의 답 |
|---|---|
| "메서드 오버로딩이 뭔가요?" | 같은 이름, 다른 시그니처 (타입/개수/순서) |
| "오버로딩과 오버라이딩의 차이는?" | 오버로딩=같은 클래스 다른 시그니처, 오버라이딩=상속에서 메서드 재정의 (Unit 2.4) |
| "접근 제어자 4가지 차이는?" | public > protected > default > private |
| "Pass by Value vs Reference?" | 자바는 항상 Pass by Value (4주차에서 자세히) |
| "void에서 return 가능?" | 값 없는 return; 만 가능 |
1️⃣ 메서드는 "객체의 행동을 정의하는 단위" 다.
절차지향의 함수가 객체에 묶인 형태로, 시그니처(접근제어자 + 반환타입 + 메서드명 + 매개변수) 로 구성된다. 메서드 없이는 캡슐화도 다형성도 불가능하므로, 객체지향의 가장 기본 도구 다.
2️⃣ 접근 제어자로 "누가 호출할 수 있는가" 를 통제한다.
private부터 시작해서 필요할 때만 넓히는 것이 캡슐화의 핵심. public > protected > default > private 순으로 좁아지며, 좁을수록 안전하다. ILIC 같은 큰 시스템에서는 의도적인 접근 제어가 유지보수 비용을 결정한다.3️⃣ 좋은 메서드는 "한 가지 일만, 짧게, 명확한 이름으로" 한다.
매개변수 4개 이상이면 객체로 묶기, 메서드는 한 화면에 들어오게 (20줄 내외), 동사 시작 이름 (
calculate,find,save), 매개변수는 안에서 변경 X — 이 규칙들이 코드 품질을 결정한다.
void 메서드에서 return; 의 의미를 설명할 수 있다Q1: void 반환 타입의 메서드에서 return을 쓸 수 있는가?
YES. 단, 값 없는 return; 만 가능. 메서드를 즉시 종료 하는 용도.
public void greet(String name) {
if (name == null) {
return; // ✅ 즉시 종료
}
System.out.println("Hello, " + name);
}
public void doSomething() {
return 42; // ❌ 값이 있는 return 불가
}
활용: Early Return 패턴 — 조건에 맞지 않으면 즉시 종료해서 중첩 if 줄이기.
Q2: 매개변수 없는 메서드와 있는 메서드의 호출 방식 차이는?
호출 시 괄호 안에 인자를 넘기느냐의 차이:
// 매개변수 없음 — 빈 괄호
obj.method();
// 매개변수 있음 — 인자 전달
obj.method(arg1, arg2);
⚠️ 주의 — 괄호는 항상 필요:
obj.method; // ❌ 메서드 호출 아님 (메서드 참조로 해석되거나 에러)
obj.method(); // ✅ 호출
⚠️ 메서드 참조 와의 차이 (3주차 람다와 연결):
list.forEach(System.out::println); // 메서드 참조 (호출 X)
list.forEach(x -> System.out.println(x)); // 람다