Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
DB_HOST=localhost
DB_PORT=5432
DB_NAME=momogo
DB_USERNAME=momogo
DB_PASSWORD=momogo
38 changes: 37 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,37 @@
# momogo-server
# momogo-server

## 패키지 규칙

- `presentation`: Controller를 두고 HTTP 요청과 응답 변환을 담당한다.
- `application`: Service를 두고 도메인 흐름과 트랜잭션을 담당한다.
- `domain`: 프레임워크에 의존하지 않는 순수 도메인 객체와 비즈니스 규칙을 둔다.
- `infrastructure.database.entity`: JPA 매핑 클래스를 두며 이름은 `Entity`로 끝낸다.
- `infrastructure.database.repository`: Service가 직접 사용하는 Spring Data `Repository`를 둔다.
- 값 검증은 도메인 객체가 담당하고 JPA Entity에는 비즈니스 로직을 두지 않는다.
- 의존 흐름은 `Controller → Service → Repository` 순서를 따른다.

## 로컬 PostgreSQL 실행

Docker Compose로 PostgreSQL을 실행한다.

```shell
docker compose up -d postgres
```

`local` 프로필로 애플리케이션을 실행한다.

```shell
./gradlew bootRun --args='--spring.profiles.active=local'
```

PostgreSQL에 직접 접속할 때는 컨테이너의 `psql`을 사용한다.

```shell
docker compose exec postgres psql -U momogo -d momogo
```

종료할 때는 다음 명령을 사용한다. 데이터는 Docker 볼륨에 유지된다.

```shell
docker compose down
```
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.jetbrains.kotlin:kotlin-reflect'
runtimeOnly 'org.postgresql:postgresql'
testAndDevelopmentOnly 'com.h2database:h2'
testRuntimeOnly 'com.h2database:h2'
testImplementation 'org.springframework.boot:spring-boot-starter-webmvc-test'
testImplementation platform('io.kotest:kotest-bom:6.1.5')
testImplementation 'io.kotest:kotest-runner-junit5'
Expand Down
19 changes: 19 additions & 0 deletions compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
services:
postgres:
image: postgres:17-alpine
environment:
POSTGRES_DB: ${DB_NAME:-momogo}
POSTGRES_USER: ${DB_USERNAME:-momogo}
POSTGRES_PASSWORD: ${DB_PASSWORD:-momogo}
ports:
- "${DB_PORT:-5432}:5432"
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 5s
timeout: 5s
retries: 10

volumes:
postgres-data:
36 changes: 36 additions & 0 deletions src/main/kotlin/com/mogumogu/momogo/domain/user/LoginAccount.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.mogumogu.momogo.domain.user

class LoginAccount(
val id: Long? = null,
val userId: Long,
var provider: LoginProvider,
providerId: String,
) {
var providerId: String = providerId
set(value) {
validateProviderId(value)
field = value
}

init {
validateProviderId(providerId)
}

fun changeProvider(
provider: LoginProvider,
providerId: String,
) {
this.providerId = providerId
this.provider = provider
}

private fun validateProviderId(providerId: String) {
require(providerId.length <= MAX_PROVIDER_ID_LENGTH) {
"로그인 제공자 식별자는 ${MAX_PROVIDER_ID_LENGTH}자 이하여야 합니다."
}
}

private companion object {
const val MAX_PROVIDER_ID_LENGTH = 255
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.mogumogu.momogo.domain.user

enum class LoginProvider {
GUEST,
KAKAO,
NAVER,
APPLE,
}
26 changes: 26 additions & 0 deletions src/main/kotlin/com/mogumogu/momogo/domain/user/User.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.mogumogu.momogo.domain.user

class User(
val id: Long? = null,
var nickname: String,
) {
init {
validateNickname(nickname)
}

fun changeNickname(nickname: String) {
validateNickname(nickname)
this.nickname = nickname
}

private fun validateNickname(nickname: String) {
require(nickname.length in MIN_NICKNAME_LENGTH..MAX_NICKNAME_LENGTH) {
Comment on lines +15 to +17

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 공백만 있는 닉네임을 거부하세요

" "처럼 공백으로만 구성된 값은 길이가 1 이상이므로 현재 검증을 통과하고, nicknamenullable = falseUserEntity에도 그대로 저장됩니다. 사용자 입력에서 공백 닉네임을 허용하면 다른 사용자가 식별할 수 없는 계정이 생성되므로, 길이를 검사하기 전에 공백 여부를 함께 검증해야 합니다.

Useful? React with 👍 / 👎.

"닉네임은 ${MIN_NICKNAME_LENGTH}자 이상 ${MAX_NICKNAME_LENGTH}자 이하여야 합니다."
}
}

private companion object {
const val MIN_NICKNAME_LENGTH = 1
const val MAX_NICKNAME_LENGTH = 12
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.mogumogu.momogo.infrastructure.database.entity

import com.mogumogu.momogo.domain.user.LoginAccount
import com.mogumogu.momogo.domain.user.LoginProvider
import com.mogumogu.momogo.global.entity.BaseEntity
import jakarta.persistence.Column
import jakarta.persistence.Entity
import jakarta.persistence.EnumType
import jakarta.persistence.Enumerated
import jakarta.persistence.GeneratedValue
import jakarta.persistence.GenerationType
import jakarta.persistence.Id
import jakarta.persistence.Table

@Entity
@Table(name = "login_account")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 운영용 스키마 마이그레이션을 추가하세요

application-dev.ymlapplication-prod.ymlddl-auto: validate를 사용하지만, 대상 커밋에는 이 테이블들을 만드는 Flyway/Liquibase/SQL 마이그레이션이 없습니다. 새 환경 또는 아직 login_account 테이블이 없는 배포 DB에서 애플리케이션을 시작하면 Hibernate의 스키마 검증이 실패하여 기동하지 못합니다. UserEntity 테이블을 포함해 이 변경으로 추가된 스키마를 생성하는 마이그레이션을 함께 추가해야 합니다.

Useful? React with 👍 / 👎.

class LoginAccountEntity(
@field:Column(name = "user_id", nullable = false)
var userId: Long,
Comment thread
ddingmin marked this conversation as resolved.
@field:Enumerated(EnumType.STRING)
@field:Column(name = "provider", nullable = false, length = 20)
var provider: LoginProvider,
@field:Column(name = "provider_id", nullable = false, length = 255)
var providerId: String,
Comment on lines +23 to +24

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge providerId 길이를 영속 컬럼과 일치시키세요

LoginAccountproviderId가 256자 이상이어도 생성·변경을 허용하며 추가된 도메인 테스트도 이를 유효한 값으로 규정합니다. 그러나 이 컬럼은 VARCHAR(255)로 생성되므로 그런 계정을 저장하는 실제 로그인 흐름에서는 PostgreSQL이 길이 초과 오류를 내고 요청이 실패합니다. 도메인에서 255자로 검증하거나 컬럼 길이를 허용 범위에 맞게 늘려야 합니다.

Useful? React with 👍 / 👎.

) : BaseEntity() {

@field:Id
@field:GeneratedValue(strategy = GenerationType.IDENTITY)
@field:Column(name = "id", nullable = false, updatable = false)
var id: Long? = null
protected set

fun toDomain(): LoginAccount =
LoginAccount(
id = id,
userId = userId,
provider = provider,
providerId = providerId,
)

companion object {
fun fromDomain(loginAccount: LoginAccount): LoginAccountEntity =
LoginAccountEntity(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 기존 로그인 계정 ID를 매핑에 보존하세요

저장된 LoginAccount를 도메인으로 변환해 changeProvider()를 호출한 뒤 LoginAccountEntity.fromDomain()으로 저장하면 loginAccount.id가 버려져 새 Entity가 만들어집니다. 이 경우 제공자 변경이 기존 로그인 계정을 갱신하지 않고 새 행을 삽입하므로, 동일 사용자에게 이전 제공자 계정과 변경 후 계정이 함께 남습니다. 테스트가 Entity의 필드를 직접 수정하는 방식이라 이 변환 경로의 중복 저장을 검증하지 못합니다.

Useful? React with 👍 / 👎.

userId = loginAccount.userId,
provider = loginAccount.provider,
providerId = loginAccount.providerId,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.mogumogu.momogo.infrastructure.database.entity

import com.mogumogu.momogo.domain.user.User
import com.mogumogu.momogo.global.entity.BaseEntity
import jakarta.persistence.Column
import jakarta.persistence.Entity
import jakarta.persistence.GeneratedValue
import jakarta.persistence.GenerationType
import jakarta.persistence.Id
import jakarta.persistence.Table

@Entity
@Table(name = "\"user\"")
class UserEntity(
@field:Column(name = "nickname", nullable = false, length = 12)
var nickname: String,
) : BaseEntity() {

@field:Id
@field:GeneratedValue(strategy = GenerationType.IDENTITY)
@field:Column(name = "id", nullable = false, updatable = false)
var id: Long? = null
protected set

fun toDomain(): User =
User(
id = id,
nickname = nickname,
)

companion object {
fun fromDomain(user: User): UserEntity =
UserEntity(nickname = user.nickname)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 기존 사용자 ID를 매핑에 보존하세요

이미 저장된 UsertoDomain()으로 읽어 닉네임을 변경한 뒤 UserEntity.fromDomain()으로 다시 저장하는 흐름에서는 user.id가 이 Entity에 전달되지 않아 ID가 항상 null입니다. 따라서 JpaRepository.save()가 기존 행 갱신 대신 새 행을 삽입하여 사용자가 중복 생성됩니다. 현재 테스트도 이 문제를 피해 조회한 Entity를 직접 수정하고 있으므로, 도메인 변환을 사용하는 Service에서는 막아주지 못합니다.

Useful? React with 👍 / 👎.

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.mogumogu.momogo.infrastructure.database.repository

import com.mogumogu.momogo.infrastructure.database.entity.LoginAccountEntity
import org.springframework.data.jpa.repository.JpaRepository

interface LoginAccountRepository : JpaRepository<LoginAccountEntity, Long> {
fun findAllByUserId(userId: Long): List<LoginAccountEntity>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.mogumogu.momogo.infrastructure.database.repository

import com.mogumogu.momogo.infrastructure.database.entity.UserEntity
import org.springframework.data.jpa.repository.JpaRepository

interface UserRepository : JpaRepository<UserEntity, Long>
14 changes: 5 additions & 9 deletions src/main/resources/application-local.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,13 @@ spring:
activate:
on-profile: local
datasource:
url: jdbc:h2:mem:momogo;MODE=PostgreSQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
driver-class-name: org.h2.Driver
username: sa
password:
h2:
console:
enabled: true
path: /h2-console
url: jdbc:postgresql://${DB_HOST:localhost}:${DB_PORT:5432}/${DB_NAME:momogo}
driver-class-name: org.postgresql.Driver
username: ${DB_USERNAME:momogo}
password: ${DB_PASSWORD:momogo}
jpa:
hibernate:
ddl-auto: create-drop
ddl-auto: update
show-sql: true
properties:
hibernate:
Expand Down
Loading