-
TECHIT 앱 스쿨 2기: Android 17일차 (23.05.17)[THEC!T] 앱 스쿨2기 : Android 2023. 5. 17. 18:57728x90
자료 출처 : 안드로이드 앱스쿨 2기 윤재성 강사님 수업 내용
오늘은 오전에 개인회고에 대한 특강으로 시작하였습니다.
회고 방법에 대한 설명과 직접 해보는 시간을 가졌었습니다.
1. Three Fs
- Fact
- Feelings
- Findings
2. KPT
- Keep
- Problem
- Try
3. CSS
- Continue
- Start
-Stop
4. 4Ls
- Liked
- Learned
- Lacked
- Longed For
오늘의 오전 회고 특강은 방법 설명과 한 번 경험해 보는 것이 주 였기에 간단하게 여기까지만 작성하도록 하겠습니다.
이제 코틀린 관련 내용입니다.
점심을 먹고 오후 1시부터는 기존에 하던대로 코틀린 관련 강의를 들었습니다.
오늘의 시작은 코틀린 Companion 관련 내용부터 시작하였습니다.
Companion
코틀린에서 Companion 은 자바에서 static 과 같은 역할을 합니다. 즉 객체의 생성 없이도 사용할 수 있는 클래스 내 메서드 나 변수등을 만들때 사용하며, 이는 즉 Companion 으로 지정한 메서드나 변수들은 프로그램 실행 시 메모리에 올라간다는 뜻 입니다. 따라서 클래스 내에 있는 다른 일반 멤버 메서드나 멤버 변수는 사용 할 수 없습니다.
그리고 코틀린에서 정의한 Companion 멤버 나 메서드를 자바에서 사용할 수 있도록 하기 위해서는
@JavaStatic 을 선언부 앞에 작성해야 합니다. ( 현재 코틀린 버전에서는 작성하지 않아도 정상 작동 되지만,사용하려는 자바프로그램의 JVM버전에 따라 안될 수 도 있기 때문에 작성하는 것을 권장합니다.)
fun main(){ // companion // Java 에서 static과 동일하다 // 클래스내에 compaion 멤버로 정의된 요소들은 객체 생성 없이 사용이 가능하며 // 클래스 이름을 통해 접근한다. // companion 변수의 경우 딱 하나만 생성되어 사용할 수 있다. // 일반 멤버 변수나 메서드를 반드시 객체를 생성해야 사용할 수 있다. val t1 = TestClass1() println("t1.a1 : ${t1.a1}") t1.testFun1() // companion 멤버는 객체 생성 없이 클래스 이름으로 사용이 가능하다. // 변수의 경우에는 프로그램 전체에서 딱 하나만 사용하는 값을 저장할 때 사용한다. println("TestClass1.a2 : ${TestClass1.a2}") TestClass1.testFun2() // java 파일에 정의된 static 멤버 사용 println("JavaMain.javaA1 : ${JavaMain.JavaA1}") JavaMain.javaMethod1() } class TestClass1{ // 일반 멤버 변수 var a1 = 100 // companion 멤버 companion object{ var a2 = 200 // @JvmStatic : companion 멤버를 자바에서 사용할 수 있게 해주는 // 어노테이션이다. @JvmStatic var kotlinA3 = 300 @JvmStatic fun kotlinMethod3(){ println("kotlinMethod3") } fun testFun2(){ println("testFun2") println("a2 : $a2") // companion object 입장에서 봤을 때 // 클래스를 가지고 객체를 생성했다는 것을 100% 보장받을 수 없기 때문에 // 일반 멤버의 접근이 불가능하다. // testFun1() // println("a1 : $a1") } } // 일반 메서드 fun testFun1(){ println("testfun1") println("a1 : $a1") // 객체 입장에서는 companion 멤버가 메모리에 올라가 있다는 것을 // 보장 받을 수 있으므로 사용이 가능하다. testFun2() println("a2: $a2") } }
다음으로는 데이터 클래스에 대하여 배웠습니다.
데이터 클래스
데이터 클래스는 간단히 말하자면 객체의 멤버를 보다 쉽게 관리할 수 있는 기능이 추가되어 있으며,
abstract, open, sealed, inner 클래스로 정의 할 수 없으며, 반드시 주 생성자를 작성해줘야 합니다.
주 생성자 작성을 강제하는 이유는 멤버 변수를 무조건 갖게 하기 위함 입니다.
+ 추가
데이터 클래스는 데이터를 기술하는 용도로 주로 사용하며, 필요할 경우에만 추가로 부 생성자나 init 블록을 넣어 데이터를 위한 간단한 로직을 포함 할 수 있습니다
데이터 클래스가 제공하는 메서드는 equals(), hashCode(), copy(), toString(),componentN() 이 있습니다.
equals() : 일반 클래스 처럼 == 연산자를 이용하여 사용하지만 조금 다르게 설정이 되어 있습니다.
데이터 클래스의 equals()는 주 생성자에 작성한 멤버변수들의 값이 같다면 true, 다르다면 false를 반환합니다.
hashCode() : 생략
copy() : 객체를 복제하여 새로운 객체를 만든다.
toString() : 생략
componentN() : 객체의 선언부 구조를 분해하기 위해 프로퍼티에 상응하는 메서드
-> 객체 분해를 지원하기 위함입니다. 즉 객체가 가지고 있는 프로퍼티를 개별 변수로 분해하여 할당하는 것을 말합니다.
사용예제
fun main(){ val t1 = Test(100,200) val one = t1.component1() val two = t1.component2() // 데이터 클래스를 이용하여 생성한 객체를 이용하여 값 가져오기 val (three, four) = t1 println("one : $one") println("two : $two") println("three : $three") println("four : $four") } data class Test(val first:Int, val second:Int)
one : 100 two : 200 three : 100 four : 200
데이터 클래스 종합
fun main(){ // DataClass // 객체의 멤버를 보다 쉽게 관리할 수 있는 기능이 추가된다. // abstract, open, sealed, inner 클래스로 정의할 수 없다. // 일반 클래스로 객체를 생성한다. var obj1 = TestClass1(100, 200) var obj2 = TestClass2(100 ,200) // 멤버 사용 println("obj1.a1 : ${obj1.a1}") println("obj1.a2 : ${obj1.a2}") println("obj2.a1 : ${obj2.a1}") println("obj2.a2 : ${obj2.a2}") obj1.testMethod1() obj2.testMethod2() println("---------------------------------------------") // 부생성자를 이용한 객체 생성 var obj3 = TestClass1(100, 200, 300) var obj4 = TestClass2(100, 200, 300) println("obj3.a1 :${obj3.a1}") println("obj3.a2 :${obj3.a2}") println("obj3.a3 :${obj3.a3}") println("obj4.a1 :${obj4.a1}") println("obj4.a2 :${obj4.a2}") println("obj4.a3 :${obj4.a3}") println("---------------------------------------------") var obj5 = TestClass1(100, 200, 300) var obj6 = TestClass1(100, 200, 300) if(obj5 == obj6){ println("obj5와 obj6은 같은 객체 입니다.") } else{ println("obj5와 obj6은 다른 객체 입니다.") } // 일반 클래스를 통해 만들어진 객체들은 객체의 ID가 같은지를 비교한다. var obj7 = TestClass2(100, 200, 300) var obj8 = TestClass2(100, 200, 300) if(obj7 == obj8){ println("obj7와 obj8은 같은 객체 입니다.") } else{ println("obj7와 obj8은 다른 객체 입니다.") } // data 클래스는 주 생성자를 통해 정의된 멤버 변수의 값이 같은지를 비교한다. println("---------------------------------------------") // copy : 객체를 복제하여 새로운 객체를 만든다. (data Class에 있는 메서드) val obj9 = obj7.copy() println("obj7.a1 : ${obj7.a1}") println("obj9.a1 : ${obj9.a1}") // obj9.a1의 값을 변경한다. obj9.a1 = 1000 println("obj7.a1 : ${obj7.a1}") println("obj9.a1 : ${obj9.a1}") println("---------------------------------------------") // data class를 통해 만든 객체는 주 생성자에 정의한 멤버 변수를 // cmponentN 메서드로 값을 받아올 수 있다. val num1 = obj7.component1() val num2 = obj7.component2() println("num1 : $num1") println("num2 : $num2") println("---------------------------------------------") // 객체 분해 : 주 생성자를 통해 정의된 멤버 변수의 값을 하나씩 추출하여 // 좌측에 작성한 변수들에 순서대로 담아준다. // 이 때, componentN 메서드들을 호출하여 값을 전달 한다. val (num10, num11) = obj7 println("num10 : $num10") println("num11 : $num11") } // 일반 클래스 class TestClass1(var a1:Int, var a2: Int){ var a3:Int = 0 init { println("TestClass1의 init") } constructor(a1:Int , a2: Int, a3:Int) : this(a1,a2){ this.a3 = a3 } fun testMethod1(){ println("TestClass1의 testMethod1입니다.") } } // DataClass // 반드시 주 생성자(클래스 이름 옆에 작성하는 생성자)를 작성해줘야 한다. // 주 생성자 작성을 강제하는 이유는 멤버 변수를 무조건 갖게 하기 위함이다. data class TestClass2(var a1:Int, var a2:Int){ var a3:Int = 0 init { println("TestClass2의 init") } constructor(a1: Int,a2: Int,a3: Int) : this(a1, a2){ this.a3 = a3 } fun testMethod2(){ println("TestClass2의 testMethod2 입니다.") } }
다음으로는 제네릭에 대해서 배웠습니다.
제네릭
제네릭은 자료형을 일반화해 내부에서 그 자료형에 맞춰 교체하는 방법으로, 형식 매개 변수라는 T를 이용해 다양한 자료형으로 대체할 수 있습니다. 제네릭을 사용하면 객체의 자료형을 컴파일할 때 체크하기 때문에 객체 자료형의 안정성을 높이고 형 변환의 번거로움이 줄어듭니다.
제네릭을 사용하기 위해 < > 사이에 형식 매개 변수를 넣어 선언하며, 이때 형식 매개 변수는 하나 이상 지정할 수 있습니다.
일반적으로는 자료형을 대표하는 용어로 T를 많이 사용 합니다.
그리고 불변성, 공변성, 반 공변성에 대해서 배웠습니다.
불변성 : 객체를 생성할 때 설정한 제네릭과 가트은 변수에 담을 수 있다.
공변성 : 변수에 설정한 제네릭이 객체를 생성했을 대 사용한 제네릭의 부모 클래스인 경우에도 변수에 담을 수 있다.
제네릭 선언시 앞에 out 키워드를 붙혀서 사용한다.
반 공변성 : 변수에 설정한 제네릭이 객체를 생성했을 때 사용한 제네릭의 자식 클래스인 경우에도 변수에 담을 수 있다.
제네릭 선언시 앞에 in 키워드를 붙혀서 사용한다.
fun main(){ // 제네릭이 정의된 클래스를 가지고 객체를 생성한다. val t1 = TestClass1<Int>() t1.testMethod1(100) val t2 = TestClass1<String>() t2.testMethod1("안녕하세요") val t3 = TestClass2<Int>(100) t3.testMethod2(200) val t4 = TestClass2<String>("문자열1") t4.testMethod2("문자열2") val t5 = TestClass3<Int, Double, Boolean, String>(100, 11.11) t5.testMethod3(true, "문자열1") val t6 = TestClass3<Double, String, Boolean, Int>(22.22, "문자열2") t6.testMethod3(false, 200) // 불변성 : 객체를 생성할 때 설정한 제네릭과 같은 변수에 담을 수 있다. // 클래스간의 관계에 상관없이 제네릭에 설정한 클래스 타입이 다르면 오류가 발생한다. val t7:TestClass5<SubClass1> = TestClass5<SubClass1>() // val t8:TestClass5<SuperClass1> = TestClass5<SubClass1>() // val t9:TestClass5<SubClass2> = TestClass5<SubClass1>() // 공변성 : 변수에 설정한 제네릭이 객체를 생성했을 때 사용한 제네릭의 부모 클래스인 경우 // 에도 변수에 담을 수 있다. val t10:TestClass6<SubClass1> = TestClass6<SubClass1>() val t11:TestClass6<SuperClass1> = TestClass6<SubClass1>() //val t12:TestClass6<SubClass2> = TestClass6<SubClass1>() // 반 공변성 : 변수에 설정한 제네릭이 객체를 생성했을 때 사용한 제네릭의 자식 클래스인 // 경우에도 변수에 담을 수 있다. val t13:TestClass7<SubClass1> = TestClass7<SubClass1>() //val t14:TestClass7<SuperClass1> = TestClass7<SubClass1>() val t15:TestClass7<SubClass2> = TestClass7<SubClass1>() } // < > : 안에 알파벳 문자를 넣어준다. 보통 대문자 한글자를 작성한다. // 이 알파벳은 아직 결정되지 않은 타입을 의미한다. // 어떠한 타입인지 결정되지 않았지만 이 타입의 변수들을 정의하여 클래스를 작성하고 // 향후, 객체를 생성할 때 타입을 결정할 수 있다. class TestClass1<T>{ fun testMethod1(a1:T){ println("a1 : $a1") } } class TestClass2<T>(var a1:T){ fun testMethod2(a2: T){ println("a1 : $a1") println("a2 : $a2") } } class TestClass3<A, B, C, D>(var a1:A, var a2 :B){ fun testMethod3(a3: C, a4:D){ println("a1 : $a1") println("a2 : $a2") println("a3 : $a3") println("a4 : $a4") } } open class SuperClass1 open class SubClass1 : SuperClass1() class SubClass2 : SubClass1() // 불변성 (제네릭에 키워드를 붙히지 않는다) class TestClass5<A>() // 공변성 class TestClass6<out A>() // 반 공변성 class TestClass7<in A>()
출력 결과
a1 : 100 a1 : 안녕하세요 a1 : 100 a2 : 200 a1 : 문자열1 a2 : 문자열2 a1 : 100 a2 : 11.11 a3 : true a4 : 문자열1 a1 : 22.22 a2 : 문자열2 a3 : false a4 : 200
다음은 코틀린에서의 중첩 클래스에 대해서 배웠습니다.
일반 중첩 클래스
내부에 있는 클래싀 객체 생성은 외부 클래스로 부터 생성한 객체를 통해 생성할 수 있습니다.
내부의 클래스를 가지고 만든 객체는 외부 클래스를 통해 만든 객체가 무조건 있다는 것을 보장 받을 수 있기
때문에 외부 클래스에 정의한 멤버의 접근이 자유롭습니다.
익명 중첩 클래스
자바에선 익명 이너 클래스라는 것을 제공해 일회성으로 객체를 생성해 사용합니다. 코틀린에서는 object 키워드를 사용하는 익명 객체로 이와 같은 기능을 수행합니다. 자바와 다른 점은 익명 객체 기법으로 다중의 인터페이스를 구현할 수 있다는 점 입니다.
fun main(){ // 외부 클래스의 객체를 생성한다. val obj1 = Outer1() // 이를 통해 내부 클래스의 객체를 생성한다. val obj2 = obj1.Inner1() obj2.innerMethod() val t1 = TestClass1() t1.interMethod1() t1.interMethod2() // 만약 인터페이스를 구현하거나 클래스를 상속받은 클래스를 통해 객체를 하나만 생성한다면 // 익명 중첩클래스를 사용해도 된다. val t2 = object :Inter1{ override fun interMethod1() { println("익명 중첩 클래스의 interMethod1") } override fun interMethod2() { println("익명 중첩 클래스의 interMethod2") } } t2.interMethod1() t2.interMethod2() } // 일반 중첩 클래스 // 내부에 있는 클래스의 객체 생성은 외부 클래스로 부터 생성한 객체를 통해 생성할 수 있다. // 내부의 클래스를 가지고 만든 객체는 외부 클래스를 통해 만든 객체가 문조건 있다는 것을 // 보장 받을 수 있기 때문에 외부 클래스에 정의한 멤버의 접근이 자유롭다. class Outer1{ var outerV1 = 100 fun outerMethod(){ println("Outer1의 outerMethod 입니다.") } inner class Inner1{ fun innerMethod(){ println("outerV1 : $outerV1") outerMethod() } } } interface Inter1{ fun interMethod1() fun interMethod2() } // 익명 중첩 클래스를 사용하지 않는 다면 // 인터페이스를 구현하거나 클래스를 상속 받은 다음에 // 메서드를 오버라이딩한 클래스를 만들고 위로 올랃가서 객체 생성해 사용해야 한다. // 만약 클래스를 통해 생성하는 객체가 두 개 이상이면 클래스를 정의하고 객체생성해서 사용한다. class TestClass1 : Inter1{ override fun interMethod1() { println("TestClass1의 interMethod1") } override fun interMethod2() { println("TestClass1의 interMethod2") } } interface Inter2{ fun interMethod3() fun interMethod4() }
다음으로는 null 처리에 대해서 배웠습니다.
코틀린에서는 개발자가 null이 담겨있는 참조변수를 이용하여 객체 접근을 싣고 할 때 오류가 발생되는 것을 방지 하고자 많은 방법을 제공하고 있으며, 이를 통하여 null 에 대한 안정성을 확보 할 수 있습니다.
!! 연산자
!! 연산자는 null 을 허용하는 변수에 담긴 객체의 주소 값을 null을 허용하지 않는 않는 형태의 변수에 담을 경우 사용하며,
변환 과정에서 해당 변수에 null 값이 들어 있다면 오류가 발생하며, 해당 연산자는 null에 대한 안정성을 확보하기 어려우므로 사용을 권장하지는 않는다.
?:연산자
?: 연산자는 참조 변수에 null 이 들어 있다면 지정된 기본값을 반환한다.
?.연산자
참조 변수를 통하여 메서드를 호출하거나 멤버 변수를 사용할 때 참조 변수에 객체의 주소값이 들어 있다면 객체에 접근하여 메서드나 변수를 사용합니다.
만약 참조 변수에 null이 들어 있다면 오류가 발생하지 않고 null을 반환합니다.
fun main(){ // null을 허용하는 변수 선언 var a1:String? = null // null을 허용하지 않는 변수 선언 // var a2:String = null testFun1("안녕하세요") // testFun1(null) testFun2("안녕하세요") testFun2(null) testFun3("안녕하세요") testFun3(null) val t100 = TestClass1(100, "문자열1") testFunction(t100) //testFunction(null) testFunction200(t100) println("--------------------------------------") testFunction200(null) println("--------------------------------------") testFunction300(t100) println("--------------------------------------") testFunction300(null) } fun testFun1(str1:String?){ // !! 연산자 // 널을 허용하는 타입의 변수 값을 널을 허용하지 않는 타입으로 변환하여 // 널을 허용하지 않는 타입의 변수에 담을 수 있도록 한다. val value1:String = str1!! println("value1 : $value1") } fun testFun2(str1 : String?){ // str1 에 null 아닌 객체의 ID가 들어있으면 그 ID를 value1 변수에 담아주고 // null이 있으면 지정한 값인 "기본문자열" 객체가 value1 변수에 담긴다. val value1:String? = str1 ?: "기본문자열" println(value1) } fun testFun3(str1:String?){ // 만약 변수의 값이 null인 경우 코드가 동작하지 않도록 처리해주면 // null 안전성을 확보할 수 있다. // 이때, null을 허용하는 변수를 null을 허용하지 않는 변수처럼 자유롭게 // 사용할 수 있다. if(str1 != null){ val value1:String = str1 println("value1 : $value1") } } class TestClass1(var str1:Int, var str2:String){ fun testMethod1(){ System.out.println("TestClass1의 testMethod1") } } fun testFunction(t1:TestClass1?){ // !! 변수를 통해 객체의 멤버에 접근한다. // null 안전성을 확보하지 않고 멤버에 접근하겠다면 !!를 붙혀준다. // !! 연산자는 null을 허용하는 변수에 담긴 값을 추출하여 null을 허용하지 않는 // 타입으로 변환하는 연산자이다. 이에 null 값이 들어있으면 오류가 발생한다. println("t1.str1 : ${t1!!.str1}") println("t1.str2 : ${t1!!.str2}") t1!!.testMethod1() } fun testFunction200(t1: TestClass1?){ // ? 연산자 // 참조변수?.멤버변수 : 참조변수에 null값 들어 있다면 null이 반환된다. // 참조변수?.멤버 메서드 : 참조변수에 null값 들어 있다면 메서드를 호출하지 않는다. println("t1.str1 : ${t1?.str1}") println("t1.str2 : ${t1?.str2}") t1?.testMethod1() } fun testFunction300(t1: TestClass1?){ // null이 저장되어 있는지 여부를 확인한다. // if 문 내부에서는 null을 아무런 연산자를 붙히지 않고 멤버 접근이 가능하다. if(t1 != null){ println("t1.str1 : ${t1?.str1}") println("t1.str2 : ${t1?.str2}") t1?.testMethod1() } }
출력 결과
value1 : 안녕하세요 안녕하세요 기본문자열 value1 : 안녕하세요 t1.str1 : 100 t1.str2 : 문자열1 TestClass1의 testMethod1 t1.str1 : 100 t1.str2 : 문자열1 TestClass1의 testMethod1 -------------------------------------- t1.str1 : null t1.str2 : null -------------------------------------- t1.str1 : 100 t1.str2 : 문자열1 TestClass1의 testMethod1 --------------------------------------
이상 오늘 배운 내용이였습니다.
오늘은 코틀린 관련 강의느 오후에만 진행을 하여 금방 마무리를 할 수 있었습니다. 그전에는 양도 많아 정리하는 것도 오래 걸렸었는데 간만에 저녁에 자유시간이 생긴 느낌입니다. 하지만 아직 공부해야할 것은 산더미 만큼 있기에 저녁을 먹고 다시 공부를 할 생각 입니다. 그리고 제가 자격증에 대해서 너무 무관심 했던 것 같습니다. 회사를 다니면서 필기를 붙어 놓고 실기를 안봐서 유효기간이 끝나버린게 있었습니다. 피곤하다는 이유로 내팽개쳐놓고 잊고 있었는데 오늘 문득 생각이 들어 보니 처참했습니다....
이제 저의 스케줄은 주말에는 자격증 공부,코딩테스트 공부, 평일에 배운 내용 복습 등을 하면서 보내야 될 것 같습니다.
음... 약 4달간 이악물고 달리겠습니다. 일단 바로 책을 주문하였습니다.
저는 좀 스스로를 몰아붙여야 하는 성격인 것을 알고 있기 때문입니다.
오늘의 마음가짐
다른사람이 가장 열심히 했던 시절을 물어 본다면 지금 이라고 말할 수 있도록 열심히 하자~!
-----------------------------------------------------추가
어제 설계에 실패하여 오늘 다시금 설계를 해봤지만 이번에는 다른 실수를 하여 구현이 불가능하지는 않지만 제가 생각했을 때 최소한의 기준을 넘지 않아 삭제하였습니다.
내일 다시 설계하여 작성해 볼 수 있도록 하겠습니다.
'[THEC!T] 앱 스쿨2기 : Android' 카테고리의 다른 글
TECHIT 앱 스쿨 2기: Android 19일차 (23.05.22) (0) 2023.05.23 TECHIT 앱 스쿨 2기: Android 18일차 (23.05.18) (0) 2023.05.18 TECHIT 앱 스쿨 2기: Android 16일차 (23.05.16) (1) 2023.05.17 TECHIT 앱 스쿨 2기: Android 15일차 (23.05.15) (0) 2023.05.15 TECHIT 앱 스쿨 2기: Android 14일차 (23.05.12) (0) 2023.05.12