From 5e01f2e062f3becfbc0e3da583f05d52cac38145 Mon Sep 17 00:00:00 2001 From: robinjoon Date: Sun, 8 Mar 2026 01:08:28 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20auth=20BC=20Command/Query=20?= =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=A0=81=EC=9A=A9=20(#26)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MemberCommand에 Login variant 추가 - Register에 encodedPassword 필드 포함 - MemberRepository.save()에서 encodedPassword 별도 파라미터 제거 - AuthService.login()이 MemberCommand.Login 수신하도록 변경 - AuthDataFetcher에서 login Command 변환 패턴 적용 Co-Authored-By: Claude Opus 4.6 --- .../#26-auth-command-query-fix/checklist.md | 12 ++++++ docs/plan/#26-auth-command-query-fix/plan.md | 12 ++++++ .../auth/application/service/AuthService.kt | 14 +++---- .../loop/auth/domain/model/MemberCommand.kt | 6 +++ .../domain/repository/MemberRepository.kt | 5 +-- .../persistence/ExposedMemberRepository.kt | 6 +-- .../datafetcher/AuthDataFetcher.kt | 5 ++- .../application/service/AuthServiceTest.kt | 25 +++++++++-- .../team/loop/auth/domain/model/MemberTest.kt | 42 +++++++++++++++++++ 9 files changed, 104 insertions(+), 23 deletions(-) create mode 100644 docs/plan/#26-auth-command-query-fix/checklist.md create mode 100644 docs/plan/#26-auth-command-query-fix/plan.md diff --git a/docs/plan/#26-auth-command-query-fix/checklist.md b/docs/plan/#26-auth-command-query-fix/checklist.md new file mode 100644 index 0000000..bd9a056 --- /dev/null +++ b/docs/plan/#26-auth-command-query-fix/checklist.md @@ -0,0 +1,12 @@ +# auth BC: Command/Query 패턴 미적용 수정 검증 체크리스트 + +## 필수 항목 +- [x] 아키텍처 원칙 준수 (docs/architecture.md 기준) +- [x] 레이어 의존성 규칙 위반 없음 +- [x] 테스트 코드 작성 완료 (Domain, Application 필수) +- [x] 모든 테스트 통과 +- [x] 기존 테스트 깨지지 않음 + +## 선택 항목 (해당 시) +- [x] auth BC 파일만 수정 (다른 BC 미수정) +- [x] Command/Query 데이터 흐름 일관성 확보 diff --git a/docs/plan/#26-auth-command-query-fix/plan.md b/docs/plan/#26-auth-command-query-fix/plan.md new file mode 100644 index 0000000..53650f9 --- /dev/null +++ b/docs/plan/#26-auth-command-query-fix/plan.md @@ -0,0 +1,12 @@ +# auth BC: Command/Query 패턴 미적용 수정 계획 + +> Issue: #26 + +## 단계 + +- [x] 1단계: Domain — MemberCommand에 Login variant 추가, Register에 encodedPassword 포함 (TDD) +- [x] 2단계: Domain — MemberRepository.save() 시그니처에서 encodedPassword 파라미터 제거 +- [x] 3단계: Application — AuthService.login()이 MemberCommand.Login을 수신하도록 변경, register()에서 command.copy로 encodedPassword 전달 (TDD) +- [x] 4단계: Infrastructure — ExposedMemberRepository.save() 구현 수정 +- [x] 5단계: Presentation — AuthDataFetcher.login()에서 MemberCommand.Login 변환 +- [x] 6단계: 전체 테스트 통과 검증 diff --git a/src/main/kotlin/kr/io/team/loop/auth/application/service/AuthService.kt b/src/main/kotlin/kr/io/team/loop/auth/application/service/AuthService.kt index 23500ba..26bd045 100644 --- a/src/main/kotlin/kr/io/team/loop/auth/application/service/AuthService.kt +++ b/src/main/kotlin/kr/io/team/loop/auth/application/service/AuthService.kt @@ -1,7 +1,6 @@ package kr.io.team.loop.auth.application.service import kr.io.team.loop.auth.application.dto.AuthTokenDto -import kr.io.team.loop.auth.domain.model.LoginId import kr.io.team.loop.auth.domain.model.MemberCommand import kr.io.team.loop.auth.domain.repository.MemberRepository import kr.io.team.loop.common.config.JwtTokenProvider @@ -24,20 +23,17 @@ class AuthService( throw DuplicateEntityException("LoginId already exists: ${command.loginId.value}") } val encodedPassword = checkNotNull(passwordEncoder.encode(command.rawPassword)) - val member = memberRepository.save(command, encodedPassword) + val member = memberRepository.save(command.copy(encodedPassword = encodedPassword)) val token = jwtTokenProvider.generateToken(member.id.value) return AuthTokenDto(accessToken = token) } @Transactional(readOnly = true) - fun login( - loginId: LoginId, - rawPassword: String, - ): AuthTokenDto { + fun login(command: MemberCommand.Login): AuthTokenDto { val member = - memberRepository.findByLoginId(loginId) - ?: throw EntityNotFoundException("Member not found: ${loginId.value}") - if (!passwordEncoder.matches(rawPassword, member.password)) { + memberRepository.findByLoginId(command.loginId) + ?: throw EntityNotFoundException("Member not found: ${command.loginId.value}") + if (!passwordEncoder.matches(command.rawPassword, member.password)) { throw AuthenticationException("Password does not match") } val token = jwtTokenProvider.generateToken(member.id.value) diff --git a/src/main/kotlin/kr/io/team/loop/auth/domain/model/MemberCommand.kt b/src/main/kotlin/kr/io/team/loop/auth/domain/model/MemberCommand.kt index f15f10f..3a27d0f 100644 --- a/src/main/kotlin/kr/io/team/loop/auth/domain/model/MemberCommand.kt +++ b/src/main/kotlin/kr/io/team/loop/auth/domain/model/MemberCommand.kt @@ -5,5 +5,11 @@ sealed interface MemberCommand { val loginId: LoginId, val nickname: Nickname, val rawPassword: String, + val encodedPassword: String? = null, + ) : MemberCommand + + data class Login( + val loginId: LoginId, + val rawPassword: String, ) : MemberCommand } diff --git a/src/main/kotlin/kr/io/team/loop/auth/domain/repository/MemberRepository.kt b/src/main/kotlin/kr/io/team/loop/auth/domain/repository/MemberRepository.kt index 41a2609..6941aa3 100644 --- a/src/main/kotlin/kr/io/team/loop/auth/domain/repository/MemberRepository.kt +++ b/src/main/kotlin/kr/io/team/loop/auth/domain/repository/MemberRepository.kt @@ -5,10 +5,7 @@ import kr.io.team.loop.auth.domain.model.Member import kr.io.team.loop.auth.domain.model.MemberCommand interface MemberRepository { - fun save( - command: MemberCommand.Register, - encodedPassword: String, - ): Member + fun save(command: MemberCommand.Register): Member fun findByLoginId(loginId: LoginId): Member? diff --git a/src/main/kotlin/kr/io/team/loop/auth/infrastructure/persistence/ExposedMemberRepository.kt b/src/main/kotlin/kr/io/team/loop/auth/infrastructure/persistence/ExposedMemberRepository.kt index add98b4..5c434a2 100644 --- a/src/main/kotlin/kr/io/team/loop/auth/infrastructure/persistence/ExposedMemberRepository.kt +++ b/src/main/kotlin/kr/io/team/loop/auth/infrastructure/persistence/ExposedMemberRepository.kt @@ -15,10 +15,8 @@ import java.time.OffsetDateTime @Repository class ExposedMemberRepository : MemberRepository { - override fun save( - command: MemberCommand.Register, - encodedPassword: String, - ): Member { + override fun save(command: MemberCommand.Register): Member { + val encodedPassword = checkNotNull(command.encodedPassword) val now = OffsetDateTime.now() val row = MemberTable.insert { diff --git a/src/main/kotlin/kr/io/team/loop/auth/presentation/datafetcher/AuthDataFetcher.kt b/src/main/kotlin/kr/io/team/loop/auth/presentation/datafetcher/AuthDataFetcher.kt index bec6845..dc35cf8 100644 --- a/src/main/kotlin/kr/io/team/loop/auth/presentation/datafetcher/AuthDataFetcher.kt +++ b/src/main/kotlin/kr/io/team/loop/auth/presentation/datafetcher/AuthDataFetcher.kt @@ -33,11 +33,12 @@ class AuthDataFetcher( fun login( @InputArgument input: LoginInput, ): AuthToken { - val result = - authService.login( + val command = + MemberCommand.Login( loginId = LoginId(input.loginId), rawPassword = input.password, ) + val result = authService.login(command) return AuthToken(accessToken = result.accessToken) } } diff --git a/src/test/kotlin/kr/io/team/loop/auth/application/service/AuthServiceTest.kt b/src/test/kotlin/kr/io/team/loop/auth/application/service/AuthServiceTest.kt index f18ddf8..92f97da 100644 --- a/src/test/kotlin/kr/io/team/loop/auth/application/service/AuthServiceTest.kt +++ b/src/test/kotlin/kr/io/team/loop/auth/application/service/AuthServiceTest.kt @@ -48,7 +48,9 @@ class AuthServiceTest : When("유효한 정보이면") { every { memberRepository.existsByLoginId(registerCommand.loginId) } returns false every { passwordEncoder.encode("password123") } returns "encoded_password" - every { memberRepository.save(registerCommand, "encoded_password") } returns savedMember + every { + memberRepository.save(registerCommand.copy(encodedPassword = "encoded_password")) + } returns savedMember every { jwtTokenProvider.generateToken(1L) } returns "jwt-token" val result = authService.register(registerCommand) @@ -76,7 +78,12 @@ class AuthServiceTest : every { passwordEncoder.matches("password123", "encoded_password") } returns true every { jwtTokenProvider.generateToken(1L) } returns "jwt-token" - val result = authService.login(LoginId("testuser"), "password123") + val loginCommand = + MemberCommand.Login( + loginId = LoginId("testuser"), + rawPassword = "password123", + ) + val result = authService.login(loginCommand) Then("accessToken을 반환한다") { result.accessToken.shouldNotBeBlank() @@ -88,8 +95,13 @@ class AuthServiceTest : every { memberRepository.findByLoginId(LoginId("unknown")) } returns null Then("예외가 발생한다") { + val loginCommand = + MemberCommand.Login( + loginId = LoginId("unknown"), + rawPassword = "password123", + ) shouldThrow { - authService.login(LoginId("unknown"), "password123") + authService.login(loginCommand) } } } @@ -99,8 +111,13 @@ class AuthServiceTest : every { passwordEncoder.matches("wrongpassword", "encoded_password") } returns false Then("예외가 발생한다") { + val loginCommand = + MemberCommand.Login( + loginId = LoginId("testuser"), + rawPassword = "wrongpassword", + ) shouldThrow { - authService.login(LoginId("testuser"), "wrongpassword") + authService.login(loginCommand) } } } diff --git a/src/test/kotlin/kr/io/team/loop/auth/domain/model/MemberTest.kt b/src/test/kotlin/kr/io/team/loop/auth/domain/model/MemberTest.kt index be80d77..e2fdf4b 100644 --- a/src/test/kotlin/kr/io/team/loop/auth/domain/model/MemberTest.kt +++ b/src/test/kotlin/kr/io/team/loop/auth/domain/model/MemberTest.kt @@ -47,5 +47,47 @@ class MemberTest : command.rawPassword shouldBe "password123" } } + + When("encodedPassword를 포함하면") { + val command = + MemberCommand.Register( + loginId = LoginId("newuser"), + nickname = Nickname("새사용자"), + rawPassword = "password123", + encodedPassword = "encoded_password", + ) + + Then("encodedPassword가 설정된다") { + command.encodedPassword shouldBe "encoded_password" + } + } + + When("encodedPassword를 생략하면") { + val command = + MemberCommand.Register( + loginId = LoginId("newuser"), + nickname = Nickname("새사용자"), + rawPassword = "password123", + ) + + Then("encodedPassword는 null이다") { + command.encodedPassword shouldBe null + } + } + } + + Given("MemberCommand.Login 생성 시") { + When("유효한 값이면") { + val command = + MemberCommand.Login( + loginId = LoginId("testuser"), + rawPassword = "password123", + ) + + Then("정상 생성된다") { + command.loginId.value shouldBe "testuser" + command.rawPassword shouldBe "password123" + } + } } })