2024. 7. 6. 12:09ㆍHigh Level Programming Language/Learning the Java Language
가장 간단한 형태의 어
어노테이션은 메타데이터의 한 형태로, 프로그램 자체의 일부가 아닌 프로그램에 대한 데이터를 제공합니다. 어노테이션은 어노테이션이 달린 코드의 작동에 직접적인 영향을 미치지 않습니다.
어노테이션은 여러 용도로 사용됩니다. 그 중 몇 가지는 다음과 같습니다:
- 컴파일러를 위한 정보 제공 — 어노테이션은 컴파일러가 오류를 감지하거나 경고를 무시하도록 사용할 수 있습니다.
- 컴파일 시점 및 배포 시점 처리 — 소프트웨어 도구는 어노테이션 정보를 처리하여 코드, XML 파일 등을 생성할 수 있습니다.
- 런타임 처리 — 일부 어노테이션은 런타임에 검토할 수 있습니다.
이 강의는 어노테이션이 사용될 수 있는 위치, 어노테이션 적용 방법, Java Platform, Standard Edition (Java SE API)에서 제공하는 미리 정의된 어노테이션 유형, 더 강력한 타입 검사를 위해 플러그 가능한 타입 시스템과 함께 타입 어노테이션을 사용하는 방법, 반복 어노테이션 구현 방법에 대해 설명합니다.
Annotations Basics
The Format of an Annotation
가장 간단한 형태의 어노테이션은 다음과 같이 생겼습니다:
@Entity
@ 문자(@)는 컴파일러에게 그 뒤에 오는 것이 어노테이션임을 나타냅니다. 다음 예시에서 어노테이션의 이름은 Override입니다:
@Override
void mySuperMethod() { ... }
어노테이션에는 엘리먼트가 포함될 수 있으며, 이 엘리먼트들은 이름이 있을 수도 있고 없을 수도 있으며, 해당 엘리먼트들에는 값이 있습니다:
@Author(
name = "Benjamin Franklin",
date = "3/27/2003"
)
class MyClass { ... }
또는
@SuppressWarnings(value = "unchecked")
void myMethod() { ... }
엘리먼트가 하나만 있고 그 이름이 value인 경우, 이름을 생략할 수 있습니다. 예를 들면 다음과 같습니다:
@SuppressWarnings("unchecked")
void myMethod() { ... }
어노테이션에 엘리먼트가 없는 경우, 앞의 @Override 예시에서 보이는 것처럼 괄호를 생략할 수 있습니다.
같은 선언에서 여러 어노테이션을 사용하는 것도 가능합니다:
@Author(name = "Jane Doe")
@EBook
class MyClass { ... }
어노테이션의 타입이 동일한 경우, 이를 반복 어노테이션(repeating annotation)이라고 합니다:
@Author(name = "Jane Doe")
@Author(name = "John Smith")
class MyClass { ... }
반복 어노테이션은 Java SE 8 릴리스부터 지원됩니다. 자세한 내용은 Repeation Annotation을 참조하십시오.
어노테이션 타입은 Java SE API의 java.lang 또는 java.lang.annotation 패키지에 정의된 타입 중 하나일 수 있습니다. 이전 예시에서 Override와 SuppressWarnings는 predefined Java annotation입니다. 또한 사용자 정의 어노테이션 타입을 정의하는 것도 가능합니다. 이전 예시의 Author와 Ebook 어노테이션은 사용자 정의 어노테이션 타입입니다.
Where Annotations Can Be Used
어노테이션은 선언, 즉 클래스, 필드, 메서드 및 기타 프로그램 요소의 선언에 적용될 수 있습니다. 선언에 사용될 때, 각 어노테이션은 관례적으로 별도의 줄에 나타납니다.
Java SE 8 릴리스부터 어노테이션은 타입 사용에도 적용될 수 있습니다. 다음은 몇 가지 예입니다:
- Class instance creation expression:
new @Interned MyObject();
- Type cast:
myString = (@NonNull String) str;
- Implements clause:
class UnmodifiableList<T> implements
@Readonly List<@Readonly T> { ... }
- Thrown exception declaration:
void monitorTemperature() throws
@Critical TemperatureException { ... }
이 형태의 어노테이션을 타입 어노테이션(type annotation)이라고 합니다. 자세한 내용은 타입 어노테이션과 플러그형 타입 시스템(Type Annotations and Pluggable Type Systems)을 참조하십시오.
이 형태의 어노테이션을 타입 어노테이션(type annotation)이라고 합니다. 자세한 내용은 타입 어노테이션과 플러그형 타입 시스템(Type Annotations and Pluggable Type Systems)을 참조하십시오.
Declaring an Annotation Type
많은 어노테이션이 코드에서 주석[comment]들을 대체합니다.
예를 들어, 소프트웨어 그룹이 전통적으로 각 클래스의 본문 시작 부분에 중요한 정보를 제공하는 주석을 작성한다고 가정해 보겠습니다:
public class Generation3List extends Generation2List {
// Author: John Doe
// Date: 3/17/2002
// Current revision: 6
// Last modified: 4/12/2004
// By: Jane Doe
// Reviewers: Alice, Bill, Cindy
// class code goes here
}
이 동일한 메타데이터를 어노테이션으로 추가하려면 먼저 어노테이션 타입을 정의해야 합니다. 이를 위한 구문은 다음과 같습니다:
@interface ClassPreamble {
String author();
String date();
int currentRevision() default 1;
String lastModified() default "N/A";
String lastModifiedBy() default "N/A";
// Note use of array
String[] reviewers();
}
어노테이션 타입 정의는 인터페이스 정의와 비슷하게 생겼으며, 키워드 인터페이스 앞에 at 기호(@)가 붙습니다. 어노테이션 타입은 인터페이스의 한 형태로, 이는 이후 강의에서 다룰 예정입니다. 지금은 인터페이스에 대해 이해할 필요는 없습니다.
이전의 어노테이션 정의 본문에는 어노테이션 타입 엘리먼트 선언이 포함되어 있으며, 이는 메서드와 매우 유사하게 생겼습니다. 이들은 선택적인 기본값을 정의할 수 있다는 점에 유의하십시오.
어노테이션 타입이 정의된 후에는 다음과 같이 해당 타입의 어노테이션을 사용하여 값을 채울 수 있습니다:
@ClassPreamble (
author = "John Doe",
date = "3/17/2002",
currentRevision = 6,
lastModified = "4/12/2004",
lastModifiedBy = "Jane Doe",
// Note array notation
reviewers = {"Alice", "Bob", "Cindy"}
)
public class Generation3List extends Generation2List {
// class code goes here
}
참고: @ClassPreamble에 있는 정보를 Javadoc에서 생성한 문서에 표시하려면 @ClassPreamble 정의를 @Documented 어노테이션으로 어노테이트해야 합니다:
// import this to use @Documented
import java.lang.annotation.*;
@Documented
@interface ClassPreamble {
// Annotation element definitions
}
Predefined Annotation Types
Java SE API에는 미리 정의된 어노테이션 타입 세트가 있습니다. 일부 어노테이션 타입은 Java 컴파일러에서 사용되며, 일부는 다른 어노테이션에 적용됩니다.
Annotation Types Used by the Java Language
java.lang에 정의된 미리 정의된 어노테이션 타입에는 @Deprecated, @Override, 그리고 @SuppressWarnings가 있습니다.
@Deprecated @Deprecated 어노테이션은 마크된 엘리먼트가 더 이상 사용되지 않음을 나타냅니다. 프로그램이 @Deprecated 어노테이션이 달린 메서드, 클래스 또는 필드를 사용할 때마다 컴파일러는 경고를 생성합니다. 이러한 엘리먼트가 더 이상 사용되지 않게 된 경우, 다음 예제와 같이 Javadoc @deprecated 태그를 사용하여 문서화해야 합니다. Javadoc 주석과 어노테이션 모두에서 @ 기호를 사용하는 것은 우연이 아닙니다. 이는 개념적으로 관련이 있습니다. 또한, Javadoc 태그는 소문자 d로 시작하고 어노테이션은 대문자 D로 시작한다는 점에 유의하십시오.
// Javadoc comment follows
/**
* @deprecated
* explanation of why it was deprecated
*/
@Deprecated
static void deprecatedMethod() { }
}
@Override @Override 어노테이션은 해당 엘리먼트(메서드)가 슈퍼클래스에 선언된 엘리먼트(메서드)를 오버라이드할 의도가 있음을 컴파일러에게 알립니다. 오버라이딩 메서드는 인터페이스와 상속에서 다룰 것입니다.
// mark method as a superclass method
// that has been overridden
@Override
int overriddenMethod() { }
메서드를 오버라이드할 때 이 어노테이션을 반드시 사용해야 하는 것은 아니지만, 오류를 방지하는 데 도움이 됩니다. @Override로 표시된 메서드가 슈퍼클래스 중 하나의 메서드를 올바르게 오버라이드하지 못하면, 컴파일러는 오류를 생성합니다.
@SuppressWarnings @SuppressWarnings 어노테이션은 컴파일러에게 특정 경고를 억제하도록 지시합니다. 다음 예시에서, 사용되지 않는(deprecated) 메서드가 사용되었고, 보통 컴파일러는 경고를 생성합니다. 그러나 이 경우, 어노테이션이 경고를 억제하도록 합니다.
// use a deprecated method and tell
// compiler not to generate a warning
@SuppressWarnings("deprecation")
void useDeprecatedMethod() {
// deprecation warning
// - suppressed
objectOne.deprecatedMethod();
}
모든 컴파일러 경고는 한 가지 카테고리에 속합니다. Java 언어 명세서(Java Language Specification)에는 두 가지 카테고리가 나열되어 있습니다: deprecation과 unchecked입니다. unchecked 경고는 제네릭이 도입되기 전에 작성된 레거시 코드와 상호 작용할 때 발생할 수 있습니다. 여러 카테고리의 경고를 억제하려면, 다음 구문을 사용합니다:
@SuppressWarnings({"unchecked", "deprecation"})
@SafeVarargs @SafeVarargs 어노테이션은 메서드나 생성자에 적용될 때, 해당 코드가 varargs 매개변수에 대해 잠재적으로 안전하지 않은 작업을 수행하지 않음을 나타냅니다. 이 어노테이션 타입이 사용되면, varargs 사용과 관련된 unchecked 경고가 억제됩니다.
@FunctionalInterface @FunctionalInterface 어노테이션은 Java SE 8에서 도입되었으며, 타입 선언이 Java 언어 명세서에 정의된 함수형 인터페이스가 될 의도임을 나타냅니다.
Annotations That Apply to Other Annotations
다른 어노테이션에 적용되는 어노테이션을 메타 어노테이션(meta-annotation)이라고 합니다. java.lang.annotation 패키지에는 여러 메타 어노테이션 타입이 정의되어 있습니다.
@Retention @Retention 어노테이션은 표시된 어노테이션이 어떻게 저장되는지를 지정합니다:
- RetentionPolicy.SOURCE – 표시된 어노테이션은 소스 레벨에서만 유지되며 컴파일러에 의해 무시됩니다
- RetentionPolicy.CLASS – 표시된 어노테이션은 컴파일 시 컴파일러에 의해 유지되지만, Java 가상 머신(JVM)에 의해 무시됩니다.
- RetentionPolicy.RUNTIME – 표시된 어노테이션은 JVM에 의해 유지되어 런타임 환경에서 사용할 수 있습니다.
@Documented @Documented 어노테이션은 지정된 어노테이션이 사용될 때마다 해당 엘리먼트들이 Javadoc 도구를 사용하여 문서화되어야 함을 나타냅니다(기본적으로 어노테이션은 Javadoc에 포함되지 않습니다). 자세한 내용은 Javadoc tools page를 참조하십시오
@Target @Target 어노테이션은 다른 어노테이션을 표시하여 해당 어노테이션이 적용될 수 있는 Java 엘리먼트의 종류를 제한합니다. Target 어노테이션은 다음 엘리먼트 타입 중 하나를 값으로 지정합니다:
- ElementType.ANNOTATION_TYPE은 어노테이션 타입에 적용될 수 있습니다.
- ElementType.CONSTRUCTOR은 생성자에게 적용될 수 있습니다.
- ElementType.FIELD은 field or property에 적용될 수 있습니다.
- ElementType.LOCAL_VARIABLE 은 local variable에 적용될 수 있습니다.
- ElementType.METHOD 은 method-level annotation에 적용될 수 있습니다.
- ElementType.PACKAGE 은 package declaration에 적용될 수 있습니다.
- ElementType.PARAMETER 은 method의 parameters에 적용될 수 있습니다.
- ElementType.TYPE 은 class의 엘리먼트에 적용될 수 있습니다.
@Inherited @Inherited 어노테이션은 어노테이션 타입이 슈퍼클래스로부터 상속될 수 있음을 나타냅니다. (기본적으로는 그렇지 않습니다.) 사용자가 어노테이션 타입을 조회할 때 해당 클래스에 해당 타입의 어노테이션이 없으면, 클래스의 슈퍼클래스에서 어노테이션 타입을 조회합니다. 이 어노테이션은 클래스 선언에만 적용됩니다.
@Repeatable @Repeatable 어노테이션은 Java SE 8에서 도입되었으며, 표시된 어노테이션을 동일한 선언 또는 타입 사용에 여러 번 적용할 수 있음을 나타냅니다. 자세한 내용은 반복 어노테이션을 참조하십시오.
Type Annotations and Pluggable Type Systems
Java SE 8 릴리스 이전에는 어노테이션이 선언에만 적용될 수 있었습니다. Java SE 8 릴리스부터 어노테이션은 모든 타입 사용에도 적용될 수 있습니다. 이는 어노테이션이 타입이 사용되는 모든 곳에서 사용될 수 있음을 의미합니다. 타입이 사용되는 몇 가지 예로는 클래스 인스턴스 생성 표현(new), 캐스트, implements 절, throws 절 등이 있습니다. 이러한 형태의 어노테이션을 타입 어노테이션(type annotation)이라고 하며, 여러 예시는 어노테이션 기본(Annotations Basics)에서 제공됩니다.
"Java SE 8 릴리스 이전에는 어노테이션이 선언에만 적용될 수 있었습니다. Java SE 8 릴리스부터 어노테이션은 모든 타입 사용에도 적용될 수 있습니다."는 다음을 의미합니다:
- Java SE 8 이전: 어노테이션은 클래스, 메서드, 필드 등과 같은 선언에만 적용될 수 있었습니다. 예를 들어, 메서드 위에 @Override 어노테이션을 붙이는 경우입니다.
- Java SE 8 이후: 어노테이션을 타입이 사용되는 모든 곳에 적용할 수 있게 되었습니다. 이는 어노테이션을 변수 선언, 객체 생성, 타입 캐스트 등과 같은 곳에도 사용할 수 있다는 것을 의미합니다. 예를 들어, 제네릭 타입을 사용하는 경우나 특정 변수에 대한 타입 정보를 추가하고 싶은 경우에 어노테이션을 사용할 수 있습니다.
이로 인해 어노테이션의 활용 범위가 넓어졌으며, 더 세밀하게 코드에 어노테이션을 달고 검증할 수 있게 되었습니다.
타입 어노테이션은 Java 프로그램의 향상된 분석을 지원하고 더 강력한 타입 검사를 보장하기 위해 만들어졌습니다. Java SE 8 릴리스는 타입 검사 프레임워크를 제공하지 않지만, Java 컴파일러와 함께 사용되는 하나 이상의 플러그형 모듈로 구현된 타입 검사 프레임워크를 작성하거나 다운로드할 수 있습니다.
예를 들어, 프로그램에서 특정 변수가 절대 null로 할당되지 않도록 하고 싶다면, NullPointerException을 방지하고 싶다면, 이를 확인하는 커스텀 플러그인을 작성할 수 있습니다. 그런 다음, 해당 변수가 null로 할당되지 않음을 나타내기 위해 코드를 수정하여 어노테이션을 추가합니다. 변수 선언은 다음과 같이 보일 수 있습니다:
@NonNull String str;
코드를 컴파일할 때, 명령줄에서 NonNull 모듈을 포함하면, 컴파일러가 잠재적인 문제를 감지할 경우 경고를 출력하여 오류를 방지하기 위해 코드를 수정할 수 있게 해줍니다. 모든 경고를 제거하도록 코드를 수정한 후에는 프로그램이 실행될 때 이러한 특정 오류가 발생하지 않습니다.
각 모듈이 다른 종류의 오류를 검사하는 여러 타입 검사 모듈을 사용할 수 있습니다. 이렇게 하면 Java 타입 시스템 위에 원하는 때와 장소에 특정 검사를 추가할 수 있습니다.
타입 어노테이션과 플러그형 타입 검사기의 현명한 사용을 통해, 더 강력하고 오류 발생 가능성이 적은 코드를 작성할 수 있습니다.
확실한 이해를 위해 몇 가지 예를 들어보겠습니다:
변수 선언에 어노테이션 적용
List<@NonNull String> strings = new ArrayList<>();
제네릭 타입 사용 시 어노테이션 적용
public class MyClass<@NonNull T> {
// 클래스 내용
}
객체 생성 시 어노테이션 적용
MyObject myObject = new @Immutable MyObject();
타입 캐스트 시 어노테이션 적용
myString = (@NonNull String) myObject;
메서드의 매개변수와 반환 타입에 어노테이션 적용
public @NonNull String getName(@NonNull String input) {
return input;
}
많은 경우에, 직접 타입 검사 모듈을 작성할 필요가 없습니다. 타사에서 이를 대신 작성해 둔 경우가 많습니다. 예를 들어, 워싱턴 대학교에서 만든 Checker Framework를 활용할 수 있습니다. 이 프레임워크에는 NonNull 모듈뿐만 아니라 정규 표현식 모듈과 뮤텍스 락 모듈도 포함되어 있습니다. 자세한 내용은 Checker Framework를 참조하십시오.
Repeating Annotations
경우에 따라 동일한 어노테이션을 선언이나 타입 사용에 적용하고자 할 때가 있습니다. Java SE 8 릴리스부터 반복 어노테이션을 사용하여 이를 수행할 수 있습니다.
예를 들어, 주어진 시간이나 일정에 따라 메서드를 실행할 수 있는 타이머 서비스를 사용하는 코드를 작성하고 있다고 가정해 보겠습니다. UNIX의 cron 서비스와 유사하게, 특정 시간이나 일정에 메서드를 실행하도록 타이머를 설정하고 싶습니다. 이제 매월 말일과 매주 금요일 오후 11시에 메서드 doPeriodicCleanup을 실행하도록 타이머를 설정하려고 합니다. 타이머를 설정하기 위해 @Schedule 어노테이션을 생성하고 doPeriodicCleanup 메서드에 두 번 적용합니다. 첫 번째 사용은 매월 말일을 지정하고 두 번째 사용은 금요일 오후 11시를 지정합니다. 다음 코드 예시와 같이 작성합니다:
@Schedule(dayOfMonth = "last")
@Schedule(dayOfWeek = "Fri", hour = 23)
public void doPeriodicCleanup() {
// 메서드 내용
}
이전 예제는 어노테이션을 메서드에 적용합니다. 표준 어노테이션을 사용하는 곳이면 어디든지 반복 어노테이션을 사용할 수 있습니다. 예를 들어, 무단 액세스 예외를 처리하는 클래스를 가지고 있습니다. 이 클래스에 하나는 관리자용으로, 또 하나는 관리자용으로 @Alert 어노테이션을 추가합니다:
@Alert(role="Manager")
@Alert(role="Administrator")
public class UnauthorizedAccessException extends SecurityException { ... }
호환성 문제로 인해 반복 어노테이션은 Java 컴파일러에 의해 자동으로 생성된 컨테이너 어노테이션에 저장됩니다. 컴파일러가 이를 수행하려면 코드에 두 가지 선언이 필요합니다.
Step 1: Declare a Repeatable Annotation Type
어노테이션 타입은 @Repeatable 메타 어노테이션으로 표시되어야 합니다. 다음 예제는 커스텀 @Schedule 반복 어노테이션 타입을 정의합니다:
import java.lang.annotation.Repeatable;
@Repeatable(Schedules.class)
public @interface Schedule {
String dayOfMonth() default "first";
String dayOfWeek() default "Mon";
int hour() default 12;
}
@Repeatable 메타 어노테이션의 값은 괄호 안에 있으며, 이는 반복 어노테이션을 저장하기 위해 Java 컴파일러가 생성하는 컨테이너 어노테이션의 타입입니다. 이 예제에서, 컨테이너 어노테이션 타입은 Schedules이며, 따라서 반복되는 @Schedule 어노테이션은 @Schedules 어노테이션에 저장됩니다.
어노테이션을 반복 가능으로 선언하지 않고 동일한 어노테이션을 선언에 적용하면 컴파일 시 오류가 발생합니다.
Step 2: Declare the Containing Annotation Type
컨테이너 어노테이션 타입은 배열 타입을 가진 value 요소를 가져야 합니다. 배열 타입의 컴포넌트(구성 요소) 타입은 반복 가능한 어노테이션 타입이어야 합니다. Schedules 컨테이너 어노테이션 타입에 대한 선언은 다음과 같습니다:
public @interface Schedules {
Schedule[] value();
}
Retrieving Annotations
Reflection API에는 어노테이션을 검색할 수 있는 여러 메서드가 있습니다. AnnotatedElement.getAnnotation(Class<T>)와 같은 단일 어노테이션을 리턴하는 메서드의 동작은 변경되지 않았으며, 요청된 타입의 어노테이션이 하나만 있는 경우 단일 어노테이션을 리턴합니다. 요청된 타입의 어노테이션이 여러 개 있는 경우, 먼저 그들의 컨테이너 어노테이션을 가져와야 합니다. 이렇게 하면 기존 코드가 계속 작동합니다. Java SE 8에서는 컨테이너 어노테이션을 스캔하여 여러 어노테이션을 한 번에 반환하는 AnnotatedElement.getAnnotationsByType(Class<T>)와 같은 메서드가 도입되었습니다. 사용 가능한 모든 메서드에 대한 정보는 AnnotatedElement 클래스 사양을 참조하십시오.
Design Considerations
어노테이션 타입을 설계할 때, 해당 타입의 어노테이션 카디널리티(즉, 사용 횟수)를 고려해야 합니다. 이제 어노테이션을 한 번도 사용하지 않거나, 한 번 사용하거나, @Repeatable로 표시된 경우 여러 번 사용할 수 있습니다. 또한, @Target 메타 어노테이션을 사용하여 어노테이션 타입이 사용할 수 있는 위치를 제한할 수도 있습니다. 예를 들어, 메서드와 필드에만 사용할 수 있는 반복 어노테이션 타입을 만들 수 있습니다. 어노테이션 타입을 신중하게 설계하여 어노테이션을 사용하는 프로그래머가 이를 최대한 유연하고 강력하게 사용할 수 있도록 하는 것이 중요합니다.
※ 컨테이너 어노테이션이란?
컨테이너 어노테이션은 반복 어노테이션을 저장하기 위해 사용되는 어노테이션입니다. Java SE 8에서는 같은 어노테이션을 여러 번 사용할 수 있도록 하기 위해 @Repeatable 메타 어노테이션이 도입되었습니다.
컨테이너 어노테이션은 반복 어노테이션을 그룹화하여 하나의 어노테이션으로 관리할 수 있도록 합니다. @Repeatable 메타 어노테이션의 값으로 지정되며, 반복 어노테이션 타입을 배열로 가지는 value 요소를 포함합니다. 이를 통해 여러 개의 반복 어노테이션이 컨테이너 어노테이션 안에 저장됩니다.
예를 들어, @Schedule 어노테이션이 반복 어노테이션으로 사용될 때, 이를 저장하기 위한 컨테이너 어노테이션 @Schedules는 다음과 같이 정의됩니다:
@Repeatable(Schedules.class)
public @interface Schedule {
String dayOfMonth() default "last";
String dayOfWeek() default "Fri";
int hour() default 23;
}
public @interface Schedules {
Schedule[] value();
}
이렇게 하면 @Schedule 어노테이션을 여러 번 사용할 수 있으며, 모든 @Schedule 어노테이션은 @Schedules 컨테이너 어노테이션에 저장됩니다. 예를 들어, 특정 메서드에 여러 @Schedule 어노테이션을 적용할 수 있습니다:
@Schedule(dayOfMonth = "last")
@Schedule(dayOfWeek = "Fri", hour = 23)
public void doPeriodicCleanup() {
// 메서드 내용
}
위의 예시에서 @Schedule 어노테이션 두 개는 @Schedules 컨테이너 어노테이션에 포함되어 관리됩니다.
'High Level Programming Language > Learning the Java Language' 카테고리의 다른 글
Lesson: Object-Oriented Programming Concepts (0) | 2024.07.01 |
---|---|
Lesson: Classes and Objects 4[Lambda, Enum] (0) | 2024.06.10 |
Lesson: Packages (0) | 2024.06.07 |
Lesson: Numbers and Strings [Autoboxing and Unboxing] (0) | 2024.06.05 |
Lesson: Numbers and Strings [Strings] (0) | 2024.06.05 |