colorful spring boot

spring boot의 banner / log 에 color 를 입혀보자

colorful spring boot

 

ASCII ART

문자 -> ASCII ART로 변환 : http://patorjk.com/software/taag/#p=display&f=Graffiti&t=Type%20Something%20

그림 -> ASCII ART로 변환 : http://picascii.com/

banner 변경

공식 문서 참조 : https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-spring-application.html

resources 디렉토리에 ASCII ART 로 디자인한 내용을 담은 banner.txt 파일을 놔두면 된다.

color 입히는 방법은 아래 예제를 참조

banner.txt
${AnsiColor.RED} ____   ____   ${AnsiColor.BLACK}    _________ __                          __
${AnsiColor.RED}|    | /  _/   ${AnsiColor.BLACK}   /   _____/|  |__   ____ ______ ______ |__| ____    ____
${AnsiColor.RED}|        <     ${AnsiColor.BLACK}   \_____  \ |  |  \ /  _ \\  __ \\  __ \|  |/    \  / __ \
${AnsiColor.RED}|    |    \    ${AnsiColor.BLACK}   /        \|   Y  (  <_> )  |_> >  |_> >  |   |  \/ /_/  >
${AnsiColor.RED}|____|\____\   ${AnsiColor.BLACK}  /_________/|___|__/\____/|   __/|   __/|__|___|__/\___  /
${AnsiColor.RED}               ${AnsiColor.BLACK}                           |__|   |__|             /_____/
${AnsiColor.BLACK} ...Running ${AnsiColor.BRIGHT_RED}Kshop sample application${AnsiColor.BRIGHT_BLACK}

 

log coloring

logback(spring boot default logger) 의 log encoder 를 설정하면 된다.

설정 방법은 아래 예제를 참조

logback.groovy
appender('console', ConsoleAppender) {
    encoder(PatternLayoutEncoder) {
        pattern = '%magenta(%d{yyyy-MM-dd HH:mm:ss.SSS}) %highlight(%5level) --- %yellow([%25thread]) %cyan(%logger{36}) - %msg%n'
    }
}
appender('rolling', RollingFileAppender) {
    file = "logs/daily.log"
    append = true
    rollingPolicy(TimeBasedRollingPolicy) {
        fileNamePattern = "/logs/daily.log.%d{yyyy-MM-dd}.gz"
        maxHistory = 10
    }
    encoder(PatternLayoutEncoder) {
        pattern = '%-5level %d{yyyy-MM-dd HH:mm:ss} [%thread] %logger{36} - %msg%n'
    }
}
root(INFO, ['console', 'rolling'])

TCP Characteristics

TCP / IP에는 표준화 된 구현에 따라 작동하는 특성이 있으며, 이러한 특성 때문에 개발을 어떻게 하느냐에 따라 성능 저하를 초래할 수 있다.

TCP / IP 기능이 응용 프로그램에 미치는 영향은 응용 프로그램의 성격이 transactional 한지 아니면 streaming 인지에 따라 다르다.

transactional 한 응용 프로그램은 connection 및 termination 오버 헤드의 영향을 받는다.

three handshake에 대한 이미지 검색결과

예를 들어, 이더넷 네트워크에서 연결이 설정 될 때마다 약 60 바이트의 세 패킷(3-handshake)을 보내야하며 교환을 위해 약 하나의 RTT가 필요하다.

연결 종료가 발생하면 네 개의 패킷이 교환된다. 이것은 매번의 연결마다 발생하며, 연결을 열고 닫는 행위는 종종 응용 프로그램 오버 헤드를 발생시킨다.

 

TCP / IP의 또 다른 측면은 연결이 설정 될 때마다 발생하는 느린 시작이다.

느린 시작이란 해당 세그먼트의 수신을 수신하기 전에 보낼 수있는 데이터 세그먼트 수에 대한 인위적인 제한이다. 느린 시작은 네트워크 정체를 제한하도록 설계되었다.

이더넷을 통한 연결이 설정된 경우 수신기의 창 크기에 관계없이 느린 시작으로 인해 4KB 전송에 3-4 RTT가 걸릴 수 있다.

 

nagle algorithm에 대한 이미지 검색결과

Nagle 알고리즘이라고하는 TCP / IP 최적화는 연결시 데이터 전송 속도를 제한 할 수도 있다. Nagle 알고리즘은 한 번에 한 문자 씩 보내는 텔넷과 같이 소량의 데이터를 보내는 응용 프로그램의 프로토콜 오버 헤드를 줄 이도록 설계되었다. 많은 헤더와 작은 데이터가있는 패킷을 즉시 보내지 않고 스택은 계속하기 전에 응용 프로그램 또는 확인 응답에서 더 많은 데이터를 기다린다. 이 데이터가 곧 도착하지 않고 송신 측이 확인을 기다리는 경우, 송신 당 약 200 밀리 초의 응답 지연이 발생할 수 있다.

 

time wait에 대한 이미지 검색결과

TCP 연결이 닫히면 노드의 연결 리소스가 시작되는데, 이때 TIME-WAIT 메시지가 보내진다. 이는 server/client 양단간의 연결 종료를 보장하기 위한 장치인데, 이 때문에 응용 프로그램을 자주 열고 닫으면 RAM 및 포트와같이 필요한 리소스가 고갈 될 수도 있다.

그밖에 congestion avoidance algorithm, 스트리밍 응용 프로그램에서 너무 작은 window size 등이 영향을 끼친다.

spring boot

참고 문서

 

spring-boot

spring boot life-cycle

Cosysto Gimbh 라는 사람이 life-cycle에 대한 flow 를 아주 잘 그려놓았다.

 


 

실제 구현체 이름은 매우 길고 복잡하다. (e.g. ConfigurationWarningsApplicationContextInitializer)

spring application 내부에서 실제로 어떤 일이 일어나는지에 집중하도록 긴 이름은 짧게 축약하였다.

 

ApplicationContext 생성(=SpringApplication.run())

  • Listener 생성 및 시작
  • Environment 준비
  • Context 생성
  • Context 준비
    • Environment 세팅
    • 후처리(post process)
    • Initializer 초기화(apply initializer)
  • Context 로드
    • Source 로드
    • BeanDefinitionLoader 에 Source , Context 탑재
    • BeanDefinitionLoader 로드
  • 종료
    • Context Refresh

 

ApplicationContext 는 SpringApplication의 몸통에 해당하는 실제 instance의 추상화이다.

SpringApplication

SpringApplication 의 Entry point 는 반드시 특정 package를 지정하고, 그 하위에 위치시켜야 한다. (=Default package path에 entry point 시작 금지)

그렇지 않으면 ** WARNING ** : Your ApplicationContext is unlikely to start due to a @ComponentScan of the default package.  이 발생한다.

 

@SpringBootApplication 을 사용하면 다음 Annotation 을 생략 가능하다.

Source Code


@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Configuration
@EnableAutoConfiguration
@ComponentScan
public @interface SpringBootApplication {
/**
* Exclude specific auto-configuration classes such that they will never be applied.
* @return the classes to exclude
*/
Class<?>[] exclude() default {};
}

 

SpringApplicationRunListener

ConfigurableEnvironment

ConfigurableApplicationContext

 

실제로 호출되는 implementation 은 AnnotationConfigEmbddedWebApplicationContext 이다.

java wildcard

참고 문서

개요

작년에 wildcard에 대해 스터디한 내용이 있었다는 사실에  한 번 개깜놀

까맣게 잊고 있었다는 사실에 두 번 개깜놀

wildcard 와 type parameter 간의 가장 중요한 차이점은 code statement 내에 선언된 param이 재참조되는지 여부이다.

재참조 되면 type parameter 써야 하고, 재참조 안되고 귀찮으면 그냥 ? 쓴다.

Wildcard ?

java syntax에는 ? 라는 것이 있다. generic type parameter로 쓰인다. (e.g. ArrayList<?>)

Long story short…

generic type parameter 를 명확히 결정하지 않은 채로 두기 위한(=unknown type) syntax

Generic 의 type parameter 는 polymorphism 을 지원하지 않는다.

generic에서는 type prarmeter 에 대하여 polymorphism 을 지원하지 않는다.  다음 예제를 보자.

Generic

Collection<String> foo = new ArrayList<String>;
Collection<Object> refFoo = foo; //compile error!!

refFoo.add(new Object());
String bar = foo.get(0); //만일 line2 가 error가 아니었다면, bar 에 Object 를 할당하는 참사가 발생하게 된다.

 

얼핏 생각하면 ArrayList<Dog> 을 List<Animal> 으로 받을 수 있을 것 같지만, 상기 예제와 같은 문제를 막기 위해 Generic 의 type parameter 는 문법적으로 subtype 변환을 허용하지 않는다.

혼란을 막기 위한 제한이지만, type 체크가 엄격하면 유연한 사용이 힘들다. Wildcard 는 이런 상황을 해결하기 위해 고안된 문법이다.

 

case Wildcard<?>
private void foo(Collection<?> c) {
  for (Object iter : c)
    System.out.println(iter);
}

List<String> bar = new ArrayList<String>;
bar.add("hello");

foo(bar); //okay!!

 

case Obect

private void foo(Collection<Object> c) {
  for (Object iter : c)
    System.out.println(iter);
}

List<String> bar = new ArrayList<String>;
bar.add("hello");

foo(bar); //compile error!!

 

Wildcard 사용시 주의할 점

 

bounded wildcard

 

spring에서 용례

spring-context 의 SimpleApplicationEventMulticaster.multicastEvent()

정확한 type을 알 수 없는 ApplicationListner<T>를 이터레이션 하기 위하여 wildcard가 사용되었다.

SimpleApplicationEventMulticaster.java

@Override
public void multicastEvent(final ApplicationEvent event) {
  for (final ApplicationListener<?> listener : getApplicationListeners(event)) {
    Executor executor = getTaskExecutor();
    if (executor != null) {
      executor.execute(new Runnable() {
        @Override
        public void run() {
          invokeListener(listener, event);
        }
      });
    }
    else {
      invokeListener(listener, event);
    }
  }
}

Abstraction 관점에서 보는 프로그래밍 패러다임

개요

1~3년차 개발자이거나 프로그래밍의 기초가 부족한 사람, 프로그래밍 패러다임이 무엇인지 모르는 사람이라면 이 글을 읽어달라.

코드 재사용이 무엇인지. 특히 객체지향 설계 패러다임에서의 코드 재사용이란 무엇인지 살펴보고자 한다.

 

왜 코드를 재사용하나

코드를 재사용하는 방법은 많다. 흔히들 사용하는 컨트롤CV 패턴이라던지 깃허브다운로드 패턴도 코드를 재사용하는 한 방법이다.

누군가가 만들어둔 코드를 어떤 방식으로든지 재사용하면 그게 바로 코드 재사용이다.

허나 남이 짜둔 코드 블럭을 복사 붙여넣기로 재사용하다 보면 언젠가는 반드시 큰 코를 다치게 된다.

만일 복붙한 코드에 문제가 있어 수정을 해야 한다면? 그런데 해당 코드블럭을 100번 정도 복붙했다면? 너가 이 생이 끝나기 전까지 제대로 수정할 수 있을까?

복붙의 갯수가 늘어날 수록 프로그램의 복잡도는 쓸데없이 높아진다. 코드 재사용에도 바람직한 방법과 그렇지 않은 방법이 있다는거고, 복붙은 그리 좋은 방법이 아니라는 거다.

묻는다. 우리는 왜 코드를 재사용하나? 너는 왜 스택오버플로우에서 검색한 코드를 복붙하여 쓰는거냐?

답은 “귀찮아서” 이다.

나와 같은 문제를 해결하기 위해 고민한 누군가의 결과물을 가져다 쓰기 위함일 수도 있고, 예전에 짜둔 코드가 여기저기서 사용될 필요가 있는데 새로 만들기는 귀찮아서이기도 하다.

과거의 언어 설계자들도 반복되는 작업이 귀찮았다. 다만 복붙보다는 고급진 방법으로 문제를 해결하려 하였다.

 

아래는 어셈블리 Macro 로 문자열을 출력하는 코드를 작성한 예시이다.

Assembly Macro
; A macro with two parameters
; Implements the write system call
   %macro write_string 2
      mov   eax, 4
      mov   ebx, 1
      mov   ecx, %1
      mov   edx, %2
      int   80h
   %endmacro
 
section .text
   global _start            ;must be declared for using gcc
    
_start:                     ;tell linker entry point
   write_string msg1, len1              
   write_string msg2, len2   
   write_string msg3, len3 
    
   mov eax,1                ;system call number (sys_exit)
   int 0x80                 ;call kernel
section .data
msg1 db 'Hello, programmers!',0xA,0xD  
len1 equ $ - msg1          
msg2 db 'Welcome to the world of,', 0xA,0xD
len2 equ $- msg2
msg3 db 'Linux assembly programming! '
len3 equ $- msg3

 

내가 수행하고자 하는 어떤 기능에 이름을 붙이고, 이 기능이 필요할 때 마다 이름을 불러 사용하겠다는 아이디어는

프로그램 언어에 따라 Macro / function / sub-program / sub-routine / method 등등등 여러 가지 형태로 구현되어 불필요한 반복작업을 대체하였다.

프로그램 내에 존재하는 프로그램(이후에는 그냥 function으로 부르도록 하겠다.)으로 반복되는 문제를 해결하기 위한 프로그래밍 사조가 Structured Programming Paradigm이다.

그렇다면, Structured Programming Paradigm은 반복되는 문제들을 일소에 해결하였을까? 아니면 새로운 단계의 복잡한 문제들이 생겨났을까?

 

No Silver Bullet

프레드릭 브룩스 께서 쓴 No Silver Bullet 이라는 에세이를 요약하면 이렇다.

No Silver Bullet

요구사항이 10개인 소프트웨어는 태생적으로 10개 만큼의 복잡한 문제가 존재하고

여기에 개발자 놈들이 싸지르는 문제들이 +α 가 되기 때문에

니들이 아무리 노력해 봐야 +α 만큼의 문제를 줄이는 것 밖에는 안되겠지만 그거라도 해라.

본질적으로 소프트웨어는 복잡하다. 거기에 개발자가 복잡도를 더하지만 않으면 그것만으로도 매우 잘 설계된 소프트웨어다.

function 의 발명은 분명히 반복되는 작업을 줄여주었고, 더욱 크고 복잡한 문제들을 다룰 수 있게 하였다.

허나 문제의 복잡한 수준이 어느 정도를 넘어가면서, 새로운 차원의 문제에 봉착하게 되었다.

 

다음과 같은 요구사항을 만족하는 소프트웨어를 개발한다고 가정해 보자.

문서 편집기 소프트웨어 요구사항

a)로컬 저장소 또는 b) 클라우드 를 통해

1)엑셀 또는 2)워드 또는 3)PPT 문서 파일을 열어서,

a)로컬 저장소 또는 b)클라우드 에 저장하는 소프트웨어

가 있다고 가정해보자.

사용자가 엑셀 파일을 열지, 워드파일을 열지, PPT 파일을 열지는 소프트웨어 실행 중에 결정되기 때문에 아무도 모른다. 그때 그때 달라지는 사용자 맘이다.

또한 사용자가 파일을 로컬 저장소에 저장할지, 클라우드에 저장할지 역시 소프트웨어 실행 중에 결정되기 때문에 마찬가지로 사용자 맘이다.

 

종래의 Structured Programming Paradigm으로 이 문제를 해결하려면

1) 다루고자 하는 시스템의 갯수 x 기능의 갯수 만큼 function을 생성하여

2) 조건에 따라 적절히 분기하여, 해당 시나리오에 맞는 function을 호출

하여야 한다.

 

위의 문제의 경우, system의 갯수는 a)로컬 저장소, b)클라우드 2 개이며

기능의 갯수는 1) 엑셀 열기, 2) 엑셀 닫기, 3) 워드 열기, 4) 워드 닫기, 5) PPT 열기, 6) PPT 닫기 총 6개이다.

따라서, 구현하여야 하는 function의 총 갯수는 2 x 6 = 12 개 이다.

 

예시의 경우 system과 기능의 갯수가 많지 않기 때문에 function의 갯수가 그리 많지 않지만,

현실에서 다루는 문제들은 보통 이것보다 크고 복잡하다. 아무튼 좋다. 까짓거 함 만들어보자.

 

실제로 이 function들을 조건에 맞게 적절히 분기하는 logic을 만든다고 생각해보자.

if (open == 파일저장소) {
    if(source == 엑셀) {
        open엑셀();
    } else if (source == 워드) {
        open워드();
    } else if (source == PPT) {
        openPPT();
    }
} else if (open == 클라우드) {
    ...
}

Structured Programming Paradigm 으로는 분기되는 조건의 갯수만큼 if else 를 발라야 한다.

오케이 좋다. 백번 양보해서 12번 if else 발라가면서 개발해보자.

 

고갱님께서 앞으로는 파일 목록에 에버노트를 추가하고, 에버노트 전용 클라우드에 저장하는 기능을 추가해 달란다.

물론 에버노트 클라우드에는 기존에 존재하던 엑셀 / 워드 / PPT 도 저장이 되어야 한다.

 

c)에버노트 전용 클라우드

7)에버노트 열기 8) 에버노트 닫기

라는 system 과 기능이 추가되었기 때문에 function은 24개를 만들어야 하고 분기로직도 수정을 하여야 한다.

이 시점에서 개발자는 절로 토가 쏠린다. 머리에 피가 쏠리며 대략 정신이 멍해진다.

 

여기서 우리가 줄일 수 없는 태생적인 복잡도는 24개의 function 이다.

허나 24개의 분기처리 로직은 줄일 수 있다.

어떻게?

Object Oriented Paradigm 으로 소프트웨어를 개발하면 된다.

 

Dynamic binding 과 run time 단계에서의 Abstraction

위 프로그램에서 각 경우의 수 만큼 분기처리를 하여야만 했던 이유는 사용자가 어느 시점에 무슨 행동을 할지 몰랐기 때문이다.

사용자가 어느 시점에 뭘할지 모르는 상태, 바로 이 상태를 표현할 수 있는 방법은 없을까?

 

다음과 같은 class 를 살펴보자.

static binding
class File {
    ...
}
 
class 워드File extends File {
    void open() {...};
}
 
워드File foo = new 워드File();
foo.open();

워드File 객체 foo의 메쏘드 foo.open() 은 언제 어느 때 호출하여도 항상 워드File을 open 할 것이다.

다시 말하면, open() 메쏘드의 동작은 컴파일 타임에 이미 결정되었으며, 바뀌지 않을 것이다.

 

그러면 다음과 같은 경우를 살펴보자.

dynamic binding
interface File {
    open();
}
 
class 워드File implements File {
    void open() {...};
}
 
class 엑셀File implements File {
    void open() {...};
}
 
File foo;
if (source == 워드)
    foo = new 워드File;
else if (source == 엑셀)
    foo = new 엑셀File;
 
foo.open();

위와는 다르게, File 인터페이스 객체 foo 의 메쏘드 open() 을 호출하면 실제로 호출되는 method 는 실행 시점의 조건에 따라 그때 그때 달라진다.

source가 워드면 워드File::open() 메쏘드가 호출되고, source가 엑셀이면 엑셀File::open() 메쏘드가 호출된다.

 

아까 전의 문제로 돌아가 보자면, 우리는 사용자가 어느 시점에 뭘할지 몰랐기 때문에 복잡하게 분기처리를 하여야만 했다.

헌데 객체지향 언어에서는 사용자가 뭘 할지 모르는 상태를 위와 같이 추상화할 수 있다.

 

위의 문제에서는 사용자가

1) 클라우드를 사용할지, 로컬저장소를 사용할지 모르고

2) 엑셀을 쓸지, 워드를 쓸지, PPT 를 쓸지 모른다.

 

이 실행 중에 사용자가 뭘 쓸지 모르는 상태를 추상화해보자.

 

Polymorphism, 객체지향 설계의 핵심

이제 개쩌는 매직쇼가 벌어질 시간이다. 최초의 프로그램에선 12가지 조건에 대한 분기 처리를 하여야만 했다.

객체지향의 가장 중요한 특징인 Polymorphism 으로 분기 처리가 어떻게 바뀌는지 살펴보자.

polymorphysm
interface File {
    open();
}
 
interface Cloud {
}
 
interface Local {
}
 
interface CloudFile extends Cloud, File {
    void open();
}
 
interface LocalFile extends Local, File {
    void open();
}
 
class Cloud워드File implements CloudFile {
    void open() {...};
}
 
class Local엑셀File implements LocalFile {
    void open() {...};
}
...
 
File foo;
AbstractFileFactory fooFactory;
if (open == 파일저장소)
    fooFactory = new LocalFileFactory();
else if (open == 클라우드)
    fooFactory = new CloudFileFactory();
    
if (source == 엑셀)
    foo = fooFactory.create(엑셀);
else if (source == 워드)
    foo = fooFileFactory.create(워드);
else if (source == PPT)
    foo = fooFileFactory,create(PPT);
... 
 
foo.open();

조건문에 따른 분기가 5개로 줄었다. 여기서 에버노트 및 에버노트 클라우드가 추가된다면 조건문은 단지 2개가 더 늘어날 뿐이다.

종전에 요구사항이 추가되었을 때 총 24개의 분기처리를 해야만 했던 것에 비하면 다루어야 할 복잡도가 훨씬 간소화되었다.

java8 consumer / supplier 를 이용한 builder pattern 구현

개요

java8 feature에 대하여 실무에서 활용 가능한 수준으로 best practice 제공

시작하는 말

디자인패턴에 대한 흔한 오해 중 대표적인 한 가지가 “디자인패턴은 프로그램 언어 독립적이다.” 입니다.

허나 실상은 그렇지 않으며, 매우 언어에 종속적이어서 특정 언어에서 제공하는 기능을 면밀히 살피지 않으면

의도한 대로 코드가 동작하지 않을 수도 있고, 해당 언어만의 장점을 살리지 못하는 반쪽짜리 코드가 될 수도 있습니다.

java8 에 추가된 functional interface 중 consumer / supplier 를 이용하면 보다 직관적인 builder pattern을 구현할 수 있습니다.

먼저, 종래에 java에서 builder pattern을 어떻게 구현하는지 살펴보고, 여기에 java8 feature 를 추가하여 어떻게 더 나아지는지 살펴보겠습니다.

builder pattern?

왜 쓰나

디자인패턴을 공부할 때 항상 염두에 두어야 할 것이 있습니다.

이 패턴을 왜 쓰나?

무엇이 좋아지나?

무엇이 안좋아지나?

 

모든 문제를 한방에 해결하는 만능의 해법이란 존재하지 않습니다.

어떤 문제를 해결하기 위한 선택을 하면 반드시 trade-off 가 발생합니다.

 

스타크래프트에서, 미네랄을 먼저 채취하여 후반에 많은 병력을 생산하기 위한 12드론 2해처리 빌드는 초반 러시에 취약하다는 단점이 존재합니다.

반대로 초반에 병력을 상대보다 먼저 뽑는 4드론 빌드는 초반러시에 강력하나 후반으로 갈수록 취약합니다.

초반에 병력도 많이 뽑고, 미네랄도 많이 캐고, 후반 병력도 많이 뽑는 그런 마법과도 같은 해법이 존재할까요?

아니면 그때 그때 상황에 맞는 적절한 젼략을 사용해야 할까요?

 

예시가 즈질스러우니 국민 패턴 싱글톤 패턴을 예로 들어봅시다.

싱글톤으로 생성된 객체는 어플리케이션 실행~종료 시까지 어디에서나 접근 가능합니다.

어플리케이션 내부에서 공유하여야 할 리소스 정보를 쉽게 참조 가능하다는 장점이 존재하지만,

싱글톤 객체는 객체지향 언어 버전의 전역변수나 마찬가지여서

1) 싱글톤 객체 – 다른 객체간 의존성이 높아짐,

2) 인스턴스 생성의 책임이 분리되지 않기 때문에 SRP 위반,

3) unittest객체가 싱글톤 객체에 종속성이 발생하기 때문에 단위 테스트 어려움

등의 단점이 있습니다.

객체지향 5원칙 (aka. S.O.L.I.D)에 만족하는 싱글톤 패턴은 존재하지 않습니다. 싱글톤 자체가 태생적으로 SRP를 위반하기 때문이죠.

허나 파일경로, ip주소 등 1) 쉽게 변경되지 않고, 2) 어플리케이션 영역 전체에 공유가 필요한 리소스는 싱글톤으로 구현하는 편이 유리합니다.

 

GoF 의 디자인패턴에 소개된 모든 패턴은 저마다 장/단점이 분명합니다.

각 패턴의 장/단점이 무엇인지 분명히 알면 장점은 부각시키고 단점은 최소화할 수 있습니다.

장점은 살리고, 단점은 감춘다. 이것이 좋은 소프트웨어 아키텍트가 갖추어야 할 설계 역량입니다.

 

이 글에서 소개할 디자인 패턴은 builder 패턴입니다. 빌더 패턴을 쓰는 이유는 크게 2가지입니다.

1) 귀찮아서

2) 보기 좋으라고

 

다음과 같은 class foo 를 가정해 봅시다.

귀찮은 class 생성
public class foo {
    private String a;
    private String b;
    private String c;
    private String d;
    private String e;
    private String f;
    private String g;
    private String h;
    private String i;
    private String j;
    private String k;
    private String l;
    public foo(String a, String b, String c, String d, String e, String f,
               String g, String h, String i, String j, String k, String l) {
        this.a = a;
        this.b = b;
        this.c = c;
        this.d = d;
        this.e = e;
        this.f = f;
        this.g = g;
        this.h = h;
        this.i = i;
        this.j = j;
        this.k = k;
        this.l = l;
    }
}

new foo(a,b,c,d,e,f…,z) 하기 귀찮습니다. 또 생성자의 parameter 가 길어지면 실수나 오타를 발견하기도 어렵습니다.

 

빌더 패턴은 new foo(a,b,c,d,e,f…,z) 를 new builder().a(a).b(b)…build() 로 보기 좋게 바꾸기 위하여 사용합니다.

‘보기 좋게 바꾼다’고 표현했지만 실제로는 부수적으로 따라오는 몇 가지 장점이 더 있습니다.

1) 실수, 오타의 여지를 줄여줌

2) 생성 parameter에 제약조건을 부여할 수 있습니다. (e.g. 타입, 필수 여부 등등)

 

java8 feature 를 사용하지 않은 builder pattern, Ver. 1

빌더 패턴의 가장 기본적인 아이디어는 객체 생성의 책임을 분리하는 데에 있습니다.

내가 만들려 하는 객체 생성의 역할을 다른 객체에 위임하는 것이죠.

 

builder pattern의 가장 기본적인 구현은 다음과 같습니다.

foo builder
public class fooBuilder {
    private String a;
    private String b;
    ...
    
    public fooBuilder setA(String a) {
        this.a = a;
        return this;
    }
    public fooBuilder setB(String b) {
        this.b = b;
        return this;
    }
    ...
    public foo build() {
        return new foo(this.a, this.b, ...);
}

 

위에서 작성한 fooBuilder class 를 사용하는 방법은 다음과 같습니다.

foo builder
//기존
foo myFoo = new foo(a,b,c,d,...);
 
//builder 사용
fooBuilder myFooBuilder = new fooBuilder();
foo myFoo = myFooBuilder.setA(a).setB(b)....build();

 

위에서 작성한 fooBuilder 클래스는 드닥 마음에 들지 않습니다. foo 객체를 생성하기 위해 불필요하게 fooBuilder 객체도 생성하여야 하니까요.

그러면 fooBuilder 의 메소드를 static으로 만들면 어떨까요? fooBuilder 객체를 생성하지 않아도 foo 객체를 생성할 수 있지 않을까요?

 

java8 feature 를 사용하지 않은 builder pattern, Ver. 2

fooBuilder class를 이렇게 바꿔봅시다.

foo builder
public class fooBuilder2 {
    private String a;
    private String b;
    ...
    
    public static fooBuilder setA(String a) {
        this.a = a;
        return this;
    }
    public static fooBuilder setB(String b) {
        this.b = b;
        return this;
    }
    ...
    public static foo build() {
        return new foo(this.a, this.b, ...);
}
foo builder
//기존
foo myFoo = new foo(a,b,c,d,...);
 
//builder 사용
fooBuilder myFooBuilder = new fooBuilder();
foo myFoo = myFooBuilder.setA(a).setB(b)....build();
 
//builder2 사용
foo myFoo = fooBuilder2.setA(a).setB(b)...build();

불필요한 builder 객체 생성을 없앴습니다. 어떤가요? 잘 작동할까요?

절대 이딴 식으로 프로그래밍 하시면 안됩니다. 오밤중에 서버실 끌려가 밤새도록 디버깅하는 수가 있어요.

fooBuilder2 는 thread safe 하지가 않습니다. 따라서 concurrent 환경에서 foo 객체가 지멋대로 만들어 지는 수가 있어요.

concurrent 와 thread safety 얘기는 길어지니 다음에 따로 다루기로 하고, 암튼 fooBuilder2 는 실패실패 대실패입니다. 절때 이딴식으로 개발하면 안되요.

 

java8 feature 를 사용하지 않은 builder pattern, Ver. 3

서두에 언급하였듯, 디자인패턴은 언어 종속적입니다.

java 언어에서만 지원하는 특별한 feature(inner class) 를 사용하면 builder pattern을 좀 더 아름답게 구현할 수 있어요.

fooBuilder를 foo class 내에 static inner class 로 구현하는 겁니다.

foo builder
public class foo {
    private String a;
    private String b;
    ...
    public foo(String a, String b, String c, String d, String e, String f,
               String g, String h, String i, String j, String k, String l) {
        this.a = a;
        this.b = b;
        ...
    }
    public static class fooBuilder {
        private String a;
        private String b;
        ...
        fooBuilder() {}
        public fooBuilder setA(String a) {
            this.a = a;
            return this;
        }
        public fooBuilder setB(String b) {
            this.b = b;
            return this;
        }
        ...
        public foo build() {
            return new foo(a, b,...);
        }
    }
}
foo builder
//기존
foo myFoo = new foo(a,b,c,d,...);
 
//builder 사용
fooBuilder myFooBuilder = new fooBuilder();
foo myFoo = myFooBuilder.setA(a).setB(b)....build();
 
//builder2 사용
foo myFoo = fooBuilder2.setA(a).setB(b)...build();
 
//static inner class builder 사용
foo myFoo = new foo.fooBuilder.setA(a).setB(b)...build();

static inner class 로 객체 생성 코드를 보기 좋게 바꾸었지만 여전히 단점은 존재합니다.

1개의 foo 객체 생성을 위하여 반드시 1개의 fooBuilder 객체를 생성해야 하는 문제는 남아 있습니다.

메모리를 효율적으로 사용해야 하는 환경에서는 적합하지 않습니다.

또한 builder class가 inner class 로 포함되면서 클래스 재사용성에 대한 고민을 다시 해야 합니다.

 

java8 feature 를 사용한 builder pattern

지금까지

1) GoF가 처음 제안한, 고전적 형태의 Builder pattern

2) java의 언어적 특징, static inner class 를 사용한 Builder pattern

을 살펴 보았습니다.

이번에는 java8에 추가된 functional interface 를 사용하여 builder pattern을 구현하는 방법을 소개할건데요.

시작하기에 앞서, java8의 Consumer / Supplier 가 무엇인지 알아보도록 합시다.

 

functional interface?

functional interface에 대하여 잘 모르는 분들이 많이 계시리라 생각되어 간단히 소개합니다.

함수형 언어의 가장 큰 특징 중 하나가 function(=method) 을 변수처럼 사용하는겁니다.

 

예를 들면,

var foo = someFunction(); //함수 definition에 대한 reference
 
var foo = (x) -> x*x; //definition이 없는 함수(=익명함수)에 대한 reference

 

예제에서의 var foo 처럼 function을 저장하는 방법이 언어적으로 제공되어야 하는데요.

java8 이전의 java에서 L-value 에 위치할 수 있는 type 들(Object나 primitive type 등)은 function reference 에 대한 저장을 고려하지 않았습니다.

때문에 함수형 언어로의 확장을 위하여 java8은

1) var foo 와 같은 역할을 제공하는 새로운 type을 정의하던가

2) 아니면 기존에 제공하던 언어 기반을 크게 변경하지 않으면서 꼼수를 쓰던가

둘 중 하나를 선택하여야 했습니다.

 

java8 은 2) 를 선택하는 꼼수를 두었는데요. 그 꼼수가 functional interface 입니다.

java compiler는 @FunctionalInterface 라는 Annotation 이 포함된 다음 조건을 충족하는 interface를 위 예제의 var foo 처럼 취급하기로 약속하였습니다.

1) non-static method

2) non-default method

3) only 1 method in interface

@FunctionalInterface
interface var {
    void execute(String arg);
}
 
var foo = (x) -> System.out.println(x);
 
foo.execute("Hello");

참 쉽죠?

 

java8에선 기본적인 형태의 functional interface 를 미리 정의해 두었습니다.

Consumer / Supplier / Function / Predicate 4가지인데요.

각각 용도가 다릅니다만 자세한 것은 다음 기회에 다루도록 하겠습니다.

 

Consumer 를 사용한 builder pattern

기본적인 아이디어는 이렇습니다.

1) 생성하고자 하는 객체의 setter method 를 변수로 저장

2) 객체 생성 시 저장해 두었던 setter method를 실행하여 객체 생성

 

구현체는 다음과 같습니다.

foo builder
public class foo {
    private String a;
    private String b;
    ...
    public foo(String a, String b, String c, String d, String e, String f,
               String g, String h, String i, String j, String k, String l) {
        this.a = a;
        this.b = b;
        ...
    }
    public static class fooBuilder {
        private foo _myFoo;
        private List<Consumer<foo>> _setters = new ArrayList<>();
        public <T> fooBuilder with(BiConsumer<foo, T> setter, T val) {
          if (_myFoo == null)
               _myFoo = new foo();
         Consumer<foo> c = _myFoo -> setter.accept(_myFoo, val);
         _setters.add(c);
         return this;
       }
       public foo build() {
           foo val = _myFoo;
           _setters.forEach(_setters -> _setters.accept(val));
           _setters.clear();
           return _myFoo;
       }
    }
}
foo builder
//function interface 로 구현한 builder class 사용
foo myFoo = new foo.fooBuilder().with(foo::setA, "A").with(foo::setB, "B")...build();

 

위 패턴의 장단점을 살펴 볼까요?

장점은

1) 코드 재사용 : 생성할 Class 에 존재하는 setter 를 Builder Class에 중복으로 생성하지 않고 변수에 담아 재사용

 

단점은

1) 대동소이한 Builder를 매번 재구현하여야 함

 

Supplier / Consumer 를 사용한 builder pattern

위의 단점을 개선하여 봅시다.

Builder 코드를 재사용하여 생산성을 높이는 데에는 다음 두 가지 방법이 있습니다.

1) 상속을 통한 재사용

2) Generic을 통한 재사용

 

여기선 2) 방법으로 builder pattern을 고쳐 보겠습니다.

 

구현체는 다음과 같습니다.

generic builder
public class GenericBuilder<T> {
    private final Supplier<T> _instance;
    private List<Consumer<T>> _setters = new ArrayList<>();
    private GenericBuilder(Supplier<T> instance) {
        this._instance = instance;
    }
    public static <T> GenericBuilder<T> of(Supplier<T> instance) {
        return new GenericBuilder<T>(instance);
    }
    public <U> GenericBuilder<T> with(BiConsumer<T, U> consumer, U value) {
        Consumer<T> c = setter -> consumer.accept(setter, value);
        _setters.add(c);
        return this;
    }
    public T build() {
        T value = _instance.get();
        _setters.forEach(iter -> iter.accept(value));
        _setters.clear();
        return value;
    }
}
foo builder
//Generic builder class 사용
foo myFoo4 = GenericBuilder.of(foo::new)
        .with(foo::setA, "A")
        .with(foo::setB, "B")
        .with(foo::setC, "C")
        ...
        .build();

 

위 패턴의 장단점은 다음과 같습니다.

장점은

1) 코드 재사용 : GenericBuilder 클래스를 범용적으로 재사용 가능

 

단점은

1) inner class 를 사용할 때 보다 직관적이지 않다.

 

결론

여기서는 builder 패턴만 다루었지만 다른 패턴 공부도 위와 비슷하게 진행하시면 많은 도움이 될 겁니다.

이 패턴을 왜 쓰는지

내가 사용하는 언어에 특화된 구현 방법은 없는지

특히 java8 feature 를 사용할 여지가 있는지

webapp framework 와 thread safety

아래 쓴 글 Servlet 의 동작방식과 thread safety 에서 응용편이다.

그러면 framework를 사용한 webapp 의 controller는 thread safe 할까?

결론부터 얘기하면 thread safe 하지 않다.

thread safe 한 코드를 짜기 위한 일반 원칙을 모두 지켜야 한다.

 

아래 참고사항은 잠들기 전 100번씩 암기해야 한다. 뭔말인지 모르겠어도 100번씩 외우고 자라.

thread safe 한 code 작성을 위한 일반 원칙

  1. thread 간 공유하는 resource가 존재하지 않는다.
  2. thread 간 공유하는 resource가 immutable 하다.
  3. thread 간 공유하는 resource가 mutable 하나, atomic 하다.
  4. thread 간 공유한느 resource가 mutable/non-atomic 하나, synchronized 하다.

 

spring이 되었든, struts 가 되었든, play 나 django가 되었든,

Servlet context 에 존재하는 resource를 접근하는 abstraction을 제공하게 되고, 여기서 접근되는 모든 데이터는 thread safe 하여야 한다.

당연히 framework 제작자는 상기 원칙대로 code를 생성하였기 때문에 framework는 멀티쓰레드 환경에서 안정적으로 동작하게 설계되어 있다.

항상 문제는 application(이라고 쓰고 bug라고 읽어라)을 만드는 개발자 놈들에게 있다.

 

아래 참고사항은 이닦기 전 1000번씩 암기해야 한다. 암기 못하면 이닦지 마라.

thread safe한 framework 코드 작성 원칙

  1. controller / action 등등등 Httprequest handler implementation에는 절대로 member variable 생성하지 마라.
  2. 만일 controller가 반드시 interface를 해야 할 일이 있다면, member variable 생성하는 대신 method parameter로 passing 하여 처리하라. 허나 좋은 설계를 하였다면 이런 일이 없어야 한다.
  3. 만일 2번으로 문제 해결이 되지 않는다면 너는 140% 잘못된 방향으로 개발을 하고 있다. 지금껏 니가 짠 코드를 지워라. 그게 프로젝트를 살리는 길이다.

요약

controller에 절대로 member 변수 만들지 마라.

servlet의 동작방식과 thread safety

 

3줄 요약

servlet, filter, listener 는 같은 어플리케이션 영역 안에 존재하며, 리소스를 공유한다.

상기 class 내의 member variable은 모두 thread safe 하지 않다.

맨 아래 예제처럼, local stack 내에서 parameter passing 하여 interface 하면 thread safe 하다.

ServletContext

When the servletcontainer (like Apache Tomcat) starts up, it will deploy and load all webapplications. When a webapplication get loaded, the servletcontainer will create the ServletContext once and keep in server’s memory. The webapp’s web.xml will be parsed and every <servlet><filter> and <listener> found in web.xml, or annotated with respectively @WebServlet@WebFilter and @WebListener, will be created once and kept in server’s memory as well. For all filters, the init() method will also be invoked immediately. When the servletcontainer shuts down, it will unload all webapplications, invoke the destroy() of all initialized servlets and filters, and finally the ServletContext and all ServletFilter and Listener instances will be trashed.

When the Servlet in question has a <servlet><load-on-startup> or @WebServlet(loadOnStartup) value greater than 0, then its init() method will also immediately be invoked during startup. Those servlets are initialized in the same order as “load-on-startup” value represents, or if they are the same, then the order in the web.xml or @WebServletclassloading. Or, if the “load-on-startup” value is absent, then the init() method will only be invoked on very first HTTP request hitting the servlet in question.

HttpServletRequest and HttpServletResponse

The servletcontainer is attached to a webserver which listens on HTTP requests on a certain port number, which is usually 8080 in development and 80 in production. When a client (user with a webbrowser) sends a HTTP request, the servletcontainer will create new HttpServletRequest and HttpServletResponse objects and pass it through the methods of the already-created Filter and Servlet instances whose url-pattern matches the request URL, all in the same thread.

In case of filters, the doFilter() method will be invoked. When its code calls chain.doFilter(request, response), then the request and response will continue to the next filter, or if there is none, hit the servlet. In case of servlets, the service() method will be invoked, which by default determines based on request.getMethod() which one of the doXxx() methods to invoke. If such method is absent on the actual servlet, then it will return HTTP 405 error.

The request object provides access to all information of the HTTP request, such as the request headers and the request body. The response object provides facility to control and send the HTTP response the way you want, such as setting headers and the body (usually with HTML content from a JSP file). When the HTTP response is committed and finished, then both the request and response objects will be trashed (actually, most containers will cleanup the state and recycle the instance for reuse).

HttpSession

When a client visits the webapp for the first time and/or the HttpSession is to be obtained for the first time by request.getSession(), then the servletcontainer will create a new HttpSessionobject, generate a long and unique ID (which you can get by session.getId()), and store it in server’s memory. The servletcontainer will also set a Cookie in the Set-Cookie header of the HTTP response with JSESSIONID as cookie name and the unique session ID as cookie value.

As per the HTTP cookie specification (a contract a decent webbrowser and webserver has to adhere), the client (the webbrowser) is required to send this cookie back in the subsequent requests in the Cookie header as long as the cookie is valid. Using browser builtin HTTP traffic monitor you can check them (press F12 in Chrome / Firefox23+ / IE9+ and check Net/Network tab). The servletcontainer will determine the Cookie header of every incoming HTTP request for the presence of the cookie with the name JSESSIONID and use its value (the session ID) to get the associated HttpSession from server’s memory.

The HttpSession lives until it has not been used for more than the <session-timeout> time, a setting you can specify in web.xml, which defaults to 30 minutes. So when the client doesn’t visit the webapp anymore for over 30 minutes, then the servletcontainer will trash the session. Every subsequent request, even though with the cookie specified, will not have access to the same session anymore. The servletcontainer will create a new one.

On the other hand, the session cookie on the client side has a default lifetime which is as long as the browser instance is running. So when the client closes the browser instance (all tabs/windows), then the session will be trashed at the client side. In a new browser instance the cookie associated with the session won’t be sent anymore. A new request.getSession() would return a brand new HttpSession and set a cookie with a brand new session ID.

In a nutshell

  • The ServletContext lives as long as the webapp lives. It’s been shared among all requests inall sessions.
  • The HttpSession lives as long as the client is interacting with the webapp with the same browser instance and the session hasn’t timed out at the server side yet. It’s been shared among all requests in the same session.
  • The HttpServletRequest and HttpServletResponse lives as long as the client has sent it until the complete response (the webpage) is arrived. It is not being shared elsewhere.
  • Any ServletFilter and Listener lives as long as the webapp lives. They are being shared among all requests in all sessions.
  • Any attribute which you set in ServletContextHttpServletRequest and HttpSession will live as long as the object in question lives. The object itself represents the “scope” in bean management frameworks such as JSF, CDI, Spring, etc. Those frameworks store their scoped beans as an attribute of closest matching scope.

Threadsafety

That said, your major concern is possibly threadsafety. You should now have learnt that Servlets and filters are shared among all requests. That’s the nice thing of Java, it’s multithreaded and different threads (read: HTTP requests) can make use of the same instance. It would otherwise have been too expensive to recreate, init() and destroy() it on every single request.

But you should also realize that you should never assign any request or session scoped data as aninstance variable of a servlet or filter. It will be shared among all other requests in other sessions. That’s threadunsafe! The below example illustrates that:

public class ExampleServlet extends HttpServlet {
private Object thisIsNOTThreadSafe;
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
Object thisIsThreadSafe;

thisIsNOTThreadSafe = request.getParameter("foo"); // BAD!! Shared among all requests!
thisIsThreadSafe = request.getParameter("foo"); // OK, this is thread safe.
}
}

java6 / java7을 쓰면 안되는 이유

개요

kth 내부 서비스 또는 kt 용역 서비스 중 상당수는 호환성이라는 명목 하에 java6 / java7 로 개발/유지보수하는 프로젝트들이 많이 있습니다.

물론 java6 / java7 로 개발한다고 잘 돌던 서비스가 당장 멈추거나 하는 일은 없을겁니다만

서비스 품질은 다음과 같은 측면에서 경쟁사에 뒤쳐진다고 보시면 됩니다.

1) 보안

2) 가용성

3) 호환성

4) 기술 역량

 

보안

oracle java6 의 End of Public Update 는 2013년 2월, java7은 2015년 4월에 종료되었습니다.

무슨 말인고 하면, 누군가가 java의 새로운 보안 결함을 발견한다면 java6 / java7 로 개발된 서비스는 더이상 보안 업데이트가 없다는 뜻입니다.

다음 사이트(oracle jre 링크)를 참고하시면, 지금까지 알려진 보안 결함 리스트를 보실 수 있는데요.

java6 은 대략 200여 개, java7은 대략 100여 개 정도의 보안 결함 업데이트가 없었습니다.

물론 네트워크 방화벽이라던지 정기 OS 패치 등으로 이중 삼중 보안 장치를 마련해 두고 있지만

때때로 이러한 모든 보안 장치를 무력화할 만한 대형 결함이 발견되기도 하는데, 그럴 때 동작하는 서비스가 java6 / java7 기반이라면 대책이 없습니다.

 

가용성

Interned String 문제

java6을 쓰면서 서비스에 특별한 장애 요소도 없는데 이상하게 보름에서 한달 정도 지나면 서비스가 내려가는 경험이 있으셨다면 십중팔구는 Interned String 문제입니다.

java6 과 java7/8 은 String pool 저장소가 근본적으로 다릅니다.

java6 이하 버전의 경우 신규 생성된 String은 PermGen 에 위치하며, 동일한 String의 경우 pooling 합니다.

 

PermGen 사이즈는 고정이므로(java8은 좀 다릅니다만),

1) 100bytes 사이즈의 서로 다른 값을 가진 String(e.g. 임시 발급 key, UUID 등등등)이

2) 100Mbytes 사이즈 PermGen

에 채울 수 있는 최대값은 100 * 1000 * 1000 = 100M 이므로

최대 100만 개의 String 이상 String이 신규로 생성되면 PermGen 영역이 가득차게 되고, 아래와 같은 익셉션과 함께 프로그램이 제대로 동작하지 않을겁니다.

exception

java.lang.OutOfMemoryError: PermGen space

 

java7부터는 String.intern() 메소드를 명시적으로 호출하지 않는 한, PermGen 영역에 String을 Poolling 하지 않습니다.

단지 jvm 버전을 올리는 것 만으로도 가용성 및 장애 요소를 한 가지 줄일 수 있습니다.

 

Fixed PermGen size 문제

허나, java7 이하 버전에서 여전히 PermGen size 는 고정이라는 문제가 존재합니다.

대개의 경우 String이 PermGen space를 잡아먹는 주된 요인이겠지만,

본래 PermGen의 주된 목적은 어플리케이션에서 사용되는(=load한) class 정보의 저장입니다.

java7에서 PermGen 사이즈를 적당히 크게 잡아줄 경우, PermGen이 고갈되는 경우는 왠만해선 발생하지 않겠습니다만

다음과 같은 시나리오에서는 PermGen이 고갈됩니다.

1) classloader 가 동적으로 신규 class를 지속적으로 load

2) 프로그램 사이즈가 정말 커서 많은 class가 필요

PermGen 사이즈는 고정이라는 원죄가 있기 때문에 jvm 옵션으로 PermGen 사이즈를 조정하더라도 항상 문제가 발생할 여지가 있습니다.

java8 에서는 PermGen 이 사라진 대신 해당 역할을 metaspace가 대체합니다.

차이점은 다음과 같습니다.

1) metaspace의 메모리 영역은 native 입니다.

2) memory가 부족하면 동적으로 할당 가능합니다.

 

cf) PermGen?

2015년도 신입사원 교육자료 중 2일차, dependency hell 작성할 때 그려둔 native application 의 memory 모델입니다.

물론 java와 디테일에는 차이가 있지만, Heap / Stack / Data / Code 섹션에 대한 포괄적인 개념은

java나 native나 혹은 이후 여러분들이 접하게 될 어떤 언어라도 대동소이합니다. 하나만 제대로 알고 있으면 되겠죠?

위 메모리 모델은 내가 짠 프로그램이 실제로 어떻게 작동하는지 알고 싶은 개발자라면 반드시 대가리헤또에 필수 탑재되어 있어야 합니다.

메모리 모델

stack : 프로그램의 흐름(=function call), local 변수 등을 저장

heap : 프로그램 내에서 동적으로 할당한 데이더를 저장

data : 전역변수를 저장

code : 프로그램 메타정보(=function meta) 등을 저장

각 메모리 영역별로 역할이 주어져 있지만, 가장 중요한 핵심은

1) 어플리케이션 시작 전에 사이즈를 결정할 수 있는 영역과(code / data)

2) 어플리케이션 실행 도중에 사이즈가 변경되는 영역(stack / heap)

이 분리되어 있다는 점입니다.

 

디테일은 다르지만 자바 메모리모델도 어플리케이션 시작 전에 사이즈를 결정할 수 있는 영역과 그렇지 않은 영역으로 구분됩니다.

다만, PermGen 영역의 정체성이 모호한데요.

본래 의도대로라면 PermGen 영역은 런타임에 증가되어선 안되는 영역입니다만 몇 가지 경우에 예외를 두었고

그 예외 때문에 자바 어플리케이션은 지금까지 고통받아 왔습니다.

 

java memory model

아래 그림은 어떤 존잘님께서 그린 java 메모리 모델입니다. (출처 : http://javarevisited.blogspot.com/2011/04/garbage-collection-in-java.html)

1) java7 이하 메모리 모델

2) java8 메모리 모델

 

Compressed Ordinary Object Pointer default enabled

jvm에는 Ordinary Object Pointer, 줄여서 OOP(객체지향 프로그래밍과 혼동 금지!, 보통 hotspot oop 라고 부릅니다.)에 대한 압축을 enable / disable 하는 옵션이 있습니다.

hotspot oop 는 기회가 된다면 나중에 따로 다루겠지만, 간단히 요약하면

데이터의 주소 정보를 효율적으로 쓰기 위한 꼼수

라고 보시면 됩니다.

compressed oop 옵션은 java7부터 default enabled 입니다.

만일 64bit 플랫폼 환경에서 java6을 쓰신다면 어플리케이션 실행 시 이 옵션을 enable 하여야 메모리를 좀 더 효율적으로 쓸 수 있습니다.

 

호환성

신규 버전의 java 에는 보안 / 기능 / 성능 / 정책 위배 등 여러가지 사유로 과거 버전중 deprecated 되는 API들이 발생합니다.

java6 또는 java7에서 java8로 올리지 못하는 여러 가지 이유중에는 deprecated API에 대한 호환성 문제가 꽤 비중이 높은 편인데요.

당장 deprecated API를 수정할 여력 없이 운영만 하는 서비스는 현실적으로 java 버전을 높이기 어려운 것이 사실입니다.

허나 신규 프로젝트에서 deprecated API 를 사용하거나, 해당 API를 사용한 library 를 쓰는 일은 없어야 합니다.

이미 배포한 API를 없앤다는 건 정말 어려운 일입니다. 그래서 보통은 왠만하면 해당 API를 없애지 않고 구현을 변경하거나 다른 더 좋은 구현을 만들어내 해당 API 사용률을 낮추도록 유도합니다.

그럼에도 불구하고 API를 없애는 결정을 했다는건 정말 그 API를 쓰지 말아야 할 중대한 이유가 있기 때문에 없애는 겁니다.(쓰지 말라면 쓰지 마루요)

아래 링크는 oracle에서 제공하는 java7 / java8 의 deprecated 된 api 리스트 및 java8 호환성 guide 문서입니다.

 

기술역량

modern language 열풍이 분지 올해로 10년 쯤 지났습니다.

무수히 많은 modern language가 생겨나기도 하였지만, 기존 language들이 modern language 의 철학을 담는 경우도 종종 있었습니다.

java는 다른 언어에 비해 modern language의 주요 개념, 기능 등을 담아내는 시점이 매우 늦은 편입니다.

심지어 java보다 30년은 오래된 c++이 오히려 java보다 100만배는 modern 합니다.

 

현대의 소프트웨어 개발 환경은 몇 가지 특징이 있습니다.

1) 과거에 비해 어플리케이션의 규모 / 복잡도가 기하급수적으로 높아졌습니다.

2) 난립하는 하드웨어에 효율적으로 대응하기 위한 수단이 필요합니다.

3) 더이상 수직적인 하드웨어 성능은 높아지지 않습니다. 허나 처리해야 할 데이터의 양은 엄청나게 늘었습니다.

 

과거, 구조적 프로그래밍의 한계를 객체지향 프로그래밍으로 극복한 시절이 있었습니다.

현재는 객체지향 프로그래밍의 한계에 다다랐다고 보아도 과언이 아닙니다.

우리가 쓰는 대부분의 CPU 는 멀티코어 프로세서로 설계되었습니다.

허나 객체지향 언어로 멀티코어 성능을 온전히 내는 어플리케이션을 만들기는 매우 어렵습니다. 디버깅은 100만배 더 어렵습니다.

 

과거, 비동기 네트워킹은 시스템 프로그래머만의 전유물이던 시절이 있었습니다.

현재는 클라이언트 프로그래머도 비동기 처리의 동작 원리를 반드시 이해하여야 합니다.

비동기 처리는 반드시 OS의 지원 / 프로그램 언어의 지원이 필요합니다.

 

modern language 에는 다음과 같은 몇 가지 공통적인 특징이 있습니다.

1) 함수형 프로그래밍 패러다임 수용

2) 불필요한 코드 노가다 지양

3) 강력한 type 추론

4) lazy execution 을 구현하기 위한 효율적인 수단 제공

5) REPL(Read – Evelauation – Print – Loop) 또는 준하는 어플리케이션 확인 수단 제공

6) thread local stack 에 대한 디버깅 수단 제공

 

java8 도 많이 늦은 편인데 이러한 소프트웨어 개발 환경의 변화를 java6 / java7 로 대응한다? 쉽지 않습니다.

지금으로부터 약 40여 년 전, 프로그래밍 세계를 평정하던 cobol이라는 언어가 있었습니다.

당시 두터운 사용자/커뮤니티층을 형성하던 cobol 개발자들은 굳이 OOP 를 학습할 필요성을 느끼지 못하였습니다.

c++이 출시되고 채 10년이 못되어 cobol 어플리케이션은 씨가 말랐고, OOP는 대세가 되었습니다.

요즘의 java를 보면서 과거 cobol의 역사가 자꾸 오버랩되는 것은 우연의 일치일까요?

 

요약

java8 써라. 두번 써라.

cpython compiler design(작성 중)

출처 : PEP 339 design of the cpython compiler

개요

보통의 python 개발자라면 굳이 cpython 의 구현/설계 원리를 알 필요가 없으며, 실제로도 코딩에 하등 도움이 되지 않는다.

허나,

  • 본인의 성향이 오덕이고
  • 요즘 언어의 추세인 managed resource 의 메모리 관리가 궁금한 아재이고
  • python compiler/interpreter 개발에 기여하고 싶거나
  • compiler 수업을 듣는 학생이라면

공부할 가치가 있다. 다시 말하지만 본인이 생업에 바빠 python 코드를 찍어내야 하는 반도의 흔한 개발자라면 굳이 PEP 같은건 볼 필요가 없다. 그냥 language reference 만으로 충분하다.

어쨋든 코딩=밥벌이에 큰 도움은 되지 않으나, 본인이 python에 대한 깊이있는 이해를 하고 있다는 착각에 빠지는 효과가 있고, 요즘 모던한 언어들이 다양한 형태의 vm을 어떻게 대응하는지에 대한 추세를 가늠할 수 있으며, 학생 시절에나 배울 법한 컴파일러/언어 이론을 찾아보다 보면 뇌단련이 되는 긍정적인 효과가 있다.

python life cycle

python code 는 크게 다음과 같은 흐름으로 실행된다.

  1. src code 작성
  2. bytecode 변환(by compiler)
  3. 실행(by interpreter on vm)

등장인물의 면면을 살펴보면,

  • 내가 짠 src code
  • compiler
  • interpreter
  • virtual machine

으로 총 4가지 이다.

일반적으로 널리 쓰이는 cpython 의 경우, compiler/interpreter/vm 을 c 로 구현하였고, 자체 정의한 bytecode로 compile/실행한다.

반면, jython 의 경우는 java bytecode로 compile/실행 하기 때문에 vm 의 경우 jvm을 사용할 수 있다.

아래 그림을 보면 관계가 이해갈 것이다.

Jython's use of Java bytecode is depicted in this Python implementation diagram.

cpython/jython 외에도 다양한 python compiler/interpreter 구현이 있다.

  • pypy
  • ironpython
  • 기타등등

cpython compiler 구조

가장 common 한 cpython 의 compiler design 을 살펴보자.

위에 내용을 상기하면, compiler 는 src code -> bytecode 로 변환하는 역할이며,

cpython 이므로 c언어로 작성되었다.

관련 정보는 PEP(python enhancement proposals)-339 문서를 찾아보면 얻을 수 있는데, python 이라는 언어 자체가 구현체에 대한 스펙을 강제하지 않기 때문에 이 내용은 cpython 에만 해당한다는걸 염두에 두길 바란다.

src code 가 bytecode 로 변환되는 과정은 다음과 같다.

  1. src code
  2. parse tree(=Concrete Syntax Tree)
  3. AST(=Abstract Syntax Tree)
  4. CFG(=Control Flow Graph)
  5. bytecode

 

parse tree(concrete syntax tree)

 

cpython parser 는 LL parser 를 base 로 한다.

문법 규칙은 graminit.h 에 명세되어 있으며, type 매핑 코드는 token.h 에 정의되어 있다.

파서 :  https://github.com/python/cpython/blob/master/Parser/pgen.c

문법 규칙 : https://github.com/python/cpython/blob/master/Include/graminit.h

type 매핑코드 : https://github.com/python/cpython/blob/master/Include/token.h

parser 의 결과물은 CST 라 부르는 parse tree 이다. CST 는 변환과정을 거쳐 AST 로 바뀐다. 여기서 CST와 AST 같은 중간단계가 왜 필요한지, 차이점은 무엇인지 의문이 들 것이다.

CST vs AST

CST와 AST 의 설명을 언뜻 보면 잘 이해가 가지 않을 수도 있다.  이해를 돕기 위해 다음 아주 간단한 python code 를 parse tree 로 변환해 보도록 하자.


1+2

이 코드를 CST(=parse tree) 로 변환하면 아래와 같다.

cst.gv.png

간단한 산술연산인데도 CST로 변환하니 뭔가 복잡하다. CST 에는 파싱된 python code 에서 요구하는 모든 syntax 및 token 의 정보가 포함되어 있다. 따라서 간단한 코드 길이에 비해 CST 의 depth 는 장황해진다.

조금 더 이해를 돕기 위해 다음 code 도 CST 로 변환해보자.

 a=1 

cst.gv.png

 

bytecode 입장에서는 단지

  1. opcode 와
  2. operand 만 필요할 뿐이어서

CST에는 불필요한 정보들이 많다. 1+2 와 a=1 을 각각 AST 로 변환해보자.

1+2

ast.gv.png

a=1

ast.gv.png

 

참고로, CST와 AST 는 모두 형식적으로 Extended BNF 를 따른다. BNF 가 궁금하면 링크를 참조하기 바란다.

 

(todo) 메모리 관리 – PyArena / PyObject

AST(abstract syntax tree) : https://github.com/python/cpython/blob/master/Python/ast.c

pyArena : https://github.com/python-git/python/blob/master/Python/pyarena.c

CFG(control flow graph) : https://github.com/python/cpython/blob/master/Python/compile.c

bytecode : https://github.com/python/cpython/blob/master/Python/compile.c

(BNF, Backus Naur Form) : CST

(Deterministic Finite Automaton) : AST

(NonDeterministic Finite AUtomaton) : AST

(memory management 추가자료)

컴파일 시 필요한 메모리 리소스는 PyArena 라 불리는 memory pool (PyObject 의 linked list) 로 관리되며 대부분의 경우 신경쓰지 않아도 되나, PyObject 를 직접 관리할 때에는 명시적으로 PyArena 에 해당 PyObject 를 추가해야 한다.

As stated above, in general you should not have to worry about memory management when working on the compiler. The technical details have been designed to be hidden from you for most cases.

The only exception comes about when managing a PyObject. Since the rest of Python uses reference counting, there is extra support added to the arena to cleanup each PyObject that was allocated. These cases are very rare. However, if you’ve allocated a PyObject, you must tell the arena about it by calling PyArena_AddPyObject().