왕초보일지

240103 TIL | step1피드백, step2정리, step3

다시은 2024. 1. 3. 21:07

 

STEP 1 피드백

 read.me 구체적으로 작성, commit message 의미 있는 코멘트, '클린코드', 강의 클론 코딩, dirty checking

=>

- read.me : 줄이 자꾸 이상해져서 헤맸는데 마크다운문법이란게 따로 있단다...3, 4단계까지 다 하고 난뒤에 정리해야겠다.

- commit 코멘트 : 기능단위별로 푸쉬하고싶은데 자꾸 이것저것 눈에 보이는걸 건들다보니까 잘 안된다.

- step2 클린코드 : 챌린지 반 2주차 세션 라이브 코딩을 보면서 뭔가 더 알아보기 쉽게 작성하고 싶다는 마음이 들었는데 어떻게 손을 대야할지 모르겠다.

 

@Transactional
    override fun updateCard(cardId: Long, request: UpdateCardRequest): CardResponse {
        val card = cardRepository.findByIdOrNull(cardId) ?: throw ModelNotFoundException("Card", cardId)
        when(request.status) {
            CardStatus.TRUE.name -> card.isCompleted()
            CardStatus.FALSE.name -> card.isNotCompleted()
        }
        card.title = request.title
        card.content = request.content
        card.name = request.name
        return card.toResponse() // update 시 영속성 컨텍스트에 대해 dirty checking 일어나므로 save 할 필요 X
        }

서비스단에서 할일의 상태변경을 아래와 같이 card entity의 메소드로 줄였다.

@Service
class CardServiceImpl( ... ) {
...
@Transactional
    override fun updateCard(cardId: Long, request: UpdateCardRequest): CardResponse {
        val card = cardRepository.findByIdOrNull(cardId) ?: throw ModelNotFoundException("Card", cardId)
        card.isCompletedOrNot(request, card)
        card.title = request.title
        card.content = request.content
        card.name = request.name
        return card.toResponse() // update 시 영속성 컨텍스트에 대해 dirty checking 일어나므로 save 할 필요 X
        }
 ...}
 
 @Entity
 calss Card( ... ) {
 ...
 fun isCompletedOrNot(request: UpdateCardRequest) {
        when(request.status) {
            CardStatus.TRUE.name ->  status = CardStatus.TRUE
            CardStatus.FALSE.name -> status = CardStatus.FALSE
        }
    }
 ...
 }

 

 

비밀번호 검사 메소드를 만들어서 댓글의 수정, 삭제 서비스단에 반영했다.

@Service
class CardServiceImpl( ... ) {
...
@Transactional
    override 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.isNameAndPasswordInCorrect(request)) throw UnauthorizedAccess()

        comment.content = request.content
        return comment.toResponse()

    }
...}

@Entity
class Comment( ... ) {
...
fun isNameAndPasswordInCorrect(request: CheckRequest): Boolean {
        return request.name != this.name || request.password != this.password
    }
...}

서비스단에서 기능이 명확하게 보이게 하기 위한 메소드를 생각하다보니 그럼 그렇게 되면 Entity에 많은 메소드들이 몰려있어도 되는걸까하는 고민이 들었다.

할일 생성과정에서 request의 값을 저장하는 것도 메소드로 만들 수 있고

업데이트할 때 값을 할당하는 것도 메소드로 만들 수 있고 얼마든지 바꿀 수 있는데 더 나은 코드는 뭘까 .!.!

 

- 클론 : 개인과제 다 끝낸 후 한 번 더 해보기

- dirty checking : 영속성 컨텍스트, update 시 save 함수 불필요


STEP2 QnA

  • 처음 설계한 API 명세서에 변경사항이 있었나요? 변경 되었다면 어떤 점 때문 일까요? 첫 설계의 중요성에 대해 작성해 주세요!
  • ERD 와 DTO 를 수정했다. 컬럼 변경사항이 있었기 때문.
  • 첫 설계를 통해 내가 개발하고자 하는 어플리케이션의 전체적인 흐름을 파악할 수 있다.

   2. ERD를 먼저 설계한 후 Entity를 개발했을 때 어떤 점이 도움이 되셨나요?

  • 내가 지금 작성하고 있는 Entity 와 다른 Entity 와의 관계를 인지하며 작성할 수 있다.

   3. 만약 댓글이 여러개 달려있는 할일을 삭제하려고 한다면 무슨 문제가 발생할까요? Database 테이블 관점에서 해결방법이 무엇일까요? 

  • 테스트 해봤을 때 응답도 성공적으로 되고 DB내부에서도 할일과 댓글이 잘 삭제되었다.
  • 할일과 댓글의 관계설정에서 cascade 를 통해 영속성을 전파하고 orphanRemoval 을 통해 부모와의 연결이 끊긴 댓글을 삭제하도록 설정했기 때문인 것 같다.
   @OneToMany(mappedBy = "card", fetch = FetchType.LAZY, cascade = [CascadeType.ALL], orphanRemoval = true)
    val comments: MutableList<Comment> = mutableListOf()

 

   4. IoC / DI 에 대해 간략하게 설명해 주세요!

IoC : 객체가 제어의 권한을 다른 곳에 위임한다. spring에서는 빈으로 등록된 객체들을 IoC 컨테이너가 관리한다.

DI : 의존성을 주입하는데 자체적으로 생성하는 것이 아니라 외부에서 주입하므로써 객체 간 결합도를 낮춘다. 


STEP 3

1. 할 일 목록에서 작성일 기준 오름차순, 내림차순 정렬 기능 추가
api를 요청할 때 정렬 기준(오름차순, 내림차순)을 포함하기
정렬 기준을 통해 정렬한 할 일 목록 반환하기
2. 할 일 목록에 작성자 기준 필터 기능 추가
api를 요청할 때 작성자 이름을 포함하기
작성자 이름이 일치하는 할 일 목록 반환하기

 

목록을 불러올 때 정렬기준과 작성자이름을 request로 받고 서비스단의 목록 조회 로직을 수정한다.

이때 쿼리메소드를 활용해야하는건가?

기본값은 어떻게 설정해놓는거지? => name 의 경우 일치하지 않으면 조회가 안되야 올바른 서비스다 !

override fun getAllCards(request: GetCardListRequest): List<CardResponse> {
       var response: List<CardResponse> = listOf()
            if(request.orderBy == CardOrderBy.DESC.name) {
            response = cardRepository.findAllByName(request.name).sortedByDescending { it.createdAt }
        } else {
            response = cardRepository.findAllByName(request.name).sortedBy { it.createdAt }
        }
        return response
    }

 

쿼리메소드로 이름반환은 하겠는데 request로 받아온 정렬은 잘 모르겠다. 그래서 if 문으로 돌리니 이렇게 지저분한데 어떻게 고쳐야 할지 모르겠다....

CardResponse의 status 가 set이 안된다는 오류가 떴었다.

 => enum 클래스인 status의 타입을 클래스로 선언

data class CardResponse(
    val id: Long,
    val status: CardStatus,
    val title: String,
    val content: String?,
    val createdAt: String,
    val name: String
)

 

 

 

*기존 단건 카드 조회 시

 override fun getCard(cardId: Long): CardWithCommentResponse {
        val card = cardRepository.findByIdOrNull(cardId) ?: throw ModelNotFoundException("Card", cardId)
        val comments = commentRepository.findByCardId(cardId).map { it.toResponse() }
        return CardWithCommentResponse(card.toResponse(), comments)
    }

card 따로 comment 따로 불러왔었는데 이 둘은 관계설정을 해놨기 때문에 card.comment 로 불러올 수 있다고...한다...😰

fetchtype 도 그냥 아~나중에 불러오는 거구나 했는데 이런 때에 쓰일 수 있다고 한다. 

 override fun getCard(cardId: Long): CardWithCommentResponse {
        val card = cardRepository.findByIdOrNull(cardId) ?: throw ModelNotFoundException("Card", cardId)
        return CardWithCommentResponse(card.toResponse(), card.comments.map { it.toResponse() })
    }

CardResponse 를 상속받아서 댓글이랑 같이 반환하는 형태로 어떻게 만들 수 있는지 나아아아중에 고민해보기

 

 

 

3. 할 일 작성, 수정시 validation 추가
 - 제목 1자 이상 200자 이내 검사
 - 본문 1자 이상 1000자 이하 검사
 - 조건 불충족 시 기능 실패 응답

 

Spring boot Validation

참조

 

복잡한 검증 로직을 어노테이션으로 해결할 수 있다.

 

의존성

implementation ("org.springframework.boot:spring-boot-starter-validation")

 

 

Controller 의 @RequestBody 에 @Validated 선언

해당 DTO 에 제약조건 @field: 로 명시

class CardController(...) {
...
@PostMapping
    fun createCard(@Validated @RequestBody createCardRequest: CreateCardRequest): ResponseEntity<CardResponse> {
        return ResponseEntity.status(HttpStatus.CREATED).body(cardService.createCard(createCardRequest))
    }
...}

data class CreateCardRequest(
    @field:Size(min = 1, max = 200, message = "제목은 1자 이상 200자 이하여야 합니다.")
    val title: String,
)

 

@RequestParam@Pathvariable 은 Controller 에 @Validated 선언하고 제약조건 추가

 

파리미터가 유효하지 않을 때

@RequestBody   MethodArgumentNotValidException 에러

@RequestParam/@PathVariable  ConstraintViolationException 에러

 

제목과 내용을 공백으로 제출해봤다.

 

기능실패 400 코드와 메시지가 잘 반환되는데 원래 이렇게 기나?! 이런 경우 친절한 에러메시지를 작성하기 위해 커스텀한다고 하는데 정확히 어떤 형식으로 하는지 잘 모르겠다.

 

 

 


Spring Data JPA

getById

@Override
public T getById(ID id) {
    Assert.notNull(id, ID_MUST_NOT_BE_NULL);
    return em.getReference(getDomainClass(), id);
}

LAZY 형태로 값을 가져온다. == 일단 proxy로 만든 다음 실제 사용할 때 DB에 접근한다.

=> 없는 데이터를 조회하면 실제로 접근할 때 EntityNotFoundException 에러가 발생한다.

 

 

findById

실제로 DB에서 값을 찾아오기 때문에 객체가 없다면 Null 을 반환한다.


STEP4

1. 할 일 카드 목록 api의 응답에, 연관된 댓글 내용을 추가해주세요.
할 일 목록과 댓글 목록을 효율적으로 가져와서 매칭하려면 어떻게 해야 할까요?
N + 1 query 문제 알아보기
2. 할 일 카드 목록 api에 pagination 기능을 추가해주세요.
사용자가 많아져서 할 일 카드가 너무 많아지면 어떤 일이 벌어질까요?
offset 기반 pagination과 cursor 기반 pagination에 대해 알아보기
3. 회원가입, 로그인 기능을 추가해주세요.
로그인 한 사용자가 자신의 할 일, 댓글만 수정, 삭제할 수 있게 해주세요.
인증, 인가에 대해 알아보기: 요청한 사용자가 누구인지, api를 호출할 권한이 있는지를 어떻게 알 수 있을까요? basic authentication과 bearer authentication에 대해 알아보기
basic auth에 비해 token 기반 auth가 가지는 장점이 무엇일까요?

목록에 댓글도 같이 가져오려면 오늘 만든거 결국은 수정해야 하는건가 😰😰😰

CardWithCommentResponse를 List로 만들어주면 되려나