2019년 8월 19일 월요일

Java 로거(jdk logger) 정리.

Java에선 파사드 패턴으로 구현된 slf4j를 쓰는게 성능이나 유지 보수 측면에서 이점이 있지만 경우에 따라선 jdk logger가 더 나은 선택일 수 있다. jdk logger를 쓸 경우 외부 라이브러리를 참조하지 않아도 되고, 설정도 단순해 사용하기 편하다. log4j 대비 느리다곤 하나 엄청 성능에 민감한 애플리케이션이 아닌 경우엔 차이가 잘 드러나지도 않는다. 그리고 jdk logger는 표준이면서 구조적인 측면에 대해서도 배울 부분이 있기 때문에 알아둘 필요는 있다고 생각한다.

jdk logger는 java.util.logging 패키지에 구현되어 있다. Logger.getLogger(String name) 메서드를 통해 로거 인스턴스를 가져오는 방식이고, 최소 로그 레벨과 핸들러를 지정하게 된다. jdk logger가 제공하는 로그 레벨은 다음과 같다. 이 중 severe, warning, info 정도의 레벨로 로그의 위험도를 구분하는 것이 일반적일 것이다.

• SEVERE: a serious failure
• WARNING: a potential problem
• INFO: reasonably significant informational message
• CONFIG: hardward configuration
• FINE, FINER, FINEST: tracing information

핸들러는 여러 개 지정할 수 있다. 일반적으로 ConsoleHandler와 FileHandler가 주로 사용된다.

• ConsoleHandler: for writing to System.err
• FileHandler: for writing to either a single log file, or a set of rotating log files
• StreamHandler: for writing to an OutputStream
• SocketHandler: for writing to a TCP port
• MemoryHandler: for writing to memory buffers.

로거 인스턴스에 getHandlers() 메서드를 호출하면 로거에 연결된 핸들러 정보를 배열로 받아올 수 있다. 핸들러 정보를 가져와 필요하지 않은 핸들러라면 제외시킬 수 있을 것이다.

// 파일 핸들러를 제거한다면,
Logger logger = Logger.getLogger(name);
Handler[] handlers = logger.getHandlers();

for (Handler h : handlers) {
    if (h instanceof FileHandler) {
        if (!doFileLog) {
            h.flush();
            h.close();
            logger.removeHandler(h);
        }
    }
}

또는 필요한 핸들러는 언제든지 추가할 수 있다. 파일 핸들러는 rotating을 지원하는데 로그 파일의 용량과 최대 개수를 생성자에 지정할 수 있다.

FileHandler fileHandler = new FileHandler(logFileName, ROTATE_SIZE, ROTATE_COUNT, true);
logger.addHandler(fileHandler);

파일 핸들러와 같은 것들은 애플리케이션이 종료될 때 같이 닫혀야 한다. Runtime의 addShutdownHook에 파일 핸들러를 닫아주는 쓰레드를 지정해주면 될 것이다.

final class FileHandlerCloser implements Runnable {
    private final FileHandler fileHandler;

    private FileHandlerClose(FileHandler fileHandler) {
        this.fileHandler = fileHandler;
    }

    public static void addHandler(FileHandler fileHandler) {
        Thread closer = new Thread(new FileHandlerCloser(fileHandler), fileHandler.toString());
        Runtime.getRuntime().addShutdownHook(closer);
    }

    public void run() {
        fileHandler.flush();
        fileHandler.close();
    }
}

마지막으로 각 핸들러에겐 기록할 로그 형식을 지정해주어야 한다. 추상 클래스 Formatter를 구현하면 된다. 구현할 메서드는 하나다.

public String format(LogRecord record);

인자로 전달되는 record 객체로부터 로그를 남긴 클래스 이름과 메서드 이름을 알 수 있다. 라인 번호까지 알고 싶으면 스택 트레이스에서 정보를 빼오면 된다. 그리고나서 record에 담긴 정보를 문자열로 내보내면 될 것이다.

int lineNumber = 0;
StackTraceElement[] stackTraceElement = Thread.currentThread().getStackTrace();
for (int i = 0; i < stackTraceElement.length; i++) {
    if (stackTraceElement[i].getClassName().compareTo(record.getSourceClassName()) == 0
            && stackTraceElement[i].getMethodName().compareTo(record.getSourceMethodName()) == 0) {
            lineNumber = stackTraceElement[i].getLineNumber();
            break;
    }
}

// [Time] [Level] [ClassName:MethodName:LineNumber] [Message]
StringBuilder builder = new StringBuilder(1000);
builder.append(new SimpleDateFormat("[yyyy/MM/dd/HH:mm:ss]").format(new Date(record.getMillis())));
builder.append(" [").append(record.getLevel()).append("]\t");
builder.append("[").append(record.getSourceClassName()).append(":");
builder.append(record.getSourceMethodName()).append(":");
builder.append(lineNumber + "]\t[");
builder.append(formatMessage(record)).append("]\r\n");

return builder.toString();

jdk logger의 전체 구조는 아래 이미지와 같다. 근사하다.


이미지 출처는



2019년 8월 14일 수요일

리버스 프록시 (Reverse Proxy)

리버스 프록시는 사용자의 요청을 받아서 반대편(reverse) 네트워크에 있는 인터넷 서버에 요청을 전달하는 일을 한다. 리버스 프록시 서버는 단순히 요청을 전달하기만 할 뿐 실제 요청의 처리는 뒷단에 위치한 웹 서버들이 맡아서 하게 된다.

하나의 리버스 프록시 서버가 여러 웹 서버로 요청을 전달하도록 구성할수도 있는데 이 경우 소프트웨어 기반의 로드밸런싱 환경을 구축할 수 있다. HAProxy, NginX, Apache 등의 웹 서버에서 이러한 기능을 제공한다.

최근 서비스의 규모가 커지면서, 분산 시스템으로 서비스가 구성되는 경우가 많은데 프록시 서버를 이용하면 분산 시스템을 뒤에 숨기는 방식으로 시스템을 단순화 할 수 있다.

NAT 사용하는 이유

NAT(Network Address Translation)

NAT은 IP 패킷의 TCP/UDP 소스 및 목적지의 IP 주소와 포트 숫자 등을 재기록하면서 라우터를 통해 네트워크 트래픽을 주고 받는 기술이다. NAT은 IPv4의 주소 부족 문제를 해결해주는데 사설 네트워크에 속한 여러 개의 호스트가 하나의 공인 IP 주소를 사용해 인터넷에 접속할 수 있게 해준다.

NAT은 주로 공인되지 않은 네트워크 주소를 사용하는 망에서 외부의 공인망(예를 들면 인터넷)과의 통신을 위해 네트워크 주소를 변환해 준다. 별도로 공공망과 연결되는 사용자들의 고유한 사설망을 침입자들로부터 보호할 수 있다는 점에서 또다른 이점이 있다.

인터넷망과 연결되는 장비인 라우터에 NAT을 설정하면 라우터는 자신에게 할당된 공인 IP 주소만 외부로 알려지게 하고, 내부 사설 IP 주소는 감출 수 있다. (필요시에만 서로 변환) 외부 침입자는 사설망의 내부 IP 주소를 알아야 공격이 가능한데 불가능해지므로 내부 네트워크를 보호할 수 있게 된다.

2019년 8월 12일 월요일

Java Application 메모리 사용량 구하기, totalMemory(), freeMemory(), maxMemory()

모든 Java 애플리케이션에는 실행되는 환경과 인터페이스 할 수 있는 단일(single) Runtime 인스턴스가 제공된다. Runtime 인스턴스는 사용자가 직접 만들 수는 없고 getRuntime() 메서드에 의해서만 얻을 수 있다. 그리고 Runtime 인스턴스를 이용해 현재 애플리케이션의 메모리 사용 현황을 확인할 수 있다.

• Runtime.getRuntime().maxMemory() : 애플리케이션이 사용할 수 있는 총 메모리 양, -Xmx 값과 같다. 어떤 Java 애플리케이션이든 이 값을 초과하는 메모리는 사용할 수 없다. 실제 사용하는 메모리가 이 값에 근접하면 GC가 자주 발생하게 되므로 성능에 안좋은 영향을 끼치게 된다.
• Runtime.getRuntime().totalMemory() : 현재 애플리케이션에 할당된 총 메모리 양. (새로운 객체에 쓰일 메모리 영역 포함.)
• Runtime.getRuntime().freeMemory() : 새로 생성될 객체를 위해 현재 사용 가능한 메모리 공간. (available size in total memory.)

위 세가지 메서드로 실제 사용 중인 메모리, 앞으로 사용 가능한 메모리 등을 확인할 수 있다.

• 실제 사용 중인 메모리 (used memory) : totalMemory - freeMemory
• 할당되지 않았으나 앞으로 사용할 수 있는 메모리 (unallocated memory) : maxMemory - totalMemory
• 앞으로 사용 가능한 메모리 (total free memory) : maxMemory - totalMemory + freeMemory


https://docs.oracle.com/javase/8/docs/api/java/lang/Runtime.html

2019년 8월 6일 화요일

getResource() vs getClassLoader().getResource()

getResource(), getResourceAsStream()는 지정된 이름의 리소스를 찾는 API다. 주어진 클래스와 관련된 리소스를 검색하는 규칙은 클래스의 클래스 로더 구현을 따른다. 만약 객체가 부트스트랩 클래스 로더에 의해 로드되었다면 이 메서드는 작업을 ClassLoader.getsystemResourceAsStream(java.lang.String)으로 위임시킨다.


getClass().getResource() vs getClass().getClassLoader().getResource()

getClass().getResource()는 호출한 클래스 패키지로 기준한 상대적인(relative) 리소스 경로로 리소스를 탐색하게 되고, getClass().getClassLoader().getResource()는 항상 절대적인(absolute) 리소스 경로로 리소스를 탐색하게 된다. 예외 규칙으로 getClass().getResource()로 전달하는 리소스 이름이 '/'로 시작하면 getClassLoader() 방식과 같이 절대 리소스 경로로 처리된다.

아래의 결과는 같다.
foo.bar.Baz.class.getResource("xyz.txt");
foo.bar.Baz.class.getClassLoader().getResource("foo/bar/xyz.txt");
아래도 같다. getClass().getResource()로 전달된 리소스 이름이 '/'로 시작하기 때문에 절대 리소스 경로로 처리된다.
foo.bar.Baz.class.getResource("/data/xyz.txt");
foo.bar.Baz.class.getClassLoader().getResource("data/xyz.txt");
[원문] https://stackoverflow.com/questions/6608795/what-is-the-difference-between-class-getresource-and-classloader-getresource


getResource() vs getResourceAsStream()

내부 동작 방식은 같은데 getResource()는 URL 객체를 반환하고, getResourceAsStream()은 InputStream 객체를 반환한다.


new FileInputStream 등으로 파일을 직접 읽지 않고 getResource(), getResourceAsStream()을 사용하는 이유

getResource(), getResourceAsStream()을 쓰면 jar로 패키징된 내부 파일을 직접 읽을 수 있다.


[참고] 자바 클래스 로더 (Java Class Loader)
https://blog.naver.com/seokcrew/221608650295

2019년 8월 5일 월요일

Java - 문자열 풀링(String Pooling, String.intern())

String Pooling은 메모리를 아끼는 작업으로써 완전히 같은 값을 가진 문자열이 서로 다른 메모리 주소에 저장되지 않도록 해준다. 이는 Java에서 String이 불변(immutable) 객체이기 때문에 가능한 장치다. JDK는 String.intern() 메서드를 통해 이러한 기능을 제공한다. Java에서 생성자가 아닌 리터럴(" ") 형식으로 선언된 String 객체는 알아서(?) Pooling 되는데 아마 String.intern()이 내부적으로 호출되는 구조를 갖을 것이다.

Java 6 버전에서는 String.intern()을 권장하지 않았다. String Pool이 PermGen 메모리 영역에 저장되었기 때문인데, PermGen 영역은 GC 대상이 아닐 뿐더러 고정된 크기(기본값 32~96MB)를 갖고 실행된다. String Pool이 커지면 OutOfMemory 에러가 발생할 수 있는 것이다. 충분히 큰 사이즈를 초기에 지정해 사용하는 방법이 있었지만 (-XX:MaxPermSize=256m) 신중한 사용을 필요로 했다.

Java 7 버전부터 String Pool은 힙 영역에 저장된다. 참조되지 않는 문자열은 GC에 의해 회수되기 때문에 메모리를 보다 효율적으로 사용할 수 있게 되었다. String Pool은 고정된 크기의 해시 맵으로 구현되는데 -XX:StringTableSize=N 설정으로 맵의 크기를 변경할 수 있다. 참고로 Java 7u40 부턴 기본 60013의 크기로 동작한다. 왜 60000이 아니고 60013이냐, 맵의 크기는 소수(prime number)로 지정하는 것이 성능에 도움이 된다고 한다.

통계 기본 용어 (표준 편차, 분산, 표준 점수, ...)


  • 표준 편차(standard deviation) : 자료의 산포도를 나타내는 수치로, 분산을 제곱근한 것. 표준편차가 작을수록 평균값에서 변량들의 거리가 가깝다. 모집단의 표준편차는 시그마로, 표본의 표준편차는 S로 나타낸다. 제곱해서 값이 부풀려진 분산을 제곱근해서 다시 원래 크기로 만들어 준다.
  • 68-95-99.7 규칙 : 통계학에서 정규 분포를 나타내는 규칙으로 3시그마 규칙이라고도 한다. 경험적인 값으로 평균에서 양쪽으로 3표준편차의 범위에 거의 모든 값(99.7%)이 들어가는 것을 나타낸다. 평균에서 양쪽으로 1표준편차 범위 내에 약 68% 값이, 2표준편차 범위 내에 약 95% 값이, 3표준편차 범위 내에 약 99.7% 내에 존재한다.
  • 정규 분포(normal distribution) : 수집된 자료의 분포를 근사하는데 쓰인다. 정규 분포는 평균과 표준편차를 기준으로 모양이 결정되는데 평균이 0이고 표준편차가 1인 정규분포를 표준 정규 분포라고 한다. 정규분포곡선은 좌우가 대칭이며 하나의 꼭지를 갖는다.
  • 편차(deviation) : 관측값에서 평균 또는 중앙값을 뺀 것.
  • 분산(variance) : 관측값에서 평균을 뺀 값을 제곱하고, 그것을 모두 더한 후 전체 개수로 나눠 구한 값. 차이값의 제곱의 평균이다. 편차를 모두 더하면 0이 나오기 때문에 제곱해서 더한다.
  • 표준 점수(standard score, z-value, z score) : 데이터가 표준편차 상에 어떤 위치(평균으로부터 얼마나 떨어져 있는지)를 차지하는지를 나타내는 수치. z score가 0이면 정확히 평균에 해당한다. (number - average) / standard_deviation)
[출처: 위키백과]

WPF 쓰레딩 모델, Invoke, BeginInvoke

일반적으로 WPF 응용 프로그램은 렌더링 처리와 UI 관리를 위한 두개의 쓰레드로 시작한다. UI 쓰레드는 작업 항목을 Dispatcher라는 오브젝트 내부에 대기시킨다. Dispatcher는 우선 순위에 따라 작업 항목을 선택하고 각각을 실행한다. 모든 UI 쓰레드에는 하나 이상의 Dispatcher가 있어야 하며 각 Dispatcher는 정확히 하나의 쓰레드에 의해서만 동작해야 한다.

응답성이 높은 애플리케이션을 구축하는 방법은 가능한 작업 항목의 실행 시간을 적게 유지하여 Dispatcher의 처리량(throughput)을 극대화 하는 것이다. 따라서 일반적으로 큰 작업은 별도의 쓰레드에서 처리하게 된다. 그리고 큰 작업이 완료되면 결과를 UI 쓰레드에 보고하여 표시하는 식이다.

WPF에서 대부분의 클래스는 DispatcherObject에서 파생된다. 생성시 DispatcherObject는 현재 실행 중인 쓰레드에 연결된 Dispatcher에 대한 참조를 저장한다. 프로그램을 실행하는 동안 DispatcherObject는 VerifyAccess 메서드를 호출할 수 있다. VerifyAccess 메서드는 현재 쓰레드와 연관된 Dispatcher와 생성시에 저장된 Dispatcher 참조 간의 비교 검사를 수행한다. 일치하지 않으면 VerifyAccess는 예외를 throw한다. VerifyAccess는 DispatcherObject에 속한 모든 메서드의 시작 부분에서 호출되도록 고안되었다.

...
하나의 쓰레드만 UI를 변경할 수 있다고 하였다. 그렇다면 백그라운드 쓰레드와 사용자 간 상호작용 하는 방법은 무엇인가? WPF에서 백그라운드 쓰레드는 UI 쓰레드에 작업을 수행하도록 요청할 수 있게 구성되어 있다. 백그라운드 쓰레드는 작업 항목을 UI 쓰레드의 Dispatcher에 등록할 수 있다. Dispatcher 클래스는 백그라운드 쓰레드가 작업 항목을 등록할 수 있게 'Invoke'와 'BeginInvoke' 메서드를 제공한다. Invoke는 동기 호출 방식이고 BeginInvoke는 비동기 호출 방식이다. 두 메서드 모두 실행을 위해 대리자를 스케줄에 등록시킨다.

아래와 같은 코드에서, Dispatcher는 startStopButton 컨트롤이 idle일 때만 delegate를 실행하게 된다. 우선순위가 SystemIdle로 지정되었으니 아래의 코드는 CheckNextNumber 호출보다 UI의 응답이 중요하다는 의미를 갖게 된다. Dispatcher는 Dispatcher 큐에 등록된 작업 항목을 우선 순위에 따라 정렬하여 순차적으로 처리한다. 제공되는 우선 순위 레벨은 10가지 이다. 이러한 우선 순위는 DispatcherPriority 열거 형에서 유지되고 관리된다.


public delegate void NextPrimeDelegate();

public void CheckNextNumber()
{
...
    if (continueCalculating)
    {
        startStopButton.Dispatcher.BeginInvoke(DispatcherPriority.SystemIdle, new NextPrimeDelegate(this.CheckNextNumber));
    }
}

자세한 내용은 아래 링크를.
https://docs.microsoft.com/en-us/dotnet/framework/wpf/advanced/threading-model

2019년 8월 1일 목요일

싸이킷런의 주요 설계 원칙 - estimator, transformer, predictor

핸즈온 머신러닝을 읽고 있는데 이 책 정말 좋다. 싸이킷런의 주요 설계 원칙이라고 101쪽에 적혀있는 내용을 옮겨본다.


싸이킷런의 주요 설계 원칙


1. 일관성 : 모든 객체가 일관되고 단순한 인터페이스를 공유한다.

  • 추정기(estimator) : 추정은 fit() 메서드에 의해 수행되고 하나의 데이터셋만 파라미터로 전달받는다. fit()에서 쓰이는 하이퍼파라미터는 생성자의 매개변수로 전달받는다.
  • 변환기(transformer) : transform() 메서드가 하나의 데이터셋을 전달받아 변환 작업을 한다. 그리고 fit()과 transform()을 연달아 호출하는 것과 동일한 결과를 갖는 fit_transform() 메서드를 가지고 있다.
  • 예측기(predictor) : predict()는 새로운 데이터셋을 전달받아 예측값을 반환한다. 그리고 테스트 세트를 사용해 예측 품질을 측정하는 score() 메서드를 갖는다.


2. 검사 기능 : 하이퍼 파라미터는 인스턴스 변수로 직접 접근할 수 있다. 학습된 모델 파라미터도 접미사로 '_'를 붙여서 공개 인스턴스 변수로 제공된다.

3. 조합성 : 기존 구성요소를 최대한 재사용한다. 예를 들어 여러 개의 변환기를 연결한 다음 마지막 추정기 하나를 배치한 Pipeline 추정기를 쉽게 만들 수 있다.

4. 합리적인 기본값 : 일단 동작하게끔 해준다. 대부분의 매개변수에 합리적인 기본값을 지정해두었다.