본문 바로가기

우테코/level1(코틀린)

[우테코]02/21 level1 다섯번째 수업 (로또 피드백)

 

자꾸 바빠서 블로그 글이 밀린다 미치것다. 자꾸 휘발성으로 날아가는데 ㅠㅠㅠㅠㅠ 그래도 얼른 정리해보자.
와 ㅋㅋㅋㅋ 방학이 되어서야 정리하고있음 레벨 2때는 어쩌냐

 

🐗로또 피드백2🐗

이번 수업은 로또 두번째 피드백이였다 

자료에서 정리되지 않은 남은 부분들을 짚어 주셨다.

 

에피타이저

우선 개인적으로 이번수업에서 나만몰라서 멍때렸던 것을 찾아봤는데 별거 아니라서 킹받아서 그 내용에 대해 찾아보았다.

코틀린에서 자바로 넘어가며 원시값으로 가는지 Boxing 하는지에 대한 어쩌구 저쩌구 잡설에 대해 설명된 블로그 글이다. 

https://gwi02379.tistory.com/14

 

코틀린의 변수와 자료형 (Kotlin)

코틀린의 자료형 코틀린은 기본적으로 null 허용을 하지 않습니다. 즉, 값이 항상 할당되어야 한다는 원칙이 있습니다. 코틀린은 참조형 자료형만을 주로 사용하는데, Int, Long 등의 null을 허용하

gwi02379.tistory.com

 

준비운동

이번 수업에서는 두가지 크루원들의 블로그글을 보고 이야기 해보는 시간이있었다.

그 중 크루원 아크의 글에서 Kotest(코틀린 전용 오픈소스 테스트 프레임워크) 에 대한 이야기가 나왔고 자바 코드로 디컴파일 해본결과

모든 Int값을 boxInt라는 함수를 통해서 받고있었다 (정확히 이거인지 기억은 안나는데 사실 말하려는 내용과 상관없음)

-> 여기서 의문이 valueOf를 안쓰고 왜 boxInt를 쓰지? 이런것 이였다.

이런 코틀린에서 공식적으로 짜놓은 코드들이 이해가 안되고 더좋은 방법이 있을텐데 라는 의문이 들경우 왜 이따구로 짰는지 의미를 알고싶을 수 있다. 

🚨이럴때 이용하는 방법🚨

https://youtrack.jetbrains.com/issues

 

https://youtrack.jetbrains.com/issues

{{ (>_<) }} This version of your browser is not supported. Try upgrading to the latest stable version. Something went seriously wrong. When using IE9.0 or higher, make sure that compatibility mode is disabled.

youtrack.jetbrains.com

이 사이트에서 찾아보면 버그 리포트나 코드가 작성된 이유 같은 잡설들이 작성되어있다고 한다.

가끔 얻어걸릴수 있으니 궁금한것이 있다면 찾아보는것도 좋을것이다.


Value Class

코틀린의 재미있고 신박한 신기능이라고 한다.  -> 신기능을 겪기 전에 잡설부터 풀고 가보자

 

oop의 대표언어를 뽑는다면 무엇이 있을까?

java,코틀린,C#(C샵은 개쩌는 C 라는 의미로 C++++이라고 한다.)

 

이중 트랜드를 이끄는 언어가 C#이라고한다(패셔니스타 C#)

C# 나한테는 생소하지만 외국에서는 흔한 언어라고 한다. 

.net이라는 강력한 프레임워크를 가지고있고 ms가 만들어내서 초강력한 언어라고 한다.

 

언어도 트렌드가 있고 각 언어들은 트렌드를 쫒아간다.

이런 트렌드에 있어서는 C#이 선두주자 역할을 한다고한다.

자바는 제일 느리고 코틀린은 빠른편이고(물론 요즘 자바는 업데이트가 빠르다)

어쨋든 이런 잡설을 꺼낸 이유는 C#의 신기능이나 앞서나가는 변천사를 보면 코틀린 또한 어떤 신기능을 추후에 탑재할것인지 알 수 있다고 한다.

 

C#에는 value class 라는 것이 있다고 하고 코틀린에서도 수용하여 value class 가 1.5 버전 부터 지원한다고 한다(근데 뭐 안되는거 아직 많음)

 

일단 잘 정리되어있는 블로그글을 하나 첨부하고 요약해보려한다.

https://velog.io/@dhwlddjgmanf/Kotlin-1.5%EC%97%90-%EC%B6%94%EA%B0%80%EB%90%9C-value-class%EC%97%90-%EB%8C%80%ED%95%B4-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90

 

Kotlin 1.5에 추가된 value class에 대해 알아보자

Kotlin 1.5에 새로 추가된 value class에 대해 알아봅시다!!

velog.io

 

일단  value class 의 용도를 요약하자면

 

class를 사용하면서 얻는 이점들은 얻어가되 컴파일러가 알아서 최적화하여 실제로 사용되는 코드상에서는 class 인스턴스가 생성되지 않아 메모리 관리의 이점을 갖을 수 있는 방법이다.

 

뭔 사실 뭔소리인지 감이 안온다.

 

그렇다면 사용처를 두가지를 살펴보자

 

1. 원시값 포장

수업시간에 다뤘던 이점이다.

 

우리는 원시값 포장을 통해 여러가지 이점을 얻을수 있었다.(값의 검증 등등)

이렇게 원시값을 포장하여 사용할때 값의 의미로 사용하는 경우가 많은데 이런 경우에 인스턴스를 매번 생성하여 사용하여 메모리 관리에서 손해를 볼수가 있었다.

 

이런것을 방지하기위해 플라이웨이트 패턴을 이용하여 캐싱하는 방법도 있는데 하면 좋지만 귀찮다.

-> 이런것을 한방에 해결하는것이 value class이다.

 

개발자 입장에서는 value class 를 사용한다면 값 검증 같은것들을 다 클래스처럼 사용할수 있다.

하지만 막상 사용될 때는 원시값으로 컴파일러가 알아서 바꿔주니 인스턴스를 만드는데 사용하는 비용이 줄어든다.

 

 

2. 래퍼 객체

첨부한 블로그에 나온 상황인데 가져오는 이점은 비슷하다.

사용자의 편의성을 위해 래퍼 객체를 사용했을때 나오는 인스턴스 생성비용을 줄이기 위하는 방법으로 value class 를 사용하는것인데

fun reserveAlarm(alarmMessage: String, durationInMillis: Long) =
    println("$durationInMillis millis 후에 [$alarmMessage] 알람이 울립니다.")

이렇게 알람을 예약하고 예약된 메시지를 출력해주는 함수가 있다고 하자
그럼 reserveAlarm() 를 호출할때 이렇게 2000L 넣어줘야한다 

reserveAlarm("학교 가야지", 2000L)

여기서 2000L이 2초라고 바로 감이 오지는 않을것이다.
그래서 보통 래퍼 객체를 사용하게 되는데


이를 적용해본다면 코드는 이렇게 바뀐다.

fun reserveAlarm(alarmMessage: String, duration: Duration) =
    println("$duration.millis millis 후에 [$alarmMessage] 알람이 울립니다.")
    
reserveAlarm("학교 가야지", Duration.seconds(2))

이런식으로 Duration 클래스와 같은 래퍼 클래스를 사용하게 되면
함수의 가독성이 좋아진다는 장점이 있지만,
함수를 호출할 때마다 객체를 생성하여 호출해야한다는 비용이 발생하게된다.

 

이걸 Duration class 자체를 value class 로 만들어주면 인스턴스를 만들지 않기 때문에 메모리상 이점을 가져온다는것이다.

(래퍼 객체가 어떻게 돌아가는지 궁금하다면 첨부한 블로그를 들어가서보면 맹글링 어쩌구 까지 더 자세히 나와있다 들어가서 봐보자)

 

 

이제 용도와 강력함은 알아봤으니 value class 에 대해서 알아야하는 것에 대해 살펴보자

1. value class 가 정식 릴리즈 되기 이전에는 Inline class 라 불렸었다!!!

inline class 라고 코틀린 1.3 버전에서 알바테스트 버전으로 사용할수 있었고

코틀린 1.4 에서 베타 테스트 버전으로 사용할수 있었다.

 

2. @JvmInline 어노테이션

@JvmInline 어노테이션은 코틀린의 (Kotlin/JS, Kotlin/Native) 와 value class 호환을 위해 존재하는 어노테이션이다.

 

3. Data class 와 비교를 많이 한다 ??????!?!?!?!.

글들을 찾아보면 data class와 비교를 많이 하는데 애초에 용도도 다르고 의미도 다르니 같은 선상에 두는것이 이상하다고 생각한다.

근데 일단 value class 도 자동생성 메서드가 있고 data class에서 만들어주는것에 비해 조금이니 그 정도는 알아두자

데이터 클래스는 
equals(), toString(), hashCode(), copy(), componentN() 메소드를 자동 생성하는 반면에
value class는 
equals(), toString(), hashCode() 메소드만 자동 생성합니다.

 

4. value 클래스는 동일성 비교가 막혀있다(===)

애초에 당연하다 원시값으로 돌아다닐건데 주소비교를 왜하는가

 

5. 반드시 val 프로퍼티만 허용한다.

 

6. 하나의 프로퍼티만 사용가능하며 부생성자 또한 만들수 없다.

이 두가지는 추후 개선될것이라고 공식입장이 있다고 한다.

그리고 부생성자 만들고 오류메시지 보면 추후 개선될 예약기능이라고 뜬다.

 

이렇듯 기존 메모리 최적화를 캐싱으로 했던것을 자연스럽게 value class 로 대체할수 있다.

-> 아직은 프로퍼티 하나만 사용가능하고 부생성자를 만들수 없는 부분은 캐싱과 팩토리 메서드를 통해 대체해서 사용하면 될것이다.

 

 

PS. 토마스가 찾아낸것이지만 value class 는 인터페이스를 상속받아 생성할경우 원시값으로 최적회되는 이점을 잃게된다.

-> class에 대한 인스턴스를 생성하게 되니 상속이나 구현하는 용도로는 사용하지말자

 


클래스 간의 의존 관계 연결

클래스를 분리할 때 또는 클래스를 분리한 후 고민해야 할 부분은 '클래스 간의 의존관계를 어떻게 연결할 것인가?' 이다.

클래스 분리를 계속 해야한다고 학습했고 코드에서 조금씩 클래스가 분리되고 있을것이다. 그렇다면 각 클래스의 의존관계를 어떻게 설정해야 할까 고민하는 시기가 온것이다.

 

의존에대해서 맨날 의존성이 있다 뭐 의존성 역전 어쩌구 저쩌구 하지만 의존성이 과연 뭘까?

A가 B에 의존한다. 

간단하게 이야기 하자면 A에서 B를 Import 하고있는 것이다. A에서 B를 사용할수있다는것은 A가 B를 의존하고 있는것이다.

제이슨 코치님은 A가 B를 알고있다는 표현을 좋아한다고 한다 -> 의존이랑 같은말이다.

 

의존성 관계에 대해서는 DIP 원칙,  의존성이 서로 맞물려 있으면 안되고 의존성은 한쪽으로만 흘러야한다 등등 많은 의존성 관련 원칙이나 고려사항이 있겠지만 이번 학습주제는 의존관계를 표현하는 방법중 상속과 조합에 대해서 살펴보고 어떤상황에 사용해야하는 지를 살펴볼 것이다.

 

상속(is-a, is a kind of 관계)과 조합(has-a 관계)
- 일급 컬렉션을 구현할 때 접근 방법으로 상속과 조합 방법으로 구현할 수 있다.객체의 중복(Lotto와 WinningLotto)을 제거할 때 - 상속과 조합 방법으로 구현할 수 있다.
상속과 조합 중 어느 방법으로 구현하는 것이 더 좋을까?

로또랑 당첨로또의 중복되는 개념과 코드를 이런 상속 혹은 조합의 개념으로 해결하려고 시도해 봤을것이다.

로또,당첨로또 같이 뭔가 같은 뿌리에서 나온것같은 느낌인데 중복되는 조건,개념,코드 가 있다면 상속 혹은 조합으로 해결할수 있을것이다.

 


상속

 

상속
- 상속은 코드를 재사용하는 강력한 수단이지만 항상 최선은 아니다.
- 객체 사이의 관계를 너무 복잡하게 만들기도 하지만 근본적인 원인은 상속 그 자체가 아니다.
- 상속을 완전히 제거하는 것이 아니라 올바르게 사용하자.

상속은 강력한 수단중의 하나이다. 강한힘에 맞춰서 강한 책임이 따른다.

즉 잘못쓰면 진짜 원치않는 큰 부작용을 선물받을 수 있으니 적절한 상황에 잘 선택해서 사용해야한다

 

또한 상속을 무조건적으로 피해야한다고 생각하고 기피하는 경향도 있는데 그것또한 좋지 못하다.

적절한 상황에 맞춰서 잘 사용하자(상속이 무조건 악은 아니다.)

 

- 가상메서드

교육 자료에서 갑자기 가상메서드가 나와있는데 가상 메서드는 이렇다고 한다.

상속에서 상위 클래스와 하위 클래스에 같은 이름의 메서드가 존재할 때 호출되는 메서드는 인스턴스에 따라 결정된다.
선언한 클래스형이 아닌 생성된 인스턴스의 메서드를 호출하는 것.
인스턴스의 메서드가 호출되는 기술을 '가상 메서드(virtual method)'라고 한다.

 

지금부터는 상속이 부적절한 상황들을 쭉 살펴보며 언제 상속을 사용할지 피할지에 대해서 알아보자

 

1번 예시

open class Document {
    fun length(): Int {
        return content().size
    }

    open fun content(): ByteArray {
        // 문서의 내용을
        // 바이트 배열로 로드한다
    }
}

이런 Document 라는 class가 있다고 해보자

 

이 클래스를 살펴보면 

content 라는 함수에서 파일의 내용을 읽어서 ByteArray형태로 돌려주는 형태이고

 

length 라는 함수에서는 content 함수의 반환값의 길이를 반환하게 되어있다.

 

즉 length 함수는 파일의 길이를 반환하는 함수이다.

 

class EncryptedDocument: Document() {
    override fun content(): ByteArray {
        // 문서를 로드해서,
        // 즉시 복호화하고,
        // 복호화한 내용을 반환한다.
    }
}

이렇게 Document 를 상속받은 EncryptedDocument를 만들고

 

content 함수를 

파일을 로드해서 복호화해서 반환하는 형태로 오버라이딩 했다고 해보자

 

이러면 복호화 하는 경우를  예쁘게 잘 상속 받았다고 생각할수 있다.

(하지만 전혀 예쁘게 되지않았다.)

 

이렇게 오버라이딩한 클래스에서 length 함수를 호출해보자 

무엇이 나올까? ->복호화된 파일의 길이가 나오게 될것이다.(과연 우리가 암호화 되서 뒤지게 길어진 파일의 길이를 원할까?)

과연 사용자가 Document의 length를 복호화되어 쓸데없이 길어진 길이를 원할까? 아닐것이다.

분명 기존 용도와 전혀다른 이상한 사이드 이펙트가 일어난 것이다.

이런것이 상속의 잘못된 예시이다.

 

 이런것을 어려운말로 리스코프 치환원칙을 어긴것이라 한다.

리스코프 치환 원칙 (Liskov substitution principle)
“프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다.”
 계약에 의한 설계를 참고하라.

https://ko.wikipedia.org/wiki/%EB%A6%AC%EC%8A%A4%EC%BD%94%ED%94%84_%EC%B9%98%ED%99%98_%EC%9B%90%EC%B9%99

 

리스코프 치환 원칙 - 위키백과, 우리 모두의 백과사전

위키백과, 우리 모두의 백과사전. 치환성(영어: substitutability)은 객체 지향 프로그래밍 원칙이다. 컴퓨터 프로그램에서 자료형 S {\displaystyle S} 가 자료형 T {\displaystyle T} 의 서브타입라면 필요한

ko.wikipedia.org

리스코프 치환 원칙을 요약하자면 자식으로 완전히 부모타입에서 같은일을 할 수 있어야 한다는것이다.

그냥 단순 끼워져서 들어가는것이 아니라 그 도메인에 맞는 컨텍스트에 적절해야한다는 것이다.

 

이러한 예시로 위에 나온 예시도 있겠지만 원래 유명한 예제로  정사각형/직사각형, 오리/오리장난감 예시가 있다.

(근데 왜 내가 지금 구글링할때는 오리가 안나오지?)

 

정사각형/직사각형

https://blog.itcode.dev/posts/2021/08/15/liskov-subsitution-principle

 

[OOP] 객체지향 5원칙(SOLID) - 리스코프 치환 원칙 (Liskov Subsitution Principle) - 𝝅번째 알파카의 개발

리스코프 치환 원칙은 부모 객체와 이를 상속한 자식 객체가 있을 때 부모 객체를 호출하는 동작에서 자식 객체가 부모 객체를 완전히 대체할 수 있다는 원칙이다. 객체지향 언어에선 객체의 상

blog.itcode.dev

 

리스코프 치환 원칙과 컨텍스트에 관한글

https://siyoon210.tistory.com/156

 

LSP(리스코프 치환 원칙) - 오리와 오리장난감은 진짜 LSP 위반일까?

LSP(Liskov Substitution Principle - 리스코프 치환 원칙) 서브타입은 언제나 자신이 기반타입 (base type) (상위 타입) 으로 교체할 수 있어야 한다. S-O-L-I-D로 유명한 다섯가지 객체지향 설계 원칙중에서 LSP

siyoon210.tistory.com

 

어쨋든 결국 이런 예시들과 설명을 살펴보면 

리스코프 치환원칙은 현재 사용처에서 객체를 어떻게 바라보냐에 따라서 달라진다.

즉 상속을 할때 현재 객체에서 어떻게 사용하는지 생각해보고 상속 받고있는 자식 클래스가 정확히 부모클래스의 행위를 대체할수있는가?(의미적으로도) 를 생각해보고 사용해봐야 할것이다.

 

코틀린은 좀 개짱짱맨 언어라 자바에서 할수있는 뻘짓을 일단 다 막아놨다. (자바는 뻘짓에 오픈마인드인편)

예를들어 코틀린은 상속하려면 open 을 굳이굳이 가서 붙여줘야한다. -> 즉 상속을 하려면 한단계 거치고 생각하도록 만들어놓은것이다.

 

이런느낌으로 상속하기전에 생각했나요?

이런거보면 자바는 실수를 하지 않도록 노력해야하지만 코틀린은 실수하려면 노력해야한다는 제이슨의 말이 맞는것 같다

 

방금의 예시를 좀더 개선해 보자면 content 가 abstract 였다면 직접 구현이 강조 되기 때문에 -> length 메서드를 사용자가 이해하면서 쓸수 있을것이다. 

 

결국 상속의 무서움은 상위클래스의 내부 구현이 달라지면 코드 한줄 안건드린 하위클래스가 오동작할 수 있다는 것이다.

 

 

2번 예시

class LottoNumbers : HashSet<LottoNumber>() {
    var addCount = 0
        private set

    override fun add(lottoNumber: LottoNumber): Boolean {
        addCount++
        return super.add(lottoNumber)
    }

    override fun addAll(c: Collection<LottoNumber>): Boolean {
        addCount += c.size
        return super.addAll(c)
    }
}

두번째 예시는 로또 번호를 직접 구현하기 귀찮으니 HashSet을 상속받아서  구현한 경우를 살펴볼것이다.

 

쭉 살펴보면 코드가 그럴듯하다. 이코드를 직접 돌려보면 맛탱이가 가있다는것을 바로 알 수 있다.

예를들어 addAll 함수에 2개 크기의 리스트를 넣어주고 addCount 를 조회해보면 2가 나와야할것이다

리스트에 2개 넣었으니까 근데 더블스코어인 4가 나온다. 이유가 뭘까?

 

- HashSet의 내부구조 다 파악하기

HashSet은 addAll을 add를 여러번 호출하는 방법으로 구현되어있다. 

그래서 addAll함수는 그냥 addCount를 올려주는 오버라이딩을 하지 않아야 정상작동하는데

이것을 개발하는 개발자 관점에서 보면 과연 addAll이 add를 여러번 호출하는 구조로 짜여있다는걸 알까?

그정도 전지전능 하다면 막 상속 받아도 될것이다.

 

- addAll을 재정의 한다

뭐 다른 방법을 찾자면 addAll을 또 오버라이딩해서 내입맞에 맞게 바꾸면될텐데 이러면 상속받는 의미가 없다 모든걸 다 오버라이딩 해야할것이다.

또한 상위클래스의 메서드 동작을 다시 구현하는것은 참어렵고 시간도 더들고 오류를 내거나 성능을 떨어트릴수 있다.

 

그리고 코틀린에서 기본적으로 제공하는것을 오버라이딩 하는것도 웃기다

-> 바퀴를 다시 만들지 마라 라는 말이있다. 이미 석학들이 머리를 짜내서 만들어놓은거 우리가 만들어봐야 삐꾸날 가능성이 크다.

 

즉 이런 상황에서는 상속이 적절치 못한것이다.

 

 

이런 연유 때문에 상속이 부적절한 경우들이 많으니 이런경우 조합을 사용하라는 것이다!!!

 

 

 

조합

- 기존 클래스를 확장하는 대신 새로운 클래스를 만들고 private 필드로 기존 클래스의 인스턴스를 참조하게 하자.
- 상속은 반드시 하위 클래스가 상위 클래스의 '진짜' 하위 타입인 상황에서만 쓰여야 한다.
- 클래스 B가 클래스 A와 is-a 관계일 때만 클래스 A를 상속해야 한다.

상속은 진짜 하위 타입 일때만 사용해야한다. -> B가 진짜 A인가를 생각해보고 사용해야한다.

ex) 당첨로또는 진짜 로또 번호인가? -> 아니다 당첨번호가 있으니까 별개의 것이 된다.

class WinningNumber(
    val lottoNumbers: List<LottoNumber>,
    val bonus: LottoNumber
) : LottoNumbers(lottoNumbers)

-> 이렇게 상속 받으면 안됨 위에 예시처럼 사이드 이펙트 맛좀 볼래????!!!!!

이런경우에 상속 대신 조합 쓰게되는 것이다.

 

 

상속이 적절한 경우란 언제일까? 클래스의 행동을 확장(extend)하는 것이 아니라 정제(refine)할 때다. 확장이란 새로운 행동을 덧붙여 기존의 행동을 부분적으로 보완하는 것을 의미하고 정제란 부분적으로 불완전한 행동을 완전하게 만드는 것을 의미한다.

상속이 적절한 경우는

확장하는것이 아니라 불완전한것을 완전하게 만들기 위해서만 사용해야한다.

->abstract를 오버라이딩 하여 부족한것을 보완하기 위한 정도로만 사용되어야한다. 

(이걸 내가 해석하자면 인터페이스 처럼 쓰되 어느정도 진짜 공통되는 상태나 함수만 공유해야한다는 것으로 받아들였다.)

 

 

객체 지향 초기에 가장 중요시 여기는 개념은 재사용성(reusability)이었지만, 지금은 워낙 시스템이 방대해지고 잦은 변화가 발생하다 보니 유연성(flexiblity)이 더 중요한 개념이 되었다.

자바 초기에는 가전기구 만드는 언어니까 메모리가 제일중요해서 코드 최대한 줄이고 변수명도 축약하고 재사용성을 제1의 미덕으로 쳤는데

언어의 용도도 바뀌고 현재는 메모리가 넘치는 시대로 바뀌어 유연성이 더 중요해진것이다.

 

 


Private 함수 테스트 하고싶어요 !!!

미리말하는 결론: 클래스 분리해서 private함수를 옮겨라 -> 밑에는 의식의 흐름이니 그냥 안읽어도 된다. 

아니 이거 그 당시에는  예시까지 빠바박해서 수업했는데 지금 아무것도 기억에 남지 않았다 그래도 기억에 의존해서 복기 해보면

 

public로 노출되어있는 함수들은 대부분 여러개의 private함수를 하위에 두고있고 좀 덩치가 커서 테스트 하기 애매하다

-> 테스트 하려면 여러가지 준비해야할 요소가 많다. 예를 들어 로또가 당첨되었는지 확인하는 match 함수 같은게 있고 그것을 테스트 하자면 로또,당첨로또, 보너스 넘버 등등 뭘 엄청 많이 만들어서 테스트 해야한다.

 

그래서 match 같은 함수보면 private로 하위함수들이 있을가능성이 큰데 이런함수를 테스트 하고싶어진다.(쪼개서 테스트 하는 느낌이니까) -> 그리고 이런거 테스트의 준비규모가 작아지거나 준비 안해도됨

 

근데 문제는 이런함수들 private로 걸려있어서 테스트가 안된다. -> public로 풀면되는거아님? (리뷰어한테 리뷰로 쳐맞고싶음?)

 

모킹 프레임워크로 테스트 할 수 있다고한다. -> 리플렉션 기법으로 해킹해서 테스트 하는것이라고함(나중에 공부하겠지)

다좋은데 테스트 가독성 안좋아지고 문제가 나름있다고 한다. 리플렉션 api 이용해서 알아서 모킹 프레임워크처럼 만들수도 있을테지만 

가장 중요하게 아름답지 않다 -> 그냥 귀찮다(아름답지않다고 포장하지말자)

 

 

그럼 private를 public 으로 바꿀 근거를 만들어주면된다고 한다!!!!

다른클래스로 메서드를 옮겨버린다 -> Public로 바꿀수밖에없다. 

클래스를 분리해야할 이유가 하나 더 생긴것이다.

 

이로써 테스트가 간결해진다.

 


by로 위임을 해봅시다

위임(delegate pattern) 맨날 위임패턴이 좋다고 어쩌구 저쩌구 하는데 위임패턴이 뭔지 위임패턴이 왜 좋은지 부터 알아보자.

 

위임 패턴은 디자인패턴중 하나이다.

 

위임패턴에 대해 정말 예쁘고 이해가 잘되는 글이있어서 첨부한다. 궁금하다면 읽어보시면 짜릿짜릿하게 글이 좋다.

https://june0122.github.io/2021/08/21/design-pattern-delegate/

 

[디자인 패턴] 위임 패턴(Delegate Pattern)

소프트웨어 엔지니어링에서 Delegate pattern(위임 패턴)은 객체 합성이 상속과 동일하게 코드 재사용을 할 수 있도록 하는 객체 지향 디자인 패턴이다.

june0122.github.io

 

결론적으로 좀 정리해보자면 

조합(합성)을 상속처럼 이용할수 있도록 해줄수있는 수단이 위임패턴인 것이다.(맨날 조합조합 위임위임 했지만 이런식으로 판단해야하는지 몰랐다.)

어쨋든 위임을 통해 조합의 유연함을 얻으며 코드를 재활용할수 있는데 

이는 많은 보일러플레이트 코드를 동반하게된다. 

 

그래서 코틀린에서는 by라는 키워드를 통해 위임의 보일러플레이트 코드를 제거할수 있도록 언어차원에서 지원한다(코틀린 짱짱맨)

 

by는 단순 위임패턴을 통해서 보일러 플레이트 코드를 없애는것 뿐만아니라 property delegate 같은 더 많은 기능이있는데 이런건 여기서는 상관없고 복잡하니 나중에 by를 좀더 공부하고 글을 하나써야겠다.

 

어쨋든 by를 사용할때 생각해 보아야할것은 

1.상위 인터페이스를 꼭 구현해줘야지 사용할수있는데 이걸 대충 설계하는순간 유연성이 떨어질테니 많은 설계를 요하게 될것이다.

interface ClosedShape {
    fun area(): Int
}

class Rectangle(val width: Int, val height: Int) : ClosedShape {
    override fun area() = width * height
}

class Window(private val bounds: ClosedShape) : ClosedShape by bounds

 by를 이용하기위해선 ClosedShape 인터페이스가 필요하다!!!

그냥 언어차원의 지원방식이 아닌 방법으로 위임패턴을 구현한다면 상위 인터페이스가 필수는 아니게 될것이다.

 

2. 다양한 기능을 가지고있는 인터페이스/클래스(ex:List) 를 그냥 by로 연결해버린다면 원하지않는 기능들도 수반할수 있다.

 

이런 상황들을 고려해본 결과 나의취향은

-> 많은 기능을 가지고있고 그것을 다 적극 활용한다면 by를 이용해서 보일러플레이트 코드를 없애되

만약 일부의 기능만 이용한다면 그냥 by를 사용하지 않고 구현할것같다.

특히나 코틀린에서 기본적으로 제공하는 set,list이런건 by로 위임하지 않을것같다. 기능도 엄청 많거니와 내가 원치않는 별의별기능들이 다 딸려올테니 그중 일부만 가져다가 쓰는것이 올바르다고 생각한다.

 

이런이야기는 모두 취향과 관련된거니까 알아서 판단하면될것이다.

 

이제 수업시간에 나온 by의 사용처 예시를 정리할것이다. 이런 상황에 by를 쓸수있다는것을 기억하면 좋을것같다.

 

로또를 구현하며 

로또 번호의 집합을 일급컬랙션으로 LottoNumbers 같은거로 만드는 경험을 할것이다.

근데 그안의 상태로 들고있는 리스트를 밖으로 꺼내서 써야할일이 분명생기는데

일단 내부에서 처리하는것이 맞지만 foreach,map 를 사용하게되는경우

LottoNumbers.value.map{} 이런식으로 value를 보기싫게 쓰게된다.

 

이런걸 class내에서 해결하려면 

foreach,map 이런 기본제공 함수들을 한번더 클래스 내부에 정의해야하는 번거로운 보일러플레이트 코드가 생기는데

class LottoNumbers(val value: List<LottoNumber>) {

    fun <T> forEach(action: (T) -> Unit) {
        value.forEach { action }
    }
}

 

이런식으로 필요한 함수들 다 재정의 해줘야한다(by없이 List에 위임중)

 

뭐 이런것이 아니라면 List를 직접 상속해줘도 되겠지만 알다시피 상속은 무서운것이다 감히 알지도 못하면서 상속쓰면 씨게맞는다.

근데 List은 사실 인터페이스이다. 그래서 List<LottoNumber>를 구현해준다면 인터페이스의 모든함수를 다 오버라이드 해야할것이다(당연하다 인터페이스니까)

 

이럴때 by를 쓴다면 쉽게 사용해결할수도 있다.

class LottoNumbers(val value: List<LottoNumber>) : List<LottoNumber> by value

이렇게 관련함수 생성안하고도 쓸 수 있게된다(강력하긴하다)

 

 

-> 하지만 아까 말했던 유의점은 분명 다 따라올것이다 그러니 어떤 방법을 사용할지는 알아서 판단해서 사용해보자