본문으로 바로가기

프로젝트 구조

 

 

 

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를 사용하는 이유
성공적으로 삭제되었음을 나타냄: 요청이 정상적으로 처리되었음을 의미함.
응답 본문이 필요 없음: 삭제된 리소스에 대한 추가적인 정보가 필요하지 않음.
클라이언트가 불필요한 데이터 처리를 하지 않음: 응답 본문이 없으므로 클라이언트가 불필요한 데이터를 처리할 필요가 없음.

 

사용자 삭제 (없는 사용자)