[Java] main 메소드 뽀개기

lee_choonghee·2021년 9월 11일
7

Java

목록 보기
1/2
post-thumbnail

발단

자바 책을 펼쳤다. Hello World 예제가 나오면서 main 메소드는 이렇게 생겨먹은거니 일단 넘어가란다. 내가 펼친 책은 자바를 처음 배우는 사람을 위해 쓰여졌지만, 나는 처음 배우는 사람이 아니므로 "그러라 그래~" 식으로 넘어가면 안될 것 같았다. 보통은 프로그래밍 공부하면서 통하지 않는 방법이다. 하지만 이번에는 "그냥 그래라" 하긴 해야한다.

어떻게 생겨먹었나

public class Main {
    public static void main(String[] args) {
        System.out.println("Hello World");
    }
}

psvm으로 외워보고 public static void main(String[] args)라고도 외워보고 자바를 처음 배울때 왜 이렇게 안 외워졌는지 모르겠다. 그런데 한가지 표현법이 더 있다. 별건 아니고 가변인자(variable arguments; varargs)를 사용하는 것이다.

public static void main(String... args)

저렇게 적어두면 실행해보지 않아도 IDE가 먼저 반응할 것이다.

public

흔히 알고있는 접근 제어자(access modifier) public이다. 어디에서나 누구나 main 메서드에 접근 가능하다는 뜻이라는 것은 대부분 알 것이다. 그럼 왜 꼭 public 이어야할까? 사실 JVM을 만드는 사람이 어떤 접근 제어자에 상관없이 하려면 그렇게 만들 수 있을 것이다. 하지만 public이어야 한다고 JVM 코드에 못박혀져있다.

JVM에서의 main 메소드 호출

나도 public에 대해 쓰려다가 JVM에서 main 메소드가 어떻게 호출되는지 쓰게될지 전혀 몰랐다 🤣. OpenJDK7의 java.c라는 파일에 적힌 내용을 살펴보았다. C언어를 잘 몰라도 고수님들이 함수이름을 예쁘게 적어두었기 때문에 흐름을 파악하는 수준에서는 큰 무리가 없었다. 읽기 쉬운 코드 작성의 필요성을 다시 한 번 느낀다.

// 중간에 생략된 코드가 많습니다.

// main 메소드가 있는 클래스를 로딩한다.
mainClassName = GetMainClassName(env, jarfile);
mainClass = LoadClass(env, classname);

// main 메소드의 아이디를 찾는다.
mainID = (*env)->GetStaticMethodID(env, mainClass, "main", "([Ljava/lang/String;)V");

/*
  mainClass, mainID를 가지고 
  java.lang.reflect.Method 객체로 변환한다.
  JNI_TRUE면 static 메소드를 찾는다.
*/
jbject obj = (*env)->ToReflectedMethod(env, mainClass, mainID, JNI_TRUE);

/*
  위에서 찾은 Method 객체를 이용하여 
  getModifiers라는 메소드의 아이디를 찾는다.
*/
mid = (*env)->GetMethodID(env, (*env)->GetObjectClass(env, obj), "getModifiers", "()I");

// getModifiers 메소드를 이용하여 
// main 메소드의 접근지시자를 알아낸다.
// public이 아니면 안된다!
mods = (*env)->CallIntMethod(env, obj, mid);
if ((mods & 1) == 0) { /* if (!Modifier.isPublic(mods)) ... */
    message = "Main method not public.";
    messageDest = JNI_TRUE;
    goto leave;
}

// String[] args를 가져온다.
mainArgs = NewPlatformStringArray(env, argv, argc);

// 드디어 실행시킨다!
(*env)->CallStaticVoidMethod(env, mainClass, mainID, mainArgs);

OpenJDK7 이라 미안하다. 여기 코드가 하나의 파일에 모여있어서 찾아보기 쉬워서 그랬다... 시간이 되면 11버전으로 업데이트를 해보겠다.

다시 public

JVM이 main 메소드는 public이라고 강제한다는 것까지 알아보았다. 그럼 실제로 다른 접근 지시자를 적으면 어떻게 될까?

private static void main(String[] args) {
    System.out.println("Hello World");
}

private 키워드로 실행해보면...

Error: Main method not found in class com.choonghee.MainMethod, please define the main method as:
   public static void main(String[] args)
or a JavaFX application class must extend javafx.application.Application

main 메소드를 찾을 수 없다고 에러를 발생시킨다! 다른 접근 지시자들도 마찬가지의 에러를 뱉어낸다. 물론, 실행전에 IDE가 먼저 반응할 것 이다. 👅

static

static 키워드는 인스턴스 생성없이 메서드를 호출할 수 있음을 의미한다. JVM 코드에서 살펴봤듯이 main 메소드 실행시 JNI_TRUE로 static 메소드를 찾는다. 이건 납득이 간다. 굳이 메모리 공간 차지해가면서 main 메소드를 실행시킬 인스턴스를 만들 이유가 없기 때문이다.

이번에는 static 키워드를 없애고 실행해봤다.

Error: Main method is not static in class com.choonghee.MainMethod, please define the main method as:
   public static void main(String[] args)

main 메소드에 static 키워드가 없다고 에러를 발생시킨다.

void

void는 메소드의 리턴 값이 없음을 의미한다. JVM이 CallStaticVoidMethod 함수를 호출시킨다. 그러므로 void 키워드여야 한다. 이것도 이해가 간다. 자바는 메소드에 리턴 타입이 있을 때 암묵적으로 값을 알아서 넘겨주는 언어가 아니다. 그리고 프로그램이 끝나는 마당에 리턴값을 넘겨줄 필요가 없다. 물론 정상적인 종료를 판단하기 위해 int를 리턴하는 C같은 언어도 있다.

리턴 타입을 다른 타입으로 변경하고 실행해봤다.

Error: Main method must return a value of type void in class com.choonghee.MainMethod, please 
define the main method as:
   public static void main(String[] args)

main 메소드의 리턴 타입을 void로 해달라고 에러를 발생시킨다.

main

JVM이 main 메소드 아이디를 찾는 부분에서 메소드명 인수에 "main"이라고 하드 코딩이 되어있다. 이것은 Java가 C언어의 영향을 받았기 때문에 협상의 여지가 없다.

여담이지만 C언어는 B언어의 영향을 받았다. B언어 튜토리얼을 보면, 프로그램의 시작은 main 부터 시작이라고 적혀있다.

-- General Layout of B Programs -- 

main( ) {
-- statements --
}
newfunc(arg1, arg2) {
-- statements --
}
fun3(arg) {
-- more statements --
}

main이라는 이름 대신 다른 이름을 적으면, main 메서드를 찾을 수 없다는 에러가 발생한다.

String[] args

커맨드 라인의 argument들을 의미한다. JVM이 main 메소드의 변수명을 강제하지 않으니 변수명은 마음대로 바꿔도 잘 동작한다. JVM의 main 함수에서 arguments들을 가져와서 main 메소드까지 여러 과정을 거쳐서 보내준다.

// 여기서 커맨드 라인의 인자들을 받아온다.
int main(int argc, char ** argv)

/*
struct JavaMainArgs {
  int     argc;
  char ** argv;
  char *  jarfile;
  char *  classname;
  InvocationFunctions ifn;
};
*/

// JavaMain 함수를 호출하기 위해 인자를 세팅한다.
struct JavaMainArgs args;
args.argc = argc;
args.argv = argv;
args.jarfile = jarfile;
args.classname = classname;
args.ifn = ifn;

// JavaMain을 새로운 스레드에서 호출한다.
ContinueInNewThread(JavaMain, threadStackSize, (void*)&args);

// JavaMain이다.
int JNICALL JavaMain(void * _args)

// 문자열 배열로 변신!
int argc = args->argc;
char **argv = args->argv;
mainArgs = NewPlatformStringArray(env, argv, argc);

역시나 JVM이 문자열 배열이여야 한다고 했기 때문에 String[]여야 한다.

다른 타입으로 바꿔보고 실행해봤다.

Error: Main method not found in class com.choonghee.MainMethod, please define the main method as:
   public static void main(String[] args)
or a JavaFX application class must extend javafx.application.Application

main 메소드를 알아보지 못 한다!

결론

제목에 [Java]라고 적어놨지만 C 코드를 더 많이 봤다. main 메소드에 대한 나의 결론은 JVM이 "그러라 그래"라고 말해주는 것이다. 왜 JVM이 그렇게 동작하도록 만들었는지 이유를 찾아서 알려주고 싶지만 고슬링 선생님을 만나봐야 알 것 같다. 답장을 줄지는 모르겠지만 DM을 한 번 날려보겠다. 응답이 있다면 다시 블로깅을 해보겠다!! I miss you ~ 💪

DM 보냈습니다. 답장 기다려봅니다!

참고한 자료

profile
팔로우 기능 생기면 팔로우 당하고 싶다

4개의 댓글

comment-user-thumbnail
2021년 9월 15일

와 이거 진짜 궁금했지만 접근자 리턴값, 클래스, 원래 그런거야,
그러고 넘어갔는데 이유랑 실험을 너무 예쁘게 잘해주셨네요!!
자바 처음에 써보는 사람이면 너무너무 궁금한 내용인데 감사합니다

1개의 답글
comment-user-thumbnail
3일 전

고슬링 형님 사진을 보니, 뽕이 차올라서 하트 박고 갑니닼ㅋㅋ

1개의 답글