자바에서 main method의 문제점에 대해서 까발려 봅시다.

2013-01-05 17:23

흔히 자바를 시작할 때 아무 생각 없이 main method를 활용해 테스트 하잖아요. 예를 들어 사칙 연산을 하는 계산기 코드를 구현했다고 해봅시다.

위와 같이 사칙 연산을 담당하는 Production Code와 이 코드를 테스트 하는 Test Code로 나눌 수 있겠죠. 여기서 main method를 활용해 위와 같이 테스트할 때 발생할 수 있는 모든 문제점을 함 찾아 본다면 어떤 것이 있을까요?

위 코드는 상당히 작은 분량의 코드인데 기본적인 구조는 위와 같다고 가정하고 복잡도가 있는 실무 프로젝트에서 main method를 활용해 테스트할 때 발생할 수 있는 문제점을 찾는다고 생각하면 됩니다.

뭐 많이 찾는다고 상품은 없고, 그냥 같이 함 찾아보자는 겁니다.

BEST 의견 원본위치로↓
2013-01-07 03:49

음... 보는 관점에 따라 여러가지 의견이 나오겠지만... Test 시에 main method를 쓰는건, 습관이 덜 들어간 것 정도로 밖에는 안보이네요. 뭐, 그거 쓸 수도 있죠. (ㅎㅎ)

예시로 들어주신 코드만 놓고 봤을 때는 크게 두가지 문제가 있어 보입니다.

  • System.out.println() 관련 문제
  • 눈(?!)으로 테스트 하는 문제

(그러고 보니, 둘 다 main()하고는 관련이 없네요. Standalone Java는 기본적으로 main()을 사용합니다. main()이 뭔 죄겠어요. 잘못 짠 사람 잘못이지... JUnit Runner 에도 main() 메소드가 있고, Tomcat Bootstrapper 에도 main()은 있는데요 뭐...)

일단, 이해가 쉬운 2번 부터 보자면, 굉장히 부정확하고 위험하다는 부분에는 기본적으로 동의 할 겁니다. 결과 값을 눈으로 확인한다는 건데요, 이는 답을 알고 있는 상황에서만 맞는 결과가 나옵니다. 기본적으로 현대 프로그래밍은 혼자 작업하지 않으며, 따라서 눈으로 테스트 할 경우 해당 메소드의 기대값과 결과값을 검증하기 어렵습니다. *Unit 계열의 테스트를 제가 권장하는 이유 중 하나는, "기대값과 결과값이 하나의 묶음으로 제공되어, 해당 메소드의 수행 결과를 예측 가능하다"입니다. 프로그래밍에서 예측 가능은 굉장히 중요한 문제이니까요. 그러니, 일단 실무에서는 '눈'으로 테스트 했을 시, 못 믿는다고 볼 수 있습니다. 제가 주로 기도하는 마음으로 프로그래밍하기라고 부르는 상황이 되죠.

1번 문제는, 사실 굉장히 중요한 문제 인데 정확하게 이해하고 있는 사람을 별로 못봤습니다. 일단, Java에서 System Class가 어떤 의미인지, out field는 뭐 하는 녀석인지, println() 메소드는 뭐 하는 녀석인지 부터 말문이 막히는 사람이 많아서요. 막연하게 쓰면 안된다, 혹은 막연하게 출력할 때는 저거 쓴다. 라는 사람들이 주로 많았습니다.

  • System : System Class는 final 이고, 생성할 수 없습니다. (final & Cannot be instantiated.) 결론은? 안에 있는 것들은 전부 static 이고, 유틸리티 클래스라는걸 쉽게 짐작할 수 있죠. JavaDoc 기준으로는, "Among the facilities provided by the System class are standard input, standard output, and error output streams; access to externally defined properties and environment variables; a means of loading files and libraries; and a utility method for quickly copying a portion of an array." 라고 소개 되어 있습니다. 주 용도는, stdin / stdout / stderr 를 제공하고, OS 수준의 환경 변수나 외부 선언 변수둘에 접근 하고, 파일 및 라이브러리 로딩이 가능하며, array 복사에 도움이 되는(성능이 좋은) 메소드 들을 제공합니다.
  • out : 기본적으로 stdout을 의미합니다. System Class의 static member field 이고, Type은 PrintStream 으로 되어 있습니다. public final field이네요. 시작 시점에 올라오고, host의 standard output console을 연결 합니다.
  • println : 넘겨 받은 인자를 출력하고, 추가적으로 개행 문자를 입력합니다. 인자 종류에 따라 여러 메소드가 선택되고, 내부적으로는 모두 print를 호출합니다. print는 write를 호출하고 뭐 그런거죠.

걍 적당히 작은 프로그램 짤 때는 뭘 써도 상관 없습니다. 별거 있나요. 잘 돌면 됐지. 그 정도로 짜도 되니까 적당히 작은 프로그램이라고 하는거죠.

예제 코드 기준으로 봤을 때는, assert에 관한 이야기 일 수 있겠지만, 그렇다고 출력을 안 쓸 수는 없으니까요. System.out.println에 관해서도 한번 짚고 넘어가긴 해야죠.

결론은, 웬만하면 logger 쓰자... 이지만, 그러면 납득하기 어려우니까요. ㅎㅎ 뭔가 문제가 있어야 바꾸지, 문제 없으면 걍 가는게 맞으니까요.

제대로 된 logger와 System.out.println은 비교하기에 차이가 너무 크지만, 보편적으로 많은 개발자가 중요하게 생각하는 성능부분에서 차이가 더 커집니다.

앞에서도 이야기 했지만, println을 호출하면 println -> print -> write() + newline() 순서로 실행됩니다. 여기서 write() / newline() 은 synchronized block 이에요. 용도를 생각해 보면, 당연히 synchronized 처리 되어 있어야 하겠지만, 겨우 로그 찍는데 저건 분명한 오버헤드 입니다. time 명령으로 체크해 보면 바로 알 수 있습니다. println은 성능이 안나와요. 작은 프로그램 짤 때야 저런게 별 문제가 안되지만, 실무에서 이런 부분 무시하고 썼다가 서버가 터져나가면 그때 되어서야 어디서 문제 발생했는지 확인 못하고 헤메는 상황이 벌어집니다. 심지어, System.out.println은 log level 구분도 없으니, SE들 한테서 로그 때문에 디스크 모자라다는 소리 들을 각오도 해야 겠지요.

잠이 안와서 멍 때리다가 좀 길어졌는데요, 정리하자면 다음과 같습니다.

  • 테스트 코드? 좋은거 많습니다. 웬만하면 가져다 쓰세요.
  • System.out.println은 Logging용이 아닙니다. 그거 하라고 나온거 아니에요. 웬만하면 Logger 씁시다. (기왕이면 Logback이 좋습니다. ㅎㅎ)

14개의 의견 from SLiPP

2013-01-05 17:39

현재 제가 강의 때도 얘기하는 것을 말씀드리면... 컴퓨터에서 프로그램을 실행할 때 메모리를 할당해야 하는데, 자바 언어에서는 static과 new 두 가지가 있습니다. static과 달리 new는 도장처럼 여러 번 메모리를 점유할 수 있고, static은 태어날 때부터 한 곳에 고정됩니다. 그리고 함수는 f(x)가 짱입니다. 특히 소시 제시카 동생 크리스탈 y = f(x) 처럼 함수(메소드)는 리턴 값이 있어야죠. 그런데 이것을 씹는 함수는 void라고 입에 마스크를 씁니다. 리턴 값이 없다는 뜻이죠. public이야 뭐 외부에서 접근해서 실행해야 하는 놈이니 private으로 해 놓고 외부에서 실행하면 좀 바보된 느낌이죠.

결론은 테스트를 main()으로 System.out.println() 으로 찍찍 싸는 코딩하시는 분들은, 토비의 스프링 책 사서 보길 권장합니다.

2013-01-05 19:20

Production과 Test를 분리하지 않았을 때 발생하는 문제를 이야기하면 끝도 없이 나올 것 같네요. 백보 양보해서, 순수하게 main()에서 테스트를 수행 했을 때 발생하는 문제만 생각하면, 1. 코드가 올라가는 Perm 영역 낭비. 2. 단독 실행이 필요한 클래스의 경우, 커맨드라인이나 직접 실행 할 때, main메소드로 인자 값 없이 넘어오는 행동을 정의 할 수 없음. (가끔 유틸리티 만들때 사용하죠. 개발자라면 파일 이름 변경하는 유틸도 직접 만들어 써야 하잖아요 ㅋ) 3. main()에 의해 진입하는 애플리케이션의 경우, 진입 main()과 테스트를 위한 main()식별의 어려움. 로직상 진입 점이 다수 존재하는 경우 실행을 어떤 클래스에서 시작해야 할지 일일히 확인해야함.

main()에서 테스트 해야 하는 상황이 온다면, 차라리 테스트 분리하고 메모장으로 개발하겠습니다.

2013-01-06 00:02

main 메서드가 프로그램의 진입점이기 때문에 보안 취약성에 문제가 있을듯합니다. 위에서 kenu님께서 말씀하신데로 main method가 static 이면서 public 이기 때문에 누구든 jar 만 접근 가능하다면 실행 시켜볼수 있는 것이 문제가 아닐까요?

2013-01-06 19:12

테스트 결과를 개발자가 눈으로 검증하고 확인하는 절차를 밟아야 하는 불편함이 있습니다. 또한, main 메소드 안에 단위 테스트 하는 기능들이 많아지면 각 기능들의 응집도? 가 낮아져 테스트에 많은 불편함이 있을 것으로 생각됩니다.

2013-01-07 03:49

음... 보는 관점에 따라 여러가지 의견이 나오겠지만... Test 시에 main method를 쓰는건, 습관이 덜 들어간 것 정도로 밖에는 안보이네요. 뭐, 그거 쓸 수도 있죠. (ㅎㅎ)

예시로 들어주신 코드만 놓고 봤을 때는 크게 두가지 문제가 있어 보입니다.

  • System.out.println() 관련 문제
  • 눈(?!)으로 테스트 하는 문제

(그러고 보니, 둘 다 main()하고는 관련이 없네요. Standalone Java는 기본적으로 main()을 사용합니다. main()이 뭔 죄겠어요. 잘못 짠 사람 잘못이지... JUnit Runner 에도 main() 메소드가 있고, Tomcat Bootstrapper 에도 main()은 있는데요 뭐...)

일단, 이해가 쉬운 2번 부터 보자면, 굉장히 부정확하고 위험하다는 부분에는 기본적으로 동의 할 겁니다. 결과 값을 눈으로 확인한다는 건데요, 이는 답을 알고 있는 상황에서만 맞는 결과가 나옵니다. 기본적으로 현대 프로그래밍은 혼자 작업하지 않으며, 따라서 눈으로 테스트 할 경우 해당 메소드의 기대값과 결과값을 검증하기 어렵습니다. *Unit 계열의 테스트를 제가 권장하는 이유 중 하나는, "기대값과 결과값이 하나의 묶음으로 제공되어, 해당 메소드의 수행 결과를 예측 가능하다"입니다. 프로그래밍에서 예측 가능은 굉장히 중요한 문제이니까요. 그러니, 일단 실무에서는 '눈'으로 테스트 했을 시, 못 믿는다고 볼 수 있습니다. 제가 주로 기도하는 마음으로 프로그래밍하기라고 부르는 상황이 되죠.

1번 문제는, 사실 굉장히 중요한 문제 인데 정확하게 이해하고 있는 사람을 별로 못봤습니다. 일단, Java에서 System Class가 어떤 의미인지, out field는 뭐 하는 녀석인지, println() 메소드는 뭐 하는 녀석인지 부터 말문이 막히는 사람이 많아서요. 막연하게 쓰면 안된다, 혹은 막연하게 출력할 때는 저거 쓴다. 라는 사람들이 주로 많았습니다.

  • System : System Class는 final 이고, 생성할 수 없습니다. (final & Cannot be instantiated.) 결론은? 안에 있는 것들은 전부 static 이고, 유틸리티 클래스라는걸 쉽게 짐작할 수 있죠. JavaDoc 기준으로는, "Among the facilities provided by the System class are standard input, standard output, and error output streams; access to externally defined properties and environment variables; a means of loading files and libraries; and a utility method for quickly copying a portion of an array." 라고 소개 되어 있습니다. 주 용도는, stdin / stdout / stderr 를 제공하고, OS 수준의 환경 변수나 외부 선언 변수둘에 접근 하고, 파일 및 라이브러리 로딩이 가능하며, array 복사에 도움이 되는(성능이 좋은) 메소드 들을 제공합니다.
  • out : 기본적으로 stdout을 의미합니다. System Class의 static member field 이고, Type은 PrintStream 으로 되어 있습니다. public final field이네요. 시작 시점에 올라오고, host의 standard output console을 연결 합니다.
  • println : 넘겨 받은 인자를 출력하고, 추가적으로 개행 문자를 입력합니다. 인자 종류에 따라 여러 메소드가 선택되고, 내부적으로는 모두 print를 호출합니다. print는 write를 호출하고 뭐 그런거죠.

걍 적당히 작은 프로그램 짤 때는 뭘 써도 상관 없습니다. 별거 있나요. 잘 돌면 됐지. 그 정도로 짜도 되니까 적당히 작은 프로그램이라고 하는거죠.

예제 코드 기준으로 봤을 때는, assert에 관한 이야기 일 수 있겠지만, 그렇다고 출력을 안 쓸 수는 없으니까요. System.out.println에 관해서도 한번 짚고 넘어가긴 해야죠.

결론은, 웬만하면 logger 쓰자... 이지만, 그러면 납득하기 어려우니까요. ㅎㅎ 뭔가 문제가 있어야 바꾸지, 문제 없으면 걍 가는게 맞으니까요.

제대로 된 logger와 System.out.println은 비교하기에 차이가 너무 크지만, 보편적으로 많은 개발자가 중요하게 생각하는 성능부분에서 차이가 더 커집니다.

앞에서도 이야기 했지만, println을 호출하면 println -> print -> write() + newline() 순서로 실행됩니다. 여기서 write() / newline() 은 synchronized block 이에요. 용도를 생각해 보면, 당연히 synchronized 처리 되어 있어야 하겠지만, 겨우 로그 찍는데 저건 분명한 오버헤드 입니다. time 명령으로 체크해 보면 바로 알 수 있습니다. println은 성능이 안나와요. 작은 프로그램 짤 때야 저런게 별 문제가 안되지만, 실무에서 이런 부분 무시하고 썼다가 서버가 터져나가면 그때 되어서야 어디서 문제 발생했는지 확인 못하고 헤메는 상황이 벌어집니다. 심지어, System.out.println은 log level 구분도 없으니, SE들 한테서 로그 때문에 디스크 모자라다는 소리 들을 각오도 해야 겠지요.

잠이 안와서 멍 때리다가 좀 길어졌는데요, 정리하자면 다음과 같습니다.

  • 테스트 코드? 좋은거 많습니다. 웬만하면 가져다 쓰세요.
  • System.out.println은 Logging용이 아닙니다. 그거 하라고 나온거 아니에요. 웬만하면 Logger 씁시다. (기왕이면 Logback이 좋습니다. ㅎㅎ)
2013-01-07 09:56

보자마자 그냥 단순하게 떠오르는 생각입니다. 위 예제에서 junit 을 쓰면 4개의 test unit 중 하나가 실패해도 전체가 다 실행이 되지만 main 을 쓰면 테스트 도중 하나만 실패해도 나머지 test unit 이 돌지 않습니다. 물론 main 도 try catch 문으로 흘러가게 할 수 있지만 귀찮죠...

2013-01-07 10:11

재미있는 주제이네요.

대형 SI회사의 프로젝트 지원을 몇 년간 다녀봤더니 위와 같이 테스트 코드를 작성하라고 표준을 제시하는 프로젝트도 있었습니다.

무조건 junit을 작성하세요. 라고 이야기하기보다는 왜 이렇게 가이드했는지 물어보면서 듣게된 이야기를 좀 공유 할께요.

2011년도 초반이니까 최근 프로젝트이고 개발자 100명 정도이니 규모도 좀 큰 규모였죠. 해당 프로젝트는 기존 시스템(레가시)에서 몇 번의 개정작업을 진행하는 프로젝트중 하나였습니다. 아키텍트 입장에서는 기존 소스, 라이브러리도 존재하는 상황이고 구 소스에도 main방식으로 테스트 소스가 작성되어 있으니 그대로 따라가는 이유도 있었지만, 테스트 소스와 실제 소스가 분리되어 있으면 어떤 소스가 테스트소스가 존재하지 않은지 확인하기 어렵다는 이유로도 main형 테스트를 표준화했더군요.

단위 테스트 통합 구동(CI를 통한), 테스트 커버리지와 소스와 매칭이 쉽지 않다. 개발자가 수시로 드나드는 상황에서 단위소스 개발 완료시 테스트 소스까지 포함되어 있는지 확인이 필요하다. 개발자들이 jUnit에 익숙하지 않다.(사실, 아키텍트도 ant/maven에 익숙하지 않다.)

등등이 원인이였던 것 같습니다.

제가 조치했던 사항은 . jenkins + ant 환경 구축 . 실행과 테스트의 관점을 분리하는 것이 좋겠다. 수준이였습니다.

그 이후 정말로 junit테스트소스를 작성했느냐는 또 다른 이야기인 것 같네요.

SI프로젝트에서 개발자가 강제이든 자발적이든 테스트 소스를 작성하고 반복수행하는 경험은 몇 년간의 경험간 손에 꼽을정도네요.

지금은 솔루션 만드는 팀에 합류해서 차근차근 테스트 소스를 작성하고 어떻게 하면 잘 작성할지 고민하면서 적용 중 입니다.

2013-01-07 10:11

main method 하나만 가지고도 할 이야기가 이렇게 많네요. 다양한 의견들 잘 봤어요.

일단 제가 main method의 문제점을 파헤치려고 한 이유는 자바를 처음 시작하는 친구들이 main method의 문제점을 인지하고 junit의 필요성을 이해하도록 하기 위해 찾아보려고 했습니다.

위 댓글에서 문제점이라고 이야기한 대부분의 내용은 junit이 등장하게 된 배경과 필요성에 대한 참고 자료가 될 것이라 생각합니다. 제가 생각했던 main method의 문제점은 다음과 같습니다.

  • Production code와 Test Code가 클래스 하나에 존재한다. 클래스 크기가 커져 복잡도가 증가한다.
  • Test Code가 실 서비스에 같이 배포됨. 위에서도 이야기했지만 PermGen 크기에 아무래도 영향을 미치겠죠.
  • main method 하나에서 여러 개의 기능을 테스해야 함. 테스트 간에 의존성, 복잡도 증가, 가독성 떨어짐.
  • method 명을 통해 테스트 의도를 드러내기 힘들다. main method의 코드를 extract method를 통해 리팩토링함으로써 일부 해결 가능함. 하지만 production method와 test method가 중복되어 유지보수 좋지 않음.
  • 테스트 결과를 사람이 수동으로 확인. 로직의 복잡도가 증가하면 검증하기 힘들어짐.

main method를 entry point 관점이 아니라 test 역할로서만 바라봤을 때 위 5가지 정도 생각해 봤어요. 해결하는 과정은 다음과 같은 전략입니다.

  • 먼저 main method를 별도의 클래스로 분리한다. CalculatorTest 클래스를 만들어 분리하는거죠.
  • 클래스를 분리할 경우 위 5가지 이슈 중 대부분의 이슈를 해결할 수 있지만 테스트 간의 의존성 문제와 테스트 결과를 사람이 수동으로 확인하는 이슈는 해결할 수 없다.
  • 이에 대한 이슈를 해결하기 위해 junit이 등장했고, 이에 대한 필요성을 이야기함.

위와 같은 과정으로 한 단계씩 접근하려고 합니다. 좋은 의견 감사드립니다.

2013-01-12 17:22

개인적으로 저런형태의 접근방법을 좋아하지는 않습니다만 꼭 나쁜방법만은 아닙니다. 지금부터 하는 얘기는 제가 위의 방법을 옹호하기 때문만은 아니니 오해 없으시길 바랍니다.

클래스 하나에 대한 테스트는 위의 방법으로 접근하고 여러 클래스를 조합하여 좀 큰덩어리의 테스트는 유닛테스트를 돌리는 것도 프로젝트의 성격에 따라 생각해볼만한 방법이기도 합니다. 게다가 개발자가 변경중인 테스트를 바로바로 확인하면서 코딩하기에는 위의 방식이 편리한것은 사실이죠. 물론 진짜 저렇게 하고자 한다면 Test코드는 이너 클래스로 분리할것 같긴 합니다만.

PermGen 크기에 영향을 미친다는 것은 테스크 코드를 어떻게 작성 하느냐에 따라 다른 문제 입니다. 다음과 같은 코드의 경우 main이 호출되지 않는 경우 Test는 PermGen에 로딩되지 않습니다.

public static class SomeClass { public static void main(String[] args) { // Run Test }

public static class Test {
    ...
}

}

물론 다음과 같은 경우도 마찬가지 입니다. public static class SomeClass { .... }

class Test { .... }

후자의 경우 클래스 본체에 테스트코드가 들어가지는 않으니 (물론 본체에 main은 넣어 주어야 합니다만) 가독성에 있어서도 딱히 나쁘지 않습니다. 같은 파일에 테스트 코드가 포함되어 있으니 테스트 코드 수정 및 실행이 딱히 불편한것은 아니구요.

그리고 System.out.println에 대해 이런저런 코멘트들이 있는데요. 우선 synchornized 성능 얘기가 나왔는데 이게 옛날 처럼 무식하게 매번 Lock잡고 동작하는게 아닙니다. 동일한 쓰레드에서 실행하는 경우에는 속도차가 거의 의미가 없습니다. 자세한 것은 찾아보시구요. (백만번쯤은 호출되야 겨우 의미있는 값이 나올겁니다.) 어차피 synchronized 블럭이 없다한들 멀티쓰레드에서 Console로 찍어내는거라면 어차피 각 메시지 간 동기처리해줘야 하는것이 사실이구요. logback에 있는 async 기능을 사용하거나 동일한 방식으로 executor등을 통해 별도로 뽑아내 처리할 것이 아니라면 이나저나 도찐개찐입니다.

게다가 간단한 테스트 코드짜는데 logger까지 동원 한다는게 좀 오버라고도 생각되구요. 디버그 출력용으로 쓰기에도 System.out.format이 훨씬 편리합니다. 물론 현실은 전체 테스트 수행시의 결과를 일괄 수집하기위해 logger를 쓰는게 맞는 방법입니다만. 꼭 logback과 slf4j 따위를 일일히 깔고 설정파일에 file설정과 async따위의 설정을 해 주고 해야하나요? 어차피 서비스용 코드도 아니고 테스트 로그 수집할 거라면 Executor를 통해 로그 bulk writing 하는식으로 직접짜도 몇줄이면 됩니다.

뭐 여하튼... 일반적인게 무조건 상식적이고 그렇지 않은것은 무조건 비상식적인것은 아닙니다.

의견 추가하기

연관태그

← 목록으로