Search

Kotlin Injection 초안

목차

발표자 소개

개발자가 되고 싶지만 개발은 하지 않던 학생
화장품 홍보 솔루션 SI → 채용 관리 솔루션(백엔드)
현재 마이다스인에서 채용관리 솔루션 백엔드 스쿼드장으로 근무중입니다.

왜 굳이 코틀린을 도입했을까?

팀원들의 전체적인 경력은 1~4년차 주니어 / 미들급
맡고 있는 프로젝트는 상당히 많은데 모두 자바기반의 프로젝트이고 대부분의 업무가 VoC 처리하는 유지 보수성의 업무들
주니어 개발자에게는 돈도 중요하지만 비전제시도 중요한 포인트라고 생각.
우아한 테크 세미나에서 이동욱님이 발표하신 제어할 수 없는 것에 의존하지 않기 라는 주제의 세미나를 들었는데 핵심적인 내용중 이런 내용이 있었음.(링크)
제어할 수 없는 영역과 제어할 수 있는 영역을 확실히 구분짓고 제어할 수 있는 영역을 강화하자.
내가 개발자들의 연봉, 복지, 인사평가를 제어할 수는 없다.
주니어 개발자들에게 개발 본연의 재미와 비전을 제시해줄 수는 있다.

첫 번째 난관: 주제 선정

토이프로젝트로 코틀린을 하는건 한계가 명확함.
회사일이 바쁘면 안하게 될 확률이 높다.
인프라부터 다 같이 새로 고민해야하는 비용이 크다.
기존 프로젝트 전체 리뉴얼이라는 과업이 조직개편으로 좌초되면서 우리끼리 할 수 있는 현실적인 범위 필요.
단기 기능 개발이 아닌 최소 세 달 정도의 시간을 가질 수 있는 기능 선정 필요.
회의끝에 기획측에서 하고싶어했던 항목 중 전체 지원자 관리 기능이 있어서 이 기능에 대해서 코틀린으로 개발하고싶다는 의견 제시
다행히 셀장님도 긍정적인 반응이였기에 긍정적으로 결정

두 번째 난관: 코틀린

팀원 중에서 코틀린을 할 줄 아는 사람이 한명도 없음.
1년 전부터 공부하자고 책도 샀지만, 일이 바쁘다는 핑계와 다른 기술 이슈등으로 다른 기술 서적을 먼저 보다보니 아무도 코틀린을 제대로 할 줄 아는사람이 없는 상황
내가 하자고 한 이상 선행학습 미리 시작하겠다고 결정
인프런에서 코틀린관련 강의중 자바개발자를 대상으로 하면서 되도록 짧은 강의를 찾아서 완강
넥스트스텝의 TDD, Clean Code With Kotlin 과정 수강및 2등으로 수료
이펙티브 코틀린 서적 구매해서 완독 및 포스팅
최소한의 기술 스택 완비

세 번째 난관: 다른팀과의 협의

우리팀끼리는 협의가 되었는데, 논의가 되다보니 다른 팀에서도 합류할 수 있다는 가능성이 생겨서 기술 스택 회의 진행
코틀린등에 대한 기술에 대해서 러닝커브 우려
자바와의 호환성 100%이고, 개발에 들어가기 전에 한달~두달 내가 미션기반으로 코틀린 강의 하겠다고 제안
넥스트 스텝에서 들었던 과정을 기반으로 내부적으로 스터디 및 코드리뷰 시작
코틀린 강의 자료
코드리뷰 흔적
이렇게 코틀린 도입에 필요한 사전준비는 모두 되었다.
하지만, 이제 본격적인 기술적 난관들을 돌파해야 했다.

막간: 우아한 멀티 모듈

새로 개발하는 기능은 아예 새로운 서버에서 새로운 마음으로 구축하기로 결정
xml 기반의 스프링 → 스프링 부트로
mybatis → jpa로
java → kotlin으로
nexus 기반의 모듈 구조 → 멀티 모듈 구조로
기존
멀티모듈

네 번째 난관: DB to Entity

우리가 관리하는 데이터베이스는 약 10개 이상의 스키마와 1000개 이상의 테이블이 있음.
하나하나 JPA Entity로 작성하는건 사실상 불가능
고려되었던 기술
JooQ Codegen
간편하게 스키마와 테이블정보를 읽어서 객체 생성 가능하다는 장점
생성 결과 보여주면 좋을 듯
하이버네이트 캐싱설정을 직접 다시 해줘야하는 문제
테이블 하나를 위해 생성되야하는 객체들이 많다보니, 모두 파악하면서 사용하기에는 비용이 큼
Bootify
DDL을 추출해서 Bootify라는 사이트에서 코틀린 엔티티로 변환 가능
연관관계도 모두 핸들링이 가능함.
하지만 너무 많은 테이블 ddl을 넣으면 인식을 못하는 문제
날짜타입등 변경하고 싶은부분들을 핸들링해주기가 힘듬.
JPA Buddy - 선택
기존 IDE(IntelliJ)의 플러그인으로 사용법이 간편
위에서 말한 문제들이 모두 해결
엔티티가 모두 open var로 지정되서 생성된다는 문제는 있지만 컨벤션으로 감수 가능
JPA Buddy를 통해 DB to Entity 를 진행하고, open var로 지정되어 위변조가 쉬운 문제는 코드 컨벤션 룰을 정해서 처리하기로 함.
프로젝트의 하위 모듈인 db 모듈은 엔티티와 기본 리포지토리를 생성을 제외하고는 어떠한 코드 수정도 허용하지 않기로 결정
그외 추가적인 작업은 도메인 모듈에서 Wrapper클래스를 만들어서 관리하도록 한다.

다섯 번째 난관: 복잡한 DB 조회 로직

검색에 들어가는 조건들이 약 80개 +@ 테이블에 다 나눠서 들어가 있음
join절, where절, grouping절, order절 모두 이 많은 테이블들을 항상 포함시키는건 문제로 파악
피드백: 기존 방식에서 어떻게 확장 함수가 도입되며 바뀌는지에 대한 코드가 있으면 좋겠다
코틀린의 확장함수를 이용해 공통로직 중복 최소화
fun searchResumeByCondition(condition: Condition) : List<CmsResumeFinderRs> { val query = jpaQueryFactory.select(resultProjection).from(지원서) query.leftJoin( condition = condition, target = Q대학교 predicate = { condition.대학교조건 != null } ) { query, target -> query.on( 지원서.대학교Sn.eq(target.대학교Sn) ) }.leftJoin( condition = condition, target = Q전공 predicate = { condition.전공조건 != null } ) { query, target -> query.on( 지원서.전공Sn.eq(target.전공Sn) ) }.leftJoin( condition = condition, target = Q최종학력 predicate = { condition.최종학력조건 != null } ) { query, target -> query.on( 지원서.최종학력Sn.eq(target.최종학력Sn) ) } }
Kotlin
복사
private fun <T, Q : EntityPath<*>> JPAQuery<T>.leftJoin( condition: CmsFilterTotalCondition, target: Q, predicate: (CmsFilterTotalCondition) -> Boolean, afterLogic: (JPAQuery<T>, Q) -> JPAQuery<T> ): JPAQuery<T> { predicate(condition).takeIf { it }?.let { return afterLogic(this.leftJoin(target), target) } ?: return this } ... private fun <T> JPAQuery<T>.applyCondition( condition: CmsFilterTotalCondition, user: SecurityUser, isSimilarityKeywordLimit: Boolean, isContentQuery: Boolean, kmsKey: String ): JPAQuery<T> { this.applyJoinByTotalCondition(condition = condition) .applyWhereByTotalCondition( user = user, condition = condition, isSimilarityKeywordLimit = isSimilarityKeywordLimit, kmsKey = kmsKey ) if (isContentQuery) { this.applyGroupByTotalCondition() .applyHavingByTotalCondition(condition = condition) .applyOrderByTotalCondition(condition = condition) } return this }
Kotlin
복사
from절에 서브쿼리가 들어가야하는 항목이 있었는데, JPA queryDSL 특성상 from절 서브쿼리를 사용할 수 없는 상황.
@SubSelect 애노테이션을 활용해 조회시에만 생성되는 virtual View 테이블 엔티티 객체를 만들어 from절 join절에 사용할 수 있도록 적용
쿼리 파라미터가 들어가야하는 상황까진 아직 반영이 안되는 문제가 있음
일단은 where절로 위치를 바꿔서 조건들을 넣고 있음.
@Entity @Immutable @SubSelect({가상 뷰 테이블 생성 쿼리}) class ViewTableName { ... }
Kotlin
복사
sql 함수는 확장함수를 이용해서 재사용성 높힘
fun DatePath<LocalDate>.diffMonths(endDate: DatePath<LocalDate>): NumberExpression<Int> { return Expressions.numberTemplate( Int::class.java, "TIMESTAMPDIFF(MONTH, {0}, {1})", this, endDate ) }
Kotlin
복사
코루틴을 활용한 페이징 처리 최적화
internal fun <T> asyncApplyPagination( pageable: Pageable, contentQuery: () -> JPAQuery<T>, countQuery: () -> JPAQuery<Long> ): CmsPageImpl<T> = runBlocking { val jpaContentQuery = contentQuery() val content = async { jpaContentQuery.applyPageable(pageable).fetch() } val count = async { countQuery().fetchFirst() } val page = PageImpl(content.await(), pageable, count.await()) CmsPageImpl.from(page) }
Kotlin
복사

난관 돌파 끝에 행복..?

아직 미숙한 언어 숙련도, 부족한 모듈, 유지보수 가능 인원 부족등의 이슈가 있음.
미숙한 언어 숙련은 지속적인 학습으로
유지보수 가능 인원 부족은 만들어둔 코틀린 교육 커리큘럼을 통한 교육으로
부족한 모듈은 지속적인 개발로 해결해야 한다.
하지만, 주니어 개발자들에게 비전제시는 성공했다고 판단.