팀프로젝트<뉴스피드 만들기>(4일차)

2024. 5. 30. 20:58TIL

🕘

팀 프로젝트 (뉴스피드 만들기)

💡 프로젝트 기간:  2024-05-27~2024-06-03

 

 

프로젝트 진행과정

API내 제약 구현

인증 부분 수정

 

 

제약 걸기

@valid 어노테이션을 사용하지 않고 내부로직으로 제약 걸기

package com.teamsparta.abrasax.domain.post.model


import com.teamsparta.abrasax.domain.exception.*
import com.teamsparta.abrasax.domain.helper.ListStringifyHelper
import com.teamsparta.abrasax.domain.member.model.Member
import com.teamsparta.abrasax.domain.post.comment.dto.CommentResponseDto
import com.teamsparta.abrasax.domain.post.dto.CreatePostRequestDto
import com.teamsparta.abrasax.domain.post.dto.PostResponseDto
import com.teamsparta.abrasax.domain.post.dto.PostResponseWithCommentDto
import jakarta.persistence.*
import java.time.LocalDateTime

@Entity
@Table(name = "post")
class Post(
    @Column(name = "title", nullable = false)
    var title: String,

    @Column(name = "content", nullable = false)
    var content: String,

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "member_id", nullable = false)
    val member: Member,

    @ElementCollection
    @CollectionTable(name = "post_tag", joinColumns = [JoinColumn(name = "post_id")])
    @Column(name = "tags", nullable = false)
    var tags: List<String>,

    @Column(name = "created_at", nullable = false)
    val createdAt: LocalDateTime,

    @Column(name = "updated_at", nullable = false)
    var updatedAt: LocalDateTime,
    @Column(name = "deleted_at")
    var deletedAt: LocalDateTime? = null,

) {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    var id: Long? = null

    fun update(newTitle: String, newContent: String, newTags: List<String>) {
//        validateTitleLength(newTitle)
        validateContentLength(newContent)
//        validateTagListSize(newTags)
        validateTagLength(newTags)

        this.title = newTitle
        this.content = newContent
        this.tags = newTags
        this.updatedAt = LocalDateTime.now()
    }

    fun delete() {
        deletedAt = LocalDateTime.now()
    }

    companion object {
        private fun validateTitleLength(request: CreatePostRequestDto): Boolean {
//            if (title.isEmpty() || title.length > 20 ) {
//                throw InvalidTitleException("제목은 비어있지 않고 20자 이하여야 합니다.")
//            }
            return request.title.isEmpty() || request.title.length > 20
        }
        private fun validateContentLength(content: String) {
            if (content.isEmpty() || content.length > 1000 ) {
                throw InvalidContentException("내용은 비어있지 않고 1000자 이하여야 합니다.")
            }
        }

        private fun validateTagListSize(request: CreatePostRequestDto): Boolean {
//            if (tagList.size > 5) throw InvalidTagSizeException("태그는 5개를 초과할 수 없습니다.")
            return request.tags.size > 5
        }

        private fun validateTagLength(tagList: List<String>) {
            if (tagList.any { it.length > 15 }) throw InvalidTagLengthException("태그는 15자 이하여야 합니다.")
        }

        fun validateCreateRequest(request: CreatePostRequestDto): List<Pair<Boolean, String>> {
            // tags = ["1","2","3","4","5","6"]
            // title = ""
            val validations = listOf(::validateTagListSize, ::validateTitleLength)
            return validations.map { Pair(it(request), it.name) }.filter { it.first == true }
        }

        fun of(title: String, content: String, member: Member, tags: List<String>): Post {
            var validTitleLength = true
//            validateTitleLength(title)
            validateContentLength(content)
//            validateTagListSize(tags)
            validateTagLength(tags)

            val timestamp = LocalDateTime.now()

            return Post(
                title = title,
                content = content,
                member = member,
                tags = tags,
                createdAt = timestamp,
                updatedAt = timestamp,
                deletedAt = null
            )
        }
    }
}

fun Post.toPostResponseDto(): PostResponseDto {
    return PostResponseDto(
        id = id!!,
        title = title,
        content = content,
        tags = tags,
        authorId = member.id!!

    )
}

fun Post.toPostWithCommentDtoResponse(
    commentResponseDto: List<CommentResponseDto>
): PostResponseWithCommentDto {
    return PostResponseWithCommentDto(
        id = id!!,
        title = title,
        content = content,
        authorId = member.id!!,
        tags = tags,
        comments = commentResponseDto
    )
}

fun main() {
    val failures = Post.validateCreateRequest(CreatePostRequestDto(title = "", content = "", tags=listOf("1","1","1","1","1","1"), authorId = 1L))
    if (failures.isNotEmpty())
        throw PostValidationException(failures.map{it.second})
}

 

오류가 발생하는 부분이 여러개여도 첫번째 오류가 발생하는 시점에 예외처리가 되기때문에 발생할 수 있는 모든 오류 부분을 알게하기 위해서

val validations = listOf(::validateTagListSize, ::validateTitleLength)

::을 이용해서 함수를 인자로 받아서 validations에 저장한다.

현재 validations 안에는 validateTagListSize함수와 validateTitleLenth함수 2개가 각각 리스트의 인자로서 저장된 상태

 

return validations.map { Pair(it(request), it.name) }.filter { it.first == true }

Pair()함수를 이용해서  it(request)와 it.name을 짝으로 구성시키는데,

it(request) :  함수의 요청(실패 혹은 성공 ,, 예외를 던졌는지, 안던졌는지)

it.name : 함수의 이름

 

.filter { it.first == true }

함수의 요청이 true라면 예외를 던졌다는 의미므로

filter를 통해서 예외처리가 발생한 함수들로만 구성됨

 

val failures = Post.validateCreateRequest(CreatePostRequestDto(title = "", content = "", tags=listOf("1","1","1","1","1","1"), authorId = 1L))
    if (failures.isNotEmpty())
        throw PostValidationException(failures.map{it.second})

 

 (failures.isNotEmpty())

이 부분은 예외처리가 최소 1개이상 발생했다는 의미이며

throw PostValidationException(failures.map{it.second})
}

그 발생한 예외들의 이름들(it.second) 을 던진다는 의미

 

 

인증 부분 수정

package com.teamsparta.abrasax.common.security

import jakarta.servlet.FilterChain
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.stereotype.Component
import org.springframework.web.filter.OncePerRequestFilter

@Component
class JwtTokenFilter(
    private val jwtTokenProvider: JwtTokenProvider,
) : OncePerRequestFilter() {

    override fun doFilterInternal(
        request: HttpServletRequest,
        response: HttpServletResponse,
        filterChain: FilterChain
    ) {
        val token = resolveToken(request)
        if (token != null && jwtTokenProvider.validateToken(token)) {
            val auth = jwtTokenProvider.getAuthentication(token)
            SecurityContextHolder.getContext().authentication = auth
        }

        filterChain.doFilter(request, response)
    }

    private fun resolveToken(request: HttpServletRequest): String? {
        val bearerToken = request.getHeader("Authorization") 
        return if (bearerToken != null && bearerToken.startsWith("Bearer ")) { 
            bearerToken.substring(7)
        } else null
    }
}

상속받는 인터페이스가  UsernamePasswordAuthenticationFilter() 에서 OncePerRequestFilter()로 수정

 

UsernamePasswordAuthenticationFilter 주로 로그인 인증을 처리하는 데 사용되고, 사용자 이름과 비밀번호를 사용하여 인증

OncePerRequestFilter는 각 요청마다 한 번 실행되며, 인증, 로깅, 요청/응답 변환 등 다양한 작업을 처리하는 데 사용

OncePerRequestFilter는 다양한 요청에 공통적으로 필요한 작업을 수행할 때, UsernamePasswordAuthenticationFilter는 특정 엔드포인트에서 로그인 인증을 처리할 때 사용되므로

지금 현재 작성되어있는 JwtTokenFilter와 같은 JWT 토큰 기반 인증 필터는 OncePerRequestFilter를 사용하는 것이 일반적이다.