-1. 지금껏 왜 작성하지 않았는가
먼저 지금까지 게을러서 작성하지 못한 점 죄송합니다...
는 아니고요.
사실 이번주 월요일부터 수요일까지 예비군 동원훈련이 잡혀 있었습니다.
오랜만에 군 부대에 가니까 옛날(작년) 생각이 새록새록 나는 게 기분이 좋지는 않았습니다.
그래도 휴식했다 생각하고 앞으로 열심히 공부할 생각입니다.
사람들이 군대에 가면, 많은 생각이 든다고 하잖아요?
나가면 꼭 부모님께 효도해야겠다...
나가면 꼭 공부 열심히 해서...
그래서 저도 안에서 생각을 좀 해봤는데, 개인 프로젝트를 진행하고 싶다는 생각이 들었습니다.
사실 캠프에 들어오기 이전부터 조금씩 해왔었지만, 솔직히 제가 추구하는 방향성 중 하나인
여태껏 보지 못한 앱 이라는 방향성과 많이 차이가 났습니다.
그리고, 이번 캠프에 참가하고 나서 제 기본기가 얼마나 구멍이 숭숭 뚫려 있는지 깨닫고,
이전에 해왔던 프로젝트를 엎어버리자고 생각했습니다.
방향성, 주제, UI, 기능, 코드 전부 다요.
이 부분은 천천히 생각해보고, 오늘 TIL 시작해보겠습니다.
0. 오늘은 무엇을 했는가
오늘은 제 입장에서는 새로운 환경에서 처음 시작하는 주차입니다.
조가 바뀌었고, 진행 방식도 약간의 변화가 있었습니다.
이에 따라, 아침 9시부터 10시까지는 코드 카타를 진행했고,
이후에는 밀려 있던 강의들을 들었습니다.
강의 내용이 아직은 어려운 내용이 별로 없어서 빠르게 진행할 수 있었습니다.
그 후, 배운 내용을 바탕으로 개인 과제를 수행했고,
마지막으로, 안드로이드 강의를 들으며, 마무리했습니다.
> 09:00~10:00 <
1. 코드 카타
오늘의 코드 카타는 75. 최댓값과 최솟값이었습니다.
먼저 문제 설명입니다.
문자열 s에는 공백으로 구분된 숫자들이 저장되어 있습니다. str에 나타나는 숫자 중 최소값과 최대값을 찾아 이를 "(최소값) (최대값)"형태의 문자열을 반환하는 함수, solution을 완성하세요.
예를들어 s가 "1 2 3 4"라면 "1 4"를 리턴하고, "-1 -2 -3 -4"라면 "-4 -1"을 리턴하면 됩니다.
이 문제를 보고 먼저 느낀 점은, 생각보다 문제 자체가 쉽다 였습니다.
이전 문제까지는 그래도 어느 정도 머리를 굴려서 풀이 방법을 찾아내야 했었다면,
이번 문제는 직관적으로 어떻게 해야 할지를 보여주고 있었습니다.
또한, 이전에 풀었던 문제 풀이 방법의 복기 같은 느낌도 들었습니다.
split() 이라는 함수를 통해, 문자열을 나누는 방식을 사용했다는 점이 특히 그랬습니다.
그럼 이제 제 풀이 방법에 대해 설명드리겠습니다.
먼저, str에 나타난 숫자 형태가 (최소값) (최대값) 형태입니다. 띄어쓰기로 구분돼 있기에, 이 부분을 잘라주고, 앞뒤 숫자들을 가져오면 되겠다고 생각했습니다.
그리고, 이 숫자들을 List에 담고, 최대, 최소값만 가져와서 다시 answer에 집어넣으면 되는 문제였습니다.
class Solution {
fun solution(s: String): String {
var answer = ""
val list_str = s.split(" ")
var list_int: List<Int> = listOf()
for(i in list_str) {
list_int += i.toInt()
}
var min = list_int[0]
var max = list_int[0]
for(i in list_int) {
if(i > max) max = i
if(i < min) min = i
}
answer = "$min $max"
return answer
}
}
이 문제를 풀기 전에 전날 문제도 봤었는데요. 풀이 자체는 어느 정도 맞혔지만, 시간 초과 문제로 또 막혔습니다.
해당 문제에서도 반복문으로 인해서 막혔다고 생각하는데, 이 고질적인 문제는 해결 방법을 찾아야 겠다는 생각이 들었습니다.
주말동안 열심히 공부하고, 시도해봐야겠네요.
> 10:00 ~ 12:00 | 13:00 ~ 14:00 <
2. Kotlin 문법 종합반 1~3강
사실 제가 기대했던 강의였습니다. 저는 개인적으로 Kotlin 문법을 어느 정도 알기는 하지만,
정말 대표적이거나, 중요한 부분에 대해서만 알고, 조금 더 자세히, 깊게는 알지 못했기 때문입니다.
1강에서는 Kotlin에 대한 소개로 이루어져 있었습니다.
Kotlin만의 장점, 어떤 IDE를 쓰는지, 그 IDE의 단축키는 무엇인지 등등 유용한 정보들이 있었습니다.
2강에서는 문법의 기초에 대해서 배웠습니다.
코딩 컨벤션이라는 변수명을 쓰는 스타일을 배웠고,
주로 사용하는 입출력 함수, 변수 var, 상수 val 등 Kotlin에서 모르면 안 되는 것들에 대해 배웠습니다.
하지만, 저는 여기까지는 다 공부했던 내용이고, 자주 사용하다보니 모르는 내용은 없었습니다.
주로 다룰 내용은 3강에서 있을 것 같습니다.
3강 내용도 알고는 있었지만, 몰랐거나, 헷갈렸던 부분도 있었기에, 그 부분에 대해 다뤄보겠습니다.
요즘은 대부분의 언어가 객체지향 프로그래밍 언어일 겁니다.
Kotlin 역시, JVM을 사용하고, Java의 대체제 같은 느낌이어서 객체지향을 지원합니다.
먼저, 객체라는 건 각각의 존재, 개체, 이런 걸 뜻한다고 생각합니다.
사람, 동물, 핸드폰, 안약, 게임기 등등...
이런 것들을 객체라고 보는데, 프로그래밍에서 표현하려면, 조금은 특별한 형태를 가져야 합니다.
그 객체가 갖고 있는 특징, 그 객체가 하는 행동같은 것들을 정해줘야 합니다.
어쨌든, 코틀린에서도 이런 객체를 사용하는데, 일반적으로는 class를 많이 사용합니다.
class 객체명 { }
class라는 건 객체들을 만드는 틀입니다.
class 자체를 객체로 쓰진 않지만, 객체를 만들 수 있는 틀의 역할을 합니다.
그리고, 이렇게 만들어진 각각의 객체를 인스턴스(실체)라고 합니다.
위키백과에서는 다음과 같이 설명하네요.
객체 지향 프로그래밍(OOP)에서 인스턴스(instance)는 해당 클래스의 구조로 컴퓨터 저장공간에서 할당된 실체를 의미한다. 여기서 클래스는 속성과 행위로 구성된 일종의 설계도이다. OOP에서 객체는 클래스와 인스턴스를 포함한 개념이다.
위 내용에 대해 어느 정도 이해하고, 생성자에 대해 알아보겠습니다.
Kotlin에서 생성자는 기본 생성자와 명시적 생성자로 나뉜다고 합니다 (강의에서 배운 내용!!!) .
기본 생성자는 함수에서 매개변수를 쓸 때처럼 클래스 옆에 ()를 붙이고 만들면 됩니다.
class 객체명(멤버 프로퍼티)
그리고 인스턴스를 만들 때, 값을 넣어주면 됩니다.
val 인스턴스 변수 = 객체명(넣을 값)
이게 기본 생성자입니다. 기본 생성자는 단순히 값만 받습니다.
반면, 명시적 생성자는 다릅니다.
먼저, 명시적 생성자는 다시 주 생성자와 부 생성자로 나뉩니다.
주 생성자는 사실 기본 생성자와 크게 다른 부분이 없습니다. 대신, 추가적인 코드를 작성해줄 수 있습니다.
init {
//생성자 내용 작성
}
부 생성자는 약간 특별합니다. 주 생성자는 하나만 만들 수 있지만, 부 생성자는 여러 형태로 만들 수 있기 때문입니다.
constructor (val a: Int) { }
constructor (val a: Int, val b: String) { }
이를 통해 다양한 형태의 생성자를 활용할 수 있습니다.
상속은 내가 친족 관계에서 이어받는 재산을 상속받거나 할 때 많이 쓰입니다.
프로그래밍에서는 살짝 다릅니다. 재산보다는 유전자를 물려받는다는 느낌이 강합니다.
한 클래스가 갖고 있는 특징, 행동들을 이어받기 때문입니다.
Kotlin에서 객체를 상속할 때는 추가적인 키워드가 필요합니다.
open class 객체명 { }
Kotlin은 문법들을 보면 좀 더 안정성을 추구합니다.
그래서 기본적인 class들은 상속을 하지 못하고, open이라는 키워드가 붙어야만 상속할 수 있습니다.
class 자식객체 : 부모객체() { }
그리고 위와 같이 상속받을 수 있습니다.
당연히 상속받게 되면, 부모 객체의 멤버들을 쓸 수 있습니다.
하지만, 모든 자식이 부모와 똑같은 형태를 갖고 있고, 똑같이 행동하지만은 않습니다.
부모는 키가 커도, 자식은 작을 수 있고, 부모는 공부를 잘해도 자식은 못할 수도 있습니다.
이렇게 표현하면 헷갈릴 순 있지만, 제가 말하고 싶은 바는 부모와 자식은 같은 행동도 다르게 할 수 있다는 걸 말하고 싶었습니다.
그리고 이를 프로그래밍에서 표현한 게 메소드 오버라이딩(재정의)입니다.
class 부모객체 {
fun 함수명() { }
}
class 자식객체: 부모객체() {
override fun 함수명() { }
}
부모 객체에 정의된 멤버 메소드를 그대로 가져와 override 키워드만 붙여주면 됩니다.
그리고, 내용만 바꿔주면, 같은 함수이지만 다른 내용을 갖고 있는
즉, 같은 행동이어도 다른 모습을 보여주게 됩니다.
이런 방법을 사용하면, 자식 객체도 자식 객체만의 특징이 생기게 됩니다.
이번 강의에서 추상 클래스를 다루진 않았습니다...만
인터페이스 하면 추상 클래스도 같이 떠올랐기에, 이번에 같이 다뤄보도록 하겠습니다.
인터페이스는 다음과 같은 뜻을 갖고 있습니다.
인터페이스(interface)는 서로 다른 두 개의 시스템, 장치 사이에서 정보나 신호를 주고받는 경우의 접점이나 경계면이다
즉, 어떤 접점을 만들어주는 건데, Kotlin에서는 interface는 클래스보다도 좀 더 틀에 가깝습니다.
클래스가 실체의 모양을 잡아주는 틀이라면, interface는 조금 더 추상적인 부분을 잡아줍니다.
interface 인터페이스명 {
abstract fun 메소드명()
}
인터페이스 안에는 추상 메소드가 반드시 존재합니다. 추상적으로 어떤 동작을 표현하고 있지만, 내용은 전혀 정해져 있지 않습니다.
기존의 상속은 부모 객체도 행동을 했지만, 인터페이스는 행동조차 하지 않고, 행동의 이름만 작성했습니다.
최근에는 추상 메소드가 필요 없다고는 하지만, 그렇게 되면 추상 클래스와 차이점을 알기 어려우니 무조건 쓴다는 가정 하에 작성하겠습니다.
추상 클래스는 추상 메소드가 있는 클래스입니다.
abstract class 객체명 {
abstract fun 함수명
}
인터페이스와 비슷한 역할을 하지만, 차이점이 분명 존재합니다.
- 인터페이스는 다중 상속이 가능하고, 추상 클래스는 단일 상속만 가능하다.
- 인터페이스는 추상 메소드만 갖고 있지만, 추상 클래스는 추상 메소드 외의 메소드도 갖고 있다.
- 인터페이스는 필드를 가질 수 없고, val로 선언된 프로퍼티는 포함시킬 수 있다.
- 인터페이스 접근자는 public 고정이다.
- 인터페이스는 구현에 초점을 맞추고, 추상 클래스는 공유에 초점을 맞춘다.
위와 같이 각자의 역할이 있으므로, 잘 숙지하고 사용하면 될 것 같습니다.
> 14:00 ~ 17:00 <
3. 개인 과제 수행
개인 과제는 계산기를 만드는 것이었습니다.
아무래도, 계산기는 어떤 언어든 기초적으로 만드는 것 같습니다.
먼저 구현 조건부터 차례대로 보겠습니다.
Lv1Lv2Lv3Lv4Lv??Lv 1은 계산기 클래스를 만들고, 함수로 기능들을 구현했습니다.
class Calculator(val num1: Int, val num2: Int) {
fun add() {
println(num1 + num2)
}
fun sub() {
println(num1 - num2)
}
fun mul() {
println(num1 * num2)
}
fun dvd() {
println(num1 / num2)
}
}
이후, 함수들을 호출했습니다.
Lv 2는 % 연산자를 추가하고, while문을 추가해, 메뉴를 만들어줬습니다.
class Calculator(val num1: Int, val num2: Int) {
var menu: Int = -1
fun exe() {
while(true) {
println("다음과 같은 숫자를 입력하여, 조작할 수 있습니다.")
println("현재 값 설정은 ${num1}, ${num2}입니다.")
println("입력: 1 = 더하기, 2 = 빼기, 3 = 곱하기, 4 = 나누기, 5 = 나머지 값 구하기, -1 = 종료")
menu = readln().toInt()
when(menu) {
1 -> add()
2 -> sub()
3 -> mul()
4 -> dvd()
5 -> rem()
-1 -> break
else -> println("잘못된 숫자를 입력하셨습니다. 입력받은 숫자: $menu")
}
}
print("종료됐습니다.")
}
fun add() {
println("값: ${num1 + num2}")
}
fun sub() {
println("값: ${num1 - num2}")
}
fun mul() {
println("값: ${num1 * num2}")
}
fun dvd() {
println("값: ${num1 / num2}")
}
fun rem() {
println("값: ${num1 % num2}")
}
}
Lv 2까지는 이전에도 많이 다루던 방식이어서 쉽게 만들었습니다.
Lv 3는 단일 책임 원칙을 적용해서 구현했습니다.
class AddOperation(private val num1:Int, private val num2:Int): Calculator(num1 = num1, num2 = num2) {
override fun printRes() {
println("값: ${num1 + num2}")
}
}
class SubOperation(private val num1:Int, private val num2:Int): Calculator(num1 = num1, num2 = num2) {
override fun printRes() {
println("값: ${num1 - num2}")
}
}
class MulOperation(private val num1:Int, private val num2:Int): Calculator(num1 = num1, num2 = num2) {
override fun printRes() {
println("값: ${num1 * num2}")
}
}
class DvdOperation(private val num1:Int, private val num2:Int): Calculator(num1 = num1, num2 = num2) {
override fun printRes() {
println("값: ${num1 / num2}")
}
}
open class Calculator(private val num1: Int, private val num2: Int) {
private var menu: Int = -1
private lateinit var operation: Calculator
fun exe() {
while(true) {
println("다음과 같은 숫자를 입력하여, 조작할 수 있습니다.")
println("현재 값 설정은 ${num1}, ${num2}입니다.")
println("입력: 1 = 더하기, 2 = 빼기, 3 = 곱하기, 4 = 나누기, 5 = 나머지 값 구하기, -1 = 종료")
menu = readln().toInt()
when(menu) {
1 -> operation = AddOperation(num1, num2)
2 -> operation = SubOperation(num1, num2)
3 -> operation = MulOperation(num1, num2)
4 -> operation = DvdOperation(num1, num2)
5 -> {
operation = this
rem()
}
-1 -> break
else -> {
operation = this
println("잘못된 숫자를 입력하셨습니다. 입력받은 숫자: $menu")
}
}
operation.printRes()
}
print("종료됐습니다.")
}
open fun printRes() {
}
fun rem() {
println("값: ${num1 % num2}")
}
}
lv 3 이후에는 코드가 조금 정리가 되지 않은 느낌이 생겼습니다.
그래서 lv 4 적용 후, 최종적으로 다음과 같은 코드를 구현했습니다.
open class Calculator(private val num1: Int, private val num2: Int) {
private var menu: Int = -1
private lateinit var operation: AbstractOperation
fun exe() {
var firstExe = true
var firstNum = num1
var secondNum = num2
while(true) {
if(!firstExe) {
println("추가 값을 입력해주세요.")
secondNum = readln().toInt()
}
println("다음과 같은 숫자를 입력하여, 조작할 수 있습니다.")
println("현재 값 설정은 ${firstNum}, ${secondNum}입니다.")
println("입력: 1 = 더하기, 2 = 빼기, 3 = 곱하기, 4 = 나누기, 5 = 나머지 값 구하기, -1 = 종료")
menu = readln().toInt()
when(menu) {
1 -> operation = AddOperation(firstNum, secondNum)
2 -> operation = SubOperation(firstNum, secondNum)
3 -> operation = MulOperation(firstNum, secondNum)
4 -> operation = DvdOperation(firstNum, secondNum)
5 -> operation = RemOperation(firstNum, secondNum)
-1 -> break
else -> operation = WrongOperation(menu)
}
println("값 : ${operation.printRes()}")
firstNum = if(menu in 1..5) operation.printRes().toInt() else firstNum
firstExe = false
}
print("종료됐습니다.")
}
}
abstract class AbstractOperation {
abstract fun printRes(): String
}
class AddOperation(private val num1:Int, private val num2:Int): AbstractOperation() {
override fun printRes(): String = (num1 + num2).toString()
}
class SubOperation(private val num1:Int, private val num2:Int): AbstractOperation() {
override fun printRes(): String = (num1 - num2).toString()
}
class MulOperation(private val num1:Int, private val num2:Int): AbstractOperation() {
override fun printRes(): String = (num1 * num2).toString()
}
class DvdOperation(private val num1:Int, private val num2:Int): AbstractOperation() {
override fun printRes(): String = (num1 / num2).toString()
}
class RemOperation(private val num1:Int, private val num2:Int): AbstractOperation() {
override fun printRes(): String = (num1 % num2).toString()
}
class WrongOperation(private val menu: Int): AbstractOperation() {
override fun printRes(): String = "잘못된 숫자를 입력하셨습니다. 입력받은 숫자: $menu"
}
여기서는 코드를 한번에 다 적었지만, 실제로는 파일을 나눠서 적었습니다.
최종적으로 해당 코드의 실행 화면입니다.
강의 수강 이후, 과제를 진행하니, 클래스 개념에 대해 더 잘 익힐 수 있었던 것 같습니다.
확실히 백문이 불여일견이란 말이 맞는 것 같네요.
4. 끝
이후, 안드로이드 강의를 수강하긴 했으나, 따라치기 바빴으므로, 이후에 Fragment를 공부하면서 다시 살펴봐야겠습니다.
오늘은 다시 12시간의 일정을 소화하는 것에 익숙해지는 과정이었던 것 같습니다.
새로운 조원분들도 다 친절하시고, 친화력이 좋아서 빠르게 적응할 것 같네요.
끝.