본문 바로가기

카테고리 없음

24.01.24 백오피스2

일부만 커밋해서 올리는 법

왼쪽에 있는 커밋을 누르고 올릴 파일들을 클릭한 뒤 밑에 Feat : "MenuEntity 작성" 이런 식으로 올린다. 고치는건 fix다. 올리는건 feat이고. 그리고 커밋 푸시를 누르면 된다.

 

 

 

 

인증 인가

package com.sparta.dianomi.authority.jwt

import io.jsonwebtoken.Claims
import io.jsonwebtoken.Jws
import io.jsonwebtoken.Jwts
import io.jsonwebtoken.security.Keys
import org.springframework.stereotype.Component
import java.nio.charset.StandardCharsets
import java.time.Duration
import java.time.Instant
import java.util.*

@Component
class JwtPlugin() {

    companion object {
        // JWT서명에 사용되는 비밀 키
        const val SECRET = "PO4c8z41Hia5gJG3oeuFJMRYBB4Ws4aZ"
        //토큰 발급자
        const val ISSUER = "team.sparta.com"
        //토큰 만료 기간 240시간
        const val ACCESS_TOKEN_EXPIRATION_HOUR: Long = 240
    }


    //JWT 유효성 검사 후에 , 클레임 반환
    fun validateToken(jwt: String): Result<Jws<Claims>> {
        return kotlin.runCatching {
            val key = Keys.hmacShaKeyFor(SECRET.toByteArray(StandardCharsets.UTF_8))
            Jwts.parser().verifyWith(key).build().parseSignedClaims(jwt)
        }
    }

    //subject와 memberName , role을 이용해서 엑세스 토큰 생성
    fun generateAccessToken(subject: String, memberName: String, role: String): String {
        // subject, 만료기간과 role을 설정
        return generateToken(subject, memberName, role, Duration.ofHours(ACCESS_TOKEN_EXPIRATION_HOUR))
    }

    // 클레임,발급자,발급시간,만료시간,서명 키를 사용하여 토큰 생성
    private fun generateToken(subject: String, memberName: String, role: String,expirationPeriod : Duration): String {
        //커스텀 클레임 설정
        val claims: Claims = Jwts.claims()
            .add(mapOf("role" to role, "memberName" to memberName))
            .build()

        //서명 키 , 현재 시간을 설정
        val key = Keys.hmacShaKeyFor(SECRET.toByteArray(StandardCharsets.UTF_8))
        val now = Instant.now()


        // JWT를 생성후 반환
        return Jwts.builder()
            .subject(subject)
            .issuer(ISSUER)
            .issuedAt(Date.from(now))
            .expiration(Date.from(now.plus(expirationPeriod)))
            .claims(claims)
            .signWith(key)
            .compact()
    }
}
  1. validateToken(jwt: String): 주어진 JWT 토큰이 유효한지 검사하고, 유효하다면 그 토큰의 클레임을 반환합니다. 클레임은 JWT의 페이로드에 들어있는 정보를 말합니다. 이 메서드에서는 서명 키를 이용해 토큰을 검증합니다.
  2. generateAccessToken(subject: String, memberName: String, role: String): 주어진 사용자 정보를 이용해 엑세스 토큰을 생성합니다. 생성된 토큰에는 사용자의 역할(role)과 이름(memberName)이 포함됩니다. 이 메서드에서는 generateToken 메서드를 호출하여 실제 토큰을 생성합니다.
  3. generateToken(subject: String, memberName: String, role: String, expirationPeriod: Duration): 주어진 정보와 만료 기간을 이용해 JWT 토큰을 생성합니다. 생성된 토큰에는 발급자, 발급 시간, 만료 시간, 사용자 정보가 포함됩니다.
package com.sparta.dianomi.authority.jwt

import com.sparta.dianomi.authority.security.UserPrincipal
import org.springframework.security.authentication.AbstractAuthenticationToken
import org.springframework.security.web.authentication.WebAuthenticationDetails

class JwtAuthenticationToken(
    private val principal: UserPrincipal,
    details: WebAuthenticationDetails
): AbstractAuthenticationToken(principal.authorities) {

    init {
        super.setAuthenticated(true)
        super.setDetails(details)
    }

    override fun getCredentials() = null


    override fun getPrincipal() = principal

    override fun isAuthenticated(): Boolean {
        return true
    }
}

이 코드는 JwtAuthenticationToken 클래스를 정의하고 있습니다. 이 클래스는 Spring Security에서 제공하는 AbstractAuthenticationToken 클래스를 상속받아 사용자 인증 정보를 표현합니다.

JwtAuthenticationToken 클래스는 다음의 필드와 메서드를 가지고 있습니다:

  1. principal: 인증된 사용자의 정보를 담은 UserPrincipal 객체입니다. UserPrincipal는 사용자의 기본적인 정보와 권한 정보를 포함하고 있습니다.
  2. details: 인증에 대한 상세 정보를 담은 WebAuthenticationDetails 객체입니다. WebAuthenticationDetails는 웹 기반 인증에 대한 세부 정보, 예를 들어 IP 주소나 세션 ID 등을 포함하고 있습니다.
  3. getCredentials(): 인증 토큰에서 자격 증명을 반환하는 메서드입니다. JWT 인증에서는 자격 증명을 사용하지 않으므로 null을 반환합니다.
  4. getPrincipal(): 인증된 사용자의 정보를 반환하는 메서드입니다. 여기서는 principal 필드를 반환합니다.
  5. isAuthenticated(): 사용자가 인증되었는지를 반환하는 메서드입니다. 여기서는 항상 true를 반환합니다.
package com.sparta.dianomi.authority.jwt

import com.sparta.dianomi.authority.security.UserPrincipal
import jakarta.servlet.FilterChain
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.springframework.http.HttpHeaders
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource
import org.springframework.stereotype.Component
import org.springframework.web.filter.OncePerRequestFilter

@Component
class JwtAuthenticationFilter(
    private val jwtPlugin: JwtPlugin
):OncePerRequestFilter() {

    companion object {
        private val BEARER_PATTERN = Regex("^Bearer (.+?)$")
    }

    override fun doFilterInternal(
        request: HttpServletRequest,
        response: HttpServletResponse,
        filterChain: FilterChain
    ) {
        val jwt = request.getBearerToken()

        if (jwt != null) {
            jwtPlugin.validateToken(jwt)
                .onSuccess {
                    val memberId = it.payload.subject.toLong()
                    val role = it.payload.get("role", String::class.java)
                    val memberName = it.payload.get("memberName", String::class.java)

                    val principal = UserPrincipal(
                        id = memberId,
                        memberName = memberName,
                        roles = setOf(role)
                    )

                    val authentication = JwtAuthenticationToken(
                        principal = principal,
                        details = WebAuthenticationDetailsSource().buildDetails(request)
                    )
                    SecurityContextHolder.getContext().authentication = authentication

                }
        }

        filterChain.doFilter(request, response)
    }

    private fun HttpServletRequest.getBearerToken(): String? {
        val headerValue = this.getHeader(HttpHeaders.AUTHORIZATION) ?: return null
        return BEARER_PATTERN.find(headerValue)?.groupValues?.get(1)
    }
}

이 코드는 JwtAuthenticationFilter 클래스를 정의하고 있습니다. 이 클래스는 클라이언트의 요청에서 JWT 토큰을 추출하고, 토큰을 검증하여 인증 정보를 생성하는 역할을 합니다. 이 클래스는 Spring의 OncePerRequestFilter를 상속받아, 클라이언트의 각 요청마다 한 번씩 실행되는 필터로 동작합니다.

JwtAuthenticationFilter 클래스의 주요 메서드는 다음과 같습니다:

  1. doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, filterChain: FilterChain): 클라이언트의 요청에서 JWT 토큰을 추출하고, 토큰을 검증하여 인증 정보를 생성하는 메서드입니다. JWT 토큰의 검증은 jwtPlugin의 validateToken 메서드를 사용합니다. 검증이 성공하면 토큰에서 사용자 정보를 추출하여 UserPrincipal 객체를 생성하고, 이를 이용하여 JwtAuthenticationToken 객체를 생성합니다. 생성된 JwtAuthenticationToken 객체는 SecurityContextHolder에 저장되어, 애플리케이션의 다른 부분에서 사용자 정보에 접근하는 데 사용됩니다.
  2. getBearerToken(): HTTP 요청 헤더에서 "Bearer"로 시작하는 JWT 토큰을 추출하는 메서드입니다. "Bearer" 토큰은 "Bearer" 다음에 공백을 한 칸 두고 토큰 값을 작성하는 방식으로 사용됩니다. 이 메서드는 HTTP 요청의 "Authorization" 헤더 값을 가져와서, 이 값이 "Bearer"로 시작하는지 확인하고, "Bearer" 다음의 토큰 값을 반환합니다.
package com.sparta.dianomi.authority.security

import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.security.crypto.password.PasswordEncoder

@Configuration
class PasswordEncoderConfig {

    @Bean
    fun passwordEncoder(): PasswordEncoder {
        return BCryptPasswordEncoder()
    }
}

PasswordEncoder는 비밀번호의 암호화와 암호화된 비밀번호의 일치 여부 검사 등의 기능을 제공하는 인터페이스입니다.

BCryptPasswordEncoder는 PasswordEncoder의 구현체 중 하나로, BCrypt 해시 알고리즘을 사용하여 비밀번호를 암호화합니다. BCrypt는 솔트를 사용하여 랜덤성을 높이고, 워크 팩터를 조절하여 암호화의 복잡성을 조절할 수 있습니다. 이로 인해 비밀번호 해킹을 어렵게 만드는 특징이 있습니다.

 

package com.sparta.dianomi.authority.security

import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.web.SecurityFilterChain

@Configuration
@EnableWebSecurity
class SecurityConfig {
    //꺼야 되는 필터들을 꺼주는 과정
    @Bean
    fun filterChain(http: HttpSecurity): SecurityFilterChain {
        return http
            .httpBasic{it.disable()}
            .formLogin{it.disable()}
            .csrf{it.disable()}
            .build()
    }
}

@EnableWebSecurity 어노테이션은 Spring Security를 활성화하며, SecurityConfig 클래스를 Spring Security 설정 클래스로 지정합니다.

filterChain 메서드는 Spring Security의 필터 체인을 설정합니다. 필터 체인은 클라이언트의 요청이 서버의 리소스에 도달하기 전에 거치는 여러 필터들의 체인을 말합니다. 이 메서드에서는 HTTP Basic 인증, 폼 로그인, CSRF 보호 등의 기능을 비활성화하고 있습니다.

  1. httpBasic{it.disable()}: HTTP Basic 인증을 비활성화합니다. HTTP Basic 인증은 사용자 이름과 비밀번호를 Base64로 인코딩하여 헤더에 포함하는 인증 방식입니다.
  2. formLogin{it.disable()}: 폼 로그인을 비활성화합니다. 폼 로그인은 로그인 페이지를 통해 사용자 이름과 비밀번호를 입력받아 인증하는 방식입니다.
  3. csrf{it.disable()}: CSRF(Cross-Site Request Forgery) 보호를 비활성화합니다. CSRF는 사용자가 자신의 의지와는 무관하게 공격자가 의도한 행동을 하도록 만드는 공격 기법입니다.

이 설정에 의해, 이 애플리케이션에서는 HTTP Basic 인증, 폼 로그인, CSRF 보호 없이 요청을 처리하게 됩니다. 이는 일반적으로 RESTful API 서버에서 주로 사용되는 설정입니다.

 

package com.sparta.dianomi.authority.security

import org.springframework.security.core.GrantedAuthority
import org.springframework.security.core.authority.SimpleGrantedAuthority

data class UserPrincipal(
    val id: Long,
    val memberName: String,
    val authorities: Collection<GrantedAuthority>
) {
    constructor(id: Long , memberName: String , roles: Set<String>): this(
        id, memberName, roles.map { SimpleGrantedAuthority("ROLE_$it") }
    )
}

UserPrincipal 클래스는 사용자의 ID, 이름, 그리고 권한(roles) 정보를 저장하며, 이 정보는 인증 과정에서 사용될 수 있습니다.

주요 구성 요소는 다음과 같습니다:

  1. id: 사용자의 고유한 식별자입니다.
  2. memberName: 사용자의 이름입니다.
  3. authorities: 사용자의 권한을 나타내는 컬렉션입니다. GrantedAuthority 인터페이스는 권한을 나타내고, SimpleGrantedAuthority 클래스는 GrantedAuthority의 간단한 구현체입니다.

또한, 이 클래스에는 두 가지 생성자가 정의되어 있습니다:

  1. 메인 생성자: id, memberName, authorities를 인자로 받아 UserPrincipal 객체를 생성합니다.
  2. 보조 생성자: id, memberName, roles를 인자로 받아 UserPrincipal 객체를 생성합니다. 이 생성자에서는 roles를 권한의 형태로 변환하여 메인 생성자에 전달합니다. 각 role 앞에 "ROLE_"을 붙여 SimpleGrantedAuthority 객체를 생성하고, 이 객체들을 컬렉션으로 만들어 authorities에 전달합니다. 이렇게 하면, 예를 들어 "USER"라는 role은 "ROLE_USER"라는 권한이 됩니다.

 

그리고 이제 생성, 수정, 삭제에 인가를 적용해야 하는데, @PreAuthorize("hasRole('ADMIN')")를 쓰면 된다.

그 이유는

data class UserPrincipal(
    val id: Long,
    val memberName: String,
    val authorities: Collection<GrantedAuthority>
) {
    constructor(id: Long , memberName: String , roles: Set<String>): this(
        id, memberName, roles.map { SimpleGrantedAuthority("ROLE_$it") }
    )
}

여기에서 ROLE_$it이라 되있는데 보통 이건 admin과 user를 가르는거라서 특별한 권한을 주려면 ADMIN이 맞기 때문이다.

 

esception은 먼저, 오류룰 총괄하는 GlobalExceptionHandler를 추가한다.

package com.sparta.dianomi.common.exception

import com.sparta.dianomi.common.exception.dto.ErrorResponse
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.RestControllerAdvice

@RestControllerAdvice
class GlobalExceptionHandler {

    @ExceptionHandler(ModelNotFoundException::class)
    fun handleModelNotFoundException(e: ModelNotFoundException): ResponseEntity<ErrorResponse> {
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ErrorResponse(e.message))
    }

    @ExceptionHandler(IllegalStateException::class)
    fun handleAlreadyAppliedException(e: IllegalStateException): ResponseEntity<ErrorResponse> {
        return ResponseEntity.status(HttpStatus.CONFLICT).body(ErrorResponse(e.message))
    }

    @ExceptionHandler(IllegalArgumentException::class)
    fun handleIllegalArgumentException(e: IllegalArgumentException): ResponseEntity<ErrorResponse> {
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ErrorResponse(e.message))
    }

    @ExceptionHandler(InvalidCredentialException::class)
    fun handleInvalidCredentialException(e: InvalidCredentialException): ResponseEntity<ErrorResponse> {
        return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(ErrorResponse(e.message))
    }

}

이 녀석이 하는일은

예를 들어, ModelNotFoundException이 발생하면 handleModelNotFoundException 메소드가 호출되어 해당 예외를 처리하게 됩니다. 이 메소드는 ModelNotFoundException의 메시지를 이용해서 ErrorResponse 객체를 생성하고, 이를 HTTP 응답 본문으로 반환합니다. HTTP 응답 코드는 404 (Not Found)로 설정됩니다.

이와 같은 방식으로 이 클래스는 다른 예외들에 대해서도 처리를 정의하고 있습니다

 

그리고 ModelNotFoundException은  특정 아이디를 찾아서 찾지 못하면 오류 메시지를 내보내준다.

data class ModelNotFoundException(val modelName: String, val id: String): RuntimeException(
    "Model $modelName not found with given id: $id"
)

이러면 해당 아이디가 없으면 오류 메시지를 출력해준다.

 

package com.sparta.dianomi.common.exception

data class InvalidCredentialException(
    override val message: String =  "The credential is invalid"
): RuntimeException()

 

이녀석은 InvalidCredentialException로 이녀석은

InvalidCredentialException 클래스는 잘못된 인증 정보가 제공됐을 때 발생하는 사용자 정의 예외 클래스입니다. 이 클래스는 RuntimeException을 상속하며, 기본 에러 메시지를 "The credential is invalid"로 설정하고 있습니다.

이 예외 클래스는 보통 사용자 인증 과정에서 잘못된 아이디, 비밀번호, 토큰 등의 인증 정보가 제공됐을 때 사용됩니다. 예를 들어, 사용자 로그인 처리를 하는 서비스 메소드에서 이 예외를 사용할 수 있습니다.

 

 

 

그리고 serviceimpl에 id가 필요한 조회, 수정, 삭제에 

package com.sparta.dianomi.domain.store.menu.service

import com.sparta.dianomi.domain.store.menu.dto.MenuCreateDto
import com.sparta.dianomi.domain.store.menu.dto.MenuDto
import com.sparta.dianomi.domain.store.menu.dto.MenuUpdateDto
import com.sparta.dianomi.domain.store.menu.model.Menu
import com.sparta.dianomi.domain.store.menu.repository.MenuRepository
import org.springframework.stereotype.Service

@Service
class MenuServiceImpl(
    private val menuRepository: MenuRepository
): MenuService {

    override fun getAllMenus(): List<MenuDto> {
        return menuRepository.findAll().map { MenuDto.from(it) }
    }

    override fun getMenu(menuId: Long): MenuDto {
        val menu = menuRepository.findById(menuId).orElseThrow { NoSuchElementException("Menu not found") }
        return MenuDto.from(menu)
    }

    override fun createMenu(menuCreateDto: MenuCreateDto): MenuDto {
        val newMenu = Menu(
            name = menuCreateDto.name,
            price = menuCreateDto.price,
            description = menuCreateDto.description
        )
        menuRepository.save(newMenu)
        return MenuDto.from(newMenu)
    }

    override fun deleteMenu(menuId: Long) {
        menuRepository.deleteById(menuId)
    }

    override fun updateMenu(menuId: Long, menuUpdateDto: MenuUpdateDto): MenuDto {
        val menu = menuRepository.findById(menuId).orElseThrow { NoSuchElementException("Menu not found") }
        menu.name = menuUpdateDto.name
        menu.price = menuUpdateDto.price
        menu.description = menuUpdateDto.description
        menuRepository.save(menu)
        return MenuDto.from(menu)
    }
}

 

이녀석을

package com.sparta.dianomi.domain.store.model.service

import com.sparta.dianomi.common.exception.ModelNotFoundException
import com.sparta.dianomi.domain.store.model.dto.MenuCreateDto
import com.sparta.dianomi.domain.store.model.dto.MenuDto
import com.sparta.dianomi.domain.store.model.dto.MenuUpdateDto
import com.sparta.dianomi.domain.store.model.Menu
import com.sparta.dianomi.domain.store.model.repository.MenuRepository
import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service

@Service
class MenuServiceImpl(
    private val menuRepository: MenuRepository
): MenuService {

    override fun getAllMenus(): List<MenuDto> {
        return menuRepository.findAll().map { MenuDto.from(it) }
    }

    override fun getMenu(menuId: Long): MenuDto {
        val menu = menuRepository.findByIdOrNull(menuId) ?: throw ModelNotFoundException("Menu", menuId.toString())
        return MenuDto.from(menu)
    }

    override fun createMenu(menuCreateDto: MenuCreateDto): MenuDto {
        val newMenu = Menu(
            name = menuCreateDto.name,
            price = menuCreateDto.price,
            description = menuCreateDto.description
        )
        menuRepository.save(newMenu)
        return MenuDto.from(newMenu)
    }

    override fun deleteMenu(menuId: Long) {
        val menu = menuRepository.findByIdOrNull(menuId) ?: throw ModelNotFoundException("Menu", menuId.toString())
        menuRepository.delete(menu)
    }

    override fun updateMenu(menuId: Long, menuUpdateDto: MenuUpdateDto): MenuDto {
        val menu = menuRepository.findByIdOrNull(menuId) ?: throw ModelNotFoundException("Menu", menuId.toString())
        menu.name = menuUpdateDto.name
        menu.price = menuUpdateDto.price
        menu.description = menuUpdateDto.description
        menuRepository.save(menu)
        return MenuDto.from(menu)
    }
}

이런식으로        

 val menu = menuRepository.findByIdOrNull(menuId) ?: throw ModelNotFoundException("Menu", menuId.toString())

를 추가하면 된다!

 

그런데, menuId.toString()쪽이 빨간줄이 드는것이 아닌가? 그래서 알아봤는데,

 

menuId가 null일 수 있다는 의미일 가능성이 높습니다. 즉, Kotlin의 null safety 기능 때문에 발생하는 문제입니다.

Kotlin에서는 모든 타입이 기본적으로 non-null입니다. 따라서 null을 허용하려면 타입 뒤에 ?를 붙여야 합니다. 그런데 menuId의 타입이 Long으로 선언되어 있으므로, 이 변수는 null이 될 수 없습니다.

menuId가 null일 수 없는데 toString() 메서드 호출 시 빨간 줄이 그어진다면, 이는 IDE의 오류일 수 있습니다. 이런 경우, IDE를 재시작하거나 캐시를 지우는 등의 방법으로 문제를 해결할 수 있습니다.

만약 menuId가 null일 수 있다면, 타입을 Long?으로 변경해야 합니다. 그런데 menuId는 메뉴의 고유 ID를 나타내므로, 이 값이 null이 되는 것은 바람직하지 않습니다. 따라서 menuId가 null인 경우를 처리하는 로직을 추가하는 것이 좋습니다. 예를 들어, menuId가 null일 때 예외를 발생시키는 등의 처리를 할 수 있습니다.

 

라는 답변이 나왔다.  그러니까, 코틀린은 널을 인정하지 않는데 널이 나올 가능성이 있으니까 오류가 떳다는 뜻이다.

그걸 해결하기위해 애쓴 결과, ModelNotFoundException을 보여달라기에 보여줬더니, ModelNotFoundException의 생성자가 Long? 타입의 id를 요구하는데, menuId.toString()의 toString() 메서드는 String 타입의 값을 반환하므로, Long? 타입의 인자를 요구하는 ModelNotFoundException 생성자에는 맞기에 그냥 toString를 없애면 된다고 했다.

따라서 

val menu = menuRepository.findByIdOrNull(menuId) ?: throw ModelNotFoundException("Menu", menuId)

 

이랬더니 문제없이 된다!

 

이제는 스토어랑 메뉴를 연결해야 한다. 조원이 작성한 스토어를 받아온 뒤, 내가 작성한 코드들을 바꿔놔야한다. 생각해보면 처음부터 바꾸는걸 전제로 했어야 했는데 잘 안됐다.

 

interface MenuService {
    fun createMenu(menuCreateDto: MenuCreateDto): MenuDto
    fun updateMenu(id: Long, menuUpdateDto: MenuUpdateDto): MenuDto
    fun getMenu(id: Long): MenuDto
    fun getAllMenus(): List<MenuDto>
    fun deleteMenu(id: Long)
}

이 녀석을

interface MenuService {
    fun createMenu(storeId: Long, menuCreateDto: MenuCreateDto): MenuDto
    fun updateMenu(storeId: Long, id: Long, menuUpdateDto: MenuUpdateDto): MenuDto
    fun getMenu(storeId: Long, id: Long): MenuDto
    fun getAllMenus(storeId: Long): List<MenuDto>
    fun deleteMenu(storeId: Long, id: Long)
}

이렇게 바꿔놓고,

 

@Service
class MenuServiceImpl(
    private val menuRepository: MenuRepository
): MenuService {

    override fun getAllMenus(): List<MenuDto> {
        return menuRepository.findAll().map { MenuDto.from(it) }
    }

    override fun getMenu(menuId: Long): MenuDto {
        val menu = menuRepository.findByIdOrNull(menuId) ?: throw ModelNotFoundException("Menu", menuId)
        return MenuDto.from(menu)
    }

    override fun createMenu(menuCreateDto: MenuCreateDto): MenuDto {
        val newMenu = Menu(
            name = menuCreateDto.name,
            price = menuCreateDto.price,
            description = menuCreateDto.description
        )
        menuRepository.save(newMenu)
        return MenuDto.from(newMenu)
    }

    override fun deleteMenu(menuId: Long) {
        val menu = menuRepository.findByIdOrNull(menuId) ?: throw ModelNotFoundException("Menu", menuId)
        menuRepository.delete(menu)
    }

    override fun updateMenu(menuId: Long, menuUpdateDto: MenuUpdateDto): MenuDto {
        val menu = menuRepository.findByIdOrNull(menuId) ?: throw ModelNotFoundException("Menu", menuId)
        menu.name = menuUpdateDto.name
        menu.price = menuUpdateDto.price
        menu.description = menuUpdateDto.description
        menuRepository.save(menu)
        return MenuDto.from(menu)
    }
}

 

그런데 지금 스웨거에 연결했더니 

 

 

 

2024-01-23T23:49:05.345+09:00 ERROR 22328 --- [nio-8080-exec-6] o.h.engine.jdbc.spi.SqlExceptionHelper   : ERROR: relation "menu" does not exist
Position: 13
2024-01-23T23:49:05.373+09:00 ERROR 22328 --- [nio-8080-exec-6] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.springframework.dao.InvalidDataAccessResourceUsageException: could not execute statement [ERROR: relation "menu" does not exist
Position: 13] [insert into menu (created_at,description,name,price,store_id,updated_at) values (?,?,?,?,?,?)]; SQL [insert into menu (created_at,description,name,price,store_id,updated_at) values (?,?,?,?,?,?)]] with root cause

org.postgresql.util.PSQLException: ERROR: relation "menu" does not exist
Position: 13

이것 때문에 고생하는 중이다. 어떻게 해결하지?