Skip to content

Commit

Permalink
User bounded context에서 JPA 사용하도록 수정 (#348)
Browse files Browse the repository at this point in the history
* User bounded context에서 JPA 사용하도록 수정

* 리뷰: JpaRepository 구현체를 따로 두지 않기

* json list로 변경하는 건 나중에
  • Loading branch information
Zeniuus authored Jul 21, 2024
1 parent 7c54659 commit ac23698
Show file tree
Hide file tree
Showing 44 changed files with 420 additions and 517 deletions.
4 changes: 4 additions & 0 deletions app-server/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import org.springframework.boot.gradle.tasks.bundling.BootJar

plugins {
kotlin("jvm")
kotlin("plugin.spring")
kotlin("plugin.jpa")
id("org.springframework.boot")
id("io.gitlab.arturbosch.detekt")
id("io.spring.dependency-management")
Expand All @@ -27,6 +29,8 @@ val detektExcludedProjects = listOf(
)
subprojects {
apply(plugin = "kotlin")
apply(plugin = "kotlin-spring")
apply(plugin = "kotlin-jpa")

java {
sourceCompatibility = JavaVersion.VERSION_17
Expand Down
2 changes: 2 additions & 0 deletions app-server/settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ pluginManagement {
plugins {
kotlin("jvm") version kotlinVersion
kotlin("plugin.serialization") version kotlinVersion
kotlin("plugin.spring") version kotlinVersion apply false
kotlin("plugin.jpa") version kotlinVersion apply false

id("org.jetbrains.kotlin.plugin.spring") version kotlinVersion
id("org.springframework.boot") version springBootVersion
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import club.staircrusher.stdlib.di.annotation.Component
import club.staircrusher.stdlib.domain.SccDomainException
import club.staircrusher.stdlib.domain.entity.EntityIdGenerator
import club.staircrusher.user.application.port.out.persistence.UserRepository
import org.springframework.data.repository.findByIdOrNull
import java.time.Clock
import java.time.Instant

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
dependencies {
implementation(projects.crossCuttingConcern.application.serverEvent)

implementation("org.springframework.boot:spring-boot-starter-data-jpa")
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import club.staircrusher.user.domain.model.UserMobilityTool
import club.staircrusher.user.domain.service.PasswordEncryptor
import club.staircrusher.user.domain.service.UserAuthService
import kotlinx.coroutines.runBlocking
import org.springframework.data.repository.findByIdOrNull

@Component
class UserApplicationService(
Expand Down Expand Up @@ -56,7 +57,7 @@ class UserApplicationService(
if (normalizedNickname.length < 2) {
throw SccDomainException("최소 2자 이상의 닉네임을 설정해주세요.")
}
if (userRepository.findByNickname(normalizedNickname) != null) {
if (userRepository.findFirstByNickname(normalizedNickname) != null) {
throw SccDomainException("${normalizedNickname}은 이미 사용된 닉네임입니다.")
}
return userRepository.save(
Expand All @@ -77,7 +78,7 @@ class UserApplicationService(
nickname: String,
password: String
): AuthTokens = transactionManager.doInTransaction {
val user = userRepository.findByNickname(nickname) ?: throw SccDomainException("잘못된 계정입니다.")
val user = userRepository.findFirstByNickname(nickname) ?: throw SccDomainException("잘못된 계정입니다.")
if (user.isDeleted) {
throw SccDomainException("잘못된 계정입니다.")
}
Expand All @@ -96,7 +97,7 @@ class UserApplicationService(
mobilityTools: List<UserMobilityTool>,
isNewsLetterSubscriptionAgreed: Boolean,
): User = transactionManager.doInTransaction(TransactionIsolationLevel.REPEATABLE_READ) {
val user = userRepository.findById(userId)
val user = userRepository.findById(userId).get()
user.nickname = run {
val normalizedNickname = nickname.trim()
if (normalizedNickname.length < 2) {
Expand All @@ -105,7 +106,7 @@ class UserApplicationService(
SccDomainException.ErrorCode.INVALID_NICKNAME,
)
}
if (userRepository.findByNickname(normalizedNickname)?.takeIf { it.id != user.id } != null) {
if (userRepository.findFirstByNickname(normalizedNickname)?.takeIf { it.id != user.id } != null) {
throw SccDomainException(
"${normalizedNickname}은 이미 사용 중인 닉네임입니다.",
SccDomainException.ErrorCode.INVALID_NICKNAME,
Expand All @@ -121,7 +122,7 @@ class UserApplicationService(
SccDomainException.ErrorCode.INVALID_EMAIL,
)
}
if (userRepository.findByEmail(normalizedEmail)?.takeIf { it.id != user.id } != null) {
if (userRepository.findFirstByEmail(normalizedEmail)?.takeIf { it.id != user.id } != null) {
throw SccDomainException(
"${normalizedEmail}은 이미 사용 중인 이메일입니다.",
SccDomainException.ErrorCode.INVALID_EMAIL,
Expand All @@ -146,7 +147,7 @@ class UserApplicationService(
fun deleteUser(
userId: String,
) = transactionManager.doInTransaction (TransactionIsolationLevel.REPEATABLE_READ) {
val user = userRepository.findById(userId)
val user = userRepository.findById(userId).get()
user.delete(SccClock.instant())
userRepository.save(user)

Expand All @@ -158,11 +159,11 @@ class UserApplicationService(
}

fun getUsers(userIds: List<String>): List<User> = transactionManager.doInTransaction {
userRepository.findByIdIn(userIds)
userRepository.findAllById(userIds).toList()
}

fun getAllUsers(): List<User> {
return userRepository.findAll()
return userRepository.findAll().toList()
}

private fun subscribeToNewsLetter(userId: String, email: String, name: String) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import club.staircrusher.stdlib.persistence.TransactionManager
import club.staircrusher.user.application.port.out.persistence.UserRepository
import club.staircrusher.user.domain.exception.UserAuthenticationException
import club.staircrusher.user.domain.service.UserAuthService
import org.springframework.data.repository.findByIdOrNull

@Component
class UserAuthApplicationService(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@ class GetUserUseCase(
private val userRepository: UserRepository,
) {
fun handle(userId: String): User = transactionManager.doInTransaction {
return@doInTransaction userRepository.findById(userId)
return@doInTransaction userRepository.findById(userId).get()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class LoginWithAppleUseCase(
appleLoginService.getAppleLoginTokens(authorizationCode)
}

val userAuthInfo = userAuthInfoRepository.findByExternalId(UserAuthProviderType.APPLE, appleLoginTokens.idToken.appleLoginUserId)
val userAuthInfo = userAuthInfoRepository.findFirstByAuthProviderTypeAndExternalId(UserAuthProviderType.APPLE, appleLoginTokens.idToken.appleLoginUserId)
if (userAuthInfo != null) {
doLoginForExistingUser(userAuthInfo)
} else {
Expand All @@ -39,7 +39,7 @@ class LoginWithAppleUseCase(

private fun doLoginForExistingUser(userAuthInfo: UserAuthInfo): LoginResult {
val authTokens = userAuthService.issueTokens(userAuthInfo)
val user = userRepository.findById(userAuthInfo.userId)
val user = userRepository.findById(userAuthInfo.userId).get()
return LoginResult(
authTokens = authTokens,
user = user,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class LoginWithKakaoUseCase(
fun handle(kakaoRefreshToken: String, rawKakaoIdToken: String): LoginResult = transactionManager.doInTransaction {
val idToken = kakaoLoginService.parseIdToken(rawKakaoIdToken)

val userAuthInfo = userAuthInfoRepository.findByExternalId(UserAuthProviderType.KAKAO, idToken.kakaoSyncUserId)
val userAuthInfo = userAuthInfoRepository.findFirstByAuthProviderTypeAndExternalId(UserAuthProviderType.KAKAO, idToken.kakaoSyncUserId)
if (userAuthInfo != null) {
doLoginForExistingUser(userAuthInfo)
} else {
Expand All @@ -36,7 +36,7 @@ class LoginWithKakaoUseCase(

private fun doLoginForExistingUser(userAuthInfo: UserAuthInfo): LoginResult {
val authTokens = userAuthService.issueTokens(userAuthInfo)
val user = userRepository.findById(userAuthInfo.userId)
val user = userRepository.findById(userAuthInfo.userId).get()
return LoginResult(
authTokens = authTokens,
user = user,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package club.staircrusher.user.application.port.out.persistence

import club.staircrusher.stdlib.domain.repository.EntityRepository
import club.staircrusher.user.domain.model.UserAuthInfo
import club.staircrusher.user.domain.model.UserAuthProviderType
import org.springframework.data.repository.CrudRepository
import org.springframework.stereotype.Repository

interface UserAuthInfoRepository : EntityRepository<UserAuthInfo, String> {
fun findByExternalId(authProviderType: UserAuthProviderType, externalId: String): UserAuthInfo?
@Repository
interface UserAuthInfoRepository : CrudRepository<UserAuthInfo, String> {
fun findFirstByAuthProviderTypeAndExternalId(authProviderType: UserAuthProviderType, externalId: String): UserAuthInfo?
fun findByUserId(userId: String): List<UserAuthInfo>
fun removeByUserId(userId: String)
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
package club.staircrusher.user.application.port.out.persistence

import club.staircrusher.stdlib.domain.repository.EntityRepository
import club.staircrusher.user.domain.model.User
import org.springframework.data.repository.CrudRepository
import org.springframework.stereotype.Repository

@Repository
interface UserRepository : CrudRepository<User, String> {
fun findFirstByNickname(nickname: String): User?
fun findFirstByEmail(email: String): User?

interface UserRepository : EntityRepository<User, String> {
fun findByNickname(nickname: String): User?
fun findByEmail(email: String): User?
fun findByIdIn(ids: Collection<String>): List<User>
fun findAll(): List<User>
data class CreateUserParams(
val nickname: String,
@Deprecated("패스워드 로그인은 사라질 예정") val password: String?,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
dependencies {
implementation("jakarta.persistence:jakarta.persistence-api")
}
Original file line number Diff line number Diff line change
@@ -1,22 +1,51 @@
package club.staircrusher.user.domain.model

import jakarta.persistence.Column
import jakarta.persistence.Convert
import jakarta.persistence.Entity
import jakarta.persistence.Id
import jakarta.persistence.Table
import jakarta.persistence.Transient
import java.time.Instant

data class User(
@Entity
@Table(name = "scc_user")
class User(
@Id
val id: String,
var nickname: String,
@Deprecated("닉네임 로그인은 사라질 예정") var encryptedPassword: String?,
var instagramId: String?,
var email: String?, // FIXME: 레거시 계정이 모두 사라지면 non-nullable로 변경
@Column(columnDefinition = "TEXT")
@Convert(converter = UserMobilityToolListToTextAttributeConverter::class)
val mobilityTools: MutableList<UserMobilityTool>,
val createdAt: Instant,
) {
var deletedAt: Instant? = null // private으로 둘 방법이 없을까? 지금은 persistence_model 모듈에서 써야 해서 안 된다.

@get:Transient
val isDeleted: Boolean
get() = deletedAt != null

fun delete(deletedAt: Instant) {
this.deletedAt = deletedAt
}

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

other as User

return id == other.id
}

override fun hashCode(): Int {
return id.hashCode()
}

override fun toString(): String {
return "User(nickname='$nickname', id='$id', encryptedPassword=$encryptedPassword, instagramId=$instagramId, email=$email, mobilityTools=$mobilityTools, createdAt=$createdAt, deletedAt=$deletedAt, isDeleted=$isDeleted)"
}
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
package club.staircrusher.user.domain.model

import club.staircrusher.stdlib.clock.SccClock
import jakarta.persistence.Entity
import jakarta.persistence.EnumType
import jakarta.persistence.Enumerated
import jakarta.persistence.Id
import java.time.Instant

@Entity
class UserAuthInfo(
@Id
val id: String,
val userId: String,
@Enumerated(EnumType.STRING)
val authProviderType: UserAuthProviderType,
val externalId: String,
var externalRefreshToken: String,
var externalRefreshTokenExpiresAt: Instant,
val createdAt: Instant = SccClock.instant()
) {

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package club.staircrusher.user.domain.model

import club.staircrusher.stdlib.jpa.ListToTextAttributeConverter
import jakarta.persistence.Converter

@Converter
object UserMobilityToolListToTextAttributeConverter : ListToTextAttributeConverter<UserMobilityTool>() {
override fun convertElementToTextColumn(element: UserMobilityTool): String {
return element.name
}

override fun convertElementFromTextColumn(text: String): UserMobilityTool {
return UserMobilityTool.valueOf(text)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ plugins {
id("org.springframework.boot")
id("io.spring.dependency-management")
kotlin("plugin.serialization")
kotlin("plugin.spring")
}

dependencies {
Expand All @@ -11,6 +12,8 @@ dependencies {
implementation(projects.crossCuttingConcern.infra.persistenceModel)
implementation(projects.crossCuttingConcern.infra.serverEvent)
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-jdbc")
implementation("org.springframework:spring-webflux")
implementation("io.projectreactor.netty:reactor-netty")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ class DeleteUserTest : UserITBase() {
.andExpect {
status { isNoContent() }
transactionManager.doInTransaction {
val deletedUser = userRepository.findById(user.id)
val deletedUser = userRepository.findById(user.id).get()
assertTrue(deletedUser.isDeleted)
}
}
Expand Down Expand Up @@ -88,7 +88,7 @@ class DeleteUserTest : UserITBase() {
val result = getResult(LoginResultDto::class)

val newUser = transactionManager.doInTransaction {
userRepository.findById(result.user.id)
userRepository.findById(result.user.id).get()
}

val newUserAuthInfo = transactionManager.doInTransaction {
Expand All @@ -113,7 +113,7 @@ class DeleteUserTest : UserITBase() {
.run {
val result = getResult(LoginResultDto::class)
val otherUser = transactionManager.doInTransaction {
userRepository.findById(result.user.id)
userRepository.findById(result.user.id).get()
}
assertNotNull(otherUser)
assertNotEquals(user.id, otherUser.id)
Expand All @@ -128,7 +128,7 @@ class DeleteUserTest : UserITBase() {
// then
status { isNoContent() }
transactionManager.doInTransaction {
val deletedUser = userRepository.findById(user.id)
val deletedUser = userRepository.findById(user.id).get()
assertTrue(deletedUser.isDeleted)

val userAuthInfo = userAuthInfoRepository.findByUserId(user.id).find { it.authProviderType == UserAuthProviderType.KAKAO }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ class LoginWithAppleTest : UserITBase() {
@BeforeEach
fun setUp() {
transactionManager.doInTransaction {
userAuthInfoRepository.removeAll()
userRepository.removeAll()
userAuthInfoRepository.deleteAll()
userRepository.deleteAll()
}
}

Expand Down Expand Up @@ -70,7 +70,7 @@ class LoginWithAppleTest : UserITBase() {
val result = getResult(LoginResultDto::class)

val newUser = transactionManager.doInTransaction {
userRepository.findById(result.user.id)
userRepository.findById(result.user.id).get()
}
assertNull(newUser.encryptedPassword)
assertNull(newUser.email)
Expand All @@ -93,7 +93,7 @@ class LoginWithAppleTest : UserITBase() {
val result = getResult(LoginResultDto::class)

val user = transactionManager.doInTransaction {
userRepository.findById(result.user.id)
userRepository.findById(result.user.id).get()
}
assertEquals(userId, user.id)
assertNull(user.email)
Expand Down
Loading

0 comments on commit ac23698

Please sign in to comment.