스프링부트 애플리케이션에서 생성하는 파일 등의 생성물은...

절대경로보다는 상대경로로
애플리케이션 실행위치를 기준으로

생성하는 것이 여러모로 편리하다.

[스프링부트] Executable JAR 내에서 ResourceUtil.getFile() 사용시 FileNotFoundException 발생

발생문제

IDE에서 실행하거나 bootrun 명령으로 실행시켰을 경우에는 별다른 이상없이 동작하던

File defaultProfileImage = ResourceUtil.getFile("static/images/default-profile.png");

가 실행가능한 jar(Excutable jar) 형태로 만들어 실행시에는 java.io.FileNotFoundException 을 일으킨다. 그 접근경로를 살펴보면,

...Exception: java.io.FileNotFoundException: class path resource [static/images/default-profile.png] cannot be resolved to absolute file path because it does not reside in the file system: jar:file:/home/ihoneymon/.../build/libs/innoquartz-server.war!/WEB-INF/classes!/static/images/default-profile.png
    at io.honeymon.test.member.ApplicantServiceImpl.setDefaultProfileImage(ApplicantServiceImpl.java:59) ~[classes!/:na]
    at io.honeymon.test.member.ApplicantServiceImpl.register(ApplicantServiceImpl.java:39) ~[classes!/:na]
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_25]
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_25]
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_25]
    at java.lang.reflect.Method.invoke(Method.java:483) ~[na:1.8.0_25]
    at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:302) ~[spring-aop-4.2.4.RELEASE.jar!/:4.2.4.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:190) ~[spring-aop-4.2.4.RELEASE.jar!/:4.2.4.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157) ~[spring-aop-4.2.4.RELEASE.jar!/:4.2.4.RELEASE]
    at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:99) ~[spring-tx-4.2.4.RELEASE.jar!/:4.2.4.RELEASE]
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:281) ~[spring-tx-4.2.4.RELEASE.jar!/:4.2.4.RELEASE]
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:96) ~[spring-tx-4.2.4.RELEASE.jar!/:4.2.4.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.2.4.RELEASE.jar!/:4.2.4.RELEASE]
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:208) ~[spring-aop-4.2.4.RELEASE.jar!/:4.2.4.RELEASE]
    at com.sun.proxy.$Proxy172.register(Unknown Source) ~[na:na]
    at io.honeymon.test.member.SignupController.signUp(SignupController.java:53) ~[classes!/:na]
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_25]
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_25]
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_25]
    at java.lang.reflect.Method.invoke(Method.java:483) ~[na:1.8.0_25]
    at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:222) [spring-web-4.2.4.RELEASE.jar!/:4.2.4.RELEASE]
    at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:137) [spring-web-4.2.4.RELEASE.jar!/:4.2.4.RELEASE]
    at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:110) [spring-webmvc-4.2.4.RELEASE.jar!/:4.2.4.RELEASE]
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:814) [spring-webmvc-4.2.4.RELEASE.jar!/:4.2.4.RELEASE]
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:737) [spring-webmvc-4.2.4.RELEASE.jar!/:4.2.4.RELEASE]
    at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:85) [spring-webmvc-4.2.4.RELEASE.jar!/:4.2.4.RELEASE]
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:959) [spring-webmvc-4.2.4.RELEASE.jar!/:4.2.4.RELEASE]
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:893) [spring-webmvc-4.2.4.RELEASE.jar!/:4.2.4.RELEASE]
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:969) [spring-webmvc-4.2.4.RELEASE.jar!/:4.2.4.RELEASE]
    at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:871) [spring-webmvc-4.2.4.RELEASE.jar!/:4.2.4.RELEASE]
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:707) [javax.servlet-api-3.1.0.jar!/:3.1.0]
    at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:845) [spring-webmvc-4.2.4.RELEASE.jar!/:4.2.4.RELEASE]
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:790) [javax.servlet-api-3.1.0.jar!/:3.1.0]
    at org.eclipse.jetty.servlet.ServletHolder.handle(ServletHolder.java:812) [jetty-servlet-9.2.14.v20151106.jar!/:9.2.14.v20151106]
    at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1669) [jetty-servlet-9.2.14.v20151106.jar!/:9.2.14.v20151106]
    at org.eclipse.jetty.websocket.server.WebSocketUpgradeFilter.doFilter(WebSocketUpgradeFilter.java:224) [websocket-server-9.2.14.v20151106.jar!/:9.2.14.v20151106]
    at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1652) [jetty-servlet-9.2.14.v20151106.jar!/:9.2.14.v20151106]
    at org.springframework.boot.actuate.autoconfigure.EndpointWebMvcAutoConfiguration$ApplicationContextHeaderFilter.doFilterInternal(EndpointWebMvcAutoConfiguration.java:243) [spring-boot-actuator-1.3.1.RELEASE.jar!/:1.3.1.RELEASE]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) [spring-web-4.2.4.RELEASE.jar!/:4.2.4.RELEASE]
    at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1652) [jetty-servlet-9.2.14.v20151106.jar!/:9.2.14.v20151106]
    at org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:77) [spring-web-4.2.4.RELEASE.jar!/:4.2.4.RELEASE]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) [spring-web-4.2.4.RELEASE.jar!/:4.2.4.RELEASE]
    at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1652) [jetty-servlet-9.2.14.v20151106.jar!/:9.2.14.v20151106]
    at org.springframework.boot.actuate.trace.WebRequestTraceFilter.doFilterInternal(WebRequestTraceFilter.java:111) [spring-boot-actuator-1.3.1.RELEASE.jar!/:1.3.1.RELEASE]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) [spring-web-4.2.4.RELEASE.jar!/:4.2.4.RELEASE]
    at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1652) [jetty-servlet-9.2.14.v20151106.jar!/:9.2.14.v20151106]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:316) [spring-security-web-4.0.3.RELEASE.jar!/:4.0.3.RELEASE]
    at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.invoke(FilterSecurityInterceptor.java:126) [spring-security-web-4.0.3.RELEASE.jar!/:4.0.3.RELEASE]
    at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.doFilter(FilterSecurityInterceptor.java:90) [spring-security-web-4.0.3.RELEASE.jar!/:4.0.3.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330) [spring-security-web-4.0.3.RELEASE.jar!/:4.0.3.RELEASE]
    at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:114) [spring-security-web-4.0.3.RELEASE.jar!/:4.0.3.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330) [spring-security-web-4.0.3.RELEASE.jar!/:4.0.3.RELEASE]
    at org.springframework.security.web.session.SessionManagementFilter.doFilter(SessionManagementFilter.java:122) [spring-security-web-4.0.3.RELEASE.jar!/:4.0.3.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330) [spring-security-web-4.0.3.RELEASE.jar!/:4.0.3.RELEASE]
    at org.springframework.security.web.authentication.AnonymousAuthenticationFilter.doFilter(AnonymousAuthenticationFilter.java:111) [spring-security-web-4.0.3.RELEASE.jar!/:4.0.3.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330) [spring-security-web-4.0.3.RELEASE.jar!/:4.0.3.RELEASE]
    at org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter.doFilter(SecurityContextHolderAwareRequestFilter.java:169) [spring-security-web-4.0.3.RELEASE.jar!/:4.0.3.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330) [spring-security-web-4.0.3.RELEASE.jar!/:4.0.3.RELEASE]
    at org.springframework.security.web.savedrequest.RequestCacheAwareFilter.doFilter(RequestCacheAwareFilter.java:48) [spring-security-web-4.0.3.RELEASE.jar!/:4.0.3.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330) [spring-security-web-4.0.3.RELEASE.jar!/:4.0.3.RELEASE]
    at org.springframework.security.web.authentication.www.BasicAuthenticationFilter.doFilterInternal(BasicAuthenticationFilter.java:158) [spring-security-web-4.0.3.RELEASE.jar!/:4.0.3.RELEASE]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) [spring-web-4.2.4.RELEASE.jar!/:4.2.4.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330) [spring-security-web-4.0.3.RELEASE.jar!/:4.0.3.RELEASE]
    at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:205) [spring-security-web-4.0.3.RELEASE.jar!/:4.0.3.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330) [spring-security-web-4.0.3.RELEASE.jar!/:4.0.3.RELEASE]
    at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:120) [spring-security-web-4.0.3.RELEASE.jar!/:4.0.3.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330) [spring-security-web-4.0.3.RELEASE.jar!/:4.0.3.RELEASE]
    at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:64) [spring-security-web-4.0.3.RELEASE.jar!/:4.0.3.RELEASE]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) [spring-web-4.2.4.RELEASE.jar!/:4.2.4.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330) [spring-security-web-4.0.3.RELEASE.jar!/:4.0.3.RELEASE]
    at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:91) [spring-security-web-4.0.3.RELEASE.jar!/:4.0.3.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330) [spring-security-web-4.0.3.RELEASE.jar!/:4.0.3.RELEASE]
    at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:53) [spring-security-web-4.0.3.RELEASE.jar!/:4.0.3.RELEASE]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) [spring-web-4.2.4.RELEASE.jar!/:4.2.4.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330) [spring-security-web-4.0.3.RELEASE.jar!/:4.0.3.RELEASE]
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:213) [spring-security-web-4.0.3.RELEASE.jar!/:4.0.3.RELEASE]
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:176) [spring-security-web-4.0.3.RELEASE.jar!/:4.0.3.RELEASE]
    at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:346) [spring-web-4.2.4.RELEASE.jar!/:4.2.4.RELEASE]
    at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:262) [spring-web-4.2.4.RELEASE.jar!/:4.2.4.RELEASE]
    at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1652) [jetty-servlet-9.2.14.v20151106.jar!/:9.2.14.v20151106]
    at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:99) [spring-web-4.2.4.RELEASE.jar!/:4.2.4.RELEASE]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) [spring-web-4.2.4.RELEASE.jar!/:4.2.4.RELEASE]
    at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1652) [jetty-servlet-9.2.14.v20151106.jar!/:9.2.14.v20151106]
    at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:121) [spring-web-4.2.4.RELEASE.jar!/:4.2.4.RELEASE]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) [spring-web-4.2.4.RELEASE.jar!/:4.2.4.RELEASE]
    at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1652) [jetty-servlet-9.2.14.v20151106.jar!/:9.2.14.v20151106]
    at org.springframework.boot.actuate.autoconfigure.MetricsFilter.doFilterInternal(MetricsFilter.java:103) [spring-boot-actuator-1.3.1.RELEASE.jar!/:1.3.1.RELEASE]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) [spring-web-4.2.4.RELEASE.jar!/:4.2.4.RELEASE]
    at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1652) [jetty-servlet-9.2.14.v20151106.jar!/:9.2.14.v20151106]
    at org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:585) [jetty-servlet-9.2.14.v20151106.jar!/:9.2.14.v20151106]
    at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:143) [jetty-server-9.2.14.v20151106.jar!/:9.2.14.v20151106]
    at org.eclipse.jetty.security.SecurityHandler.handle(SecurityHandler.java:577) [jetty-security-9.2.14.v20151106.jar!/:9.2.14.v20151106]
    at org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:223) [jetty-server-9.2.14.v20151106.jar!/:9.2.14.v20151106]
    at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1127) [jetty-server-9.2.14.v20151106.jar!/:9.2.14.v20151106]
    at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:515) [jetty-servlet-9.2.14.v20151106.jar!/:9.2.14.v20151106]
    at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:185) [jetty-server-9.2.14.v20151106.jar!/:9.2.14.v20151106]
    at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1061) [jetty-server-9.2.14.v20151106.jar!/:9.2.14.v20151106]
    at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:141) [jetty-server-9.2.14.v20151106.jar!/:9.2.14.v20151106]
    at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:97) [jetty-server-9.2.14.v20151106.jar!/:9.2.14.v20151106]
    at org.eclipse.jetty.server.Server.handle(Server.java:499) [jetty-server-9.2.14.v20151106.jar!/:9.2.14.v20151106]
    at org.eclipse.jetty.server.HttpChannel.handle(HttpChannel.java:311) [jetty-server-9.2.14.v20151106.jar!/:9.2.14.v20151106]
    at org.eclipse.jetty.server.HttpConnection.onFillable(HttpConnection.java:257) [jetty-server-9.2.14.v20151106.jar!/:9.2.14.v20151106]
    at org.eclipse.jetty.io.AbstractConnection$2.run(AbstractConnection.java:544) [jetty-io-9.2.14.v20151106.jar!/:9.2.14.v20151106]
    at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:635) [jetty-util-9.2.14.v20151106.jar!/:9.2.14.v20151106]
    at org.eclipse.jetty.util.thread.QueuedThreadPool$3.run(QueuedThreadPool.java:555) [jetty-util-9.2.14.v20151106.jar!/:9.2.14.v20151106]
    at java.lang.Thread.run(Thread.java:745) [na:1.8.0_25]

로그를 살펴보면 cannot be resolved to absolute file path because it does not reside in the file system: jar:file:/home/ihoneymon/.../build/libs/innoquartz-server.war!/WEB-INF/classes!/static/images/default-profile.png 항목이 보일 것이다. 이에 대해서는,

해결책

war 파일이나 IDE로 application run as로 실행하였다면 실제 resource 파일인 file:// 프로토콜을 쓰기 때문에 File객체를 생성해 줄수 있지만, executable jar로 실행 했다면 FileNotFoundException이 발생 하게 됩니다.

에서 설명하고 있다. jar 파일로 실행시에는 VfsResourceDelegate 를 사용하면서 실제 파일시스템에서 지원하는 프로토콜로 처리가 되기에 의도했던 것과는 다르게 동작하게 된다. 이를 해결하기 위한 방법도 위의 포스팅에서 설명되어 있다.

해결적용

ClassPathResource classPathResource = new ClassPathResource("static/images/default-profile.png");
// classPathResource.getInputStream(); 를 사용하도록 처리 

문제 해결!

참고


아시는 분이 가지고 계신 이 책을 빌려서 잠시 읽은 적이 있다.

그 때는 무슨 내용일지 제대로 이해할 수 없었다.

지금도 그렇겠지... Orz...

그래도 지금은 요령이 생겨서... 한번의 완독을 하고, 시간이 지난 후 다시 완독하면서 '반복적인 책 읽기' 를 통해서 내가 쌓은 경험 만큼 깨달음을 얻을 수 있다는 요령을 터득했다.


프로그램을 짤 때는 자신과 컴퓨터뿐 아니라, 다른 사람들을 생각해야 한다!

이것이 구현 패턴이 전하는 메시지입니다. 이 책에서는 다른 사람을 배려하며 프로그래밍할 때의 경제적 이득을 강조했지만, 사실 이는 프로그래머 자신을 위한 것이기도 합니다. 자신이 더 큰 공동체의 일원임을 깨닫게 되면 공동체에 기여하는 것에 대한 만족을 느낄 수 있게 되기 때문입니다. 여러분도 이와 같은 만족감을 느낄 수 있으면 좋겠습니다. 즐거운 코딩 하시길.


- 미국 오레곤 주 메를린에서, 켄트 백 드림


기능적으로 올바르게 동작한다고 해서 모두 훌륭한 코드는 아니다. 훌륭한 코드는 프로그래머의 의도를 일관되게 전달해서, 다른 프로그래머들이 코드를 쉽게 이해하고 사용하며 자신 있게 수정할 수 있어야 한다. 그러나 훌륭한 코드는 쉽게 탄생하지 않는다. 훌륭한 코드는 프로그래머가 하루에도 수백 번 이상 내리는 작지만 중요한 결정의 산물이다. 이러한 중요한 결정들에 대해, 전설적인 소프트웨어 혁신자인 캔트 백이 "강력한 구현 패턴" 을 공개했다. 구현 패턴을 사용하면 더 간결하고 명쾌하며 체계적이고 비용 대비 효과적인 코드를 작성할 수 있다.


이 책은 다른 사람들이 이해하기 쉬운 코드를 만드는 프로그래밍에 대한 내용을 담고 있다. 하지만 너무 큰 기대는 금물이다. 아쉽게도 그런 코드를 만드는 비법 같은 것은 없다. 읽기 쉬운 코드를 작성하는 과정은 읽기 쉬운 글을 쓰는 것과 같다. 대상 독자를 정해야 하고, 명확한 전체 구조를 갖고 잇어야 하며, 전체 줄거를 생각해서 세부를 묘사해야 한다. 자바는 사람이 이해할 수 있는 코드를 작성하는 여러가지 방법을 제공한다. 이 책에는 읽기 쉬운 코드를 작성하는 자바 프로그래밍 습관을 모았다.


이 책은 "어떻게 하면 다른 사람들에게 코드를 전달(커뮤니케이션) 할 것인가?" 라는 고민에 대한 답이기도 하다. 프로그래머는 혼자 생각하면서 보내는 시간이 너무 많으므로, 다른 사람의 관점에서 코드를 바라보려 시도하는 것 자체가 커다란 변화이다. 프로그래머는 "컴퓨터가 이 코드를 어떻게 처리할까" 뿐 아니라 "내 생각을 다른 사람에게 어떻게 전달할까" 하는 고민까지 해야된다. 하지만 기존 코드를 이해하는 데 엄청난 소프트웨어 개발 비용이 투입되는 것을 감안하면, 이러한 변화는 건전할 뿐 아니라 경제적 이득을 가져올 수도 있다.


...


사실 이 책은 "좋은 코드는 중요하다" 라는 빈약한 전제를 기반으로 만들어졌다. 좋은 코드가 상업적 성공이나 광범위한 사용자 확보에 대한 필요조건 혹은 충분조건이라고 믿기에는, 사람들이 조잡한 코드로 돈을 많이 버는 사례를 너무 많이 봐왔다. 코드 품질이 회사나 개인의 미래를 좌우하는 요소가 아니라 할지라도, 나는 여전히 코드 품질이 매우 중요하다고 믿는다. 자신있게 코드를 개발, 출시하고 기회와 경쟁 상황에 따라 개발 방향을 바꿀 수 있으며 위기 속에서도 직원들의 사기를 높일 수 있는 회사는 조잡하고 버그가 있는 코드를 작성하는 회사에 의해 성공할 확률이 높다.


설사 좋은 코딩이 장기적으로 경제적 이득을 가져오지 못한다고 하더라도 나는 여전히 내가 작성할 수 있는 최고의 코드를 작성할 것이다. 인생이 70년이라 할 때 우리 인생은 20억 초에 불과하다. 그 소중한 순간들을 자랑스럽지 않은 일을 하면서 낭비하고 싶지는 않다. 코딩을 잘하는 것은 그 자체로도 프로그래머에게 만족감을 주지만, 다른 사람들이 내 코드를 이해하고 감탄해주며 내 코드를 사용하고 점차 발전시킨다는 점을 생각할 때 매우 중요하다.


결국 이 책은 책임감에 대한 이야기다. 여러분이 프로그래머로서 시간과 재능과 돈과 기회를 부여 받았다. 이러한 자원들을 책임감 있게 잘 사용하려면 어떻게 해야 하는가? 이 책은 이러한 고민에 대한 나의 답이다. 프로그래머는 자신과 CPU뿐 아니라, 자신의 코드를 보고 사용할 다른 사람들을 배려해서 코딩을 해야 한다.



아는 만큼 보인다.

라는 이 말을 나는 좋아한다. 내 자신에게 적용했을 때 '내가 보지 못하는 부분들에 대해서는 내가 알고 있지 못하기 때문이다.' 라는 위안 과

모른다는 거지. 모르는 건 모른는 거다 라는 것부터 인정해야 알아갈 수 있다.

'알아가야할 것들을 살피고 모르는 것을 알기 위한 노력을 아끼지 말자' 라는 자기반성을 주기 때문이다.


나는 '개발자'로 밥벌이를 하고 있다. 나는 스스로 '지식 노동자'라고 한다.

어떤 요청을 받고 그에 대한 문제해결을 위한 소프트웨어를 개발하는 것이 내 일이다. 이 일은 꾸준하게 '흥미'를 유발하고 더욱 성장하라는 자극을 부여한다.

물론... 그 자극의 효과는 지속적이지는 않다.

지속성을 유지하기 위해서는 '흥미'에 의한 단발적 시도가 아닌 '습관'과 '태도'로 만들어야 한다.

세상에는 많은 개발자가 있다. 그 개발자들 중에는 나보다 뛰어난 개발자들이 있다.

그 개발자들이 나보다 뛰어나고 많은 것을 할 수 있는 능력자들이라는 것을 아는 순간, 그들에게서 많은 것을 보고 배울 수 있게 된다.

아는 것을 늘리기 위해 멈추지 말자.


임백준님이 쓰신 글이 오늘 회자되었다:

거기에 나오는 다음 구절들이 인상깊다.

“진짜가 될 때까지 진짜처럼 행동하라 (Fake it till you make it)”는 원리는 프로그래밍만이 아니라 여러 분야에서 널리 회자되는 행동요강이다.

  • 성공을 위해서 필요한 기술과 재능을 이미 갖추고 있는 사람처럼 행동하라.
  • 되고 싶은 부류의 사람이 이미 된 것처럼 행동하라.
  • 승부가 이미 끝났으며 자신이 크게 승리한 것처럼 행동하라.
  • 처음 가보는 길을 이미 여러 번 왕래한 길인 것처럼 여기며 행동하라.

“캐리어를 통틀어서 저는 9,000번의 슛을 실패했습니다. 300번 가량의 시합에서 패했고요. 동료들이 믿고 패스해준 마지막 슛 찬스를 26번이나 놓쳤습니다. 인생을 살아오면서 저는 끊임없이 실패를 되풀이했습니다. 바로 그랬기 때문에 저는 성공을 거둘 수 있었습니다. - 마이클 조던”




P.S. 이 글은...

함수형 프로그래밍 언어를 공부하면서...

배치성 작업을 멀티스레드로 돌릴방법을 궁리하면서 문득 들었던 생각을 정리해봤다.


그리고 브런치로도 써봤다. https://brunch.co.kr/@ihoneymon/10

도메인주도설계(Domain-Driven Design, DDD)  읽는 중



소프트웨어 시스템을 분리하는 방법.

계층화. 계층화의 핵심 원칙은 한 계층의 모든 요소는 오직 같은 계층에 존재하는 다른 요소나 계층상 "아래"에 위치한 요소에만 의존한다는 것이다. 위로 거슬러 올라가는의사소통은 반드시 간접적인 메커니즘을 거쳐야 하며...

계층화의 가치는 각 계층에서 컴퓨터 프로그램의 특정 측면만을 전문적므로 다룬다는 데 있다. 이러한 전문화를 토대로 각 측면에서는 더욱 응집력 있는 설계가 가능해지며, 이로써 설계를 더욱 쉽게 이해할 수 있다.


가장 바람직한 아키텍처 프레임워크라면 도메인 개발자가 모델을 표현하는 것에만 집중하게 해서 복잡한 기술적 난제를 해결한다.
...
프레임워크를 적용할 때 팀은 프레임워크의 목적에 집중해야 하는데, 그러한 프레임워크의 목적은 도메인 모델을 표현하고 해당 도메인 모델을 이용해 중요한 문제를 해결하는 구현을 만들어내는 데 있다.


한 프레임워크를 이용해 해결하기 힘든 갖가지 측면은 어려운 문제를 해결하고자...여러 프레임워크를 선택적으로 적용해서 극복할 수 있다. 프레임워크의 가장 유용한 기능만 분별력있게 적용한다면 구현과 프레임워크 간의 결합이 줄어들어 차후 설계 의사결정을 더욱 유연하게 내릴 수 있을 것이다.


도메인 로직이 프로그램상의 다른 관심사와 섞여 있다면 그와 같은 대응을 달성하기가 수월하지 않다. 따라서 도메인 주도 설계의 전제조건은 도메인 구현을 격리하는 것이다.

결국 도메인을 격리할 때의 가장 좋은 점은 부수적인 것을 배제하고 도메인 설계에만 집중할 수 있다는 것이다.


+ Recent posts