해결된 질문
작성
·
172
·
수정됨
2
안녕하세요! 최태현 지식공유자님 코틀린 강의 만들어주셔서 감사합니다! 💚
실제로 코프링 개발자로 1년정도 근무했던 경험이 있었는데.. 일했던 날짜와 한참 늦은 2024년 말이지만 추후에 강의를 완강하게 됐는데 매우 유익했습니다 몇가지 질문이 있어서 질문 남깁니다!!
질문들
TabWidth에 대해서
IntelliJ에 보시면 tabWidth를 2로 설정하셨는데, 특별한 이유가 있으신가요?
보통 저는 4로 설정하는 편이어서요!
순수함수에 대해서
제가 알기로는 내부의 scope에서 동작할때 외부의 요인이 내부의 값을 바꾸지 못하거나 내부의 요인으로 외부의 값이 바뀌지 못하계 경계가 있어서 side effect가 없는 함수를 뜻하는 걸로 알고있는데요
Java는 그렇기에 Stream api를 사용할때 final이 아닌 외부의 값을 참조하지 못하기때문에 좀 더 안전하게 작성할 수 있는데, 코틀린은 Closure, Currying 등을 허용하기 때문에 개발자의 실수를 더 많이 일으킬 가능성을 열어둔 것은 아닌지 궁금합니다
OOP와 FP에 대해서
자바같은 경우는 함수형 인터페이스또는 인터페이스 구현체를 익명클래스나 익명함수 등으로 넘겨서 FP를 모방하는걸로 알고있는데요, 대신 최소한의 getter/setter를 추가하고 행위를 나타내는 메서드명을 외부로 노출하고(public..), 그 메서드는 내부호출(private)로 캡슐화를 할 수 있는데, 코틀린처럼 invoke가 될 수 있는 함수를 인자로 넘기면 아래와 특정한 행위를 나타내는 이름이 아닌 행위 자체를 넘기게 돼서 객체의 역할과 책임을 나타내는 메서드가 없게 되서 rich domain model이 아닌 anemic domain model이 되는게 아닌지 궁금합니다
// OOP
fun doHobby(person: Person?) {
println("${person?.hobby}생활을 즐기고 있습니다")
}
fun beAging(person: Person?) {
println("내년에 ${person?.age?.plus(1)}만큼 늙어갈 예정입니다")
}
fun mainOOP() {
val person = Person("보키", 20)
doHobby(person)
beAging(person)
}
// FP
fun doSomething(person: Person?, run: () -> Unit) {
}
fun mainFP() {
Person("보키", 20).run {
doSomething(this) {
println("${hobby}생활을 즐기고 있습니다")
}
}
Person("보키", 20).run {
doSomething(this) {
println("내년에 ${age+1}만큼 늙어갈 예정입니다")
}
}
Person("보키", 20).run {
doSomething(this) {
println("런닝중입니다 건들지마시오")
}
}
}
data class에 대해서
대부분의 기능들이 강의에 녹아있기는 하지만 data class의 강력한 기능 중 하나인 copy 메서드는 백엔드의 프로덕션 개발과 테스트 코드에서 어디 부분에 사용될 수 있을까요?
이건 많이 강의 주제를 넘어간 질문이긴 하지만.... Kotlin+Springboot+JPA 를 사용할때 data class/class 중 어떤 클래스로 Entity를 만드시는지 궁금합니다.
MySQL or MongoDB or PostgreSQL에서 어떤 클래스와 궁합이 좋은지. data class를 사용한다면 copy를 어느 부분에 사용하면 좋을지도 알고싶습니다!
강의내용과는 많이 무관하기에(코틀린이 아닌 서버, 타 라이브러리, 인프라(DB)까지 논의확장을 하였음) 답하기 어렵다면 안해주셔도 괜찮습니다
Scope Func에 대해서
저는 Kafka, Configuration 등에서 기본객체를 가져와서 apply를 이용해서 초기값 -> 덮어씌우는 방식을 주로 사용했고 with구문을 이용해서 log를 편하게 했었는데 scope func의 또 다른 좋은 선례도 알고싶습니다!
Ex1)
private fun <K : Any, V : Any> initConsumerProps(
keyDeSerClass: Class<out Deserializer<K>>,
valueDeSerClass: Class<out Deserializer<V>>
): Properties = Properties().apply {
put(BOOTSTRAP_SERVERS_CONFIG, "localhost:9092")
put(KEY_DESERIALIZER_CLASS_CONFIG, keyDeSerClass.name)
put(VALUE_DESERIALIZER_CLASS_CONFIG, valueDeSerClass.name)
put(GROUP_ID_CONFIG, "group-02")
put(MAX_POLL_INTERVAL_MS_CONFIG, "60000")
}
Ex2)
for (record in consumerRecords) {
with(record) {
logger.info { "key: ${key()}, partition: ${partition()}, offset: ${offset()}, value: ${value()}" }
}
}
method reference에 대해서
어떤 코드에서는 let(::XX), 또 다른 코드에서는 let { ... }방식이 사용될 때가 있는데, 어떤게 성능상 또는 가독성에서 좋은지도 궁금합니다!
답변 2
1
[4. data class]
질문을 상세히 나눠서 답변드리겠습니다. 🙇
data class의 강력한 기능 중 하나인 copy 메서드는 백엔드의 프로덕션 개발과 테스트 코드에서 어디 부분에 사용될 수 있을까요?
저는 둘 모두 사용할 수 있다고 생각합니다.
대표적으로는 어떠한 객체의 값을 바꿔야 하는데 val
필드를 var
로 변경하기는 싫을 때, copy()
를 사용할 수 있지 않을까 싶어요. 예를 들어 class Person(val name: String, val age: Int)
를 설계 했다고 해보죠. 이제 Person
의 나이가 올라가는 것을 구현해야 하는데 이 Person 클래스를 불변으로 유지하고 싶다면 this.copy(age = age + 1)
로 쉽게 구현할 수 있지 않을까 싶습니다.
이 예시는 필드가 2개라서 다른 방법을 사용해도 간단하지만..
class Person (..) { // 필드 생략
fun gerOlder(): Person {
return Person( // copy 미사용
name = this.name,
age = this.age,
)
}
}
필드가 수십개가 된다면 copy가 조금 더 편할거에요!
또한 꼭 불변 객체가 아니더라도 비슷한 use case 라면 (= 똑같은 타입의 인스턴스가 새로 필요한데 일부 값만 바꿔야 한다면) 종종 copy를 유용하게 사용할 수 있을 것 같습니다. ☺
Kotlin+Springboot+JPA 를 사용할때 data class/class 중 어떤 클래스로 Entity를 만드시는지 궁금합니다.
MySQL or MongoDB or PostgreSQL에서 어떤 클래스와 궁합이 좋은지. data class를 사용한다면 copy를 어느 부분에 사용하면 좋을지도 알고싶습니다!
아마 이 질문은 특정 DB에서 ORM을 사용할 때 테이블을 어떤 종류의 클래스로 매핑하는지 질문을 주신것 같아요! 개인적으로는 RDB (= JPA)를 사용할 때는 값 객체라면 (ex. @Embeddable
) data class, 그렇지 않다면 class를 사용하는 편이고요, 혹시나 간혹 계층 구초가 필요하면 sealed class도 활용해서 테이블에 매핑하기도 합니다! 몽고도 동일한 규칙을 선호하는 편입니다. 🙂
[5. Scope Function]
아마 개인마다 선호하시는 방식이 다를 것 같아요! 제 개인적으로 scope function을 가장 많이 활용하는 경우는..
nullable 한 값을 처리해야 할 때 ?.let { } ?: XXX
처리를 많이 하는 편이고요!
apply는 프로덕션 코드에서 잘 사용하지 않는 편입니다. 🥲 apply는 결국 setter를 활용하는 것 같은 느낌이 들고 (혹은 setter를 쉽게 접근할 수 있도록 하고) 코드 상에서 들여쓰기가 깊어 지는 것을 경계하는 편이라서요.
그래서 작성해주신 카프카 설정을 예시로 보여드리면,
private fun <K : Any, V : Any> initConsumerProps(
keyDeSerClass: Class<out Deserializer<K>>,
valueDeSerClass: Class<out Deserializer<V>>
): Properties {
val config = mapOf(
BOOTSTRAP_SERVERS_CONFIG to "localhost:9092",
KEY_DESERIALIZER_CLASS_CONFIG to keyDeSerClass.name,
... // 생략
)
return Properties(config)
}
라고 작성하는 편입니다.
물론 정말 setter를 직접적으로 쓰지 않는 이상 큰 차이는 없다고 생각해서, 기존에 적어주신 스타일대로 코드가 적혀 있다면 굳이 변경하지는 않을 것 같습니다.
with
같은 경우는 객체 변환에 잘 활용하는 편인 것 같아요! 보내주신 예시는 저라면 다음과 같이 작성했을 것 같습니다.
consumerRecords.forEach { record ->
logger.info { "key: ${record.key()}, partition: ${record.partition()}, offset: ${record.offset()}, value: ${record.value()}" }
}
그리고 만약 이런 코드가 2회 이상 발견된다면... 어딘가에
fun Record<K, V>.toLog(): String {
return "key: ${key()}, partition: ${partition()}, offset: ${offset()}, value: ${value()}"
}
와 같은 확장함수를 만들고..
consumerRecords.forEach { record -> record.toLog() }
만 다양한 곳에서 쓰지 않았을까 싶습니다. (List<Record>
단위로 확장 함수를 만들 수도 있고요!)
워낙 case가 다양하다보니 기억이 바로 나는 건 이정도인 것 같습니다. 😊
[6. method reference에 대해서]
결론부터 말씀드리면, 저는 개인적으로 레퍼런스 방식과 x -> x() 방식 모두 괜찮다고 생각하는데
중요한건 들어가 있는 타입을 명확하게 알게 해주는 거라고 생각해요!
예를 들어 let { it.xxx() }
보다는 let { num -> num.xxx() }
처럼 사용하는 편이 가독성 면에서 더 좋다라고 생각합니다.
아이고~ 답변이 충분했으면 좋겠네요!
강의를 의미 있게 들어 주시고, 질문도 열심히 해주셔서 정말 감사드립니다! 🙇
1
안녕하세요 보키님!! ☺ 아유~ 경험이 있으시면 다소 쉬우셨을 수도 있었을텐데 도움이 되었다니 다행이네요! 🙏
질문 주신 내용에 대해 하나씩 답변 드려 보겠습니다!
[1. TabWidth]
시작은 가벼운 질문이군요~~ 저는 개인적으로 tab-width 2를 선호하는데 4로 했을 경우 depth 가 살짝 깊어지거나 변수/함수 등의 네이밍이 길어지면, x-scroll이 생겨서 화면 분할을 하더라도 한 눈에 코드가 들어오지 않는 경우가 간혹 있더라고요! 🥺
그래서 개인적으로는 tab-width 2를 선호하고, 그렇다고 4 depth를 싫어하는 것은 아니라 회사에서는 기존 convention을 따라가는 편입니다. 🙂
[2. 순수함수와 Closure]
맞는 말씀이십니다. 👍 다만, 원래 자바에서도 가변 필드를 다음과 같은 꼼수를 통해 lambda에 넣을 수 있었습니다. 때문에 오히려 가변 필드를 lambda에 넣을 때에 불필요한 보일러 플레이트 코드가 필요한 경우도 있었죠.
int num = 0
list.stream()
.map { x -> x.handle(num) } // 마치 final이 아닌 값은 절대 못넘길 것 같지만..
public class MyNumber {
private int value = 0;
}
final MyNumber num = new MyNumber(0);
list.stream()
.map { x -> x.handle(num) } // 이렇게 하면 num이라는 final Object를 넘겨서 내부 적으로 int value 필드를 수정할 수 있게 됩니다
때문에 제 개인적인 생각으로는, 차라리 가변 필드도 쉽게 람다에 넘길 수 있게 해서 언어 편의성을 높이는 선택을 한게 아닐까 싶습니다.
추가로, 개인적으로는 람다에 가변 변수를 넘길 일 자체가 드물다고 생각합니다. 경험이 생기면 자연스럽게 명령-질의 분리를 통해 특정 값을 바꿀 수 있는 scope을 제한하기 때문이죠. 이런 코드가 훨씬 직관적이기도 하고요.
[3. OOP와 FP에 대해서]
rich domain model이 아닌 anemic domain model이 되는게 아닌지 궁금합니다
라는 부분이 핵심으로 보입니다. 🙂 우선 잘 알고 계시겠지만, Java 8 이전에는 FP에 대한 지원이 충분하지 않아서 인터페이스 + 익명 클래스 조합을 많이 사용했고, 이 방식의 번거로움으로 (자바에 대한 선호도가 떨어지다가..) 자바 8의 람다가 등장하게 되었는데요. 람다의 등장으로 인해 Java도 말씀해주신 방식대로 프로그래밍을 할 수 있습니다. (실제로 XXTemplate
은 대부분 그러한 패턴이 적용되어 있고요!)
하지만 코틀린은 자바 보다 조금 더 편한 방식으로 FP를 지원하기 때문에, 자칫 잘못하면 본래 객체 안에 있어야 할 코드를 밖으로 빠지게 된다거나 지나치게 많은 인자를 함수로 사용해 유지보수 비용을 높이는 선택을 하기 조금 더 쉬워진 것은 맞다고 생각합니다.
그렇지만 저는 결국 도메인 모델을 풍부하게 혹은 빈약하게 만드는 것은 개발자에게 달려 있다고 생각합니다. 누군가는 똑같은 요구사항을 구현할 때 절차지향적으로 작성하기도 하고, 객체지향적으로 작성하기도 하며, 함수를 지나치게 많이 쪼개 대다수의 사람들이 이해하기 어렵게 만들 수도 있죠. 결국 프로그래밍 언어는 하나의 도구일 뿐이고, 그 도구를 어떻게 활용하는지는 개발자의 몫이 아닐까 싶습니다. ☺
(이어서 답변 드릴게요!!)
아이고 태현님!!
1번부터 6번까지 상세히 답변해주셔서 감사합니다!!
경험이 있었어도 사수는 없었던지라 코틀린 기초문법만 보고 코프링을 들어갔었습니다..ㅠ
1번: 그렇군요! 코드 스타일! 저도 동의해요.
개인 선호 스타일의 주장이 팀원들의 consensus(합의)로 이어지거나
회사에서 써오던 convention을 쓰는게 맞죠!
2번: 오! 그러네요 외부변수도 쓸 수 있군요..하핫 저도 stream api나 kotlin collection 확장함수시에 외부변수랑 같이 짝짜꿍했던 적이 드물어서 가능한지 몰랐었어요..! where구문이랑 projection으로 필요한 부분만 떼와서 dto 변환만 했던지라..! 결국 프로그래머가 어떻게 하는지 차이군요
3번: 2번과 이어서 애플리케이션에 절차/객체/함수 지향으로 갈지는 개발자의 몫이라는 거군요! 감사합니다
4번: 지식공유자님의 copy 예시를 들어주셔서 감사합니다 :)
5번: apply가 그런단점이 있을수도 있군요! 감사합니다. with는 저는 주로 안썼던것같은데 저도 객체변환에 써봐야겠어요
6번: 전 method ref를 어떤 때에 언제 써야할지 궁금했었는데 네이밍을 강조해주셨군요! 저도 프론트쪽 했을때
2번 방식으로 쓰자고 팀원분들한테도 전파했던것 같아요. 감사합니다
++
바로 고급편도 이어서 들을 예정이에요! ㅎㅎ
덕분에 많이 알아가고 성장합니다. 🌱 🙇🏻♂️