공공부하자개발 · 영어 학습 노트
자바
초급자바의 뼈대0/8 완료
  • 00개발 환경과 첫 프로그램
  • 01변수, 데이터 타입, 형변환
  • 02연산자와 제어문
  • 031차원 · 2차원 배열
  • 04클래스, 객체, 생성자, 오버로딩
  • 05접근 제어자와 캡슐화
  • 06메서드 활용 패턴 (초급)
  • 07enum, 패키지와 import, static 과 final
사이트 소개개인정보처리방침연락처
© 2026 공부하자
홈 › 초급 › 06 / 8

메서드 활용 패턴 (초급)

섹션 6진행 0 / 8
1왜 배우는가2핵심 원리3패턴 모음4자주 하는 실수 (Tip)5연습 문제6정리‹ 이전다음 ›

3. 패턴 모음

각 패턴의 코드는 java-src/beginner/06_method_patterns/Main.java에서 실행해 볼 수 있습니다.

패턴 1: 시그니처 해부 — void와 반환형

메서드는 "무언가를 하고 끝나는" void형과 "값을 계산해 돌려주는" 반환형으로 나뉩니다. 출력이 목적이면 void, 결과를 다른 계산에 이어 쓰려면 반환형입니다. 반환값이 있으면 그것을 변수에 담거나 식 안에서 바로 쓸 수 있습니다.

java
static void greet(String name) { System.out.println("안녕하세요, " + name); }
static int add(int a, int b) { return a + b; }
static int square(int n) { return n * n; }

greet("Kim");                                   // 출력: 안녕하세요, Kim
int sum = add(3, 4);
System.out.println(sum);                        // 출력: 7
System.out.println(square(5) + square(2));      // 출력: 29

주의: void 메서드의 결과를 int x = greet("Kim");처럼 받으려 하면 컴파일 오류입니다. 출력하는 메서드보다 값을 반환하는 메서드가 재사용성이 높습니다.

패턴 2: 원시 값 전달 — 메서드 안에서 바꿔도 원본은 그대로

int, double 같은 원시 타입을 넘기면 값이 복사되어 새 스택 프레임에 들어갑니다. 메서드 안에서 아무리 바꿔도 호출한 쪽 변수는 다른 프레임에 있으므로 영향이 없습니다.

java
static void addTen(int n) { n += 10; }     // 이 n은 복사본

int x = 10;
addTen(x);
System.out.println(x);                     // 출력: 10

주의: 바꾼 값이 필요하면 return해서 받습니다: x = addTen(x);.

패턴 3: 배열 참조 전달 — 요소 변경은 반영, 재대입은 무시

배열을 넘기면 주소가 복사됩니다. 주소를 따라가 요소를 바꾸면 같은 배열이므로 원본에 반영됩니다. 하지만 매개변수에 새 배열을 대입하면 복사된 주소만 바뀔 뿐, 원본 변수는 여전히 옛 배열을 가리킵니다.

java
static void doubleAll(int[] a) { for (int i = 0; i < a.length; i++) a[i] *= 2; }
static void replace(int[] a) { a = new int[]{9, 9, 9}; }

int[] arr = {1, 2, 3};
doubleAll(arr);
System.out.println(Arrays.toString(arr));   // 출력: [2, 4, 6]
int[] arr2 = {1, 2, 3};
replace(arr2);
System.out.println(Arrays.toString(arr2));  // 출력: [1, 2, 3]

주의: 메서드가 인자로 받은 배열을 바꾸는 것은 호출자를 놀라게 할 수 있습니다. 바꿀 거면 이름에 드러내거나(sortInPlace), 새 배열을 반환하세요.

패턴 4: 문자열 불변 — 반드시 반환값을 받아야 한다

String은 참조 타입이지만 불변(immutable)이라 내용을 바꾸는 메서드가 없습니다. s.toUpperCase()는 s를 바꾸는 것이 아니라 새 문자열을 만들어 반환합니다. 반환값을 버리면 아무 일도 일어나지 않습니다.

java
static void upper(String s) { s.toUpperCase(); }            // 반환값을 버림
static String upper2(String s) { return s.toUpperCase(); }

String s = "java";
upper(s);
System.out.println(s);      // 출력: java
s = upper2(s);
System.out.println(s);      // 출력: JAVA

주의: s.trim();, s.replace(...)를 한 줄로 쓰고 끝내는 실수가 매우 흔합니다. String 메서드는 항상 결과를 대입하세요.

패턴 5: 여러 값 반환 — 배열

return은 하나만 돌려줄 수 있습니다. 같은 타입의 값 몇 개를 돌려줄 때 가장 단순한 방법은 배열입니다. 최솟값과 최댓값처럼 짝을 이루는 결과에 씁니다.

java
static int[] minMax(int[] a) {
    int min = a[0], max = a[0];
    for (int v : a) { if (v < min) min = v; if (v > max) max = v; }
    return new int[]{min, max};
}

int[] mm = minMax(new int[]{7, 2, 9, 4});
System.out.println("min=" + mm[0] + " max=" + mm[1]);   // 출력: min=2 max=9

주의: mm[0]이 min인지 max인지 호출자가 기억해야 합니다. 의미가 있는 값이면 패턴 6의 record가 낫습니다.

패턴 6: 여러 값 반환 — record

타입이 다르거나 각 값에 이름이 필요하면 record로 묶어 반환합니다. 한 줄로 선언되고 min(), max() 접근자와 toString이 자동으로 생깁니다. 05 레슨의 record가 "반환 타입"으로 쓰이는 대표적인 경우입니다.

java
record Range(int min, int max) {}

static Range range(int[] a) {
    int[] mm = minMax(a);
    return new Range(mm[0], mm[1]);
}

Range r = range(new int[]{7, 2, 9, 4});
System.out.println(r.min() + " " + r.max() + " " + r);   // 출력: 2 9 Range[min=2, max=9]

주의: 메서드 안에 지역 record를 선언할 수도 있지만, 여러 메서드가 쓰는 결과 타입은 클래스 수준에 선언합니다.

패턴 7: 가변 인자(varargs)

인자 개수가 정해지지 않았을 때 타입... 이름으로 선언하면 0개부터 몇 개든 받을 수 있습니다. 메서드 안에서는 그냥 배열입니다. 배열을 직접 넘겨도 됩니다.

java
static int total(int... nums) {          // nums는 int[]
    int sum = 0;
    for (int n : nums) sum += n;
    return sum;
}

System.out.println(total());                    // 출력: 0
System.out.println(total(5));                   // 출력: 5
System.out.println(total(1, 2, 3, 4));          // 출력: 10
System.out.println(total(new int[]{10, 20}));   // 출력: 30

주의: 호출할 때마다 배열이 새로 만들어집니다. 성능이 중요한 곳에서는 total(int a, int b) 오버로딩을 따로 둡니다(List.of가 그렇게 되어 있습니다).

패턴 8: 가변 인자 + 일반 매개변수

가변 인자는 마지막 매개변수 하나만 가능합니다. 구분자처럼 반드시 필요한 값은 앞에 일반 매개변수로 두고, 나머지를 가변 인자로 받습니다.

java
static String join(String sep, String... parts) {
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < parts.length; i++) {
        if (i > 0) sb.append(sep);
        sb.append(parts[i]);
    }
    return sb.toString();
}

System.out.println(join("-", "a", "b", "c"));   // 출력: a-b-c
System.out.println("[" + join(", ") + "]");     // 출력: []

주의: void f(String... a, int b)는 컴파일 오류입니다. 가변 인자가 2개인 것도 불가능합니다.

패턴 9: 오버로딩 기본

같은 일을 다른 입력으로 할 때 이름을 같게 하고 매개변수만 다르게 합니다. 호출자는 area라는 이름 하나만 기억하면 됩니다. 매개변수의 개수, 타입, 순서 중 하나가 달라야 합니다.

java
static String area(int radius) { return "원 " + Math.PI * radius * radius; }
static String area(int w, int h) { return "사각형 " + w * h; }
static String area(double a, double b, double c) {
    double s = (a + b + c) / 2;
    return "삼각형 " + Math.sqrt(s * (s - a) * (s - b) * (s - c));
}

System.out.println(area(5));               // 출력: 원 78.53981633974483
System.out.println(area(3, 4));            // 출력: 사각형 12
System.out.println(area(3.0, 4.0, 5.0));   // 출력: 삼각형 6.0

주의: 반환 타입만 다른 오버로딩은 불가능합니다. 오버로딩은 같은 의미의 일에만 쓰세요. area(int)가 원이고 area(int, int)가 사각형인 것은 사실 경계선입니다. 실무라면 circleArea, rectArea가 더 분명합니다.

패턴 10: 오버로딩 선택 — 자동 형변환 우선순위

컴파일러는 2.4의 순서대로 후보를 찾습니다. 정확히 맞는 것이 없으면 확대 변환(char→int, float→double, int→long)을 먼저 시도하고, 그래도 없을 때만 박싱을 고려합니다.

java
public class Overload {
    void call(int n)    { System.out.println("int"); }
    void call(long n)   { System.out.println("long"); }
    void call(double n) { System.out.println("double"); }
    void call(Object o) { System.out.println("Object"); }

    void callNoInt(long n)    { System.out.println("long"); }
    void callNoInt(Integer n) { System.out.println("Integer"); }
}

Overload o = new Overload();
o.call(1);                    // 출력: int
o.call('A');                  // 출력: int      (char → int 승격)
o.call(1L);                   // 출력: long
o.call(1.5f);                 // 출력: double   (float → double)
o.call(Integer.valueOf(1));   // 출력: Object   (Integer는 Object의 자식. 언박싱보다 우선)
o.callNoInt((short) 1);       // 출력: long     (short → long)
o.callNoInt(1);               // 출력: long     (int → long 확대가 Integer 박싱보다 우선)

주의: callNoInt(1)이 Integer가 아니라 long으로 가는 것이 핵심입니다. "타입이 더 비슷해 보이는" 것이 아니라 규칙의 단계가 우선합니다.

패턴 11: 오버로딩 모호성 — null과 두 후보

null은 모든 참조 타입에 대입될 수 있어서, 참조 타입 오버로딩이 여러 개면 컴파일러가 고를 수 없습니다. 상속 관계가 있으면(String은 Object의 자식) 더 구체적인 String을 고르지만, String과 Integer처럼 관계가 없으면 ambiguous 오류입니다. 캐스팅으로 의도를 밝힙니다.

java
static String pick(String s) { return "String"; }
static String pick(Object o) { return "Object"; }

System.out.println(pick("text"));            // 출력: String
System.out.println(pick((Object) null));     // 출력: Object
System.out.println(pick((String) null));     // 출력: String
// pick(null)만 쓰면? String이 Object보다 구체적이므로 "String". 
// 만약 pick(Integer)가 하나 더 있었다면 pick(null)은 컴파일 오류(ambiguous)

주의: null을 직접 넘기는 코드는 오버로딩이 늘어나는 순간 깨질 수 있습니다. 캐스팅하거나 아예 넘기지 마세요.

패턴 12: 재귀 — 팩토리얼

n! = n × (n-1)!이고 1! = 1이라는 정의를 코드로 그대로 옮긴 것이 재귀입니다. 기저 조건(n <= 1)이 먼저 오고, 그 다음 문제를 작게 만들어 자기 자신을 호출합니다.

java
static long factorial(int n) {
    if (n <= 1) return 1;            // 기저 조건
    return n * factorial(n - 1);     // 재귀 단계: 문제를 하나 작게
}

System.out.println(factorial(5));    // 출력: 120
System.out.println(factorial(20));   // 출력: 2432902008176640000

주의: factorial(21)은 long 범위를 넘어 음수가 나옵니다. 재귀 자체의 문제가 아니라 오버플로우입니다. BigInteger가 필요합니다.

패턴 13: 재귀 vs 반복 — 피보나치

재귀 피보나치는 정의는 예쁘지만 fib(n-1)과 fib(n-2)가 같은 계산을 반복해 지수적으로 느립니다. fibRec(50)은 수십 초가 걸립니다. 반복문은 순식간입니다. 같은 문제를 두 방식으로 써 보고 차이를 느끼는 것이 목적입니다.

java
static long fibRec(int n) {
    if (n < 2) return n;
    return fibRec(n - 1) + fibRec(n - 2);     // 호출 횟수가 2^n 에 가깝게 폭발
}

static long fibLoop(int n) {
    long a = 0, b = 1;
    for (int i = 0; i < n; i++) { long t = a + b; a = b; b = t; }
    return a;
}

System.out.println(fibRec(10));      // 출력: 55
System.out.println(fibLoop(10));     // 출력: 55
System.out.println(fibLoop(50));     // 출력: 12586269025

주의: 재귀가 느린 이유는 호출 비용이 아니라 중복 계산입니다. 고급 레슨의 메모이제이션으로 재귀를 유지하면서 빠르게 할 수 있습니다.

패턴 14: 재귀 — 트리(디렉터리) 탐색

디렉터리 안에 디렉터리가 있고 그 안에 또 디렉터리가 있는 구조는 반복문으로 쓰기 어렵고 재귀로는 자연스럽습니다. "자기 자신을 출력하고, 자식마다 같은 일을 한다"가 전부입니다. 깊이를 매개변수로 넘겨 들여쓰기를 만듭니다.

java
public class Node {                       // 파일/폴더 흉내
    String name; int size; Node[] children;
    Node(String name, int size, Node... children) { ... }
}

static void printTree(Node n, int depth) {
    System.out.println("  ".repeat(depth) + n.name);
    for (Node child : n.children) printTree(child, depth + 1);
}
static int totalSize(Node n) {
    int sum = n.size;
    for (Node child : n.children) sum += totalSize(child);
    return sum;
}

Node root = new Node("root", 0,
        new Node("docs", 0, new Node("a.txt", 10), new Node("b.txt", 20)),
        new Node("src", 0, new Node("Main.java", 5)));
printTree(root, 0);
// 출력:
// root
//   docs
//     a.txt
//     b.txt
//   src
//     Main.java
System.out.println("total=" + totalSize(root));   // 출력: total=35

주의: 파일이 없는 폴더(children.length == 0)가 기저 조건입니다. 명시적 if가 없어도 for문이 0번 돌면서 재귀가 멈춥니다.

패턴 15: 스택 오버플로우 한계와 반복문 전환

재귀 깊이가 수만을 넘으면 스택이 가득 차 StackOverflowError가 납니다. 이것은 예외(Exception)가 아니라 오류(Error)라서 보통 잡지 않지만, 실험을 위해 잡아 봅니다. 같은 일을 반복문으로 바꾸면 깊이 제한이 없습니다.

java
static int countDownRec(int n) { return n == 0 ? 0 : countDownRec(n - 1); }
static int countDownLoop(int n) { while (n > 0) n--; return n; }

System.out.println(countDownRec(1000));          // 출력: 0
try {
    countDownRec(10_000_000);
} catch (StackOverflowError e) {
    System.out.println("StackOverflowError");    // 출력: StackOverflowError
}
System.out.println(countDownLoop(10_000_000));   // 출력: 0

주의: 한계 깊이는 JVM 옵션(-Xss)과 프레임 크기에 따라 다릅니다(보통 1만~10만). "재귀 깊이 = 입력 크기"인 알고리즘은 반복문이나 명시적 스택(ArrayDeque)으로 바꿉니다.

패턴 16: static 유틸 메서드 vs 인스턴스 메서드

clamp는 입력만으로 결과가 정해지고 아무 상태도 안 건드리므로 static입니다. Counter.increment는 객체마다 다른 count를 바꾸므로 인스턴스 메서드입니다. 이 구분이 흐릿하면 "이 메서드는 어느 객체의 값을 바꾸는가"를 알 수 없는 코드가 됩니다.

java
public class MathUtils {
    static int clamp(int v, int min, int max) {
        if (v < min) return min;
        if (v > max) return max;
        return v;
    }
}
public class Counter {
    private int count;
    void increment() { count++; }
    int value() { return count; }
}

System.out.println(MathUtils.clamp(15, 0, 10));   // 출력: 10
Counter c1 = new Counter();
Counter c2 = new Counter();
c1.increment(); c1.increment(); c2.increment();
System.out.println(c1.value() + " " + c2.value());   // 출력: 2 1

주의: static 메서드가 static 필드를 바꾸기 시작하면 "전역 변수"가 됩니다. 테스트하기 어렵고 멀티스레드에서 위험합니다. 상태가 있으면 객체로 만드세요.

패턴 17: 긴 main → 작은 메서드로 분리

main에 평균, 최고점, 합격자 계산이 한 덩어리로 있으면 읽기도 고치기도 어렵습니다. 각 계산을 이름 있는 메서드로 빼면 printReport는 "무엇을 하는지"만 남고 "어떻게"는 각 메서드로 숨습니다. 이것이 리팩터링의 가장 기본인 메서드 추출(extract method)입니다.

java
static void printReport(int[] scores) {
    System.out.println("평균: " + Stats.average(scores));
    System.out.println("최고: " + Stats.max(scores));
    System.out.println("합격: " + countPassed(scores, 70) + "명");
}
static int countPassed(int[] scores, int cut) {
    int n = 0;
    for (int s : scores) if (s >= cut) n++;
    return n;
}

printReport(new int[]{88, 92, 75, 60, 95});
// 출력:
// 평균: 82.0
// 최고: 95
// 합격: 4명

주의: 분리 기준은 "이 조각에 이름을 붙일 수 있는가"입니다. 이름이 안 떠오르면 아직 한 덩어리인 것이고, 이름에 And가 들어가면 둘로 나눠야 합니다.

패턴 18: 조기 반환(early return) / 가드 절(guard clause)

예외적인 입력(null, 범위 밖)을 메서드 맨 앞에서 걸러내고 바로 반환하면 본문의 들여쓰기가 얕아지고 정상 흐름이 잘 보입니다. if-else if-else가 깊게 중첩되는 것을 막는 가장 쉬운 방법입니다.

java
static int discountRate(String grade, int amount) {
    if (grade == null) return 0;          // 가드 절
    if (amount < 10_000) return 0;        // 가드 절
    if (grade.equals("VIP")) return 20;
    return 10;
}

System.out.println(discountRate(null, 0));           // 출력: 0
System.out.println(discountRate("VIP", 50_000));     // 출력: 20
System.out.println(discountRate("NORMAL", 50_000));  // 출력: 10
System.out.println(discountRate("NORMAL", 5_000));   // 출력: 0

주의: "메서드에 return은 하나만"이라는 옛 규칙은 자바에서 지키지 않아도 됩니다. 단, 가드 절 뒤에 정리 작업이 필요하면(파일 닫기 등) try-finally가 필요합니다.

패턴 19: 메서드 명명 규칙

메서드 이름은 동사로 시작하고, 결과가 boolean이면 is/has/can으로 시작해 if (isEmpty(s))처럼 문장으로 읽히게 합니다. 변환 메서드는 toXxx, 가져오는 것은 getXxx/findXxx, 계산은 calculateXxx/computeXxx가 관례입니다.

java
static boolean isEmpty(String s) { return s == null || s.length() == 0; }
static boolean hasDigit(String s) {
    for (char c : s.toCharArray()) if (Character.isDigit(c)) return true;
    return false;
}
static boolean canDivide(int a, int b) { return b != 0; }
static String toSlug(String title) { return title.toLowerCase().replace(' ', '-'); }

System.out.println(isEmpty(""));                  // 출력: true
System.out.println(hasDigit("abc1"));             // 출력: true
System.out.println(canDivide(10, 0));             // 출력: false
System.out.println(toSlug("Hello World Java"));   // 출력: hello-world-java

주의: check(), process(), handle(), doIt()은 무엇을 하는지 알 수 없는 이름입니다. 이름을 못 짓겠으면 메서드가 너무 많은 일을 하는 것입니다.

패턴 20: 실무 예제 — 입력 검증 유틸

사용자 입력 검증은 어디서나 필요하고, 같은 규칙을 여러 곳에서 씁니다. static 메서드로 모아 두면 규칙이 한 곳에서 관리됩니다. boolean을 돌려주는 것과 메시지를 돌려주는 것을 구분합니다.

java
public class Validator {
    static boolean isValidEmail(String s) {
        if (s == null) return false;
        int at = s.indexOf('@');
        return at > 0 && s.indexOf('.', at) > at + 1;
    }
    static boolean isInRange(int v, int min, int max) { return v >= min && v <= max; }
    static String requireNonBlank(String s, String field) {
        if (s == null || s.isBlank()) return field + "은 필수입니다";
        return "OK";
    }
}

System.out.println(Validator.isValidEmail("a@b.com"));        // 출력: true
System.out.println(Validator.isValidEmail("a.b.com"));        // 출력: false
System.out.println(Validator.isInRange(5, 1, 10));            // 출력: true
System.out.println(Validator.requireNonBlank("  ", "name"));  // 출력: name은 필수입니다
System.out.println(Validator.requireNonBlank("Kim", "name")); // 출력: OK

주의: 실제로는 requireNonBlank가 문자열 대신 예외를 던지는 것이 일반적입니다(중급 03 레슨). 초급에서는 반환값으로 결과를 알리는 방식으로 시작합니다.

패턴 21: 실무 예제 — 문자열 포맷 헬퍼

금액에 콤마, 번호 앞에 0 채우기, 퍼센트, 마스킹은 어느 프로그램에나 나옵니다. String.format과 repeat을 감싸 이름 있는 메서드로 만들어 두면 포맷 규칙이 바뀌어도 한 곳만 고칩니다.

java
public class Fmt {
    static String money(long won) { return String.format("%,d원", won); }
    static String padLeft(String s, int width, char pad) {
        StringBuilder sb = new StringBuilder();
        for (int i = s.length(); i < width; i++) sb.append(pad);
        return sb.append(s).toString();
    }
    static String percent(double ratio) { return String.format("%.1f%%", ratio * 100); }
    static String mask(String phone) {
        return phone.substring(0, 7) + "*".repeat(phone.length() - 7);
    }
}

System.out.println(Fmt.money(1234567));           // 출력: 1,234,567원
System.out.println(Fmt.padLeft("42", 6, '0'));    // 출력: 000042
System.out.println(Fmt.percent(0.256));           // 출력: 25.6%
System.out.println(Fmt.mask("01012345678"));      // 출력: 0101234****

주의: mask는 7자 미만 입력에서 예외가 납니다. 실무 코드라면 가드 절(패턴 18)을 추가해야 합니다.

패턴 22: 실무 예제 — 배열 통계 메서드 모음

작은 메서드들을 모아 두고 서로 호출하게 하면(average가 sum을 사용) 각 메서드가 짧아지고, 하나를 고치면 나머지에 자동으로 반영됩니다. "못 찾으면 -1"처럼 실패를 반환값으로 표현하는 관례도 익혀 둡니다.

java
public class Stats {
    static int sum(int[] a) { int s = 0; for (int v : a) s += v; return s; }
    static double average(int[] a) { return a.length == 0 ? 0 : (double) sum(a) / a.length; }
    static int max(int[] a) { int m = a[0]; for (int v : a) if (v > m) m = v; return m; }
    static int indexOf(int[] a, int target) {
        for (int i = 0; i < a.length; i++) if (a[i] == target) return i;
        return -1;
    }
    static int countIf(int[] a, int threshold) { int n = 0; for (int v : a) if (v >= threshold) n++; return n; }
}

int[] data = {3, 9, 1, 7, 5};
System.out.println(Stats.sum(data));           // 출력: 25
System.out.println(Stats.average(data));       // 출력: 5.0
System.out.println(Stats.max(data));           // 출력: 9
System.out.println(Stats.indexOf(data, 7));    // 출력: 3
System.out.println(Stats.indexOf(data, 100));  // 출력: -1
System.out.println(Stats.countIf(data, 5));    // 출력: 3

주의: average에서 (double) 캐스팅이 빠지면 정수 나눗셈이 되어 5.0 대신 5가 잘립니다. max는 빈 배열에서 예외가 납니다.

패턴 23: boolean 메서드를 조합해 규칙 만들기

복잡한 조건은 작은 boolean 메서드로 쪼개고 조합합니다. "2월의 일수"는 "윤년인가"에 의존하는데, isLeapYear를 따로 두면 규칙이 한 곳에 모이고 이름으로 의도가 읽힙니다.

java
static boolean isLeapYear(int y) { return (y % 4 == 0 && y % 100 != 0) || y % 400 == 0; }
static int daysInMonth(int y, int m) {
    if (m == 2) return isLeapYear(y) ? 29 : 28;
    if (m == 4 || m == 6 || m == 9 || m == 11) return 30;
    return 31;
}

System.out.println(isLeapYear(2024));      // 출력: true
System.out.println(isLeapYear(1900));      // 출력: false
System.out.println(daysInMonth(2024, 2));  // 출력: 29

주의: 조건식을 인라인으로 풀어 쓰면(if ((y % 4 == 0 && ...) ...)) 같은 규칙이 여러 곳에 복사됩니다.

패턴 24: 메서드 조합으로 큰 문제 풀기

"회문(palindrome)인가"는 "문자와 숫자만 남기고 소문자로 만든 뒤, 뒤집은 것과 같은가"입니다. 이 문장의 각 동사가 메서드 하나씩입니다. 상위 메서드는 하위 메서드를 부르기만 하므로 문장처럼 읽힙니다.

java
static boolean isPalindrome(String s) {
    String clean = normalize(s);
    return clean.equals(reverse(clean));
}
static String normalize(String s) {
    StringBuilder sb = new StringBuilder();
    for (char c : s.toCharArray())
        if (Character.isLetterOrDigit(c)) sb.append(Character.toLowerCase(c));
    return sb.toString();
}
static String reverse(String s) { return new StringBuilder(s).reverse().toString(); }

System.out.println(isPalindrome("A man, a plan, a canal: Panama"));   // 출력: true
System.out.println(isPalindrome("hello"));                            // 출력: false

주의: normalize, reverse는 다른 곳에서도 재사용됩니다. 한 메서드 안에 다 넣었다면 재사용은 불가능했을 것입니다.

패턴 25: 반환값으로 성공/실패 알리기

상태를 바꾸는 메서드가 "성공했는지"를 boolean으로 알려 주면 호출자가 다음 행동을 정할 수 있습니다. 재고 차감이 대표적입니다. 실패 시에는 상태를 건드리지 않는 것이 중요합니다.

java
static boolean take(int[] stock, int qty) {
    if (qty > stock[0]) return false;     // 실패: 상태 변경 없음
    stock[0] -= qty;
    return true;
}

int[] stock = {5};
System.out.println(take(stock, 3) + " 남은 " + stock[0]);   // 출력: true 남은 2
System.out.println(take(stock, 3) + " 남은 " + stock[0]);   // 출력: false 남은 2

주의: 호출자가 반환값을 무시하면(take(stock, 3);) 실패를 모르고 지나갑니다. 실패를 무시하면 안 되는 상황은 예외(중급 03)로 바꿉니다.

패턴 모음
  • 패턴 1: 시그니처 해부 — void와 반환형
  • 패턴 2: 원시 값 전달 — 메서드 안에서 바꿔도 원본은 그대로
  • 패턴 3: 배열 참조 전달 — 요소 변경은 반영, 재대입은 무시
  • 패턴 4: 문자열 불변 — 반드시 반환값을 받아야 한다
  • 패턴 5: 여러 값 반환 — 배열
  • 패턴 6: 여러 값 반환 — record
  • 패턴 7: 가변 인자(varargs)
  • 패턴 8: 가변 인자 + 일반 매개변수
  • 패턴 9: 오버로딩 기본
  • 패턴 10: 오버로딩 선택 — 자동 형변환 우선순위
  • 패턴 11: 오버로딩 모호성 — null과 두 후보
  • 패턴 12: 재귀 — 팩토리얼
  • 패턴 13: 재귀 vs 반복 — 피보나치
  • 패턴 14: 재귀 — 트리(디렉터리) 탐색
  • 패턴 15: 스택 오버플로우 한계와 반복문 전환
  • 패턴 16: static 유틸 메서드 vs 인스턴스 메서드
  • 패턴 17: 긴 main → 작은 메서드로 분리
  • 패턴 18: 조기 반환(early return) / 가드 절(guard clause)
  • 패턴 19: 메서드 명명 규칙
  • 패턴 20: 실무 예제 — 입력 검증 유틸
  • 패턴 21: 실무 예제 — 문자열 포맷 헬퍼
  • 패턴 22: 실무 예제 — 배열 통계 메서드 모음
  • 패턴 23: boolean 메서드를 조합해 규칙 만들기
  • 패턴 24: 메서드 조합으로 큰 문제 풀기
  • 패턴 25: 반환값으로 성공/실패 알리기
이전 섹션2 핵심 원리3 / 6다음 섹션4 자주 하는 실수 (Tip)