프로젝트 구조
CommonException.kt
package com.contact.management.exception
class CommonException(val exceptionCode: CommonExceptionCode) : RuntimeException()
CommonExceptionCode.kt
package com.contact.management.exception
import org.springframework.http.HttpStatus
enum class CommonExceptionCode(
val status: HttpStatus,
val message: String,
) {
USER_NOT_FOUND(HttpStatus.NOT_FOUND, "사용자를 찾을 수 없습니다."),
EMAIL_ALREADY_EXISTS(HttpStatus.BAD_REQUEST, "이미 존재하는 이메일 입니다."),
}
ErrorResponse.kt
package com.contact.management.exception
import com.fasterxml.jackson.annotation.JsonInclude
import java.time.LocalDateTime
@JsonInclude(JsonInclude.Include.NON_NULL) // null 필드는 JSON 응답에서 제외
data class ErrorResponse(
val timestamp: LocalDateTime = LocalDateTime.now(),
val status: Int,
val error: String,
val message: String,
val errors: Map<String, String>? = null // validation 에러 처리를 위한 필드 추가
)
GlobalExceptionHandler.kt
package com.contact.management.exception
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.MethodArgumentNotValidException
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.RestControllerAdvice
@RestControllerAdvice
class GlobalExceptionHandler {
@ExceptionHandler(CommonException::class)
fun handleCommonException(e: CommonException): ResponseEntity<ErrorResponse> {
val errorResponse = ErrorResponse(
status = e.exceptionCode.status.value(),
error = e.exceptionCode.message,
message = e.message ?: "예외 발생"
)
return ResponseEntity(errorResponse, e.exceptionCode.status)
}
}
-> api에서 CommonException을 throw 하였을때 선언한 httpStatus, message를 return 하게 설정
UserController.kt
package com.contact.management.controller
import com.contact.management.dto.UserDto
import com.contact.management.service.UserService
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*
@RestController
@RequestMapping("/api/users")
class UserController(
private val userService: UserService
) {
@GetMapping
fun getUsers(): ResponseEntity<List<UserDto>> {
val users = userService.getAllUsers()
return ResponseEntity.ok(users)
}
@GetMapping("/{id}")
fun getUser(@PathVariable id: Long): ResponseEntity<UserDto> {
return ResponseEntity.ok(userService.getUserById(id))
}
@PostMapping
fun createUser(@RequestBody userDto: UserDto): ResponseEntity<UserDto> {
val createdUser = userService.createdUser(userDto)
return ResponseEntity
.status(HttpStatus.CREATED) // 201 Created
.header(HttpHeaders.LOCATION, "/api/users/${createdUser.id}") // 생성된 리소스의 URI 반환
.body(createdUser)
}
@PutMapping("/{id}")
fun updateUser(@PathVariable id: Long, @RequestBody userDto: UserDto): ResponseEntity<UserDto> {
val updatedUser = userService.updateUser(id, userDto)
return ResponseEntity.ok(updatedUser)
}
@DeleteMapping("/{id}")
fun deleteUser(@PathVariable id: Long): ResponseEntity<Void> {
userService.deleteUser(id)
return ResponseEntity.noContent().build() // 204 No Content
}
}
rest api 설계 원칙
1. request, response 파라미터는 dto를 사용한다. (entity 사용 x)
why : entity는 테이블 생성에 영향을 주기때문에 분리함
2. 간혹 api를 전부 @PostMapping으로 모두 선언해서 설계하는 경우가 있는데, 이는 올바른 설계가 아님
3. api가 정상적인경우는 데이터를 return, 정상적이지 않는경우 status, message, timeStamp 등 에러메시지를 return
4. api는 버전을 통한 관리가 이뤄줘야함
why : 버전관리가 안되고 기존 api를 변경이되면 api를 사용하고 있던 client들에게 장애가 발생할수 있음
ex) 기존 api -> /api/v1/users
변경된 api -> /api/v2/users
5. api를 설계할때 명칭은 계층적으로 만들어야함
ex) /api/v1/car/suv/genesis
UserDto.kt
package com.contact.management.dto
data class UserDto(
val id: Long?= null,
val name: String,
val email: String
)
UserRepository.kt
package com.contact.management.repository
import com.contact.management.entity.User
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository
import java.util.Optional
@Repository
interface UserRepository : JpaRepository<User, Long>{
fun findByEmail(username: String): User?
}
-> 사용자 등록시 이메일 중복 체크를 위해 findByEmail을 생성함
-> Kotlin에서는 Optional보다는 User?(nullable)로 반환, Optional 사용 시 let 블록 오작동
UserService.kt
package com.contact.management.service
import com.contact.management.dto.UserDto
import com.contact.management.entity.User
import com.contact.management.exception.CommonException
import com.contact.management.exception.CommonExceptionCode.EMAIL_ALREADY_EXISTS
import com.contact.management.exception.CommonExceptionCode.USER_NOT_FOUND
import com.contact.management.repository.UserRepository
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
@Service
@Transactional
class UserService(
private val userRepository: UserRepository
) {
@Transactional(readOnly = true)
fun getAllUsers() : List<UserDto> {
return userRepository.findAll().map { UserDto(it.id, it.name, it.email) }
}
@Transactional(readOnly = true)
fun getUserById(id: Long): UserDto {
val user = userRepository.findById(id).orElseThrow{ CommonException(USER_NOT_FOUND) }
return UserDto(user.id, user.name, user.email)
}
fun createdUser(userDto: UserDto): UserDto {
userRepository.findByEmail(userDto.email)?.let {
throw CommonException(EMAIL_ALREADY_EXISTS)
}
val user = userRepository.save(User(name = userDto.name, email = userDto.email))
return UserDto(user.id, user.name, user.email)
}
// 사용자 업데이트
fun updateUser(id: Long, userDto: UserDto): UserDto{
val updatedUser = userRepository.findById(id).orElseThrow{ CommonException(USER_NOT_FOUND) }
// 엔티티의 속성 수정
updatedUser.name = userDto.name
updatedUser.email = userDto.email
// DB에 업데이트 로직
return UserDto(
id = updatedUser.id,
name = updatedUser.name,
email = updatedUser.email
)
}
fun deleteUser(id: Long){
if(!userRepository.existsById(id)){
throw CommonException(USER_NOT_FOUND)
}
userRepository.deleteById(id)
}
}
-> 넘어온 id에 해당하는 사용자가 없을경우 CommonException(USER_NOT_FOUND)로 예외처리
-> 사용자 등록시 이메일 계정이 이미 등록되어있는경우 CommonException(EMAIL_ALREADY_EXISTS)로 예외처리
-> getAllUsers, getUserById는 데이터를 조회만 하기 떄문에 @Transactional(readOnly = true)를 적용
why : 이렇게 하면 변경감지를 하지않으므로 성능향상
주의!!
데이터를 추가/수정/삭제하는 메서드에 @Transactional(readOnly = true)를 적용을 하면 데이터 변경이 반영되지 않음
postman
사용자 등록
201 새로운 리소스(사용자)가 성공적으로 생성되었음을 명확하게 표시 또한 Location 헤더에 생성된 리소스의 URL을 포함
사용자 등록 (이메일 중복)
400 Bad Request | 잘못된 요청 (유효성 검사 실패) | 클라이언트가 유효하지 않은 데이터를 보냈을 때 |
409 Conflict | 리소스 충돌 (중복 데이터) | 데이터베이스의 고유 값 제약 조건을 위반할 때 |
사용자 전체조회
특정 사용자 조회
특정 사용자 조회 (없는 id)
404 Not Found는 요청한 리소스(사용자)가 존재하지 않을 때 사용하는 표준 상태 코드
사용자 변경
사용자 삭제
리소스를 삭제할 때 204 No Content 응답 코드를 사용하는 것이 일반적
204 No Content를 사용하는 이유
성공적으로 삭제되었음을 나타냄: 요청이 정상적으로 처리되었음을 의미함.
응답 본문이 필요 없음: 삭제된 리소스에 대한 추가적인 정보가 필요하지 않음.
클라이언트가 불필요한 데이터 처리를 하지 않음: 응답 본문이 없으므로 클라이언트가 불필요한 데이터를 처리할 필요가 없음.
사용자 삭제 (없는 사용자)