Kotlin uncovered gonda cover

Kotlin과 Java 전격 비교

Kotlin을 사용하면 기반 코드를 줄이는 데 큰 도움이 됩니다. 하지만 그 이상으로 어떤 일을 할 수 있을까요? 디컴파일된 Kotlin을 탐구해서 어떤 작업을 하는지 알아보겠습니다.


소개

저는 미시간 주 홀랜드에 위치한 컨설팅 회사인 Collective Idea에서 일하는 Victoria Gonda입니다. 저희는 클라이언트에게 커스텀 소프트웨어 솔루션을 제공하고 있습니다.

처음 프로그래밍을 시작하면서 저는 여러 다른 프로그래밍 언어에 관심이 있었습니다. 특히 Kotlin에 대해 알고 나서 이 언어를 배워가는 것이 정말 즐거웠습니다. 처음에는 큰 기대를 하지 않았지만, 현재에는 Kotlin과 Java를 함께 사용해서 매일 일하고 있습니다.

Kotlin이란?

Kotlin이란 무엇일까요? Kotlin은 JVM, Android, 브라우저를 위한 정적 타입의 프로그래밍 언어입니다. 여기에 대해서 좀 더 자세히 설명하겠습니다.

정적으로 타입이 지정되므로 Java와 같은 동일한 타입 안정성을 갖습니다. 따라서 자동 완성 기능을 훌륭하게 제공할 수 있습니다. 정적으로 타입이 지정되므로 IDE에게 무엇이 가능한지 알려줄 수 있죠.

Java와 비교해보면, 타입의 유추도 가능합니다. 문자열인 것이 명확한 경우 문자열이라고 특정할 필요가 없습니다. 즉, Java에서는 String name = "Victoria";와 같이 꼭 문자열을 지정해줘야 하지만, Kotlin에서는 문자열임이 명확하므로 name = "Victoria"라고만 해도 충분합니다. 세미콜론도 필요 없고, val이라는 키워드가 Java의 final 선언과 같이 작용합니다.

Null 안정성도 타입 시스템에 내장됩니다. 따라서 언제 무엇이 null일 수 있을지 알 수 있으며 컴파일러가 이를 확인하도록 강제합니다. 따라서 지긋지긋한 NullPointerExceptions에서 해방될 수 있습니다. 또한, Kotlin은 기반 코드를 생성해주는 방식과 함수형 언어 기능 부분이 매력적입니다. Kotlin은 Java보다 훨씬 간결하면서도 좋은 방식을 사용합니다. 다른 언어들이 그렇듯 지나치게 간결해져서 가독성을 해치는 일도 없습니다.

또한, Kotlin은 Java와 함께 사용할 수 있고, JVM이 실행할 수 있는 바이트 코드로 컴파일됩니다.

예제

이제 간단한 클래스를 디컴파일해볼까요? 여기서 사용자는 성과 이름을 가지며, 이름은 불변으로 null이 될 수 없습니다.

class User(
   var firstName: String,
   var lastName: String?
)

성의 타입인 문자열 다음에 물음표를 붙여서 nullable로 만듭니다.

디컴파일러로 클래스를 분해하면 다음과 같은 결과가 나옵니다.

public final class User {
 	@NotNull
 	private final String firstName;
 	@Nullable
 	private String lastName;

 	public User(@NotNull String firstName, @Nullable String lastName) {
 		Intrinsics.checkParameterIsNotNull(firstName, "firstName");
 		super();
 		this.firstName = firstName;
 		this.lastName = lastName;
 	}

 	@NotNull
 	public final String getFirstName() {
 		return this.firstName;
 	}

 	@Nullable
 	public final String getLastName() {
 		return this.lastName;
 	}

 	public final void setLastName(@Nullable String var1) {
 		this.lastName = var1;
 	}
}

클래스가 기본적으로 final이므로 확장할 수 없는 것이 보이시나요? 불변을 사용해서 간단하고 스레드 안전성을 보장하는 클래스를 만들 수 있습니다.

필드는 기본적으로 private입니다. Firstname은 불변이므로 final로 표시됩니다. nullable과 nonnull 어노테이션도 보입니다. 생성자의 첫 번째 줄에서 확인 매개변수는 null이 아닙니다. Null 가능성이 타입 시스템에 내장되므로 일반적으로 안전하게 사용할 수 있습니다.

Java에서 Kotlin 코드를 호출하면 Kotlin은 자동으로 다음처럼 확인합니다.

	public static void checkParameterIsNotNull(
 		Object value, String paramName) {
 	  if (value == null) {
 		// prints error with stack trace
 		throwParameterIsNullException(paramName);
 	  }
 	}

이런 개발 뉴스를 더 만나보세요

null이 되면 안 되는 것이 null이 된 경우 명시적으로 무엇이 어디서 어떤 것이 null이 되었는지 명확하게 알려줍니다.

Caused by:
	java.lang.IllegalStateException: firstName must not be null
	at com.project.User.<init>(User.kt:8)

이 클래스를 데이터 클래스로 만들 수 있습니다. 간단하게 클래스 선언 시작 부분에 data 키워드를 추가하면 됩니다.

	data class User(
		val firstName: String,
		val lastName: String?
	)

Java 코드는 다음과 같은 모습입니다.

public final class User {
 	@NotNull
 	private final String firstName;
 	@Nullable
 	private String lastName;
 	public User(@NotNull String firstName, @Nullable String lastName) {
 		Intrinsics.checkParameterIsNotNull(firstName, "firstName");
 		super();
 		this.firstName = firstName;
 		this.lastName = lastName;
 	}
 	@NotNull
 	public final String getFirstName() {
 		return this.firstName;
 	}
 	@Nullable
 	public final String getLastName() {
 		return this.lastName;
 	}
 	public final void setLastName(@Nullable String var1) {
 		this.lastName = var1;
 	}
 	@NotNull
 	public final String component1() {
 		return this.firstName;
 	}
 	@Nullable
 	public final String component2() {
 		return this.lastName;
 	}
 	@NotNull
 	public final User copy(@NotNull String firstName, @Nullable String lastName) {
 		Intrinsics.checkParameterIsNotNull(firstName, "firstName");
 		return new User(firstName, lastName);
 	}
 	// $FF: synthetic method
 	// $FF: bridge method
 	@NotNull
 	public static User copy$default(User var0, String var1, String var2, int var3, Object var4) {
 		if((var3 & 1) != 0) {
 			var1 = var0.firstName;
 		}
 		if((var3 & 2) != 0) {
 			var2 = var0.lastName;
 		}
 		return var0.copy(var1, var2);
 	}
 	public String toString() {
 		return "User(firstName=" + this.firstName + ", lastName=" + this.lastName + ")";
 	}
 	public int hashCode() {
 		return (this.firstName != null?this.firstName.hashCode():0) * 31 + (this.lastName != null?this.lastName.hashCode():0);
 	}
 	public boolean equals(Object var1) {
 		if(this != var1) {
 			if(var1 instanceof User) {
 				User var2 = (User)var1;
 				if(Intrinsics.areEqual(this.firstName, var2.firstName) && Intrinsics.areEqual(this.lastName, var2.lastName)) {
 					return true;
 				}
 			}
 			return false;
 		} else {
 			return true;
 		}
 	}
}

다른 부가 요소를 가지고도 비슷한 것을 해왔습니다. 예를 들어 구조를 분해하고 클래스 선언에 사용하는 컴포넌트가 있습니다.

불변 타입으로 작업할 때 매우 유용한 복사 메서드도 있습니다. 복사를 위한 합성 브릿지 메서드도 있는데 JVM이 클래스 등을 다루는 방법입니다.

또한, 모든 변수와 그틀의 타입, hashCode와 equal 메서드를 명확하게 출력해주는 toString도 있습니다.

data 키워드는 기본값을 선언할 때 쓸 수도 있습니다.

data class User(
 val firstName: String = "Victoria",
 var lastName: String?
)

만약 여기서 이름이 설정되지 않는다면 기본값으로 Victoria라는 문자열을 사용합니다. 빌더를 사용하는 대신 이름이 있는 매개 변수와 기본값을 연결할 수 있습니다. 다음 예제에서는 객체를 생성하면서 필요한 것만 제공하고 있습니다.

val user = User(
 lastName = "Gonda"
)

이름이 제공되지 않은 것이 보이시나요? 기본값이 있거나 null일 수 있는 변수라면 이처럼 생략할 수 있습니다.

Null 안정성

Kotlin의 여러 장점 중 자주 거론되는 것은 null 안정성입니다. 변수에 물음표를 붙이면 nullable이 됩니다.

// Wont compile
var maybeString: String? = "Hello"
maybeString.length

위 코드를 보면 문자열 다음에 물음표가 붙었습니다. 물음표는 바로 nullable을 뜻하죠. 물음표가 없다면 null이 될 수 없으므로 안전하고, null에 대해 걱정할 필요가 없습니다. Swift의 옵셔널과도 비슷한 개념입니다.

nullable 값을 부르기 전에 null을 확인하지 않으면 컴파일되지 않습니다.

아래 코드는 maybeString 다음과 .length 전에 쓰인 물음표인 안전 호출 오퍼레이터(Safe Call Operator)를 사용했습니다.

val maybeString: String? = “Hello"
maybeString?.length

이렇게 쓸 경우 객체가 null이 아닌 경우 .length를 호출하고, 아니면 null을 반환합니다.

Java에서는 다음과 같은 모습입니다.

String maybeString = "Hello";
maybeString.length();

하지만 null을 확인하지 않았는데요, 해당 변수가 null인 경우 어떻게 될까요? null 포인터 예외가 발생할까요?

maybeString를 null로 설정하면 어떻게 되는지 보겠습니다.

String maybeString = this.getString();
if(maybeString != null) {
 maybeString.length();
}

null 확인을 추가했습니다. val을 사용해서 불변 값에 문자열을 할당했으므로 컴파일러가 null이 될 수 없는 값임을 알아차리고 필요 없는 코드를 대신 제거해줍니다.

두 개의 느낌표를 사용하면 컴파일러 오류를 피할 수 있습니다.

val maybeString: String? = getString()
maybeString!!.length

length를 호출하기 전에 maybeString 다음에 두 개의 느낌표를 사용하면 확인 과정 없이 해당 변수에서 해당 메서드를 호출하게 됩니다. Java 코드는 다음과 같습니다.

String maybeString = this.getString();
if(maybeString == null) {
 Intrinsics.throwNpe();
}
maybeString.length();

이 경우 NullPointerException이 발생하겠죠.

Kotlin에는 Null 안전성을 위한 몇 가지 옵션이 더 있습니다. Null 안정성 오퍼레이터(Null Safety Operator)를 let과 함께 사용해서 null 안전 범위를 지정할 수도 있습니다.

val maybeString: String? = getString()
return maybeString?.let { string ->
 string.length
}

위 코드에서는 블럭 내에서 변수 문자열의 이름을 지정했습니다. maybeString이 null이 아니라면, 블럭 내의 코드들이 실행됩니다. null이라면, 단순히 null을 반환합니다. 블럭 내부에 문자열을 지정하지 않으면, 기본적으로 변수 이름으로 “it”이 사용됩니다.

이 코드 자체만으로는 크게 유용하지 않아 보일 수 있지만, 여러 줄의 코드가 있는 경우 매우 편리합니다. 맵과 비슷하지만 하나의 값만 있는 구조라고 생각하면 됩니다. Java 코드는 다음과 같습니다.

String maybeString = (String) null;
if(maybeString != null) {
	String string = (String)maybeString;
	string.length();
}

null을 확인하고 string이라는 변수에 값을 할당한 다음 람다에 포함한 오퍼레이션을 수행합니다. 기본값으로 허용했다면 변수 이름은 string이 아닌 기본값이 될 겁니다.

마지막으로 보여드릴 null 확인 방법은 Elvis 오퍼레이터입니다. ?: 마치 사람 얼굴처럼 보이네요.

val maybeString: String? = getString()
return maybeString?.length ?: 0

이 오퍼레이션을 사용해서 변수가 null인 경우 옵셔널 값을 줄 수 있습니다. 이 코드는 maybeString이 null이 아닌 경우 string의 길이를 반환하고, null인 경우 0을 반환합니다.

String maybeString = this.getString();
return maybeString != null ?
 maybeString.length() : 0;

델리게이션

제가 가장 좋아하는 Kotlin의 장점인 Null 안전성과 데이터 클래스에 더해 한 가지를 더 소개하겠습니다.

Delegation은 상속을 대신할 수 있습니다. 상속 대신 조합을 사용하면 상속을 사용할 때 벌어지는 긴밀한 결합을 피할 수 있으며, 델리게이션은 일종의 조합입니다. 델리게이션을 사용하면 코드를 읽고 따라가면서 발생하는 인지를 위한 간접 비용과 시간 낭비, 추적해야 할 가짓수를 줄일 수 있습니다.

Kotlin의 CopyPrinter 예제를 보실까요?

class CopyPrinter(copier: Copy, printer: Print)
 : Copy by copier, Print by printer
interface Copy {
 fun copy(page: Page): Page
}
interface Print {
 fun print(page: Page)
}

이 예제에서 주목할 점은 클래스 선언부입니다. copy로 copy를, print로 print를 그 자리에서 바로 선언하고 있습니다. 이렇게 간단하게 copy와 print를 위한 인터페이스를 만들었습니다. 정말 간결하고 단순하지 않나요? Java 코드는 다음과 같습니다.

public final class CopyPrinter implements Copy, Print {
 // $FF: synthetic field
 private final Copy $$delegate_0;
 // $FF: synthetic field
 private final Print $$delegate_1;
 public CopyPrinter(@NotNull Copy copier, @NotNull Print printer) {
 	Intrinsics.checkParameterIsNotNull(copier, "copier");
 	Intrinsics.checkParameterIsNotNull(printer, "printer");
 	super();
 	this.$$delegate_0 = copier;
 	this.$$delegate_1 = printer;
 }
 @NotNull
 public Page copy(@NotNull Page page) {
 	Intrinsics.checkParameterIsNotNull(page, "page");
 	return this.$$delegate_0.copy(page);
 }
 public void print(@NotNull Page page) {
 	Intrinsics.checkParameterIsNotNull(page, "page");
 	this.$$delegate_1.print(page);
 }
}
public interface Copy {
 @NotNull
 Page copy(@NotNull Page var1);
}
public interface Print {
 void print(@NotNull Page var1);
}

코드가 정말 많이 늘어나긴 했지만 무슨 일을 하는 지 쉽게 알 수 있습니다. 클래스가 있고 copy와 print를 구현합니다. 다음으로 copy와 print 객체를 저장할 필드가 있습니다.

다음으로 생성자에서 copy와 print를 받아서 해당 필드들에 할당합니다. 그다음 copy와 print 메서드를 생성자에서 받은 copy와 print 객체에 넘깁니다.

마지막으로 자주 보던 형태대로 copy와 print를 위한 인터페이스가 있습니다.

정적 도구 클래스(Static Utility Class)

보통 메서드들은 일관성이 있고 클래스에 특정된 것이지만 애플리케이션에 특정한 메서드를 적용할 수도 있습니다.

Kotlin은 익스텐션을 사용해서 이들을 부드럽게 처리합니다. 익스텐션을 사용하면 클래스 인스턴스로부터 이런 메서드에 접근할 수 있습니다. String과 같은 final 클래스라도 수정할 수 있습니다.

TextUtils.isEmpty("hello");

과거에 도움이 된 것은 시간에 대한 계산 작업을 할 때였습니다. date time에 익스텐션 함수를 추가해서 시간에 경계를 쉽고 빠르게 설정할 수 있었습니다.

string 클래스의 익스텐션을 예로 들어 보겠습니다. String은 final이므로 상속할 수 없다는 사실을 이미 아실 텐데요. string을 두 번 반복하는 함수를 추가해보겠습니다. Kotlin 코드는 다음과 같습니다.

// StringExt.kt
fun String.double(): String() {
	return this + this
}

클래스를 수정하는 코드입니다. String.double처럼 클래스 이름 뒤에 점을 찍고 함수 이름을 추가해서 선언합니다.

이를 String에 있는 다른 메서드와 똑같이 호출할 수 있습니다.

"hello".double()

해석형 언어(interpreted language)에서는 자주 사용되는 형태이지만 Java와는 사뭇 다릅니다. Java 코드로 만들면 다음과 같습니다.

public final class StringExtKt {
  @NotNull
  public static final String double(
    @NotNull String $receiver) {
      Intrinsics
        .checkParameterIsNotNull($receiver, "$receiver");
      return $receiver + $reveiver;
    }

이 코드는 우리가 util 클래스를 사용해서 자동으로 Final 클래스로 만드는 작업을 합니다. 호출을 하는 경우 클래스의 static 메서드를 호출합니다. 실제로 이것이 Kotlin에 익스텐션을 만들고 다른 Java 코드에서 호출하는 방법입니다.

StringExtKt.double("hello");

함수형 언어 속성

예제부터 살펴보겠습니다. N개의 제곱 값을 넣는 리스트를 생성해 보겠습니다. Java라면 카운터와 루프를 사용해서 정해진 N에 도달할 때까지 목록에 제곱을 추가하겠죠. Kotlin이라면 어떻게 할까요?

fun firstNSquares(n: Int): Array<Int>
  = Array(n, { i -> i * i })

여기서 람다가 리스트 생성자로 전달됩니다. 입력 변수 i가 있고 수행하려는 연산은 ii의 곱입니다.

0부터 n까지 루프가 돌면서 해당 연산을 수행한 후 값을 리스트에 넣습니다.

Java라면 다음과 같은 코드를 사용할 겁니다.

@NotNull
public static final Integer[] firstNSquares(int n) {
 	Integer[] result$iv = new Integer[n];
 	int i$iv = 0;
 	int var3 = n - 1;
 	if(i$iv <= var3) {
 		while(true) {
 			Integer var9 = Integer.valueOf(i$iv * i$iv);
 			result$iv[i$iv] = var9;
 			if(i$iv == var3) {
 				break;
 			}
 			++i$iv;
 		}
 	}
 	return (Integer[])((Object[])result$iv);
}

변수 이름은 좀 다르지만 같은 코드로, 캐스팅이 제거되므로 좀 더 이해하기 쉽습니다.

@NotNull
public static Integer[] firstNSquares(int n) {
 Integer[] resultArray = new Integer[n];
 int i = 0;
 int max = n - 1;
 if(i <= max) {
   while(true) {
 	Integer square = i * i;
 	resultArray[i] = square;
 	if(i == max) {
 	  break;
 	}
 	++i;
   }
 }
 return resultArray;
}

리스트를 생성하고 숫자들을 순회하면서 연산을 수행해서 값을 리스트에 넣다가 N에 도달하면 멈춥니다. Java에서 작성하는 것과 같은 개념입니다. 다른 루프를 사용하거나 다른 조건을 사용할 수도 있겠지만, 아이디어는 동일하죠.

간단하게 해당 람다에 함수를 추가할 수도 있습니다.

fun firstNSquares(n: Int): Array<Int>
  = Array(n, { i -> square(i + 1) })

0부터 시작하지 않고 1부터 시작하는 N개의 제곱 값 리스트를 만들기 위해 i + 1을 제곱했습니다. 람다 내의 메서드만이 바뀌었습니다. Java 예제에서는 메서드를 호출하는 한 줄만 바뀝니다.

Integer square = square(i+1);

함수 예제는 모두 inline으로 만들어졌습니다. let 선언을 보시면 다음처럼 inline 키워드가 있습니다.

public inline fun <T, R> T.let(block: (T) -> R): R
	= block(this)

이렇게 사용하면 컴파일러가 이를 사용하는 함수 본문에 삽입할 코드를 생성합니다.

inline fun beforeAndAfter(
 startString: String,
 function: (string: String) -> String
) {
 print("Before: $startString")
 val after = function(startString)
 print("After: $after")
}

Java 코드는 다음과 같습니다.

public final void beforeAndAfter(
 	@NotNull String startString,
 	@NotNull Function1 function) {
 Intrinsics
 	.checkParameterIsNotNull(startString, "startString");
 Intrinsics
 	.checkParameterIsNotNull(function, "function");
 String after = "Before: " + startString;
 System.out.print(after);
 after = (String)function.invoke(startString);
 String var4 = "After: " + after;
 System.out.print(var4);
}

함수가 매개 변수로 문자열과 Function1을 받고 있습니다. Function1의 인터페이스는 다음과 같습니다.

 public interface Function1<in P1, out R> : Function<R> {
 	public operator fun invoke(p1: P1): R
 }

1은 매개 변수의 수를 나타냅니다. 두 개의 매개 변수가 있다면 Function2가 될 테죠. invoke라는 메서드가 하나 있습니다. 본문의 첫 줄은 앞서 살펴본 것과 유사한 null 확인입니다. 다음으로 문자열을 출력하기 위해 연결합니다.

public final void beforeAndAfter(
 	@NotNull String startString,
 	@NotNull Function1 function) {
 Intrinsics
 	.checkParameterIsNotNull(startString, "startString");
 Intrinsics
 	.checkParameterIsNotNull(function, "function");
 String after = "Before: " + startString;
 System.out.print(after);
 after = (String)function.invoke(startString);
 String var4 = "After: " + after;
 System.out.print(var4);

전달받은 함수에 대해 invoke를 호출합니다. 이 방식으로 함수에 전달된 람다를 수행할 수 있습니다. 다음으로 결과 문자열을 연결해서 출력합니다.

fun example() {
 beforeAndAfter("hello", { string -> string + " world" })
}

정말 쉽게 문자열을 전달할 수 있습니다. 전달된 람다는 중괄호 안에 포함됩니다. 그다음 화살표가 수행하고자 하는 작업을 나타냅니다. Java 코드는 다음과 같습니다.

public final void example() {
 String startString$iv = "hello";
 String after$iv = "Before: " + startString$iv;
 System.out.print(after$iv);
 String string = (String)startString$iv;
 after$iv = (String)(string + " world");
 string = "After: " + after$iv;
 System.out.print(string);
}

여러 가지 방법으로 before와 after 함수를 호출할 수 있습니다.

beforeAndAfter("hello", { string -> string + " world" })
beforeAndAfter("hello", { it + " world" })
beforeAndAfter("hello") { it + " world" }

첫 번째 줄은 앞서 살펴본 것과 같습니다. 변수의 이름을 지정하지 않으면 두 번째 줄에서 보이는 “it”이라는 기본값이 사용됩니다. 람다가 함수의 마지막 매개 변수인 경우 괄호 밖에 넣을 수도 있습니다.

inline 함수가 아닌 경우라면 어떻게 보일까요?

fun beforeAndAfter(
 startString: String,
 function: (string: String) -> String
) {
 print("Before: $startString")
 val after = function(startString)
 print("After: $after")
}

이를 디컴파일하면 다음과 같습니다.

public final void example() {
 this.beforeAndAfter("hello", (Function1)null.INSTANCE);
}

null.INSTANCE는 무엇일까요? 바이트 코드를 검사하면 다음과 같습니다.

의사 코드(pseudocode)라면 다음과 같습니다.

// Pseudocode for bytecode
final class BytecodeClass
 extends Lambda
 implements Function1 {
 public void invoke(String string) {
 	StringBuilder sb = new StringBuilder("hello");
 	sb.append(" world");
 	returnValue = sb.toString();
 }
 static Function1 INSTANCE = new BytecodeClass();
}

람다를 확장하고 Function1을 구현하는 클래스를 생성합니다. 그다음 invoke 메서드를 구현하고 StringBuilder를 생성한 다음 주어진 문자열과 World라는 문자열을 연결합니다. 그런 다음 값을 반환할 때마다 저장합니다.

결론

데이터 클래스, null 안정성, 델리게이션, 클래스 익스텐션과 람다를 살펴 봤습니다. 다루지는 않았지만 주목할 만한 기능들을 소개하면 다음과 같습니다.

  • Companion 객체와 스마트 캐스팅
  • 컬렉션 함수
  • 컨트롤 제어 구조
  • 오퍼레이터 오버 로딩

안드로이드 스튜디오로 손쉽게 Java를 Kotlin으로 변환할 수 있습니다. 변환할 Java 파일로 이동한 후 menu>Code>Convert Java File to Kotlin File 을 차례로 선택해서 수행할 수 있습니다.

Kotlin으로 오랜 시간을 작업하다 보니 이제 Java가 길고 긴 우회로처럼 느껴집니다. Java로도 원하는 바를 달성할 수는 있지만, 더 오래 걸리고 성가시기 때문이죠.

Kotlin을 사용하기 시작하면서 제가 좀 더 나은 프로그래머로 성장했다고 느낍니다. 혹시 저와 소통하고 싶거나 궁금한 점이 있다면 제 트위터, @TTGonda로 문의해 주세요.

컨텐츠에 대하여

2017년 4월에 진행한 Droidcon Boston 행사의 강연입니다. 영상 녹화와 제작, 정리 글은 Realm에서 제공하며, 주최 측의 허가 하에 이곳에서 공유합니다.

Victoria Gonda

Victoria는 모바일 및 웹 애플리케이션을 개발하는 Collective Idea의 개발자입니다. 기술을 사용해서 사람들의 삶을 개선하고 싶어 합니다. 대학에서는 컴퓨터 공학과 댄스를 모두 전공했고, 여가 시간에는 댄스 교실에서 춤 실력을 연마하고 있습니다.

4 design patterns for a RESTless mobile integration »

close