코프링스러운 코드 작성법
https://github.com/woowacourse/service-apply
GitHub - woowacourse/service-apply: 우아한테크코스 지원부터 최종 합격까지 모든 과정을 관리한다.
우아한테크코스 지원부터 최종 합격까지 모든 과정을 관리한다. Contribute to woowacourse/service-apply development by creating an account on GitHub.
github.com
이 코드를 보면서 코틀린스러움에 익숙해지고 있다.. 수많은 멀티모듈, msa 속에서 찾아낸 모놀리ㅜㅜ
크롤링을 코틀린으로 해본 경험이 있긴 한데 함수형 프로그래밍 흉내만 낸 셈이라 코프링에 완전 익숙해지지 않은 거 같다
게시판 하나 제대로 만들고, 코루틴을 한번 사용해보는 게 이 프로젝트의 목적이다.
asciidocs를 사용하고 있는 프로젝트라 TDD도 살짝 곁들이고, 하나씩 정리하면서 체화해보자
1. Kotlin에서는 파일명=클래스명 이 아니다
Java에서는 관행적으로, 컴파일 에러를 피하기 위해 public 클래스 하나당 하나의 .java파일이 필수다.
이 제약 때문에 파일이 엄청나게 쪼개지고, 의미적으로 연관된 클래스들이라도 흩어지게 된다.
하지만 Kotlin은, 하나의 .kt 파일에 관련 있는 여러 클래스를 자유롭게 선언할 수 있다
예를 들어, 아래처럼 Dto들을 하나의 파일로 묶어줄 수 있다.
data class UserResponse(
val id: Long,
val name: String,
val email: String,
val phoneNumber: String,
val gender: Gender,
val birthday: LocalDate,
) {
constructor(user: User): this(
user.id,
user.name,
user.email,
user.phoneNumber,
user.gender,
user.birthday,
)
}
data class RegisterUserRequest(
...
){
fun toEntity(): User {
return User(name, email, phoneNumber, gender, birthday, password)
}
}
data class LoginUserRequest(
...
){
}
이처럼 의미적으로 묶인(User)클래스를 한 파일 안에 자연스럽게 같이 선언할 수 있다.
이렇게 하면 관련 로직을 한 눈에 파악하기 쉽고, 지나치게 쪼개진 파일 구조에서 오는 유지보수 부담이 적어지는 이점이 있다.
만약 자바라면.. 예
Kotlin에서는 interface의 외부 확장이 자연스럽다
Java에서는 UserRepository처럼 인터페이스를 만들면, 그 안에서 구현하지 않는 이상 해당 인터페이스 자체의 기능은 제한적이었다.
그리고 비즈니스 로직이 들어간 확장은 보통 UserService, Component같은 다른 클래스에 둬야 했다.
예를 들어 현재 진행중인 Co-op 프로젝트에서는 자주 사용되는 Member관련 Repository 조회 + 예외 처리 메서드들을
Optional 처리가 필요한 객체들 때문에 Component로 분리하여 관리하고 있었다.
서로 다른 파일에서 관리중이라 유지보수가 불편한 점이 있는데,
Kotlin에서는 인터페이스 외부에서 바로 확장함수로 기능을 추가할 수 있다.
fun UserRepository.findByEmail(email: String): User? = findByInformationEmail(email)
fun UserRepository.findById(id: Long): User = findByIdOrNull(id)
?: throw NoSuchElementException("회원이 존재하지 않습니다. id: $id")
fun UserRepository.existsByEmail(email: String): Boolean = existsByInformationEmail(email)
interface UserRepository : JpaRepository<User, Long> {
fun findByInformationEmail(email: String): User?
fun existsByInformationEmail(email: String): Boolean
}
이처럼 확장함수로 도메인 로직을 같은 파일, 인터페이스 밖에서 자연스럽게 정의할 수 있다.
불필요한 Service, Component없이도 단순한 로직들을 정의 가능하고, 선언부가 인터페이스 근처에 있으니 가독성도 좋고 한눈에 역할이 보이는 장점이 있다.
Private class(파일 단위 접근 제어)
Java에서는 기본적으로 클래스 단위로 파일을 구성하기 때문에, 클래스는 대부분 Public이어야 재사용이 가능하다.
하지만 Kotlin은 클래스 단위가 아닌, 파일 단위로 접근 제어가 가능하다.
그래서 어떤 클래스가 외부에 노출될 필요가 없고, 오직 내부 구현에만 사용된다면 private class로 선언해도 문제가 없다.
// AwsMailSender.kt
@Component
class AwsMailSender(...) : MailSender {
...
override fun sendBcc(...) {
...
val multipartMimeMessage = MultipartMimeMessage(...)
...
}
}
// 여기는 외부에서 보이지 않음
@Component
private class MultipartMimeMessage {
...
}
이처럼 이메일 전송 클래스 내부에서만 사용되는 MultipartMimeMessage는, 외부에서 접근할 필요가 없으므로 이처럼 선언할 수 있다.
정말 유용하다고 생각되는 게, 트랜잭션 분리가 필요한 로직을 캡슐화 하기에 너무 좋을 거 같다!
private class로 선언하더라도, Component를 사용하면 spring bean으로 등록할 수 있다.
테스트도 코틀린스럽게
진짜 경악한 부분인데..
java에서는 같은 뿌리를 가지고 있더라도 테스트코드를 메서드 하나하나 정의해줘야 한다.
그렇기 때문에 given, when이 일정부분 중복되어도 어쩔 수 없는 부분이 존재한다.
@Test
void 비밀번호_일치하면_변경된다() {
// given
// when
// then
}
@Test
void 비밀번호_불일치하면_예외() {
// given (거의 똑같음)
// when
// then
}
같은 뿌리에서 파생된 테스트라도 메서드 단위로 분리되기 때문에 재사용이 어렵다 ㅠ
하지만 Kotlin DSL을 사용한다면 ..
Given("회원이 존재할 때") {
val user = createUser()
every { repo.findByEmail(any()) } returns user
When("올바른 정보로 비밀번호를 초기화하면") {
userService.resetPassword(...)
Then("초기화됨") {
...
}
}
When("틀린 정보로 초기화하면") {
Then("예외 발생") {
...
}
}
}
이처럼 Given 블록에서 공통된 상태를 한번 정의해두고, When-Then 블록에서 분기처리하는 방식을 사용할 수 있다!
각 람다에다 이름을 붙여서 테스트할 수 있다. 미친 생산성;;
이는 Kotlin이 가진 함수형 문법, 람다 등등 장점 덕분이다. 테스트코드도 결국 읽히는 문장이어야 한다는 함수형 철학이 반영된 거 같다
아 재밌네