Moshi, Moshi~ 직렬화를 원하신다구요?

JSON 직렬화 라이브러리 중 하나인 Moshi가 무엇인지, 어떤 장∙단점이 있는지, GSON과는 어떤 차이점이 있는지에 대해 정리해 보았습니다.

작성일 2023년 08월 25일

안녕하세요. 저는 IMQA 개발팀 SDK 파트에서 Android SDK를 개발하는 김성윤 연구원입니다.
개발을 하면서 JSON 데이터를 직접 다룰 때 JSON 직렬화 라이브러리를 접하게 되는데요! 이번 글에서는 JSON 직렬화 라이브러리 중 하나인 Moshi가 무엇인지, 어떤 장∙단점이 있는지, 또 많은 모바일 개발자가 선택한 GSON과는 어떤 차이점이 있는지에 대한 내용을 공유하고자 글을 쓰게 되었습니다.

JSON 직렬화? 역직렬화?

Moshi에 대해 알아보기 전에 JSON 직렬화와 역직렬화가 무엇인지 간단하게 짚고 넘어가겠습니다!

JSON?

{
  "name": "John Doe",
  "age": 30,
  "email": "john@example.com"
}

JSON(JavaScript Object Notation)에 대해 모르시는 분들은 없으시겠지만, 간단하게 설명드리자면 JSON은 데이터를 교환하기 위해 경량화된 형식입니다. 인간이 읽고 쓰기에 용이하며 기계가 파싱하고 생성하기도 간편한 형태이며, 웹 애플리케이션에서 데이터 교환에 매우 널리 사용되고 있습니다. (현재는 JONS을 사용하지 않는 웹 애플리케이션을 찾기 어려울 정도죠..!)

JSON이 무엇인지 간단하게 알았으니, 이제 JSON 직렬화와 역직렬화에 대해 하나씩 살펴보겠습니다.

JSON 직렬화 (Serialization)
JSON 직렬화란, 프로그래밍 언어에서 사용되는 데이터 구조를 JSON 형식으로 변환하는 과정을 의미합니다. 이렇게 직렬화된 데이터는 네트워크 통신 시 전송하거나 파일에 저장할 때 유용하게 사용됩니다. 예를 들어, 객체나 배열과 같은 복잡한 데이터를 문자열 형태로 변경하는 방식입니다.

JSON 직렬화 전

{
  "name": "John Doe",
  "age": 30,
  "email": "john@example.com"
}

JSON 직렬화 후 (문자열 형태로 변환)

"{\"name\":\"John Doe\",\"age\":30,\"email\":\"john@example.com\"}"

JSON 역직렬화 (Deserialization)
JSON 직렬화는 JSON 형식의 문자열을 다시 프로그래밍 언어에서 사용할 수 있는 데이터 구조 형태로 변환하는 과정입니다. 즉, 직렬화된 JSON 데이터를 원래의 프로그래밍 언어의 객체나 자료구조로 복원하는 방식입니다.

직렬화된 데이터 (JSON 형식의 문자열)

"{\"name\":\"Jane Smith\",\"age\":25,\"email\":\"jane@example.com\"}"

JSON 역직렬화 후의 데이터 구조

{
  "name": "Jane Smith",
  "age": 25,
  "email": "jane@example.com"
}

JSON 직렬화와 역직렬화는 데이터 교환과 저장에 매우 중요한 사항인데요. 웹 애플리케이션, 모바일 앱, 서버와의 통신 등 다양한 상황에서 JSON 형식을 활용하여 데이터를 효율적으로 처리할 수 있습니다.

JSON 직렬화의 장∙단점

<장점>

  • 가독성: JSON 직렬화는 사람이 읽고 이해하기 쉬운 텍스트 기반 형식으로 데이터를 저장하므로 가독성이 뛰어납니다.
  • 데이터 교환 용이성: JSON 직렬화를 사용하여 데이터를 텍스트 형식으로 변환하면 서로 다른 플랫폼 간에 데이터 교환과 상호 운용성이 용이합니다.
  • 경량성: JSON은 간결하고 경량적으로 데이터를 표현하기 때문에 데이터 크기가 작아 전송 및 저장에 효율적입니다.

<단점>

  • 복잡한 데이터 타입 처리: JSON은 간단한 데이터 타입만 지원하므로 복잡한 데이터 구조를 직렬화하거나 처리하기 어려울 수 있습니다.
  • 누락된 필드 처리: JSON은 필드가 누락되는 것을 허용하기 때문에 역직렬화 시, 어플리케이션 단에서 이 누락된 필드에 대한 처리를 따로 진행해 주어야 합니다.
  • 큰 숫자 처리: JSON은 큰 정수나 소수점을 처리하는 데 한계가 있어서 정확성과 성능에 영향을 미칠 수 있습니다.

JSON 직렬화는 주로 클라이언트-서버 데이터 통신 시에 사용됩니다. JSON을 사용하지 않고 데이터를 통신하는 게 어색하다고 느껴질 정도로 대부분 당연하듯이 사용되고 있는데요.

개인적으로 JSON 직렬화의 큰 장점은 가독성데이터 교환 용이성이 아닐까 하는 생각이 듭니다. '가독성'은 디버깅 및 데이터 확인을 직접하게 되는 경우 사람이 읽기 쉬운 형태이기 때문에 많은 러닝 커브 없이 쉽게 확인할 수 있다는 것이 가장 큰 장점이라고 생각합니다. '데이터 교환 용이성' 역시 어느 플랫폼이든 큰 제약 없이 데이터를 통신할 수 있다는 점에서 개발자에게는 큰 혜택이 아닐까 싶습니다.

JSON 직렬화 라이브러리 종류

그럼 JSON 직렬화 라이브러리는 어떤 것이 있을까요? 이번에는 JSON 직렬화 라이브러리에는 어떤 것이 있는지 알아보겠습니다.

1. Jackson

JSON 직렬화 라이브러리 종류 - 1. Jackson

Jackson은 Java 기반의 오픈 소스 JSON 데이터 처리 라이브러리입니다. JSON 데이터를 자바 객체로 변환하거나, 자바 객체를 JSON 형식으로 직렬화하는데 사용됩니다. Jackson은 강력한 성능과 다양한 기능을 제공하여 대용량 데이터 처리와 다양한 데이터 형식을 지원하는데 용이합니다.

2. json-simple

JSON 직렬화 라이브러리 종류 - 2. json-simple

json-simple은 Java 기반의 오픈 소스 JSON 데이터 처리 라이브러리로, JSON 데이터를 다루는 데 사용됩니다. JSON 데이터를 파싱하여 자바 객체로 변환하거나, 자바 객체를 JSON 형식으로 직렬화하는데 사용되는 간단하고 가벼운 라이브러리입니다.

3. GSON

JSON 직렬화 라이브러리 종류 - 3. GSON

GSON은 Google에서 개발한 Java 기반의 오픈 소스 JSON 데이터 처리 라이브러리로, JSON 데이터를 자바 객체로 변환하거나, 자바 객체를 JSON 형식으로 직렬화하는데 사용됩니다. GSON은 다양한 기능과 편리한 API를 제공하여 JSON 데이터를 손쉽게 다룰 수 있도록 도와줍니다.

💡 GSON은 현재 maintenance mode로 새로운 기능을 개발하는 것이 아닌
bug fix와 같은 유지보수성 업데이트만 진행되고 있습니다.

Moshi?

JSON 직렬화와 역직렬화에 대해 간단하게 알아보았으니, 이번에는 Moshi가 무엇인지 한번 알아보겠습니다.

“Moshi is a modern JSON library for Android, Java and Kotlin. It makes it easy to parse JSON into Java and Kotlin”
(https://github.com/square/moshi)

Moshi는 Square 사에서 개발한 오픈 소스 JSON 직렬화 라이브러리로, 자바와 Kotlin에서 JSON 데이터를 처리하는 데 사용됩니다.  Moshi는 JSON 데이터를 자바 객체로 변환하거나, 자바 객체를 JSON 형식으로 역직렬화하는 데 있어서 강력하고 효율적인 기능을 제공하고 있습니다.

Moshi의 주요 특징은 다음과 같습니다.

  • 높은 성능과 속도: Moshi는 경량화된 디자인과 최적화된 구조를 통해 높은 성능과 빠른 속도를 지원합니다. 특히 안드로이드 앱과 같이 제한된 자원을 가진 환경에서도 효율적으로 동작하며 대용량 데이터를 빠르게 처리할 수 있습니다.
  • Kotlin 호환성: Moshi는 Kotlin과의 높은 호환성을 가지고 있습니다. Kotlin의 Nullable 형식을 자동으로 처리하여 개발자가 간편하게 JSON 데이터를 다룰 수 있도록 지원합니다.
  • 정적 타입 지원: Moshi는 코드 생성(codegen)을 통해 정적 타입 어노테이션을 지원합니다. 이를 활용하여 JSON 데이터의 파싱 과정에서 타입 안정성을 제공하고, 컴파일 시간에 발생할 수 있는 오류를 미리 방지할 수 있습니다.
  • 코드 자동 생성(codegen): Moshi는 어노테이션 기반 코드 생성을 지원하여 JSON 데이터 처리를 런타임이 아닌 컴파일 시점에 처리할 수 있도록 도와줍니다. 이로 인해 더 빠른 성능과 개발 생산성을 제공합니다.
  • GSON 차용: Moshi는 GSON을 차용하여 개발되어 GSON 라이브러리를 사용 중에 Moshi로 전환하여도 큰 문제가 없습니다. (Moshi를 사용하다 GSON으로 전환해도 큰 문제는 없습니다.)

이 중 높은 성능과 속도, Kotlin과의 호환성과 같은 특징 때문에 GSON 사용 중인 사용자들도 Moshi로 전환하고 있는 추세입니다.

추가로 말씀드리자면 Moshi는 Okio를 기반으로 개발되어 강력한 I/O 처리로 높은 성능과 속도의 장점을 가지고 있습니다.

💡 Okio는 java.io 와 java.nio를 보완하여
데이터에 훨씬 쉽게 액세스하고 저장하여 처리할 수 있는 라이브러리입니다.

코드 자동 생성 기능?

Moshi에서 코드 자동 생성 기능은 컴파일 시점에 JSON 데이터 처리(직렬화/역직렬화)를 위한 어댑터를 자동으로 생성해주고 이를 통해서 런타임 성능이 향상되고 정적 타입 지원이 가능해집니다.

💡 Moshi에서 코드 자동 생성 기능은 Kotlin에서만 지원합니다.

코드 자동 생성 기능에 대해 생소하실 분의 이해를 돕기 위해 실제 소스 코드를 통해 Moshi의 CodeGen 기능을 통해 어떤 것을 할 수 있는지 확인해 보겠습니다.

간단한 예시로 JSON 직렬화를 진행할 User라는 데이터 클래스가 있다고 했을 때,

간단한 예시로 JSON 직렬화를 진행할 User라는 데이터 클래스가 있다고 했을 때,

@JsonClass 어노테이션만 추가해주고 빌드를 진행합니다.

@JsonClass 어노테이션만 추가해주고 빌드 진행

그다음 빌드된 클래스들의 경로로 이동해서 확인해보면 UserJsonAdapter 클래스 파일이 하나 추가되었는데요.

UserJsonAdapter 클래스 파일이 하나 추가됨

빌드 이후에 자동으로 생성된 UserJsonAdapter 클래스의 내용을 살펴 보면

💡 '@JsonClass(generateAdapter = true)' 어노테이션은
   Moshi Annotation Processor에 의해 분석되어
   자동으로 AdapterClass를 생성해 주고 있습니다.
빌드 이후에 자동으로 생성된 UserJsonAdapter 클래스의 내용

이런 식으로 나오는데요. 저는 Kotlin 코드 보다는 Java 코드가 좀 더 보기 편하기 때문에 Java로 decompile 해보겠습니다.

실제 코드를 보았을 때 많은 코드가 들어가 있는 것을 보실 수 있습니다.

public final class UserJsonAdapter extends JsonAdapter {
   @NotNull
   private final Options options;
   @NotNull
   private final JsonAdapter intAdapter;
   @NotNull
   private final JsonAdapter stringAdapter;

   public UserJsonAdapter(@NotNull Moshi moshi) {
      Intrinsics.checkNotNullParameter(moshi, "moshi");
      super();
      String[] var2 = new String[]{"id", "name", "email"};
      Options var10001 = Options.of(var2);
      Intrinsics.checkNotNullExpressionValue(var10001, "of(\"id\", \"name\", \"email\")");
      this.options = var10001;
      JsonAdapter var3 = moshi.adapter((Type)Integer.TYPE, SetsKt.emptySet(), "id");
      Intrinsics.checkNotNullExpressionValue(var3, "moshi.adapter(Int::class.java, emptySet(), \"id\")");
      this.intAdapter = var3;
      var3 = moshi.adapter((Type)String.class, SetsKt.emptySet(), "name");
      Intrinsics.checkNotNullExpressionValue(var3, "moshi.adapter(String::cl…emptySet(),\n      \"name\")");
      this.stringAdapter = var3;
   }

   @NotNull
   public String toString() {
      byte var1 = 26;
      StringBuilder var2 = new StringBuilder(var1);
      int var4 = false;
      var2.append("GeneratedJsonAdapter(").append("User").append(')');
      String var10000 = var2.toString();
      Intrinsics.checkNotNullExpressionValue(var10000, "StringBuilder(capacity).…builderAction).toString()");
      return var10000;
   }

   @NotNull
   public User fromJson(@NotNull JsonReader reader) {
      Intrinsics.checkNotNullParameter(reader, "reader");
      Integer id = null;
      String name = null;
      String email = null;
      reader.beginObject();

      while(reader.hasNext()) {
         JsonDataException var5;
         String var10000;
         switch(reader.selectName(this.options)) {
         case -1:
            reader.skipName();
            reader.skipValue();
            break;
         case 0:
            Integer var6 = (Integer)this.intAdapter.fromJson(reader);
            if (var6 == null) {
               var5 = Util.unexpectedNull("id", "id", reader);
               Intrinsics.checkNotNullExpressionValue(var5, "unexpectedNull(\"id\", \"id\", reader)");
               throw (Throwable)var5;
            }

            id = var6;
            break;
         case 1:
            var10000 = (String)this.stringAdapter.fromJson(reader);
            if (var10000 == null) {
               var5 = Util.unexpectedNull("name", "name", reader);
               Intrinsics.checkNotNullExpressionValue(var5, "unexpectedNull(\"name\", \"name\",\n            reader)");
               throw (Throwable)var5;
            }

            name = var10000;
            break;
         case 2:
            var10000 = (String)this.stringAdapter.fromJson(reader);
            if (var10000 == null) {
               var5 = Util.unexpectedNull("email", "email", reader);
               Intrinsics.checkNotNullExpressionValue(var5, "unexpectedNull(\"email\", …ail\",\n            reader)");
               throw (Throwable)var5;
            }

            email = var10000;
         }
      }

      reader.endObject();
      User var7 = new User;
      if (id != null) {
         int var8 = id;
         if (name == null) {
            JsonDataException var10003 = Util.missingProperty("name", "name", reader);
            Intrinsics.checkNotNullExpressionValue(var10003, "missingProperty(\"name\", \"name\", reader)");
            throw (Throwable)var10003;
         } else if (email == null) {
            JsonDataException var10004 = Util.missingProperty("email", "email", reader);
            Intrinsics.checkNotNullExpressionValue(var10004, "missingProperty(\"email\", \"email\", reader)");
            throw (Throwable)var10004;
         } else {
            var7.<init>(var8, name, email);
            return var7;
         }
      } else {
         JsonDataException var10002 = Util.missingProperty("id", "id", reader);
         Intrinsics.checkNotNullExpressionValue(var10002, "missingProperty(\"id\", \"id\", reader)");
         throw (Throwable)var10002;
      }
   }

   public void toJson(@NotNull JsonWriter writer, @Nullable User value_) {
      Intrinsics.checkNotNullParameter(writer, "writer");
      if (value_ == null) {
         throw new NullPointerException("value_ was null! Wrap in .nullSafe() to write nullable values.");
      } else {
         writer.beginObject();
         writer.name("id");
         this.intAdapter.toJson(writer, value_.getId());
         writer.name("name");
         this.stringAdapter.toJson(writer, value_.getName());
         writer.name("email");
         this.stringAdapter.toJson(writer, value_.getEmail());
         writer.endObject();
      }
   }

   // $FF: synthetic method
   // $FF: bridge method
   public Object fromJson(JsonReader p0) {
      return this.fromJson(p0);
   }

   // $FF: synthetic method
   // $FF: bridge method
   public void toJson(JsonWriter p0, Object p1) {
      this.toJson(p0, (User)p1);
   }

코드가 자동으로 생성된 건 알겠는데.. 여러분들은 위 코드를 보셨을 때 Moshi의 CodeGen 기능이 빠른 이유에 대해서 이해되실까요..? 그렇다면, 아래 코드를 한번 확인해 보겠습니다!

   public UserJsonAdapter(@NotNull Moshi moshi) {
      Intrinsics.checkNotNullParameter(moshi, "moshi");
      super();
      String[] var2 = new String[]{"id", "name", "email"};
      Options var10001 = Options.of(var2);
      Intrinsics.checkNotNullExpressionValue(var10001, "of(\"id\", \"name\", \"email\")");
      this.options = var10001;
      JsonAdapter var3 = moshi.adapter((Type)Integer.TYPE, SetsKt.emptySet(), "id");
      Intrinsics.checkNotNullExpressionValue(var3, "moshi.adapter(Int::class.java, emptySet(), \"id\")");
      this.intAdapter = var3;
      var3 = moshi.adapter((Type)String.class, SetsKt.emptySet(), "name");
      Intrinsics.checkNotNullExpressionValue(var3, "moshi.adapter(String::cl…emptySet(),\n      \"name\")");
      this.stringAdapter = var3;
   }

여러분들이 보시기엔 이 소스 코드가 어떤 역할을 하는 소스 코드인지 이해되시나요? 이 소스 코드는 JSON 데이터를 파싱하기 전에 미리 해당 데이터 클래스의 필드 타입 정보를 미리 알기 위해서 Type을 Set해 주는 부분입니다.

Moshi에서는 미리 파악된 이 정보를 사용하여 JSON 데이터를 파싱할 때 필드와 클래스의 필드를 매핑하여 사용하고 있습니다. 만약 리플렉션을 통해서 JSON 데이터 파싱을 진행하게 되면 런타임 시점에 동적으로 클래스 구조를 검사하고 클래스의 필드에 접근하기 위해 많은 오버헤드가 발생하게 되어 성능에 문제가 발생할 수 있습니다.

그래서 Moshi는 클래스 필드의 타입 정보를 컴파일 타임에 미리 알아두어 런타임 시점에서 클래스 필드 Type을 일일이 탐색하는 비용을 없앨 수 있고 이로 인해서 성능상에 큰 이점을 취할 수 있게 됩니다.

정리하자면 Moshi의 codegen 기능은 컴파일 시점에 필요한 코드를 생성하고 리플렉션을 회피하여 JSON 파싱 작업을 효율적으로 수행하게 해줍니다.

💡 Moshi의 코드 생성 기능은 편리하지만,
  큰 프로젝트의 경우 CodeGen 기능을 사용하게 된다면
  빌드 시간이 증가될 수 있습니다.

Moshi vs GSON

Android 환경에서 JSON 직렬화 라이브러리는 주로 GSON이 사용되고 있는데요. Moshi에 대해서 조금만 찾아봐도 Moshi가 GSON보다 빠르다는 소개가 많이 있습니다. 그럼, 이번에는 Moshi와 GSON을 비교하여 왜 Moshi가 GSON보다 빠르다고 소개하고 있는지 알아보겠습니다.

1. Moshi와 GSON의 주요 특징 비교

특징 Moshi GSON
지원 언어 Java, Kotlin Java, Kotlin
코드 생성 (codegen) 지원 미지원
Kotlin 호환성 뛰어남 일반적
정적 타입 지원 강력함 미지원
성능 및 속도 높음 높음
커스터마이징 기능 강력함 제한적

위 특징에서 확인할 수 있는 것처럼 Moshi의 특장점은 Kotlin 호환성과 코드 생성(codegen) 기능인데요. 특히 코드 생성 기능을 통해 GSON과의 직렬화/역직렬화 시, 큰 성능 차이를 볼 수 있습니다.

또 Moshi는 Kotlin으로 개발되어 있어 Kotlin 호환성이 뛰어난데요. 현재 많은 개발자들이 Java 대신에 Kotlin으로 개발하고 있어 Kotlin과의 호환성은 큰 장점으로도 볼 수 있습니다.

그리고 현재 GSON은 유지 관리 모드(maintenance mode)로 bug fix 외 새로운 기능을 추가 개발을 하고 있지 않아 GSON을 사용하는 개발자들은 새로운 기능은 기대할 수 없습니다. (많은 사용자들이 GSON을 떠나가고 있는 큰 이유 중 하나입니다.)

2. Moshi와 GSON의 JSON 직렬화 방식 비교

그러면 이번엔 GSON과 Moshi의 직렬화 방식을 보겠습니다.

GSON의 직렬화 방식

💡 Reflection은 런타임에 객체의 필드와
  메서드에 접근하여 정보를 얻는 기법입니다. 
  • Reflection 사용: reflection을 사용하여 객체의 필드에 접근하고 값을 추출하여 JSON으로 직렬화합니다.
  • 데이터 타입 인식: 객체의 데이터 타입을 런타임에 확인하여 JSON에 포함될 데이터 형식을 결정합니다.
  • 유연한 사용:  JSON 데이터와 자바 객체 간의 변환을 간단한 코드로 지원하고 있고, 커스텀 직렬화/역직렬화를 구현하는 데 있어서 유연성을 제공하고 있습니다.

Moshi의 JSON 직렬화 방식

  • 코드 생성 사용: reflection 대신 코드 생성(codegen) 기능을 통해서 컴파일 시점에 직렬화/역직렬화 로직을 생성하여 직렬화하고 있습니다.
  • 정적 타입 안전성: Kotlin의 data class와 같은 정적 타입 안전성을 지원하고 있습니다. 이 장점을 통해서 컴파일러가 데이터 타입을 미리 검사할 수 있고, 런타임 시점에서 데이터 타입 불일치로 인한 오류를 방지할 수 있습니다.
  • 불변 객체 지원: 불변(immutable) 객체를 쉽게 다룰 수 있도록 지원하고 있습니다. 데이터 클래스에 대한 코드 생성을 통해 불변 객체를 JSON으로 직렬화하거나 JSON을 불변 객체로 역직렬화할 수 있습니다.

두 개의 라이브러리의 직렬화 방식을 비교하면 GSON은 reflection을 사용하여 JSON 직렬화를 수행하고, 런타임 시점에서 데이터의 타입들을 확인합니다. 반면, Moshi는 코드 생성(codegen)을 사용하여 정적 타입 안전성을 제공하고, 불변 객체를 지원하며, 컴파일 시점에 직렬화/역직렬화 로직을 생성하여 런타임 오버헤드를 줄여줍니다.

정리

  • Moshi는 코드 생성과 정적 타입 지원을 통해 높은 성능과 안정성을 제공
  • GSON은 Reflection을 사용하여 더 간단한 사용성을 제공

3. 실제 성능 비교 (GSON vs Moshi)

그럼 GSON과 Moshi의 성능을 비교하여 확인해 보겠습니다. 각 라이브러리 별 성능 비교를 위해 JSON Placeholder Posts API를 JVM에서 JSON 데이터를 1번, 1000번, 5000번 파싱하는 테스트를 진행하였고 해당 테스트의 결과는 다음과 같습니다.
(본 내용은 Gson vs Jackson vs Moshi: The Best Android JSON Parser?, github - moshi를 참고하여 작성하였습니다.)

  • 1번 (Parsing time in milliseconds) 진행했을 때
Short Medium Long
Gson 6 14 35
Moshi 8 11 24
  • 1,000번 (Parsing time in milliseconds) 진행했을 때
Short Medium Long
Gson 0.53 0.81 4.20
Moshi 0.38 0.61 3.57
  • 5,000번 (Parsing time in milliseconds) 진행했을 때
Short Medium Long
Gson 2.28 2.85 17.83
Moshi 1.59 2.68 19.69

결과를 보면 1번, 1,000번을 봤을 때 moshi가 좀 더 나은 성능을 보여주었습니다. 반면 5,000번일 때는 Long Json에서는 성능이 조금 떨어지는 것을 볼 수 있습니다. 참고한 글에 의하면 정확히 무엇 때문에 5,000번 파싱했을 때 성능이 떨어졌다는 가에 대한 설명은 없었으나, 다음 테스트에서는 명확한 성능 차이를 보여주었습니다. 이번 테스트에서는 구조가 좀 더 복잡한 nested Json 데이터 구조를 5,000번 파싱했을 때 결과를 확인할 수 있습니다.

  • 복잡한 구조의 JSON 데이터를 5,000번 Parsing했을 때  (Parsing time in milliseconds)
Short Medium Long
Gson 2.33 2.59 7.22
Moshi 1.75 2.14 4.50

Moshi가 좀 더 확연하게 성능의 차이를 보여주는 것을 보실 수 있습니다. 시원하게 해소가 되진 않았지만 1번, 1000번, 5000번 또는 복잡한 구조의 JSON을 5000번 파싱했을 때 평균적으로 Moshi가 JSON보다 성능적으로 조금 더 뛰어난 것을 확인할 수 있습니다.

그럼 Moshi가 GSON 보다 왜 빠를까?

이것의 해답은 Moshi가 codegen을 사용하여 컴파일 타임에 코드를 조작하는 점도 있겠지만, Moshi의 개발사가 Square 사란 점도 있습니다.

Squere 사는 기본적으로 네트워크 통신 라이브러리 Retrofit, OkHttp 등을 개발했고, 이 라이브러리들 모두 Okio를 기반으로 개발되어 강력한 I/O 처리를 제공해주어 성능에 큰 이점을 주고 있습니다.

실제로 Mosih는 네트워크 통신 라이브러리인 Retrofit과 같이 사용했을 때 메모리 버퍼를 재사용하여 메모리 할당 및 해제에 드는 오버헤드를 줄여주고 있습니다. 이 장점을 통해서 리소스가 제한적인 모바일 환경에서 큰 효과를 볼 수 있습니다.

한번 써봅시다!

이제 실제로 Moshi를 사용하여 JSON 직렬화/역직렬화를 해보겠습니다.  (적용 환경으로는 Android에서 Java를 사용하였습니다.)

  • build.gradle ( module )에 라이브러리 추가
...

dependencies {
		...
    implementation 'com.squareup.moshi:moshi:1.12.0'
}

...
  • JSON 데이터를 처리할 VO 클래스 정의
public class User {
    
		private int id;
    private String name;
    private String email;

    public void setId(int id){
			this.id = id;
		}

    public int getId(){
			return this.id;
		}

		....

}
  • Java 객체를 JSON 데이터로 직렬화
import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.Moshi;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        User user = new User(1, "홍길동", "moshi@moshi.com"); // 1
        
        Moshi moshi = new Moshi.Builder().build(); // 2

        JsonAdapter<User> jsonAdapter = moshi.adapter(User.class); // 3
        
        String jsonString = jsonAdapter.toJson(user); // 4

    }
}

순서별로 설명해 드리면

  1. 객체 생성 (id : 1 , name : “홍길동”, email : "moshi@moshi.com")
  2. JSON으로 직렬화 해주기 위해 Moshi 객체를 생성
  3. JsonAdapter를 생성하여 JSON Parser를 생성
  4. 객체를 JSON 형식의 문자열로 직렬화

결과적으로 이렇게 직렬화가 된 데이터를 확인하실 수 있습니다.

"{\"id\": 1, \"name\": \"홍길동\", \"email\": \"moshi@moshi.com\"}";

그러면 이제 직렬화된 JSON 데이터를 역직렬화 해보겠습니다!

  • JSON 데이터를 Java 객체로 역직렬화
import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.Moshi;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // 1
        String jsonString = "{\"id\": 1, \"name\": \"홍길동\", \"email\": \"moshi@moshi.com\"}";

        Moshi moshi = new Moshi.Builder().build(); // 2
        
        JsonAdapter<User> jsonAdapter = moshi.adapter(User.class); // 3
        
        User user = jsonAdapter.fromJson(jsonString); // 4

    }
}
  1. 직렬화된 샘플 데이터 생성
  2. Java 객체로 역직렬화 해주기 위해 Moshi 객체를 생성
  3. JsonAdapter를 생성하여 JSON Parser를 생성
  4. JSON 파싱하여 Java 객체로 변환

이렇게 하면 직렬화된 JSON 데이터가 Java 객체로 역직렬화되는 것을 확인하실 수 있습니다.

정리하며

이번 글을 통해 간단하게나마 Moshi에 대해 알아보았는데요. 저는 특히 안드로이드 앱 개발 환경에서는 GSON을 제외하고 다른 라이브러리는 사용을 해본 적이 없었던 것 같습니다. 이번 기회를 통해 저도 Moshi를 시범적으로나마 도입하여 성능에 이점이 있다고 한다면 적극적으로 Moshi를 활용하는 사례를 만들어 볼 생각입니다.

이 글이 많은 개발자분께 도움이 됐길 바라며, 이만 물러가겠습니다. :)
감사합니다.


<참고 자료>


IMQA와 관련하여 궁금하신 사항은 언제든 아래 연락처로 문의해 주시면 상세히 안내해 드리겠습니다. 이미지를 클릭하시면 1:1 채팅 창으로 이동합니다.

  • 02-6395-7730
  • support@imqa.io
IMQA 가격 안내 배너

Share on

Tags

IMQA 뉴스레터 구독하기

국내외 다양한 기술 소식을 선별하여 매월 전달해드립니다. IMQA 뉴스레터를 통해 기술 이야기를 함께해보세요.

구독하기