안녕하세요 😊
제가 아는 내용을 이해하기 쉽고, 재미있게 설명드려 여러분들이 성장하실 때 행복함을 느끼는 개발자 최태현입니다.
(현) 캐치테이블[와드] 소프트웨어 엔지니어
(전) 스타트업 소프트웨어 엔지니어 리드
(전) 배달의민족[우아한형제들] 소프트웨어 엔지니어
(교육활동) Next Step 리뷰어 다수 참여, 공기관 & 스타트업 경진대회 강사 및 멘토, 스파르타 코딩클럽 멘토
한국과학기술원 (KAIST) 졸업
강의
수강평
- 자바와 스프링 부트로 생애 최초 서버 만들기, 누구나 쉽게 개발부터 배포까지! [서버 개발 올인원 패키지]
- 2시간으로 끝내는 코루틴
- 자바 개발자를 위한 코틀린 입문(Java to Kotlin Starter Guide)
- 자바 개발자를 위한 코틀린 입문(Java to Kotlin Starter Guide)
- 실전! 코틀린과 스프링 부트로 도서관리 애플리케이션 개발하기 (Java 프로젝트 리팩토링)
게시글
질문&답변
Dispatchers 별 차이점 관련 질문
안녕하세요 기우님! ☺ 아주 좋은 질문 감사드립니다.답변을 드리기에 앞서 https://myungpyo.medium.com/%EC%BD%94%EB%A3%A8%ED%8B%B4-%EB%94%94%EC%8A%A4%ED%8C%A8%EC%B3%90-%EC%A1%B0%EA%B8%88-%EB%8D%94-%EC%82%B4%ED%8E%B4%EB%B3%B4%EA%B8%B0-92db58efca24 와 같은 좋은 글도 한 번 보고 오시면 좋을 것 같아요! 이제 하나씩 답변 드려 보겠습니다.각 Dispatcher는 내부적으로 스레드풀을 가지고 있고, 작업이 넘어왔을 때 내부적으로 관리하는 스레드 풀에서 스레드에 코루틴을 할당하는 방식으로 동작하는걸까요?이 내용 먼저 말씀드려볼게요!네 맞습니다. Default와 IO는 내부적으로 스레드 풀을 갖고 있고 (예를 들어 IO의 경우 최대 64개를 갖고 있습니다.) 코루틴이 들어오면 내부적으로 관리하는 pool 내의 thread에서 코루틴이 실행되고 중단되었다가 다시 pool 내의 어떤 thread에 배정해 코루틴을 실행하는 식으로 동작합니다.다만, Default의 스레드 풀을 IO가 포함하고 있는 관계라 (ex. https://sandn.tistory.com/112) IO 에서 실행하더라도 Default 라는 thread name을 확인할 수도 있습니다. Uncofined는 조금 특이합니다. 자체 스레드 풀이 존재하지 않고, suspend 함수가 재개될 때의 그 스레드에서 다음 코드를 이어서 실행시켜 버려요! (Default나 IO는 suspend 함수가 재개 되면 다시 내 pool의 스레드로 코드를 가져와 실행하죠) 이런 특수성 때문에 Unconfined의 경우 실무에서 사용 할 일이 없는 Dispatcher 라고 봐주시면 될 것 같습니다. 내부 코드를 봤을땐 스레드 관리 방식과 스케줄링 방식 등에 차이가 있는 것 같은데, 각각의 Dispatcher가 언제 사용되는지, 가장 주요한 차이점은 뭔지가 궁금해요!!그럼 남은 Dispatcher는 Default와 IO인데요! 간단하게 Default는 CPU-intensive한 작업, I/O는 IO-intensive한 작업에 사용하시면 됩니다. ☺ 마지막으로Default 나 IO Dispatcher의 스레드 개수나 graceful shutdown 설정 등을 커스텀할 수 있는지도 궁금합니다!에 대해서도 답변 드리면 Dispatcher 자체적으로 graceful shutdown hook을 제공하지는 않지만, 마치 Executors 에 대해 저희가 직접 graceful shutdown 기능을 구현할 수 있는 것처럼 직접 구현할 수 있습니다.(ex. https://easywritten.com/post/best-way-to-shutdown-executor-service-in-java/ 에서 Java 표준 docs에 적혀 있는 Executor graceful shutdown 구현을 설명과 함께 보실 수 있습니다)// val scope = CoroutineScope(...) suspend fun shutdownGracefully(timeoutMs: Long = 5000) { // 더 이상 새 작업을 받지 않게 하고 scope.coroutineContext[Job]?.cancel() // 자식들에게 취소 신호 // 일정 시간 내 정상 종료를 대기 withTimeoutOrNull(timeoutMs) { rootJob.join() } // 필요시 미완료 작업 강제 중단 로직 추가 }와 같은 코드를 구현할 수 있겠죠. (구현하기 나름입니다) 역시 비슷하게 custom dispatcher를 만드는 경우도 보통은 Executors.newFixedThreadPool(n).asCoroutineDispatcher 를 사용하니 이 때 만들어지는 Executor 를 이용해 graceful shutdown을 집어 넣을 수 있습니다 ☺ 또한 Executor 를 이용해 custom 스레드 풀을 직접 만들어 스레드 수를 제어하는 방법을 쓰실 수도 있고요!아니면 원래 존재하는 Default pool에서 개수를 격리하거나 아예 원하는 thread 수를 만들어 IO 처럼 사용할 수도 있습니다. 바로 limitedParallelism 함수인데요 공식 문서는 https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-dispatcher/limited-parallelism.html 에서 보실 수 있고, https://velog.io/@geronimo124/Kotlin-Coroutines-dispatchers 와 같은 블로그에서는 Default와 IO에 limitedParallelism 함수를 사용했을 때 어떤 메커니즘이 적용되는지 그림으로 쉽게 보실 수 있습니다! ☺ 답변이 도움이 되었으면 좋겠습니다. 감사합니다. 🙇
- 0
- 1
- 19
질문&답변
Kotlin과 Java의 현업에서의 활용
안녕하세요 건영님! ☺ 질문 남겨 주셔서 감사합니다.혹시 Kotlin + SpringBoot의 프로젝트 작업을 진행하실 때 전혀 Java를 사용하지 않고 활용하시는지 아니면 Java와 Kotlin을 같이 사용하는 지 현업에서 실제 어떻게 활용하시는지 궁금합니다.부터 말씀드리면 선택권이 저에게 있다면 Kotlin + Spring 만 사용하는 편이고요! 만약 Java로 된 Legacy 코드가 이미 존재한다면 상황에 따라 유연하게 대응하는 편입니다. (점진적으로 Kotlin으로 된 코드를 늘려 Java와 Kotlin을 혼용하거나 Java로 코드를 작성할 수도 있죠 ☺) 추가적으로 apache 같은 외부 라이브러리를 가져와 사용할 때는 Kotlin으로 따로 제작된 코드를 굳이 사용하지는 않고 Java로 된 외부 라이브러리를 바로 사용하는 편입니다. 물론, 간단한 함수만 필요하다면 굳이 외부 의존성을 가져오지 않고 해당 함수만 Kotlin으로 옮겨오거나 Kotlin 표준 라이브러리에서 이미 제공하고 있는 기능은 아닌지 검색해보기는 합니다. 답변이 도움이 되었으면 좋겠습니다. 감사합니다. 🙇
- 0
- 2
- 29
질문&답변
aws 프리티어 정책
안녕하세요! best11gh님~ 좋은 질문 남겨주셔서 감사합니다. ☺결론부터 말씀드리면, 강의 실습을 얼마나 빠르게 하는지에 따라 달라질 수 있어 보이는데요.. 저희가 사용했던 EC2 저사양 인스턴스는 (t2.micro) 시간당 요금이 0.0144 USD 로 중단 없이 사용했을 때 1년 반 정도는 쓸 수 있는 것으로 보입니다.(물론, 프리티어 제한이 6개월이라 실제로는 6개월 리밋에 걸릴 것 같네요~) 때문에 강의를 듣다가 중도에 멈춰 시간이 아주 지연되는 경우는 크레딧 안에서 커버되기 어렵겠지만 그래도 몇 달 안에 강의를 모두 마무리 하시면 충분히 프리티어 리소스로 실습이 가능하실 것으로 보입니다.구체적인 비용 내용은https://aws.amazon.com/ko/ec2/pricing/on-demand/에서 리전 (region, 지역) 을 서울로 선택하고 micro 시리즈를 확인해보시면 되어요! 또한 62강 내용에서 다룬리소스를 정리하는 방법비용을 계산하는 방법모두 여전히 적용 가능한 내용입니다! ☺ 답변이 도움이 되었으면 좋겠습니다. 감사합니다.
- 1
- 2
- 22
질문&답변
Querydsl 도입
안녕하세요! Mola-Mola님! ☺ 좋은 질문 감사합니다.말씀해주신 레거시 쿼리가 무엇인지에 따라 답변이 조금 달라질 것 같아요~ native raw level 쿼리인지 아니면 Spring Data JPA 레벨의 쿼리인지 또 다른 무언가인지에 따라서요!다만 저는 개인적으로 Querydsl을 좋아하는 이유가 말씀해주신 컴파일 레벨의 오류 감지 외에도동적 쿼리 작성의 편안함query의 중복 제거 원활함 도 있는데요! 동적 쿼리란, https://jojoldu.tistory.com/394 에서도 확인할 수 있는 것처럼 하나의 API 에서 N개의 변수를 사용자 선택에 따라 조합하는 경우를 의미합니다. 만약 raw level SQL을 직접 사용한다면, 상황에 맞는 쿼리를 모두 작성해주거나 꽤 높은 확률로 오류 위험성이 높은 문자열 연산을 사용해야 하죠. 반면 Querydsl은 매우 용이한 동적 쿼리 작성을 도와줍니다. 또한 Query를 code-level에서 building 하는 방식이기에 중복 제거도 매우 원할합니다.예를 들어 A + B + C 테이블이 조인된 상황에서 where c1 = ? 만 쓰는 쿼리가 있고, where 조건 없이 group by 만 덧붙이는 경우가 있다고 할 때 "A + B + C 테이블이 조인된 상황" 자체를 baseQuery로 한 번만 선언한 후 각 쿼리의 변화되는 부분만 JPAQuery에 이어 붙일 수 있죠이를 활용하면 코틀린의 확장 함수를 적용해 레고 블록을 조립하듯 타입 안전하게 쿼리를 작성할 수 있게 됩니다. 아주 간단한 예시로 아래와 같은 확장 함수를 선언해두면 Querydsl 기반 쿼리 어디든 paging(pageable)만 덧붙여 페이징 쿼리를 만들어 낼 수 있죠. fun JPAQuery.paging(pageable: Pageable): JPAQuery { return this.limit(pageable.pageSize.toLong()) .offset(pageable.offset) } fun JPAQuery.paging(page: Int, size: Int): JPAQuery { val pageable = Pageable.ofSize(size).withPage(page) return this.limit(pageable.pageSize.toLong()) .offset(pageable.offset) }아주 작은 예시이고 활용법은 매우 무궁무진 합니다. ☺ 추가적으로 "레거시는 두고 신규 추가되는 부분만 Querydsl를 도입하는 식으로 가면 될까요?"라고 말씀해주셨는데요~ 참 어려운 부분입니다. 상황에 따라신규 추가되는 부분만 새로운 기술을 도입하기신규 피처 외에도 기존 기능을 변경할 때도 새로운 기술을 적용하기최선을 다해 기존 기능을 완전히 마이그레이션 하기등 여러 전락이 있거든요. 이는 제가 답변 드리기 어렵고 프로젝트 상황에 따라, 함께 일하는 분들의 성향에 따라 많이 달라질 수 있습니다.답변이 도움이 되었으면 좋겠습니다. 감사합니다. 🙇
- 0
- 2
- 24
질문&답변
fetch join DISTINCT 중복제거
안녕하세요! 지원님 ☺ 좋은 질문 감사드립니다.양쪽에서 다르게 동작한다는 것은 다음과 같은 의미일 것 같아요~SQL distinct 의 경우 동일한 column 조합으로만 Database가 데이터를 걸러낸다는 뜻이고JPA에서 사용한 JPQL distinct는 join을 통해 나온 엔티티를 부모 엔티티 기준으로 서버가 (즉, 애플리케이션이) 묶어 낸다는 뜻이죠!또한 두 방식 모두 제가 아는 선에서는 "완전한 중복 제거"를 보장합니다. 그래서 GPT가 어떤 맥락에서 완전한 중복 제거가 보장되지 않는다고 한지는 정확히 모르겠네요~ 🤔 관련해서 한 가지 주의하면 좋은 부분은 데이터 양이 꽤 많아지면 (가령 수백만 건의 부모의 1:N 구조를 한 번에 fetch join + distinct 하게 되면...) 수백만개의 컬렉션을 연산해야 하기에 속도가 느려질 수 있다는 문제가 있고, 이를 해결하는 접근은 다양하게 있어 상황에 따라 적당한 방법을 선택해야 합니다. 가장 대표적으로는 조회되는 패턴을 확인해 미리 집계해 두는 방식도 있어요~ 답변이 도움이 되었으면 좋겠습니다. 감사합니다. 🙇
- 1
- 2
- 22
질문&답변
표준 예외와 커스텀 예외 사용 전략 질문
안녕하세요 민우님! 🙂 좋은 질문 감사합니다. AI 인턴이 남겨준 링크도 한 번 읽어보시면 좋을 것 같고요!남겨주신 질문에 대해서도 직접적인 답변을 드려보면...스프링이 제공하는 표준 예외(IllegalArgumentException, IllegalStateException)와 비즈니스 로직을 표현하는 커스텀 예외(NotFoundUserException) 사이에서 표준 예외를 사용하는 경우와 커스텀 예외를 사용하는 경우에 대한 기준을 알 수 있을까요? 우선 IllegalArgumentException, IllegalStateException 와 같은 Exception class는 스프링이 제공하는 것이 아니라 자바 (JDK) 에서 제공하고 있습니다. 둘을 구분해주시면 조금 더 좋을 것 같아요!또한, 저는 다음과 같은 기준을 갖고 커스텀 예외를 사용하는 편입니다. 매개변수에 대한 검증이 실패한 경우이 경우는 매개변수, 즉 Argument가 잘못되었다는 의미이기에 표준 예외 IllegalArgumentException 를 사용합니다.또한 들어온 매개변수로 인해 특정 Entity를 찾지 못하는데 예외를 던져야 하는 경우도 매개변수가 잘못되어 Entity를 찾지 못했을 확률이 높기에 IllegalArgumentException 를 사용하는 편입니다.특정 로직을 수행하다 객체의 pre state가 잘못되었을 경우비즈니스 로직을 구현하다보면 특정 로직에는 precondition 이라 불리는 가정이 들어갑니다. 예를 들어 신호등이 초록색으로 바뀌기 위해서는 빨간색이거나 노란색이어야 한다는 가정이죠. 이런 경우 색을 초록색으로 하려고 했는데 진작 초록색이라면, 로직 실행전의 객체 필드 (= 상태) 가 잘못 되었다는 의미이기에 IllegalStateException 을 사용합니다.클라이언트에서 예외를 분기 처리 해야 하는 경우단순히 HTTP status로 내용을 전달하기에 충분하지 않고, 상황별로 적절한 문구를 사용자에게 노출해야 한다거나 FE 로직에 대한 분기를 태워야하는 경우는 custom JSON을 내려줘야만 하고, (보통 @ControllerAdvice 같은 친구를 사용하죠 ☺) 이런 경우는 커스텀 예외를 사용합니다.다만, 매번 커스텀 예외 클래스를 만드는 것은 매우 번거롭고 클래스 수가 늘어나 관리가 어렵기에 Exception class는 1개를 만들고, 상황에 따른 enum을 구성하는 것을 매우 선호합니다.https://velog.io/@jds7979/Spring-%EC%A0%84%EC%97%AD%EC%98%88%EC%99%B8%EC%B2%98%EB%A6%AC-with-EnumGlobal-Exception-Handler 와 같은 글을 참고해보시면 좋을 것 같아요! (이 글에 100% 동의한다기 보다 예시로 봐주시면 되어요!)감사합니다. 🙇
- 0
- 3
- 21
질문&답변
data class와 자바의 Lombok을 이용한 DTO 클래스 차이 질문
안녕하세요! 🙂 eunseo lay님~ 질문 주셔서 감사합니다.실무(특히 스프링 기반 개발)에서는 DTO를 작성할 때 자바의 Lombok을 주로 사용하는지, 아니면 코틀린의 data class를 더 많이 사용하는지입니다.또한, 두 방식의 차이점과 선택 기준에 대해서도 알고 싶습니다.에 대해서 답변 드려 보면 대부분의 프로젝트는 단일 언어로 구성되는 경우가 많습니다. (물론 하나의 시스템은 여러 컴포넌트로 구성되고 각 컴포넌트는 또 다시 여러 프로젝트로 구성되어 상호 간의 언어가 다를 수 있죠!) 때문에 다음과 같이 경우의 수를 나눠서 설명 드리겠습니다.JDK 17 이상의 자바 프로젝트JDK 버전이 17 이상이라면 record class를 언어 레벨에서 정식 지원하기에 굳이 lombok을 사용할 필요가 없어집니다. 때문에 recor class를 쓰시면 되어요!JDK 17 이전의 자바 프로젝트만약 JDK 17 이전이라면 lombok을 사용하시면 됩니다. 1번, 2번 경우 모두 자바로 구성된 단일 프로젝트라 data class와 lombok을 고민하지 않아도 됩니다. ☺코틀린 프로젝트반대로 코틀린 프로젝트인 경우는 당연히 data class를 사용하시면 됩니다. 이 경우도 고민할 필요가 없죠.자바와 코틀린이 혼재되어 있는 프로젝트이 경우를 상정하고 질문 주신 것 같아요. 두 언어가 혼재되어 있다면 사실 어디론가 이동하는 중간 과정일 확률이 매우 높습니다. 같은 JDK-based 언어라서 굳이 언어를 둘 다 사용할 필요가 없거든요. 때문에 이 프로젝트가 가장 이상적으로 되었을 때 자바만 남을지, 코틀린만 남을지를 팀원분들과 함께 고민해보시고, 결론에 따라 위 1~3번 중 하나를 사용하시면 됩니다. ☺ 답변을 보시면 아시겠지만, 결론적으로 코틀린 data class와 java lombok을 고민하는 경우는 거의 없었어요! 제가 지나온 대부분의 프로젝트는 최근 몇 년간 kotlin 으로 구성되어 있어 data class만 위주로 사용했습니다.답변이 도움이 되었으면 좋겠습니다. 감사합니다. 🙇
- 0
- 1
- 22
질문&답변
spring 개념적인 질문
안녕하세요 리꼬님! 🙂 좋은 질문 감사합니다.AI 인턴이 잘 짚어준 것 같아요!스프링 컨테이너는 기본적으로 싱글톤 패턴을 적용하여 Bean을 관리합니다라고 얘기해준 것처럼 Bean을 선언하게 되면 default 설정이 싱글톤으로 동작하도록 되어 있습니다.즉 하나의 bean을 여러 곳에서 사용해도 (= 주입 받아도) 하나의 instance인 것이죠 물론 여러 옵션이 있어 특정 경계마다 새로 객체를 만들 수도 있고, 주입 받을 때마다 새로운 객체를 받을 수도 있지만, 제 개인적인 경험상 99.9%는 싱글톤을 사용하면 충분했습니다.답변이 도움이 되었으면 좋겠습니다. 감사합니다. 🙇
- 0
- 2
- 23
질문&답변
코틀린에서 상속 시, 주의점에 대한 질문
안녕하세요! 보내주신 블로그 글까지 확인 완료했습니다. ☺상위 클래스와 하위 클래스의 초기화 시점에 따라 기본값이 들어갈 수 있다는 것 까지는 확인하신 것 같아요!분명 선언된 것은 non-nullable 타입인데 어떻게 null이 나오는거야?에 대해서만 더 말씀드리면. 코틀린은 결국 컴파일을 통해 .class 파일로 변환되어 JVM에서 실행됩니다!! 그리고 코틀린의 변수가 non-nullable인지, nullable인지 구분할 수 있는 것은 코틀린 언어 단에서 지원하는 것이지 .class 레벨에서는 관련한 스펙이 없습니다.따라서 코틀린의 String? 을 디컴파일 하건 String 을 디컴파일 하건 코드가 실행될 때는 java의 String 처럼 동작하게 되고, 아직 초기화 되지 않은 경우는 reference type의 기본값인 null이 들어가게 되는 것입니다. 마찬가지로 이런 원리 때문에 "플랫폼 타입" 이라는 개념이 존재하게 됩니다! 코틀린의 코드가 자바의 코드를 부르는 경우, 결국 .class 코드가 .class 코드를 부르는 것이기에 코틀린 쪽에서 non-nullable 선언을 하더라도 자바 쪽 .class 에서 null이 넘어오면 코드가 깨질 수 있게 되는 것이죠.답변이 도움이 되었으면 좋겠습니다! 감사합니다. 🙇
- 1
- 3
- 37
질문&답변
내용 이해 질문
안녕하세요! 🙂 네 맞습니다! number를 가져오려고 하지만, 하위 클래스의 number는 아직 생성자가 호출되기 전이라 기본값 (이 경우는 Java로 decompile 해보시면 primitive int가 있기에 기본값이 0이 됩니다) 이 나오게 됩니다.다음 질문에서도 한 번에 답변 드릴 수 있도록 하겠습니다. 🙇
- 0
- 1
- 28