[스프링 3.0] 로깅은 SLF4J를 이용한 Log4J로

스프링을 사용하면 기본적으로 JCL(자카르타 커먼스 로깅)을 사용하게 되는데 JCL이 실제 로거를 선택하는 시점이 런타임이라 클래스로더 문제라던가 런타임시 오버헤드가 생길 수 있는데 이것을 개선한 구현체 SLF4J로 갈아타면 그러너 문제 걱정을 덜 수 있겠습니다. 보너스로 문자열 연결로 발생하는 오버 헤드도 개선할 수 있으며 귀찮은 if문 추가하는 코딩에서 벗어날 수 있는 문법? API?를 제공해줍니다. 그래서인지 스프링도 3.0부터는 본격적으로 spring-core가 의존하는 JCL을 SLF4J로 교체하고 사용하는 예제 및 레퍼런스 설명을 보여주고 있습니다..

갈아타는 방법은 다음과 같습니다.

1. 스프링에서 참조하는 JCL 라이브러리를 빼버리기.
2. JCL-over-SLF4J 추가하기.
3. SLF4J API 추가.
4. SLF4J-log4j 추가.
5. log4j 추가.

이전에 스프링소스 블로그와 레퍼런스에 올라온 방법은 이걸 그대로 메이븐 의존성으로 옮겨적고 있지요.

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>${org.springframework.version}</version>
            <exclusions>
                <!– Exclude Commons Logging in favor of SLF4j –>
                <exclusion>
                    <groupId>commons-logging</groupId>
                    <artifactId>commons-logging</artifactId>
                 </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>${org.slf4j.version}</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>jcl-over-slf4j</artifactId>
            <version>${org.slf4j.version}</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>${org.slf4j.version}</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.15</version>
            <exclusions>
                <exclusion>
                    <groupId>javax.mail</groupId>
                    <artifactId>mail</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>javax.jms</groupId>
                    <artifactId>jms</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>com.sun.jdmk</groupId>
                    <artifactId>jmxtools</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>com.sun.jmx</groupId>
                    <artifactId>jmxri</artifactId>
                </exclusion>
            </exclusions>
            <scope>runtime</scope>
        </dependency>

1번은 단순히 JCL 라이브러리에 의존할 생각이 없기 때문에 빼버리는 것이고, 그때 JCL에 의존하는 클래스들이 깨질 텐데 그것을 JCL-over-SLF4J를 이용해서 겉은 JCL 같지만 내부에서는 SLF4J API를 호출하도록 일종의 어댑터 나 다리 역할을 해주는 라이브러리를 추가하고, 인터페이스 격인 3. SLF4J API를 추가한 뒤 실제 사용할 로거로 SL4J를 구현한 4. SLF4J-Log4J 라이브러리를 추가합니다. 마지막으로 최종적으로 사용할 로거 5. Log4J를 추가한 것입니다. 이때 Log4J가 불필요하게 참조하는 jmx, mail, jms 등의 라이브러리를 제외시켜줍니다.

만약에 Log4J가 아니라 다른 로거를 사용할 거라면 4번과 5번을 교체하면 될테고 JCL 뿐 아니라 log4j에 대한 직접 호출도 slf4j로 오게 하려면 2번 대신 log4j-over-slf4j을 추가하면 되겠습니다.

굉장히 장황니다. @_@;; 하지만 뭐.. 좋다는데.. 갈아타긴 해야겠죠.

출처: SLF4J in 10 slides, by Ceki Gülcü

위와 같은 설정은 스프링 3.0 예제와 레퍼런스에서 사용하고 있습니다. 정말 장황합니다. 일단 여기서 1, 2번은 필수다 하지만 레퍼런스와 블로그 글에도 나와있듯이 그 이하 3, 4, 5는 Logback이라는 SLF4J API 구현체로 한방 설정이 가능합니다.

logback은 크게 세 가지 모듈로 나뉘는데 그 중에 SLF4J API 구현체인 classic 모듈이 있고, Log4J를 개선한 core 모듈이 있습니다. logback 모듈을 가져오면 위에서처럼 log4j가 끌고오는 부가적인 라이브러리도 없고 깔끔하게 log4j API를 사용할 수 있습니다.

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>${org.springframework.version}</version>
            <exclusions>
                <!– Exclude Commons Logging in favor of SLF4j –>
                <exclusion>
                    <groupId>commons-logging</groupId>
                    <artifactId>commons-logging</artifactId>
                 </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>jcl-over-slf4j</artifactId>
            <version>${org.slf4j.version}</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>com.springsource.ch.qos.logback.classic</artifactId>
            <version>0.9.9</version>
        </dependency>

따라서 굳이 Log4J가 아닌 다른 로거로 갈아탈 계획이 없다면 이렇게만 설정해도 되겠습니다. 로깅 설정만 거의 1/3로 줄어듭니다.

사용법은

1. SLF4J API를 이용해서 로거를 만들고..

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Logger log = LoggerFactory.getLogger(AccountJsonBindingTests.class);

2. 다음과 같이 로깅하면 됩니다.

log.info(“name is {}”, name);

+를 사용해서 문자열 연산을 할 필요도 없고, if문을 줘서 문자열 연산을 막을 필요도 없습니다.

3. log4j 설정은 프로퍼티 파일이나 XML로 할 수 있는데 스프링 3.0 예제는 보통 XML을 사용하더군요. src와 test 소스 폴더 하위의 클래스패스 루트에 각각 다음과 같은 log4j.xml 파일을 둡니다.

<?xml version=”1.0″ encoding=”UTF-8″?>
<!DOCTYPE log4j:configuration PUBLIC “-//LOGGER” “log4j.dtd”>

<log4j:configuration xmlns:log4j=”http://jakarta.apache.org/log4j/”>

    <!– Appenders –>
    <appender name=”console” class=”org.apache.log4j.ConsoleAppender”>
        <param name=”Target” value=”System.out” />
        <layout class=”org.apache.log4j.PatternLayout”>
            <param name=”ConversionPattern” value=”%-5p: %c – %m%n” />
        </layout>
    </appender>

    <!– 3rdparty Loggers –>
    <logger name=”org.springframework.core”>
        <level value=”info” />
    </logger>

    <logger name=”org.springframework.beans”>
        <level value=”info” />
    </logger>
   
    <logger name=”org.springframework.context”>
        <level value=”info” />
    </logger>

    <logger name=”org.springframework.web”>
        <level value=”info” />
    </logger>

    <!– Root Logger –>
    <root>
        <priority value=”warn” />
        <appender-ref ref=”console” />
    </root>
   
</log4j:configuration>

src와 test 간의 차이는 마지막 부분의 <root> 안의 <priority>가 src에서는 warn이고 test에서는 info라는 것 뿐이 없습니다. 이 설정에서는 Log4J를 사용하고 있는데.. logback API를 이용해서 설정해도 당근 잘 동작합니다.

자바 System.out.println 콘솔 출력 가로채기

public class Sout {

    public void hi(){
        System.out.println(“hi”);
    }

}

이렇게 콘솔에 어떤 메시지를 출력하는 경우가 있을 때 저걸 애플리케이션에서 캡춰할 수 있는 걸 만들어 보는 과제가 떨어졌다.

public class SoutTest {

    SoutInterceptor soutInterceptor = new SoutInterceptor();

    @Test
    public void sout() throws IOException {
        soutInterceptor.active();

        Sout sout = new Sout();
       sout.hi();

        assertThat(soutInterceptor.getMessages(), is(“hi”));
    }

}

간단하게 테스트를 만들고 돌려보기 시작했다. 캡춰한 메시지를 어떻게 가져올지가 고민이었는데 그냥 생각난 가장 단순한 방법으로 가져오게했다. 이제 남은건 SoutInterceptor라는 녀석을 만드는 일이다. 뭘 어찌해야 한담 @_@;

가장 먼저 떠오른 방법은 콘솔을 모니터링 하는것이다. 그런데.. 넘 복잡할 것같고 막연하다. 다음으로 떠오른게 AOP. out.println()을 할 때 가로챌 수 있지 않을까? 하지만 힌트가 전달됐다. out을 교체할 수 있단다. 크헉.. 이건 뭐 거의 정답 수준의 힌트이지만 그렇게 간단하지는 않다고 한다. 좋아 해보자.

코딩은 구글신과 함께.. (또는 사부님 말씀대로 이클립스 코드를 뒤지면 나올지도 모른다. 사부님은 이미 뒤져본 것 같다. 자신이 생각한 방법과 동일한 방법을 사용했다고 한다. 어떤 건지는 안 찾아봐서 모르겠다;; 수천 수만 개나 되는 소스 코드를 받아오기도 귀찮고 그걸 IDE에 로딩하는데 엄청 오래 걸릴 것이며 잘못해서 뻑나거나 빌드가 안되고 컴파일 에러잡고 그러면서 삼천포로 가고 싶진 않았다.)

public class SoutInterceptor {

    private PipedInputStream pipedInputStream;
    private PrintStream originalPrint;

    public SoutInterceptor() {
        originalPrint = System.out;
        this.pipedInputStream = new PipedInputStream();
    }

    public String getMessages() throws IOException {
        byte[] messages = new byte[pipedInputStream.available()];
        pipedInputStream.read(messages, 0, messages.length);
        return new String(messages);
    }

    public void active() throws IOException {
        final PipedOutputStream pipedOutputStream = new PipedOutputStream(pipedInputStream);
        PrintStream saveStream = new PrintStream(pipedOutputStream) {
            @Override
            public void println(String x) {
                try {
                    pipedOutputStream.write(x.getBytes());
                } catch (IOException e) {
                    System.out.println(“error”);
                }
                originalPrint.println(x);
            }
        };
        System.setOut(saveStream);
    }

}

오호.. 잘 돌아간다.. +_+.

테스트를 좀 더 해보자.

        sout.hello();
       
        assertThat(soutInterceptor.getMessages(), is(“hello”));

아래에 이렇게 추가하고 hello() 메서드 안에서는 hello를 출력하게 했다. 또 테스트가 잘 돌아간다. 흠.. 이제 맞게 한건가?

println(Stirng x)를 재정의 했는데 print(int), println(boolean) 를 호출할 때도 잘 동작한다. 왜그럴까?

[봄싹 DevTerms] 활성화 맞춤형 개발


현재 봄싹 DevTerms는 오픈을 한지 얼마 안되서 인지 사용자가 몇 안되고 쌓인 데이터가 별로 없지만 장차 사용자도 늘어나고 데이터가 많아지면 다음과 같은 기능을 추가할 계획입니다. (어제 오늘 오픈 하고 난 뒤 생간난 아이디어들입니다.)

봄싹 DevTerms 트위터가 활성화 되면..
– 개발 용어 및 한글 용어가 올라갈 떄마다 트위터에 등록되는데, 만약 봄싹 DevTerms 트위터가 활성화 되어 올라온 메시지에 누군가 reply나 retweet등을 달면 해당 메시지들을 읽어와서 봄싹 DevTerms에서 보여줄 수 있겠습니다.

개발 용어가 많이 등록되면..
– 오늘 용어, 이번주 용어 탭을 추가하여 오늘 하루에 몇 개나 등록됐으며 어떤 단어들이 등록됐는지 볼 수 있게 하겠습니다. 그러나.. 아직은 뭐..

등록된 개발 용어가 2000개가 넘으면…
– 아이폰과 안드로이드폰 용 애플리케이션을 개발하겠습니다.

개발 용어가 빈번히 등록 또는 수정 된다면..
– 코멧을 적용하여 트위터처럼 실시간으로 새로 등록된 용어가 있음을 알려주고 페이지 업데이트를 하라는 메시지를 전달하거나 자동으로 페이지를 업데이트 해주겠습니다. 리버스 Ajax를 적용해야되서 기술적으로도 재밌을 것 같네요.

등록자 외에도 개발 용어를 편집하고 싶은 분들이 있다면..
– 위키 형태로 히스토리를 저장하고 누가 언제 수정했는지 기록을 남기겠습니다. 현재는 관리자 권한을 가진 사용자는 모든 용어를 편집할 수 있지만 히스토리를 남기고 있지 않아서 위키라고 볼 수는 없는데 아직 이 기능이 필요한 시점은 아닌 것 같습니다. 활성화가 된다면 모를까..

뷰 카운트 및 추천 카운트가 1000이 넘는다면..
– 현재 봄싹 막내(스루)가 미래를 위한 대비 작업으로 천천히 진행 중이긴 한데, StackOverFlow 처럼 카운트 단위를 도입해서 1000건이 넘는 조회수에는 k를 붙여서 1000v -> 1kv로 카운트 단위를 적용하겠습니다.

The more you use it, the more you will get.

2010 대한민국 매쉬업 참가했습니다.

“개발 용어 한글화 프로젝트 DevTerms”로 참가했습니다. 이 녀석을 어제 배포하고 밤새 나머지 주요 기능 중 하나였던 ‘관심 용어’까지 구현한 뒤 배포하고 참가 신청까지 완료했습니다. 2010 매쉬업 참가 목록에서 보실 수 있습니다.


위에 보시면 아시겠지만 작년 11월부터 이미 DevTerms를 만들고 싶다는 이야기를 봄싹 그룹스에서 했었습니다. 그러던 중 매쉬업 소식을 듣게 되었고 DevTerms에 오픈 API를 적용해서 내보내게 된 겁니다. 1석 2조가 됐지요. 어차피 봄싹에 새 기능으로 추가할 계획이었고 그 기능에 필요한 오픈 API를 적용해서 매쉬업까지 나갔으니 말이죠.

그래서 그런지 제가 제출한 제품은 매쉬업이 메인이 아니라 ‘개발 용어 한글화’ 서비스가 메인입니다. (벌써 43개나 제출됐네요.. 캬오.)그걸 보조하는 수단으로 사전 API와 트위터 API 사용하고 있죠. 사전 API는 기대 했던 것 보다 효과가 좋다고 생각하고 있습니다. 개발 용어는 영어 사전으로 검색한 결과를 보여주고 한글 용어는 한글 사전으로 검색한 결과를 보여주고 있습니다. 트위터 API를 이용하여 등록되는 용어들을 트위터 메시지로 등록해주고 링크를 타고 봄싹 DevTerms로 올 수 있게 해뒀습니다. 트위터 서비스만 해두면 사용자들이 트위터 RSS 피드나 노티 애플리케이션을 이용해서 등록되는 용어들을 실시간으로 받아 볼 수도 있지요.

사실 중간에 매쉬업 참가를 포기할까도 고민했습니다. 혼자서 디자인까지 신경쓰면서 기능 구현을 하자니 중간에 탁탁 끊기는 느낌이고 누군가 제가 집중해서 기능을 구현하고 있으면 그 동안 만든 페이지들 디자인을 점검하고 개선해주면 좋겠다 싶어서 봄싹 팀원에게 도움을 요청했습니다. 다행히 그 친구가 흔쾌히 승낙하고 휴가까지 반납하고 우리 회사로 찾아와 개발을 해주었기 때문에 일정과 품질을 다 기대했던 것 만큼 맞출 수 있었다고 생각합니다. 회사나 계약 등으로 묶여서 같이 일하는 것이 아니라 순수한 열정으로 묶여서 ‘함께 일한다’는 것이 무엇인지 조금은 알 수 있을 것 같은 기분입니다.

개발에 투자한 일수는 얼마 되지 않습니다. 1월 초까지도 번역을 하고 있었고 JSF를 공부하던 중에 갑자기 매쉬업이 생각나서 저번주부터 오늘까지 한 10일 정도 달린 것 같습니다. 하지만 투자한 시간은 거의 한달 정도 회사일에 투자한는 시간이었던 것 같습니다. 아침 10시부터 새벽 5시까지 전철 타고 밥먹는 시간 뺴고 코딩만 해본건 처음이었습니다. @_@ (어제와 오늘은 출근 길에도 전철에서 코딩을 했습니다. 아흑.. ‘쟤 모야… ‘이런 사람들의 시선도 무시한 채 꾿꾿히..) 이렇게 빠져버린 날 이해해준 아내에게 그저 고맙고 미안할 뿐입니다.

원래 계획은 1월 20일. 오늘부터 다시 번역에 집중해서 2월 달 안에 번역을 마무리하는게 계획이었습니다. 그런데 왠걸.. 24일까지 수정할 수 있게 해준다는군요. 뭐 기존에 생각했던 기능들은 다 만들었지만 막상 써보니까 생기는 요구사항들을 무시할 순 없을 것 같습니다. 24일까지만 손을 대고 후딱 번역을 마무리 한 다음 3월부터 진정한 2010년을 맞이해야겠습니다. 후아…

결과가 어찌될지 몰겠지만 수고했다. 기선아. 조금만 더 하고 마무리하자.