본문 바로가기

Language/Java

Java에서 Multi String 선언 할 수 있는 @Multiline

이 포스트의 핵심 내용은 아래와 같습니다.

  • Java에는 여러 줄에 걸친 문자열을 선언하는 문법이 없어서 긴 문자열을 편집하는 작업이 불편합니다.
  • 이를 보완하는 방법을 찾던 중 Adrian Walker라는 개발자가 만든 Multiline-string 이라는 라이브러리를 발견했고, Eclipse에서도 쓸 수 있도록 코드를 수정해서 원저자의 허락을 받고 Github에 올렸습니다. (https://github.com/benelog/multiline)
  • 이 과정 중에 Annotation Processing과 ECJ(Eclipse compiler for Java)에 대해서 알게 된 것들을 정리했습니다.
  • 이 라이브러리와 비슷한 기능을 Lombok에 추가하거나, 안드로이드에서 annotation Processing을 활용할만한 방안을 더 연구해볼만 합니다.

Java에는 언제 따옴표 3개 문법이 들어갈까?

몇 가지 언어에서 여러 줄에 걸친 문자열을 선언해보면 아래와 같습니다.

  • Python

    sql = """
       SELECT name , email 
       FROM user
     """
    
  • Groovy, Scala

    var sql = """
       SELECT name , email 
       FROM user
     """
    
  • Xtend

    var sql = '''
       SELECT name , email 
       FROM user
     '''
    

    Xtend는 세련된 문법을 지원하면서 자바 소스코드를 생성해주는 방식으로 동작합니다. 스칼라와 유사한 문법이 많이 들어가 있습니다.

  • Ruby

     sql = <<END
       SELECT name , email 
       FROM user
      END
    
  • C#

    string sql = @"
           SELECT name , email 
           FROM user
         ";
    

그러나 자바는 아직 이런 문법이 없어서 중간에 + 기호와 따옴표를 넣어야합니다.

String sql = "SELECT name , email \r" + 
              "FROM user ";

상수끼리의 스트링 더하기는 컴파일을 하면서 최적화 되기 때문에 성능에 손해는 없습니다. Eclipse에서 'Windows-Preference > Java-Editor > Typing' 메뉴에서 "Escape text When pasting into a string literal"를 선택하거나 SQL문을 Java 문자열 선언으로 변환해주는 도구를 쓰면 중간에 들어가는 '+'나 따옴표를 자동으로 넣을 수도 있습니다. 그렇게 해도 중간에 편집을 할 때는 늘 신경이 쓰이고 불편합니다.

그래서 긴 문자열인 SQL이나 HTML, XML, JSON등을 '.java' 파일 안에 선언해야 할때는 굉장히 번거롭습니다. 자바에서 iBatis, MyBatis 같은 프레임워크들이 많이 쓰이는 이유는 한편으로는 JDBC API의 불편함도 있지만, XML파일에서 SQL을 편집하는 작업이 훨씬 편하기 때문이기도 하다고 생각합니다.
전에 쓴 아래 글에서 이를 자세히 설명한 적이 있습니다.

위의 글에서는 그루비를 문자열 관리만을 위해 사용하는 대안을 제시했었습니다.

자바에서 곧 비슷한 문법이 추가된다면 그렇게 둘러서 갈 필요가 없을테니 조금만 더 기다릴까 하는 생각도 듭니다. 그러나 현재의 상황을 보니 금방 기능 추가가 된다는 확신이 들지 않습니다.

이렇게 진행이 미지근하다보니 Java 프로그래머들의 불만도 커져갑니다. 컨퍼런스에서 이를 논의하다가 사람들이 분노해서 거리로 뛰쳐나가 시위를 했다는 우스개글까지 나왔습니다.

위의 글에서 재밌는 부분을 하나 인용을 하면, 다음 문장이 있습니다.

앞으로 30년 내에 multiline-string을 지원하지 않는 언어를 발명하는 사람은 비웃음을 사게 될 것이다.

(In another thirty years people will laugh at anyone who tries to invent a language without multi-line strings.)

이 말은 Higher-Order Perl의 저자인 Mark Jason Dominus가 했던 아래 이야기를 패러디한 것입니다.

지금 '회귀호출'이 없는 언어를 발명하려는 사람을 비웃듯이, 앞으로 30년 안에는 'closure'가 없는 언어를 발명하려는 사람은 비웃음을 사게 될 것이다.

(In another thirty years people will laugh at anyone who tries to invent a language without closures, just as they'll laugh now at anyone who tries to invent a language without recursion. )

대체 구현

Multi-line String이 Java 자체 문법으로 언제 추가될지 모르니, 당장이라도 쓸 수 있는 대체 수단을 찾아보았습니다.

Brod cox의 Java+

Xtend와 유사하게 'Java+'는 결과물로 Java소스코드를 만들어주는 전처리기입니다. Java를 확장한 문법을 제공하는데, Multi-line String을 중괄호 2개 {{ }} 사이에 선언하는 문법을 가지고 있습니다. 이 문법을 도입한 배경은 Brad Cox가 쓴 How Do I Handle Multiline Strings? 라는 글에 자세히 설명되어 있습니다. (이글에 언급된 'MLS Preprocessor'가 Java+의 이전 이름입니다.)

전처리기를 도입하면 소스관리나 빌드과정이 많이 달라질것 같고, 그런 변화를 받아들이면서 쓰기에는 Xtend에 비해서는 다른 문법이 획기적인 것도 아니라서 다소 애매한 위치라는 느낌이 듭니다. Xtend나 Groovy, Scala에 비해서 큰 장점이 느껴지지 않았습니다.

Sven Efftinge의 S()

주석문으로 일반문자열을 선언하고, 내부적으로 Exception 발생과 코드 라인 정보를 이용한 구현입니다. 고의로 Runtime Exception을 일으키고, 디버깅 정보에 포함된 행번호를 읽어서 그 라인의 주석문을 찾습니다. 아이디어는 신선하지만 추가적인 콜스택이 생기고, 정상적인 실행경로에 일부러 Exception을 내는 것이 관례와는 어긋난 편법 같아서 마음이 걸렸습니다.

Bruce Chapman의 @LongString

아래에 소개된 Adrian Walker의 @Multiline과 유사한 방식으로 동작한다고 추정됩니다. 소스 저장소가 현재는 사라졌는지 구현 코드를 찾지 못했습니다. 몇 곳에서 이를 이용한 @LongString의 예제만이 보였습니다.

Adrian Walker의 @Multiline

이 글에서 소개하고, 제가 기능을 추가한 라이브러리입니다. Annotation Processing과 Javadoc 주석을 이용했습니다. 소스가 간단하고, 런타임에 콜스택이 추가되지 않는다는 장점이 있습니다. 코드도 공개되어 있었기 때문에 이 라이브러리를 선택했습니다.

@Multiline

사용법은 간단합니다. 스트링 멤버변수에 @Multiline을 붙이고, 위에 javadoc로 주석을 달면 javadoc의 내용이 문자열 값으로 치환됩니다.

아래 코드는

    /**
     Hello!
     Multiline-string!!
    */
    @Multiline String msg;

Groovy에서 아래코드와 같습니다.

String msg = """
     Hello!
     Multiline-string!!        
"""

@Multiline 개선 작업

Adrian Walker의 블로그 포스트에 있었던 원래의 코드를 그대로 사용해 보니 Javac나 Maven에서는 잘 실행되지만 Eclipse 안에서는 의도한 결과가 나오지 않았습니다. 그리고 Maven repository에 올라가 있지 않아서 직접 소스를 받아서 'mvn install'으로 Local repository에 설치를 한 후에만 쓸 수 있다는 점도 불편했습니다.

불편한 점을 해결하기 위해서 몇가지 작업을 했습니다.

  • Eclipse compiler(ECJ) 지원 기능 추가. Eclipse에서 Ctrl+S로 저장을 해도 Javac의 결과와 동일하게 컴파일됩니다.
  • Github을 Maven repository로 활용. 이 라이브러리를 Maven에서 dependency 추가로 바로 사용할 수 있습니다.
  • Annotation Processor 클래스를 컴파일러에서 자동으로 인식하도록 메타 정보 파일 추가. 'Maven compiler pluing 등에서 Processor 역할을 하는 클래스를 직접 지정하지 않아도 되게 했습니다.
  • Eclipse와 Maven 설정, Eclipse template 추가 등에 대한 문서 정리

프로젝트는 https://github.com/benelog/multiline에 공개되어 있습니다. 이 라이브러리를 처음 만들었던 Adrian Walker에게 Github의 주소와 제가 수정한 부분을 알리고, 이 프로젝트를 계속 진행해도 될지 문의를 했는데, 아래와 같이 시원하게 허락을 해 주었습니다.

Dear Sanghyuk

Please feel free to use, modify and distribute my code in any way you like without any restrictions. Thank you very much for emailing, its nice to know people want to use my code.

Best regards

Adrian

설정법

Maven을 쓰지 않는다면 jar파일을 다운로드하고 Eclipse Buidpath와 FactoryPath에 추가하면 됩니다. 자세한 내용은 아래 문서에 정리되어 있습니다.

Maven을 쓴다면 파일을 직접 다운로드할 필요없이 Dependency과 Repository추가합니다. 대신 Eclipse에서 Plugin 설정을 신경써야 하는데, m2eclipse가 설치되어 있어야하고 m2eclipse 버전이 0.13이상이라면 m2e-apt도 추가로 설치해야 합니다. 자세한 내용은 아래 문서를 참고하시기 바랍니다.

그리고 이 라이브러리를 쓴다고 해도 여전히 언어 차원에서 따옴표 3개 문법이 있는 것보다는 많은 키 입력을 해야 해서 불편합니다. 아래와 같이 Eclipse에서 Template 설정을 하면 약간은 더 편하게 @Multiline을 사용하실 수 있습니다.

물론 이런 방법을 써도 여전히 언어 자체에서 지원하는 문법보다는 깔끔하지 못한 면이 있습니다. 그래도 현재 Java문법의 안에서는 부작용이 없으면서도 빌드 구조에 변화가 적은, 가장 덜 불편한 방법정도로 생각이 됩니다.

@Multiline의 기술 요소

Annotationg Processing

Annotation Processing은 애노테이션 정보를 읽어서 소스코드를 조작할 수 있습니다. QueryDSL,Android AnnotationsLombok등이 이를 활용하고 있습니다. @Multiline도 해당 애노테이션이 붙은 멤버변수를 찾고, 거기에 붙은 Javadoc 주석을 읽어서 변수의 선언값으로 대체해줍니다.

@Multiline을 이용해서 아래처럼 선언을 하고

public class Hello {
    /**
     Hello!
     Multiline-string!!
    */
    @Multiline private static String msg;

    public static void main(String[] args) {
        System.out.println(msg);
    }
}

컴파일을 한 후 다시 역컴파일을 하면 다음과 같이 나옵니다.

public class Hello {
    private static String msg = "        Hello!\n        Multiline-string!!\n";
    ... 이하 생략
 }

이 방식의 장점은 런타임에서는 아무런 추가 부담이 없다는 것입니다. staic final String으로 상수 선언을 하면, 이를 사용하는 코드도 직접 문자열 치환이 되기 때문에 JVM이 할 수 있는 최선의 최적화가 이루어집니다.

XML로 긴 문자열을 관리할때보다 런라임라이브러리의 크기와 성능면에서 유리합니다. XML을 파싱해서 미리 Map같은 객체에 넣어두더라도 실제 값을 찾아가는 콜스택이 길어질 수 밖에 없습니다. 간단한 문자열 참조대신 Map.get()을 쓴다면 hashCode연산, Hash bucket찾기, equals메소드 호출 등이 줄줄이 이어지기 때문입니다. 물론 요즘 같이 고성능 장비가 쓰이는 시대에 이 정도 연산이 성능에 병목을 일으키는 경우는 없고, 네트웍 호출 같은 비용에 비하면 미미한 부담입니다. 서버 개발 쪽에서는 마음이 약간 더 편한 정도의 의미만 있을지도 모르겠습니다.

Annotation Processing은 Java6부터 본격적으로 자바의 기본 컴파일러인 javac의 기능으로 들어갔습니다. 그전까지는 명령행에서 'apt'(Annotation Processing Tool)이라는 도구를 별도로 실행해야 했습니다. 그리고 관련된 패키지도 'com.sun.mirror'에서 'javax.annotation.processing'등으로 바꾸었습니다. @Multiline을 처리하는 Processor는 JDK6의 API에 맞추었기 때문에 반드시 Java6이상을 써야 합니다.

ECJ (Eclipse Compiler for Java)

약간 더 깊이 들어가서, 제가 이 라이브러리에 Eclipse를 위해서 코드 추가를 했었던 배경인 ECJ에 대해서 정리해보겠습니다.

Eclipse는 컴파일러 구현체로 Javac를 쓰지 않고 자체적으로 만든 ECJ를 씁니다. ECJ는 Eclipse 내부 뿐만이 아니라 ECJ는 Tomcat에서 JSP를 컴파일할 때나 IntelliJ IDEA에서도 쓰이고 있습니다.

Java명세를 지켜서 만들려고 했겠지만 Javac로 컴파일한 결과와 Eclipse로 컴파일한 결과나 실행결과가 다른 경우도 있습니다. Javac의 구현이 버그였던 적도, ECJ 쪽이 버그였던 적도 있습니다.

예를 들면 Stackoverflow에 올라온 아래 사례들이 있습니다.

의도적으로 넣은 특징은 Javac에서는 아예 컴파일이 되지 않는 코드도 ECJ에서는 부분적으로 컴파일이 된다는 것입니다. 아래 코드는 'now...'라는 해석할 수 없는 문자열이 있기 때문에 javac에서는 당연히 컴파일에러가 납니다..

public class Hello {
    public void say(){
        System.out.println("say!");
    }

    public void listen(){
        now....
    }
}

하지만 Eclipse에서는 자동 컴파일 기능을 쓰면서 파일을 저장하면 위의 클래스도 컴파일 되어 'Hello.class'파일이 생깁니다. 그 파일을 다시 역컴파일 패보면 아래와 같이 컴파일 에러가 난 부분을 Error로 대체한 결과가 보입니다.

public class Hello {
    public void say()    {
        System.out.println("say!");
    }

    public void listen() {
        throw new Error("Unresolved compilation problem: \n\tSyntax error on tokens, delete these tokens\n");
    }
}

컴파일을 할 수 있는 부분이라도 돌아가게 하는 것이 IDE같은 개발환경에서는 좀 더 에러가 난 부분을 찾는데 도움이 되기 때문에 이런 특성이 생긴 것으로 보입니다. 이런 부분적인 compile을 허용하지 않는 'batch mode'로 ECJ를 사용할 수도 있습니다.

ECJ를 위한 Annotation Processor 구현

최초 버전의 @Multiline은 Eclipse 안에서는 사용할 수 없었습니다. Eclipse에서 Build path만 추가해서는
@Multiline으로 문자열을 선언해도 javadoc 주석의 값이 치환되지 않고, null값이 나옵니다.

Eclipse에서는 'Project > Preference > Annotation Processing > Factory Path' 메뉴에서 Annotation processor역할을 하는 클래스가 포함된 jar파일을 지정해야 합니다.

Factory Path

그런데 최초의 Multiline-string 라이브러리는 여기에 추가를 하면 에러 메시지가 나왔었습니다. 처음 코드에서 인터페이스인 ProcessingEnvironment를 구체적 클래스인 JavacProcessingEnvironment로 캐스팅하는 부분이 있었는데, Eclipse에서는 같은 인터페이스를 구현했지만 다른 클래스의 인스턴스가 넘어오기 때문이였습니다.

public void init(final ProcessingEnvironment procEnv) {
    super.init(procEnv);
    JavacProcessingEnvironment javacProcessingEnv = (JavacProcessingEnvironment) procEnv;
... 생략
}

Annotation Processor에 관련된 javax.annotation.processing.ProcessingEnvironmentjavax.lang.model.element.Element 와 같은 인터페이스 규약이 있기는 합니다. 인터페이스에 정의된 메소드만으로 원하는 작업이 가능하다면 ECJ에서도 코드 수정없이 같은 Processor를 쓸 수 있습니다. 그러나, Multiline-string의 Processor 구현클래스는 Javac에서 제공하는 구체적인 클래스를 사용한 코드가 많고, 직접 멤버변수에 접근하기도 합니다. 변수 값을 치환한다는 작업들은 인터페이스에 정의되지 있지 않기 때문입니다.

어쩔 수 없이 ECJ용 Annotation processor를 별도로 만들고, 메소드 파라미터로 넘어오는 클래스의 패키지명을 검사해서 Javac인지 ECJ인지를 판단해서 각각에 맞는 클래스로 분기해주는 방식을 선택했습니다.

@SupportedAnnotationTypes({"org.adrianwalker.multilinestring.Multiline"})
@SupportedSourceVersion(SourceVersion.RELEASE_6)
public final class MultilineProcessor extends AbstractProcessor {
    private Processor delegator = null;

    @Override
    public void init(final ProcessingEnvironment procEnv) {
        super.init(procEnv);
        String envClassName = procEnv.getClass().getName();
        if (envClassName.contains("com.sun.tools")) {
            delegator = new JavacMultilineProcessor();
        } else {
            delegator = new EcjMultilineProcessor();
        }
        delegator.init(procEnv);
    }
... 생략..    
}

그리고 ECJ용 구현체를 만드는 작업도 생각보다는 어려웠습니다. 관련 문서가 깊이 정리된 것이 없어서 소스코드를 보면서 많은 부분을 파악했고 넘겨 짚어서 구현을 해본 부분도 과정도 있었습니다.

향후 Java 8 이후에서는 javax.lang.model.*의 인터페이스에 기능이 더 정의되어서 ECJ와 Javac사이의 인식성이 더 높아졌으면 좋겠습니다.

참고로 Lombok에서도 같은 애노테이션을 처리하는 Javac 구현과 ECJ 구현을 따로 하고 있습니다. 예를 들어 @Getter를 처리하는 클래스도 같은 클래스이름으로 다른 패키지 아래에 따로 존재합니다.

각각의 구현은 Lombok에서 제공하는 JavacAnnotationHandler, EclipseAnnotationHandler를 상속하고 있습니다. 2가지 컴파일러를 지워하는 자체 기반구조를 가지고 있는 것입니다.

한가지 또 재미있는 점은, Lombok에서는 Eclipse에서 쓸 때 @Multiline이나 QueryDSL등과는 다르게 'Facotory path' 설정을 하지 않아도 됩니다. 'java -jar lombok.jar'를 실행시키고 Eclipse가 설치된 위치를 지정하면, eclipse.ini 파일등을 고쳐서 java-agent 옵션으로 lombok.jar를 추가해 줍니다. 이는 런타임에 동적으로 바이트코드를 조작하는 Instrumentation을 활용한 기법입니다. 이 때문에 Lombok은 유사하게 instrumentation을 쓰는 AspectJ와 충돌이 일어나기도 합니다.

개발환경에서 굳이 약간 다른 방법은 쓴 이유는 그렇게 설정하는 Factory path를 설정하는 것보다 훨씬 간편하기 때문인것 같습니다. Android annotations의 Maven+Eclipse 설정법 문서나 제가 만든@Multiline의 Maven + Eclipse설정법 을 보면 Eclipse 설정을 위한 여러 단계가 있습니다. 앞에서 말씀드린 것처럼 Pluing의 버전도 신경써야 하고, 선택할 수 있는 방법도 여러 가지 입니다. Lombok에서는 'lombok.jar를 실행하고 지시에 따르라'로 단 한줄로 끝내고 있습니다. 문서를 정리한다고 한참 시간을 쓰고 나니, Lombok이 왜 그런 선택을 했는지 이해가 되었습니다.

앞으로 할만한 일

자바 언어는 이미 성숙한 언어이고 한걸음 더 나아가는데에는 많은 짐들이 있습니다. 하위 호환성과 많은 이해관계자들을 고려하다보면 스펙 하나하나가 결정이 신중해지고, 확정과 구현에 많은 시간이 걸립니다. 이런 상황에서 Annotation processing은 Java 언어 자체를 바꾸지 않으면서 기본 언어 문법에서 아쉬운 점들을 많이 보강할 수 있는 소중한 경로입니다.

아직 annotation processing에는 개발과 사용에 여러가지 불편한 점들이 있습니다. javax.lang.model.* 등에서 표준화 되지 않는 기능에 대해서는 Javac용과 ECJ용 처리기를 따로 구현해야 하고, 각 컴파일러마다 다른 구조로 제공하는 그런 기능들은 문서화가 잘 되어 있지 않다고 느껴집니다.
Maven과 같이 쓸때의 설정도 plugin의 버전과 조합을 잘 알고 설정해야 합니다.

이미 어느 정도 그런 문제를 보강한 Lombok의 기반구조를 활용한다면 그런 단점이 그나마 적어질 것 같습니다. 이 multiline-string 프로젝트도 Lombok의 추가기능으로 다시 구현해볼만하다고 생각합니다. Lombok에는 아직 충분히 테스트되지 않은 Lombok experimental features들이 있는데, 커뮤니티로부터 긍정적인 프드백을 받으면 핵심 기능으로 채택될 수도 있다고 합니다. 그런 과정을 거쳐서 Multiline string도 언젠가는 채택되면 좋겠습니다.

그리고 AndroidAnnotations처럼 안드로이드 환경에서 annotation processing으로 할 수 있는 일들도 연구해볼 가치가 있습니다. 모바일에서는 추가되는 라이브러리의 용량이나 로딩되는 클래스의 크기, 추가되는 콜스택에 대한 부담이 더 큽니다. Annotation processing은 컴파일 시점에서 코드를 추가하기 때문에 런타임에서 추가되는 라이브러의 크기나 콜스택을 줄일 수 있습니다. 모바일 환경에서는 이 장점이 더욱 두드려져 보입니다.



참조: https://github.com/benelog/multiline/wiki/Java%EC%97%90%EC%84%9C-%EC%97%AC%EB%9F%AC%EC%A4%84%EC%97%90-%EA%B1%B8%EC%B9%9C-%EB%AC%B8%EC%9E%90%EC%97%B4-%EC%84%A0%EC%96%B8%EC%9D%84-%ED%8E%B8%ED%95%98%EA%B2%8C-%ED%95%98%EB%8A%94-@Multiline



'Language > Java' 카테고리의 다른 글

Gradle 에서 Executable jar 만들기  (0) 2017.12.11
JMS(Java Message Service)  (0) 2014.04.07
이클립스에서 JAVA API 소스 보기  (0) 2014.04.04
Apache Commons Library  (0) 2014.04.04
[java] Serialization IO 속도 향상  (0) 2014.03.18