[Java] 06. 함수 사용법(함수의 오버로딩과 재귀적 방법에 대한 설명)


Study/Java  2020. 5. 5. 10:46

안녕하세요. 명월입니다.


이 글은 Java에서의 함수 사용법(함수의 오버로딩과 재귀적 방법에 대한 설명)에 대한 글입니다.


모든 프로그램 언어에서 함수 사용법은 문법적인 차이가 있지만 사용하는 방법이나 개념에 대해서는 C언어 이후의 프로그램 언어는 모두 비슷합니다.

프로그램의 언어에서 함수는 두가지 개념으로 접근을 합니다.

첫번째는 함수는 변수와 달리 메모리 할당의 접근이 없고 단순한 처리의 개념만 있기 때문에, main함수에서 시작하는 프로그램 설계를 좀 더 보기 좋게 나누는 역할을 합니다.

두번째는 수학적 함수와 같은 개념으로 「f(x) = x + 1」 처럼 함수의 목적을 분명히 해서 「f(1) = 2, f(2) = 3」처럼 함수와 파라미터의 조합으로 하나의 이항 관계를 만들어 프로그램의 정의를 단순하게 합니다.


먼저 자바에서 함수의 사용법은 다음과 같습니다.

[접근제한자] [반환 타입] 메소드 이름 (파라미터 정의) {
  ........
  return 반환 타입;
}

접근 제한자는 함수의 접근 설정인데 이는 함수의 기능에 대한 내용보다 클래스의 캡슐화와 관계가 있으니 따로 글을 작성해서 설명하겠습니다.

함수를 정의하는 것은 메소드 이름이 가장 중요하지만, 자바의 오버로딩 기능으로 이 메소드 이름 + 파라미터 타입이 묶여서 함수를 정의합니다.

오버 로딩은 조금 후에 자세히 설명하고 일단 함수는 반환 타입(데이터 타입), 메소드 이름, (파라미터 정의)의 구조로 작성이 되고 반환 타입이 void(반환값이 없음)가 아닌 이상 반환 타입과 함수 내의 return 타입은 일치해야 합니다.

public class Example {
  // 실행 함수
  public static void main(String... args) {
    // 파라미터에 사용될 두 변수
    int a = 50;
    int b = 10;

    // 더하기 함수에 두 변수를 넣었다.
    // add(50, 10) = 60
    int c = add(a, b);
    // 콘솔 출력
    System.out.println("50 + 10 = " + c);

    // 빼기 함수에 두 변수를 넣었다.
    // sub(50, 10) = 40
    int d = sub(a, b);
    // 콘솔 출력
    System.out.println("50 - 10 = " + d);

    // 곱하기 함수에 두 변수를 넣었다.
    // multi(50, 10) = 500
    int e = multi(a, b);
    // 콘솔 출력
    System.out.println("50 * 10 = " + e);

    // 나누기 함수에 두 변수를 넣었다.
    // divi(50, 10) = 5
    int f = divi(a, b);
    // 콘솔 출력
    System.out.println("50 / 10 = " + f);
  }

  // main에서 호출할 수 있는 함수는 static이 붙어 있는 함수 뿐이다.
  // 반환형은 int, 함수 이름은 add, 파라미터는 int형 타입의 두개
  // 더하기 함수
  public static int add(int a, int b) {
    // 두 개의 파라미터의 값을 더하고 반환한다.
    return a + b;
  }

  // 반환형은 int, 함수 이름은 add, 파라미터는 int형 타입의 두개
  // 빼기 함수
  public static int sub(int a, int b) {
    // 두 개의 파라미터의 값을 빼고 반환한다.
    return a - b;
  }

  // 반환형은 int, 함수 이름은 add, 파라미터는 int형 타입의 두개
  // 곱하기 함수
  public static int multi(int a, int b) {
    // 두 개의 파라미터의 값을 곱하고 반환한다.
    return a * b;
  }

  // 반환형은 int, 함수 이름은 add, 파라미터는 int형 타입의 두개
  // 나누기 함수
  public static int divi(int a, int b) {
    // 두 개의 파라미터의 값을 나누고 반환한다.
    return a / b;
  }
}

만약 위의 처리식을 따로 add나 sub를 함수를 사용하지 않고 main 함수에서 두개의 변수를 a + b나 a - b로 계산하면 굳이 함수를 사용하지 않고도 프로그램을 만들 수 있습니다.

위의 예제대로 라면 굳이 함수를 만들 필요가 없습니다.

public class Example {
  // 실행 함수
  public static void main(String... args) {
    // 물건이 a, b, c, d가 있다.
    // a의 가격은 1000원
    int a = 1000;
    // b의 가격은 2000원
    int b = 2000;
    // c의 가격은 1500원
    int c = 1500;
    // d의 가격은 1300원
    int d = 1300;
    // 결과 변수
    int ret = 0;
    // a, c, d를 구매 했을 때, 세금 포함한 가격을 계산.
    ret = cal(a, c, d);
    // 콘솔 출력
    System.out.println("a + c + d = " + ret);
    // a, c를 구매 했을 때, 세금 포함한 가격을 계산.
    ret = cal(a, c);
    // 콘솔 출력
    System.out.println("a + c = " + ret);
    // b, c, d를 구매 했을 때, 세금 포함한 가격을 계산.
    ret = cal(b, c, d);
    // 콘솔 출력
    System.out.println("b + c + d = " + ret);
  }
  // 가변 파라미터는 ...으로 표시한다.
  // 가변 파라미터는 파라미터의 개수가 정해지지 않은 것이다.
  // 함수를 호출하는 곳에서는 int형 파라미터를 1개를 넣어도 되고 2개를 넣어도 된다.
  // 함수 내부에서는 배열로 취급한다.
  // 함수는 int형으로 물건의 가격을 모두 더한 뒤 10퍼센트의 세금을 붙여서 총 가격(세금 포함)을 계산하는 함수이다.
  public static int cal(int... data) {
    // 총액의 변수
    int sum = 0;
    // 입력된 파라미터를 모두 더한다.
    for (int i = 0; i < data.length; i++) {
      sum += data[i];
    }
    // 세금은 총 금액의 10퍼센트
    int tax = sum / 10;
    // 총액 + 세금
    return sum + tax;
  }
}

위처럼 조금 복잡한 계산을 함수식으로 바꾸면 꽤 편리해집니다. cal이라는 함수는 가변의 파라미터의 가격이 들어오면 세금 10퍼센트의 계산까지 처리해서 결과를 반환하는 함수입니다.

만약 이 함수의 내용도 main에 넣고 계산을 하면 가능합니다. 단지 똑같은 계산식이 main에 작성되어서 함수가 엄청 길어지겠네요. 이건 수학의 인수 분해와 치환성과 관계가 있는 것입니다.

즉, main함수에서 a, b, c, d의 변수 선언 후에 a, c, d의 총액 합산 + 세금+ 콘솔 출력, a,c의 총액 합산 + 세금+ 콘솔 출력, b,c,d의 총액 합산 + 세금+ 콘솔 출력의 처리가 될 것을 총액 합산과 + 세금을 함수로 떼어 냈습니다.

그런 후, a, b, c, d의 변수 선언 후에 함수(a, c, d) + 콘솔 출력, 함수(a, c) + 콘솔 출력, 함수(a,c,d) + 콘솔 출력, 마지막으로 함수(x) = 총액 합산과 + 세금으로 인수 분해와 치환을 한 것입니다.

이것이 좀 더 복잡해지는 프로그램으로 갈 수록 함수의 재사용성과 가독성은 더욱 중요하게 될 것입니다. 모든 계산식을 main에 작성을 해도 프로그램은 작동됩니다. 하지만 수정을 하려고 한다면 엄청난 공수와 사람이 이해하기 힘든 매우 복잡한 프로그램이 됩니다.


함수의 오버로딩은 제가 함수를 정의할 때 함수의 이름과 파라미터로 구분한다고 했습니다.

그래서 함수의 이름은 모두 같은 데, 파라미터를 다르게 해도 함수는 선언이 가능합니다. 이것을 프로그램에서 오버로딩, 즉 다형성이라고 합니다. 이 다형성은 OOP(객체 지향)의 4대 원칙 중 하나입니다.

public class Example {
  // 실행 함수
  public static void main(String... args) {
    // 함수 호출
    func(1);
    func(1, 2);
    func(1, 2, "test");
  }
  // 함수의 이름은 func, 파라미터는 int형 타입 한개
  public static void func(int p1) {
    // 콘솔 출력
    System.out.println("func parameter count - 1, param = " + p1);
  }
  // 함수의 이름은 func, 파라미터는 int형 타입 두개
  public static void func(int p1, int p2) {
    // 콘솔 출력
    System.out.println("func parameter count - 2, param = " + p1 + "," + p2);
  }
  // 함수의 이름은 func, 파라미터는 int형 타입 두개, String형 타입 한개
  public static void func(int p1, int p2, String p3) {
    // 콘솔 출력
    System.out.println("func parameter count - 3, param = " + p1 + "," + p2 + "," + p3);
  }
}

확실히 func의 함수 이름으로 3개의 함수가 작성되었습니다. 그러나 파라미터 개수와 타입은 다릅니다. 그렇다 보니 main에서 호출하는 함수명은 같아도 파라미터의 형태에 따라 호출되는 함수는 다르다는게 확인이 됩니다.


함수 안에서 호출된 함수를 다시 호출 할 수도 있습니다. 이를 함수의 재귀적 용법이라고 합니다.

public class Example {
  // 실행 함수
  public static void main(String... args) {
    // 콘솔 출력
    System.out.println("factorial - 10! ");
    // 팩토리얼 factorial(10)의 계산
    int ret = factorial(10);
    // 콘솔 출력
    System.out.print(" = " + ret);
    System.out.println();
  }
  // 팩토리얼 함수
  public static int factorial(int param) {
    // 팩토리얼은 1보다 아래의 수라면 에러가 발생한다.
    if (param < 1) {
      return -1;
    }
    // 1! = 1이다.
    if (param == 1) {
      // 콘솔 출력
      System.out.print(param);
      // 1을 반환
      return 1;
    }
    // 콘솔 출력
    System.out.print(param + " + ");
    // f(x) = f(x-1) + x가 팩토리얼 수학적 함수식, 을 고대로 프로그램에 가져왔다.
    return factorial(param - 1) + param;
  }
}

팩토리얼은 「10! = 9! + 10」이고 「9! = 8! + 9」라는 수학적 함수식이 있습니다.(결국에는 수학이 약하면 프로그램의 수학적 계산이 힘들어 집니다.)

즉, f(x) = f(x-1) + x의 식이 나오는데 이를 그대로 함수에 적용하면 됩니다. 여기서 함수식 안에 다시 함수를 부르는데 이것을 재귀적 함수 호출식이라고 합니다.

일반적으로 함수를 호출할 때는 stack interupt가 발생하는 데, 이는 성능상에 치명적인 역할을 합니다.


즉, 탐색이나 수학적 반복성을 계산할 때는 재귀 함수만큼 편하고 쉽게 작성할 수 있는 게 없기는 하지만, 성능에 치명적이므로 가능하면 함수의 재귀적 용법보다는 되도록 for문을 통한 제어문을 통해 계산식을 만드는 것이 더 좋습니다.

public class Example {
  // 실행 함수
  public static void main(String... args) {
    // 콘솔 출력
    System.out.println("factorial - 10! ");
    // 팩토리얼 factorial(10)의 계산
    int ret = factorial(10);
    // 콘솔 출력
    System.out.print(" = " + ret);
    System.out.println();
  }
  // 팩토리얼 함수
  public static int factorial(int param) {
    // 결과 변수
    int sum = 0;
    for (int i = param; i > 0; i--) {
      // 모두 더한다.
      sum += i;
      // 콘솔 출력
      System.out.print(i);
      if (i != 1) {
        // 콘솔 출력
        System.out.print(" + ");
      }
    }
    // 결과 리턴
    return sum;
  }
}

이렇게 보면 함수의 재귀가 필요 없을 것 같지만, 탐색 알고리즘에서는 반대로 for문으로 작성하면 알고리즘Big-O에 의해 복잡도가 엄청 올라가고 속도가 기하급수적으로 느려지기 때문에 이럴 때는 재귀가 더 빠른 경우도 많습니다.

즉, 사양에 따라 재귀를 사용할 것인지 반복문 for문을 사용할 것인지 결정을 해야 합니다.


여기까지 Java에서의 함수 사용법(함수의 오버로딩과 재귀적 방법에 대한 설명)에 대한 글이었습니다.


궁금한 점이나 잘못된 점이 있으면 댓글 부탁드립니다.