간단한 API를 직접 만들어보자 (5) -환경변수 활용 및 댓글 기능 추가-

2024. 5. 23. 21:26TIL

✔오늘 배운 중요한 🔑 point

  • 데이터베이스의 PASSWORD같은 중요한 정보들은 환경변수를 활용해서 숨길 수 있다
  • 새로운 기능을 추가할때 기존의 Controller와 Service 로직에 추가를 하는것보다 새로운 Controller와 Service를 작성하는 것이 더 모듈화 되고 깔끔해지기도 하니 이를 잘 선택해야한다.

🎯 오늘 배운 내용

 

환경변수 활용

환경변수로 중요한 보안정보를 숨기자

application.yml

현재 application.yml 파일안에 중요한 정보들이 들어가 있는 상태인데 GitHub에 코드를 공유할경우에 보안 문제가 발생할 수 있으니 이를 환경변수로 감추어보자

 

 

Edit Configuraton 클릭

Edit Configuraton

 

 

Modify Option -> Envirionment variables 클릭

 

환경변수 설정

 

DB_URL=DB의 URL주소;DB_USERNAME= DB유저 이름;DB_PASSWORD= DB비밀번호

 

 

yml 수정

spring:
  datasource:
    url: ${DB_URL}
    username: ${DB_USERNAME}
    password: ${DB_PASSWORD}
  jpa:
    hibernate:
      ddl-auto: update

 

정상적으로 잘 작동하는것을 확인!

 

 

 

API 기능추가

 

할일 카드의 완료여부 추가

내가 작성한 할일목록의 카드에 대해서 다 수행했으면 완료라는 표시가 나오도록 기능을 추가해보자

@Entity
@Table(name="card")
class Card (
    @Column(name="title")
    var title:String,

    @Column(name= "description" )
    var description: String? = null,

    @Column(name="finished")
    var finished: Boolean = false


){

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    var id:Long?=null
}
fun Card.toResponse(): CardResponse {
    return CardResponse(
        id = id!!,
        title = title,
        description = description,
        finished = finished
    )
}

당연하게도 데이터베이스에 완료라는 Column이 있어야 하는게 너무나도 당연하니 데이터베이스와 매핑이 되는 Entity의 card라는 테이블에 finsihed 라는 이름의  Column을 생성

 

카드가 처음 생성이 될때에는 당연히 완료여부가 false일 것이므로 default값을 false로 생성

data class CardResponse(
    val id:Long,
    val title: String,
    val description: String?,
    val finished: Boolean
)

cardResponse dto에도 해당 완료여부를 추가해주자

 

마지막으로 카드를 수정을 할때 완료여부에 대한 값을 변경가능하도록 설계할 것이므로

data class UpdateCardRequest(
    val title:String,
    val description:String?,
    val finished:Boolean
)

사용자가 요청을 할때 finished가 true인지 false인지 값을 받도록 dto를 설정

 

false -> true로 변경

 

 

완료여부가 잘 적용이 된것을 확인할 수 있다.

 

조회할때 완료여부가 True인 카드들을 밑으로 내리고 아직 완료가 되지 않은 카드 목록이 상단에 차지하도록 코드를 수정해보자

 

조회를 할때의 비즈니스 로직을 수정하면 되기 때문에

override fun getAllCardList(): List<CardResponse> {
        val cardRepositoryWithFinished= cardRepository.findAll().sortedWith(compareBy<Card>{it.finished}.thenBy{it.id})
        return cardRepositoryWithFinished.map{ it.toResponse() }
    }

Card Entity의 finished가 false인 값들을 맨 앞으로 정렬시키고 만약 동일하게 false인 목록이 있다면 id를 기준으로 정렬 시키는 코드를 추가 작성해주면 완성이다

 

id순서대로 조회가 되는것이 아닌  완료여부가 false인 카드 목록이 상위에 위치하게 된다

 

댓글 API  기능 추가

 

댓글기능을 추가하기 위해서 Card 패키지와 User 패키지 처럼 별도의 패키지를 만들어서 관리하자

 

 

comment 또한 card API를 만든것과 비슷하게 하면 되는데 

한가지 차이가 있다면 댓글은 반드시 존재하는 CARD에만 등록할 수 있다는것!!

존재하지도 않는 CARD에다가 댓글을 달수는 없기때문에 이를 고려해서 설계해보자

 

Entity

@Entity
@Table(name = "comment")
class Comment(
    @Column(name = "Name")
    val commenterName:String,

    @Column(name = "Password")
    val commenterPassword: String,

    @Column(name = "comment")
    val commentInform:String,

    @ManyToOne
    @JoinColumn(name="card_id")
    val card: Card

){
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    var id:Long?=null
}
fun Comment.toResponse():CommentResponse{

    return CommentResponse(
        commenterName=this.commenterName,
        commentInform = this.commentInform,
        id= id!!,
        card= this.card

    )
}
Entity를 먼저 생성한 뒤

 

Dto

data class CommentResponse(
    val id:Long,
    val commenterName:String,
    val commentInform:String,
    val card:Card
)
사용자로부터 이름과 비밀번호, 댓글내용, 그리고 card dto의 정보를 담는 dto를 생성

 

data class CreateCommentRequest(
    val commenterName: String,
    val commenterPassword: String,
    val commentInform:String
)
간단한  Request dto 작성

 

 

data class DeleteCommentRequest(
    val commenterPassword:String
)
원래는 삭제시에는 dto를 작성하지 않았지만 비밀번호가 일치해야만 삭제가 이루어지도록 하기위해서  작성

 

Controller

@RestController
class CommentController(
    private val commentService: CommentService
){

    @GetMapping("/cards/{cardId}/comments")
    fun getComment(@PathVariable cardId:Long):ResponseEntity<List<CommentResponse>>{
        return ResponseEntity.status(HttpStatus.OK).body(commentService.getCommentList(cardId))
    }

    @PostMapping("/cards/{cardId}/comments")
    fun createComment(@PathVariable cardId:Long,@RequestBody createCommentRequest: CreateCommentRequest): ResponseEntity<CommentResponse> {
        return ResponseEntity.status(HttpStatus.CREATED).body(commentService.createComment(cardId, createCommentRequest))
    }

    @PutMapping("/cards/{cardId}/comments/{commentId}")
    fun updateComment(@PathVariable cardId:Long,@PathVariable commentId:Long,@RequestBody updateCommentRequest: UpdateCommentRequest): ResponseEntity<CommentResponse> {
        return ResponseEntity.status(HttpStatus.OK).body(commentService.updateComment(cardId,commentId,updateCommentRequest))

    }

    @DeleteMapping("/cards/{cardId}/comments/{commentId}")
    fun deleteComment(@PathVariable cardId:Long,@PathVariable commentId:Long):ResponseEntity<Unit>{
        commentService.deleteComment(cardId,commentId)
        return ResponseEntity.status(HttpStatus.NO_CONTENT).build()
    }

}
card 컨트롤러와 다른점은 cardId 경로변수 뿐만 아니라 commentId 경로변수도 인자로 받는다는것이 차이

 

 

Service

 

package org.example.spartatodolist.domain.comment.service

import jakarta.transaction.Transactional
import org.example.spartatodolist.domain.card.repository.CardRepository
import org.example.spartatodolist.domain.comment.dto.CommentResponse
import org.example.spartatodolist.domain.comment.dto.CreateCommentRequest
import org.example.spartatodolist.domain.comment.dto.DeleteCommentRequest
import org.example.spartatodolist.domain.comment.dto.UpdateCommentRequest
import org.example.spartatodolist.domain.comment.model.Comment
import org.example.spartatodolist.domain.comment.model.toResponse
import org.example.spartatodolist.domain.comment.repository.CommentRepository
import org.example.spartatodolist.domain.exception.ModelNotFoundException
import org.springframework.data.repository.findByIdOrNull
import org.springframework.http.ResponseEntity
import org.springframework.stereotype.Service

@Service
class CommentService(
    private val commentRepository: CommentRepository,
    private val cardRepository: CardRepository
) {

    fun getCommentList(cardId:Long):List<CommentResponse>{
        val card=cardRepository.findByIdOrNull(cardId)?: throw ModelNotFoundException("Card",cardId)
        return commentRepository.findAllByCard(card).map{it.toResponse()}
    }

    @Transactional
    fun createComment(cardId:Long, request:CreateCommentRequest):CommentResponse{
        val card=cardRepository.findByIdOrNull(cardId)?: throw ModelNotFoundException("Card",cardId)

        return commentRepository.save(Comment(
            commenterName= request.commenterName,
            commenterPassword = request.commenterPassword,
            commentInform = request.commentInform,
            card = card // ?수상한 부분
        )).toResponse()
    }


    @Transactional
    fun updateComment(cardId:Long,commentId:Long, request: UpdateCommentRequest): CommentResponse {
        val card=cardRepository.findByIdOrNull(cardId)?: throw ModelNotFoundException("Card",cardId)
        val comment=commentRepository.findByIdOrNull(commentId)?: throw ModelNotFoundException("Comment",commentId)

        if(comment.commenterPassword != request.commenterPassword){
            throw IllegalArgumentException("비밀번호가 틀려요")
        }



        val(commenterName, commenterPassword, commentInform) = request

        comment.commenterName =commenterName
        comment.commenterPassword =commenterPassword
        comment.commentInform =commentInform

        return commentRepository.save(comment).toResponse()


    }

    @Transactional
    fun deleteComment(cardId:Long,commentId:Long,request: DeleteCommentRequest){
        val card=cardRepository.findByIdOrNull(cardId)?: throw ModelNotFoundException("Card",cardId)
        val comment=commentRepository.findByIdOrNull(commentId)?: throw ModelNotFoundException("Comment",commentId)

        if(comment.commenterPassword != request.commenterPassword){
            throw IllegalArgumentException("비밀번호가 틀려요")
        }


        commentRepository.delete(comment)
    }

}

 

if(comment.commenterPassword != request.commenterPassword){
    throw IllegalArgumentException("비밀번호가 틀려요")
}
사용자가 처음에 생성한 비밀번호와 다르면 수정이나 삭제가 이루어지지 않고 오류메시지를 출력된다

 

 

비밀번호가 틀리면 status code 400 오류메시지가 출력되며 

 

 

비밀번호가 맞을시에 정상적으로 수정이 된다

 

 

현재 보완해야 할 점

댓글이 존재하는 카드의 경우에 카드를 삭제하려고 시도하면 연결되어있는 댓글이 있기 때문에 오류가 발생한다

이를 보완해서 코드 수정이 필요하다

 

 

🤔 어떻게 활용할까?

댓글기능 추가로 인해서 댓글기능을 사용할 수 있게되었다. 

📓 오늘의 한줄

Brevity is the soul of wit.

- William Shakespeare-

 

 

'TIL' 카테고리의 다른 글

(알고리즘) 옹알이  (1) 2024.05.25
간단한 API를 직접 만들어보자(6) -댓글이 있는 카드 삭제하기-  (0) 2024.05.24
인증  (0) 2024.05.22
간단한 API를 직접 만들어보자(4) Status code 500 해결  (0) 2024.05.21
JPA  (0) 2024.05.20