목차
발표자 소개
•
개발자가 되고 싶지만 개발은 하지 않던 학생
•
화장품 홍보 솔루션 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
복사
난관 돌파 끝에 행복..?
•
아직 미숙한 언어 숙련도, 부족한 모듈, 유지보수 가능 인원 부족등의 이슈가 있음.
•
미숙한 언어 숙련은 지속적인 학습으로
•
유지보수 가능 인원 부족은 만들어둔 코틀린 교육 커리큘럼을 통한 교육으로
•
부족한 모듈은 지속적인 개발로 해결해야 한다.
•
하지만, 주니어 개발자들에게 비전제시는 성공했다고 판단.