[#26] feat: controller 규격화된 response 적용

- 모든 controller에 ApplicationResponse 적용
- 인증 프로세스 내 발생 가능한 예외 BusinessException 처리
- 불필요한 클래스 정리
This commit is contained in:
beaniejoy
2022-12-04 01:52:49 +09:00
parent cab20de7b1
commit 8463fcf932
17 changed files with 90 additions and 91 deletions

View File

@@ -1,9 +1,10 @@
package io.beaniejoy.dongnecafe.common.security
import io.beaniejoy.dongnecafe.common.error.constant.ErrorCode
import io.beaniejoy.dongnecafe.common.error.exception.BusinessException
import io.beaniejoy.dongnecafe.security.SecurityUser
import mu.KLogging
import org.springframework.security.authentication.AuthenticationProvider
import org.springframework.security.authentication.BadCredentialsException
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.Authentication
import org.springframework.security.core.userdetails.UserDetailsService
@@ -29,7 +30,7 @@ class ApiAuthenticationProvider(
val user = userDetailsService.loadUserByUsername(email) as SecurityUser
if (!passwordEncoder.matches(password, user.password)) {
throw BadCredentialsException("Input password does not match stored password")
throw BusinessException(ErrorCode.AUTH_PASSWORD_NOT_VALID)
}
logger.info { "User password ${user.password}" }

View File

@@ -1,13 +1,13 @@
package io.beaniejoy.dongnecafe.common.security
import io.beaniejoy.dongnecafe.common.error.constant.ErrorCode
import io.beaniejoy.dongnecafe.common.error.exception.BusinessException
import io.beaniejoy.dongnecafe.domain.member.entity.Member
import io.beaniejoy.dongnecafe.domain.member.repository.MemberRepository
import io.beaniejoy.dongnecafe.error.MemberDeactivatedException
import io.beaniejoy.dongnecafe.security.SecurityUser
import mu.KLogging
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.core.userdetails.UsernameNotFoundException
import org.springframework.stereotype.Component
import org.springframework.transaction.annotation.Transactional
@@ -22,12 +22,12 @@ class UserDetailsServiceImpl(
return memberRepository.findByEmail(email)?.let {
logger.info { "[LOAD MEMBER] email: ${it.email}, role: ${it.roleType}, activated: ${it.activated}" }
createSecurityUser(it)
} ?: throw UsernameNotFoundException("${email} is not found")
} ?: throw BusinessException(ErrorCode.AUTH_MEMBER_NOT_FOUND)
}
private fun createSecurityUser(member: Member): SecurityUser {
if (member.activated.not()) {
throw MemberDeactivatedException(member.email)
throw BusinessException(ErrorCode.AUTH_MEMBER_DEACTIVATED)
}
return SecurityUser(

View File

@@ -1,11 +0,0 @@
package io.beaniejoy.dongnecafe.common.security.model
import com.fasterxml.jackson.annotation.JsonInclude
import org.springframework.security.core.GrantedAuthority
data class AuthenticationResult(
val email: String,
@JsonInclude(JsonInclude.Include.NON_EMPTY)
val authorities: Collection<GrantedAuthority> = listOf(),
val msg: String
)

View File

@@ -1,5 +1,6 @@
package io.beaniejoy.dongnecafe.controller
import io.beaniejoy.dongnecafe.common.response.ApplicationResponse
import io.beaniejoy.dongnecafe.security.JwtTokenUtils
import io.beaniejoy.dongnecafe.domain.member.model.request.SignInRequest
import io.beaniejoy.dongnecafe.model.TokenResponse
@@ -16,7 +17,7 @@ class AuthController(
private val jwtTokenUtils: JwtTokenUtils
) {
@PostMapping("/authenticate")
fun signIn(@RequestBody signInRequest: SignInRequest): TokenResponse {
fun signIn(@RequestBody signInRequest: SignInRequest): ApplicationResponse {
val authentication = authService.signIn(
email = signInRequest.email,
password = signInRequest.password
@@ -24,6 +25,8 @@ class AuthController(
val accessToken = jwtTokenUtils.createToken(authentication)
return TokenResponse(accessToken)
return ApplicationResponse
.success("success authenticate")
.data(TokenResponse(accessToken))
}
}

View File

@@ -1,5 +1,6 @@
package io.beaniejoy.dongnecafe.controller
import io.beaniejoy.dongnecafe.common.response.ApplicationResponse
import io.beaniejoy.dongnecafe.domain.member.model.request.MemberRegisterRequest
import io.beaniejoy.dongnecafe.service.MemberService
import org.springframework.web.bind.annotation.PostMapping
@@ -13,7 +14,11 @@ class MemberController(
private val memberService: MemberService
) {
@PostMapping("/sign-up")
fun signUp(@RequestBody resource: MemberRegisterRequest): Long {
return memberService.registerMember(resource)
fun signUp(@RequestBody resource: MemberRegisterRequest): ApplicationResponse {
val registerMemberId = memberService.registerMember(resource)
return ApplicationResponse
.success("success sign up")
.data(registerMemberId)
}
}

View File

@@ -1,3 +0,0 @@
package io.beaniejoy.dongnecafe.error
class MemberExistedException(email: String): RuntimeException("Member[$email] is already existed")

View File

@@ -1,3 +0,0 @@
package io.beaniejoy.dongnecafe.error
class MemberDeactivatedException(email: String): RuntimeException("Member[$email] is deactivated")

View File

@@ -1,27 +0,0 @@
package io.beaniejoy.dongnecafe.error.handler
import io.beaniejoy.dongnecafe.common.response.ApplicationResponse
import mu.KLogging
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.security.core.AuthenticationException
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.RestControllerAdvice
// TODO 통합된 에러 핸들링 필요(ErrorResponse 규격화)
@RestControllerAdvice
class CommonControllerAdvice {
companion object : KLogging()
@ExceptionHandler(AuthenticationException::class)
fun handleAuthenticationException(e: AuthenticationException): ResponseEntity<ApplicationResponse<Any?>> {
logger.error { "AuthenticationException: ${e.message}" }
return ResponseEntity.ok().body(
ApplicationResponse(
code = HttpStatus.BAD_REQUEST.value(),
message = "계정 혹은 비밀번호가 일치하지 않습니다."
)
)
}
}

View File

@@ -1,9 +1,10 @@
package io.beaniejoy.dongnecafe.service
import io.beaniejoy.dongnecafe.common.error.constant.ErrorCode
import io.beaniejoy.dongnecafe.common.error.exception.BusinessException
import io.beaniejoy.dongnecafe.domain.member.entity.Member
import io.beaniejoy.dongnecafe.domain.member.model.request.MemberRegisterRequest
import io.beaniejoy.dongnecafe.domain.member.repository.MemberRepository
import io.beaniejoy.dongnecafe.error.MemberExistedException
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
@@ -16,7 +17,7 @@ class MemberService(
) {
fun registerMember(resource: MemberRegisterRequest): Long {
memberRepository.findByEmail(resource.email!!)?.also {
throw MemberExistedException(resource.email!!)
throw BusinessException(ErrorCode.MEMBER_EXISTED)
}
val registeredMember = memberRepository.save(

View File

@@ -11,13 +11,13 @@ import org.springframework.web.bind.annotation.RestControllerAdvice
@RestControllerAdvice
class BasicControllerAdvice {
companion object: KLogging()
companion object : KLogging()
// 비즈니스 로직 상 에러 처리
@ResponseStatus(HttpStatus.OK)
@ExceptionHandler(BusinessException::class)
fun handleBusinessException(e: BusinessException): ApplicationResponse {
logger.error { "[BusinessException] ${e.errorCode.name}" }
return ApplicationResponse.fail(e.errorCode, "error")
logger.error { "[${BusinessException::class.simpleName}], <ErrorCode>: ${e.errorCode.name}, <ErrorMessage>: ${e.message}" }
return ApplicationResponse.fail(errorCode = e.errorCode)
}
}

View File

@@ -2,6 +2,7 @@ package io.beaniejoy.dongnecafe.common.error.constant
enum class Domain {
AUTH,
MEMBER,
CAFE,
CAFE_MENU
}

View File

@@ -1,13 +1,28 @@
package io.beaniejoy.dongnecafe.common.error.constant
import io.beaniejoy.dongnecafe.common.error.constant.Domain.CAFE
import io.beaniejoy.dongnecafe.common.error.constant.Domain.*
import io.beaniejoy.dongnecafe.common.error.constant.SubCategory.*
enum class ErrorCode(
val domain: Domain,
val subCategory: SubCategory
) {
// AUTH(security 관련)
AUTH_COMMON_EXCEPTION(AUTH, COMMON),
AUTH_MEMBER_NOT_FOUND(AUTH, INVALID_AUTHENTICATE_REQUEST),
AUTH_PASSWORD_NOT_VALID(AUTH, INVALID_AUTHENTICATE_REQUEST),
AUTH_MEMBER_DEACTIVATED(AUTH, DEACTIVATED),
// MEMBER
MEMBER_EXISTED(MEMBER, EXISTED),
// CAFE
CAFE_NOT_FOUND(CAFE, NOT_FOUND),
CAFE_EXISTED(CAFE, EXISTED);
companion object {
fun convertOrNull(value: String?): ErrorCode? {
return values().find { it.name === value }
}
}
}

View File

@@ -1,6 +1,9 @@
package io.beaniejoy.dongnecafe.common.error.constant
enum class SubCategory {
COMMON,
INVALID_AUTHENTICATE_REQUEST,
NOT_FOUND,
EXISTED
EXISTED,
DEACTIVATED
}

View File

@@ -30,11 +30,15 @@ class ApplicationResponse {
companion object {
fun success(message: String? = null): ApplicationResponse {
return ApplicationResponse(ResultCode.SUCCESS, message)
return ApplicationResponse(resultCode = ResultCode.SUCCESS, message = message)
}
fun fail(errorCode: ErrorCode, message: String?): ApplicationResponse {
return ApplicationResponse(ResultCode.FAIL, errorCode, message)
fun fail(errorCode: ErrorCode, message: String? = null): ApplicationResponse {
return ApplicationResponse(
resultCode = ResultCode.FAIL,
errorCode = errorCode,
message = message
)
}
}

View File

@@ -6,10 +6,10 @@ import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.autoconfigure.security.servlet.PathRequest
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.http.HttpMethod
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer
import org.springframework.security.config.http.SessionCreationPolicy
import org.springframework.security.web.SecurityFilterChain
@Configuration
@@ -27,6 +27,10 @@ class SecurityConfig {
.authorizeRequests()
.anyRequest().permitAll()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 토큰 방식(세션 불필요)
.and()
.also { jwtAuthenticationConfigurer(it) }
.build()

View File

@@ -3,10 +3,7 @@ package io.beaniejoy.dongnecafe.domain.cafe.controller
import io.beaniejoy.dongnecafe.common.response.ApplicationResponse
import io.beaniejoy.dongnecafe.domain.cafe.model.request.CafeRegisterRequest
import io.beaniejoy.dongnecafe.domain.cafe.model.request.CafeUpdateRequest
import io.beaniejoy.dongnecafe.domain.cafe.model.response.CafeDetailedInfo
import io.beaniejoy.dongnecafe.domain.cafe.model.response.CafeSearchInfo
import io.beaniejoy.dongnecafe.domain.cafe.service.CafeService
import org.springframework.data.domain.Page
import org.springframework.data.domain.Pageable
import org.springframework.data.domain.Sort
import org.springframework.data.web.PageableDefault
@@ -30,7 +27,9 @@ class CafeController(
cafeMenuRequestList = resource.cafeMenuList
)
return ApplicationResponse.success("OK").data(newCafeId)
return ApplicationResponse
.success("OK")
.data(newCafeId)
}
/**
@@ -40,15 +39,23 @@ class CafeController(
fun searchCafeList(
@PageableDefault(sort = ["name"], direction = Sort.Direction.ASC, page = 0, size = 10) pageable: Pageable
): ApplicationResponse {
return ApplicationResponse.success().data(cafeService.searchCafeList(pageable))
val searchCafes = cafeService.searchCafeList(pageable)
return ApplicationResponse
.success()
.data(searchCafes)
}
/**
* 단일 카페 상세 조회
*/
@GetMapping("/{id}")
fun getDetailedInfo(@PathVariable("id") id: Long): CafeDetailedInfo {
return cafeService.getDetailedInfoByCafeId(id)
fun getDetailedInfo(@PathVariable("id") id: Long): ApplicationResponse {
val cafeDetailedInfo = cafeService.getDetailedInfoByCafeId(id)
return ApplicationResponse
.success()
.data(cafeDetailedInfo)
}
/**
@@ -59,7 +66,7 @@ class CafeController(
fun updateInfo(
@PathVariable("id") id: Long,
@RequestBody resource: CafeUpdateRequest
): String {
): ApplicationResponse {
cafeService.updateInfo(
id = id,
name = resource.name!!,
@@ -68,6 +75,7 @@ class CafeController(
description = resource.description!!
)
return "Successfully Cafe[$id] Info Updated"
return ApplicationResponse.success("Successfully Cafe[$id] Info Updated")
}
}

View File

@@ -1,16 +1,10 @@
package io.beaniejoy.dongnecafe.domain.cafe.controller
import io.beaniejoy.dongnecafe.common.response.ApplicationResponse
import io.beaniejoy.dongnecafe.domain.cafe.model.request.CafeMenuBulkDeleteRequest
import io.beaniejoy.dongnecafe.domain.cafe.model.request.CafeMenuUpdateRequest
import io.beaniejoy.dongnecafe.domain.cafe.model.response.CafeMenuDetailedInfo
import io.beaniejoy.dongnecafe.domain.cafe.service.CafeMenuService
import org.springframework.web.bind.annotation.DeleteMapping
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PatchMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.bind.annotation.*
@RestController
@RequestMapping("/api/cafes/{cafeId}/menus")
@@ -24,11 +18,15 @@ class CafeMenuController(
fun getDetailedInfo(
@PathVariable("cafeId") cafeId: Long,
@PathVariable("menuId") menuId: Long
): CafeMenuDetailedInfo {
return cafeMenuService.getDetailedInfoByMenuId(
): ApplicationResponse {
val cafeMenuDetailedInfo = cafeMenuService.getDetailedInfoByMenuId(
menuId = menuId,
cafeId = cafeId
)
return ApplicationResponse
.success()
.data(cafeMenuDetailedInfo)
}
/**
@@ -42,14 +40,14 @@ class CafeMenuController(
@PathVariable("cafeId") cafeId: Long,
@PathVariable("menuId") menuId: Long,
@RequestBody cafeMenuUpdateRequest: CafeMenuUpdateRequest
): String {
): ApplicationResponse {
cafeMenuService.updateInfoAndBulkUpdate(
menuId = menuId,
cafeId = cafeId,
resource = cafeMenuUpdateRequest
)
return "Success Update Cafe[$cafeId]'s CafeMenu[$menuId]"
return ApplicationResponse.success("Success Update Cafe[$cafeId]'s CafeMenu[$menuId]")
}
/**
@@ -59,13 +57,13 @@ class CafeMenuController(
fun delete(
@PathVariable("cafeId") cafeId: Long,
@PathVariable("menuId") menuId: Long
): String {
): ApplicationResponse {
cafeMenuService.deleteByCafeMenuId(
menuId = menuId,
cafeId = cafeId
)
return "Success Delete Cafe[$cafeId]'s CafeMenu[$menuId]"
return ApplicationResponse.success("Success Delete Cafe[$cafeId]'s CafeMenu[$menuId]")
}
/**
@@ -75,9 +73,9 @@ class CafeMenuController(
fun bulkDelete(
@PathVariable("cafeId") cafeId: Long,
@RequestBody resource: CafeMenuBulkDeleteRequest
): String {
): ApplicationResponse {
cafeMenuService.bulkDelete(cafeId, resource.cafeMenuIdList)
return "Success Delete Cafe[$cafeId]'s CafeMenu List"
return ApplicationResponse.success("Success Delete Cafe[$cafeId]'s CafeMenu List")
}
}