Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Impl likeCount and isLiked Logic #79

Merged
merged 16 commits into from
Dec 1, 2022
12 changes: 12 additions & 0 deletions api/src/main/kotlin/com/wafflestudio/snuttev/ErrorHandler.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import org.slf4j.LoggerFactory
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.http.converter.HttpMessageNotReadableException
import org.springframework.orm.ObjectOptimisticLockingFailureException
import org.springframework.web.HttpMediaTypeNotSupportedException
import org.springframework.web.HttpRequestMethodNotSupportedException
import org.springframework.web.bind.MethodArgumentNotValidException
Expand Down Expand Up @@ -49,6 +50,17 @@ class ErrorHandler {
return ResponseEntity(HttpStatus.BAD_REQUEST)
}

@ExceptionHandler(
ObjectOptimisticLockingFailureException::class,
)
fun handleHttpMessageConflict(
e: Exception,
request: HttpServletRequest,
response: HttpServletResponse,
): ResponseEntity<Any> {
return ResponseEntity(HttpStatus.CONFLICT)
}

@ExceptionHandler(SnuttException::class)
fun handlerSnuttException(e: SnuttException): ResponseEntity<ErrorResponse> {
return ResponseEntity(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@ enum class ErrorType(
val code: Int,
val httpStatus: HttpStatus,
) {
// 200
EVALUATION_LIKE_ALREADY_EXISTS(20001, HttpStatus.OK),

// 400
WRONG_CURSOR_FORMAT(20001, HttpStatus.BAD_REQUEST),
WRONG_MAIN_TAG(20002, HttpStatus.BAD_REQUEST),
Expand All @@ -31,4 +28,6 @@ enum class ErrorType(
EVALUATION_ALREADY_EXISTS(29001, HttpStatus.CONFLICT),
MY_LECTURE_EVALUATION(29002, HttpStatus.CONFLICT),
EVALUATION_REPORT_ALREADY_EXISTS(29003, HttpStatus.CONFLICT),
EVALUATION_LIKE_ALREADY_EXISTS(29004, HttpStatus.CONFLICT),
EVALUATION_LIKE_ALREADY_NOT_EXISTS(29005, HttpStatus.CONFLICT),
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ object WrongSearchTagException : SnuttException(ErrorType.WRONG_SEARCH_TAG)
object MyLectureEvaluationException : SnuttException(ErrorType.MY_LECTURE_EVALUATION)
object EvaluationReportAlreadyExistsException : SnuttException(ErrorType.EVALUATION_REPORT_ALREADY_EXISTS)
object EvaluationLikeAlreadyExistsException : SnuttException(ErrorType.EVALUATION_LIKE_ALREADY_EXISTS)
object EvaluationLikeAlreadyNotExistsException : SnuttException(ErrorType.EVALUATION_LIKE_ALREADY_NOT_EXISTS)
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.wafflestudio.snuttev.core.common.model

import org.hibernate.annotations.OptimisticLock
import org.hibernate.annotations.UpdateTimestamp
import java.time.LocalDateTime
import javax.persistence.Column
Expand All @@ -19,5 +20,6 @@ open class BaseEntity(

@field:UpdateTimestamp
@Column(name = "updated_at", nullable = false)
@OptimisticLock(excluded = true)
open val updatedAt: LocalDateTime? = LocalDateTime.now(),
)
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,12 @@ internal final class Cache(
) {
private val log: Logger get() = LoggerFactory.getLogger(Cache::class.java)

inline fun <reified T : Any> withCache(builtCacheKey: CacheKey.BuiltCacheKey, supplier: () -> T?): T? {
get<T>(builtCacheKey)?.let { return it }
inline fun <reified T : Any> withCache(
builtCacheKey: CacheKey.BuiltCacheKey,
postHitProcessor: (T) -> T = { it },
supplier: () -> T?,
): T? {
get<T>(builtCacheKey)?.let { return postHitProcessor(it) }

val value = supplier()
set(builtCacheKey, value)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,9 @@ data class EvaluationWithSemesterResponse(
@JsonProperty("is_reported")
val isReported: Boolean,

@JsonProperty("is_liked")
val isLiked: Boolean,

@JsonProperty("from_snuev")
val fromSnuev: Boolean,

Expand Down Expand Up @@ -175,6 +178,9 @@ data class EvaluationWithLectureResponse(
@JsonProperty("is_reported")
val isReported: Boolean,

@JsonProperty("is_liked")
val isLiked: Boolean,

@JsonProperty("from_snuev")
val fromSnuev: Boolean,

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.wafflestudio.snuttev.core.domain.evaluation.dto

import com.fasterxml.jackson.annotation.JsonProperty
import com.querydsl.core.annotations.QueryProjection

data class EvaluationWithLectureDto @QueryProjection constructor(
Expand All @@ -13,12 +12,9 @@ data class EvaluationWithLectureDto @QueryProjection constructor(
val lifeBalance: Double,
val rating: Double,
val likeCount: Long,
@get:JsonProperty("isHidden")
@field:JsonProperty("isHidden")
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FasterXML/jackson-module-kotlin#80 이런 이슈 때문에 넣었던 건데 예전에 업데이트 하면서 필요 없어짐.

val isHidden: Boolean,
@get:JsonProperty("isReported")
@field:JsonProperty("isReported")
val isReported: Boolean,
val isLiked: Boolean,
val fromSnuev: Boolean,
val year: Int,
val semester: Int,
Expand All @@ -38,6 +34,7 @@ data class EvaluationWithLectureDto @QueryProjection constructor(
likeCount = this.likeCount,
isHidden = this.isHidden,
isReported = this.isReported,
isLiked = this.isLiked,
fromSnuev = this.fromSnuev,
year = this.year,
semester = this.semester,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.wafflestudio.snuttev.core.domain.evaluation.dto

import com.fasterxml.jackson.annotation.JsonProperty
import com.querydsl.core.annotations.QueryProjection

data class EvaluationWithSemesterDto @QueryProjection constructor(
Expand All @@ -13,12 +12,9 @@ data class EvaluationWithSemesterDto @QueryProjection constructor(
val lifeBalance: Double,
val rating: Double,
val likeCount: Long,
@get:JsonProperty("isHidden")
@field:JsonProperty("isHidden")
val isHidden: Boolean,
@get:JsonProperty("isReported")
@field:JsonProperty("isReported")
val isReported: Boolean,
val isLiked: Boolean,
val fromSnuev: Boolean,
val year: Int,
val semester: Int,
Expand All @@ -36,6 +32,7 @@ data class EvaluationWithSemesterDto @QueryProjection constructor(
likeCount = this.likeCount,
isHidden = this.isHidden,
isReported = this.isReported,
isLiked = this.isLiked,
fromSnuev = this.fromSnuev,
year = this.year,
semester = this.semester,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,20 @@ package com.wafflestudio.snuttev.core.domain.evaluation.model

import com.wafflestudio.snuttev.core.common.model.BaseEntity
import com.wafflestudio.snuttev.core.domain.lecture.model.SemesterLecture
import org.hibernate.annotations.DynamicUpdate
import org.hibernate.annotations.OptimisticLockType
import org.hibernate.annotations.OptimisticLocking
import java.time.LocalDateTime
import javax.persistence.Column
import javax.persistence.Entity
import javax.persistence.FetchType
import javax.persistence.JoinColumn
import javax.persistence.ManyToOne
import javax.persistence.OneToMany

@Entity
@DynamicUpdate
@OptimisticLocking(type = OptimisticLockType.DIRTY)
Comment on lines +17 to +18
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Version을 위한 칼럼 따로 만드는 게 번거롭다고 여겨져서 이렇게 했습니다. 의견 주셔도 좋습니다.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요것도 괜찮네요

class LectureEvaluation(
@ManyToOne(optional = false, fetch = FetchType.LAZY)
@JoinColumn(name = "semester_lecture_id", nullable = false)
Expand Down Expand Up @@ -49,4 +55,7 @@ class LectureEvaluation(
val fromSnuev: Boolean = false,

createdAt: LocalDateTime = LocalDateTime.now(),

@OneToMany(mappedBy = "lectureEvaluation")
val evaluationLikes: List<EvaluationLike> = listOf()
) : BaseEntity(createdAt = createdAt)
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import com.wafflestudio.snuttev.core.domain.evaluation.model.LectureEvaluation
import org.springframework.data.jpa.repository.JpaRepository

interface EvaluationLikeRepository : JpaRepository<EvaluationLike, Long> {
fun existsByLectureEvaluationAndUserId(lectureEvaluation: LectureEvaluation, userId: String): Boolean

fun findAllByLectureEvaluationIdIn(lectureEvaluationIds: List<Long>): List<EvaluationLike>
fun deleteByLectureEvaluationAndUserId(lectureEvaluation: LectureEvaluation, userId: String): Long
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ interface LectureEvaluationRepositoryCustom {
): List<EvaluationWithLectureDto>

fun findEvaluationWithLectureByTagAndClassification(
userId: String,
tag: Tag,
classification: LectureClassification,
cursor: Long?,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.wafflestudio.snuttev.core.domain.evaluation.repository

import com.querydsl.core.BooleanBuilder
import com.querydsl.core.types.dsl.CaseBuilder
import com.querydsl.jpa.JPAExpressions.select
import com.querydsl.jpa.impl.JPAQueryFactory
import com.wafflestudio.snuttev.core.common.error.WrongMainTagException
Expand All @@ -10,6 +11,7 @@ import com.wafflestudio.snuttev.core.domain.evaluation.dto.EvaluationWithLecture
import com.wafflestudio.snuttev.core.domain.evaluation.dto.EvaluationWithSemesterDto
import com.wafflestudio.snuttev.core.domain.evaluation.dto.QEvaluationWithLectureDto
import com.wafflestudio.snuttev.core.domain.evaluation.dto.QEvaluationWithSemesterDto
import com.wafflestudio.snuttev.core.domain.evaluation.model.QEvaluationLike.evaluationLike
import com.wafflestudio.snuttev.core.domain.evaluation.model.QLectureEvaluation.lectureEvaluation
import com.wafflestudio.snuttev.core.domain.lecture.model.QLecture.lecture
import com.wafflestudio.snuttev.core.domain.lecture.model.QSemesterLecture.semesterLecture
Expand All @@ -23,7 +25,7 @@ class LectureEvaluationRepositoryImpl(private val queryFactory: JPAQueryFactory)
cursor: EvaluationCursor?,
pageSize: Int,
): List<EvaluationWithSemesterDto> = queryFactory
.selectEvaluationWithSemesterDto()
.selectEvaluationWithSemesterDto(userId)
.where(semesterLecture.lecture.id.eq(lectureId))
.where(lectureEvaluation.userId.ne(userId))
.where(lectureEvaluation.isHidden.isFalse)
Expand All @@ -36,7 +38,7 @@ class LectureEvaluationRepositoryImpl(private val queryFactory: JPAQueryFactory)
lectureId: Long,
userId: String,
): List<EvaluationWithSemesterDto> = queryFactory
.selectEvaluationWithSemesterDto()
.selectEvaluationWithSemesterDto(userId)
.where(semesterLecture.lecture.id.eq(lectureId))
.where(lectureEvaluation.userId.eq(userId))
.where(lectureEvaluation.isHidden.isFalse)
Expand All @@ -48,7 +50,7 @@ class LectureEvaluationRepositoryImpl(private val queryFactory: JPAQueryFactory)
cursor: Long?,
pageSize: Int,
): List<EvaluationWithLectureDto> = queryFactory
.selectEvaluationWithLectureDto()
.selectEvaluationWithLectureDto(userId)
.where(lectureEvaluation.userId.eq(userId))
.where(lectureEvaluation.isHidden.isFalse)
.where(getEvaluationIdCursorPredicate(cursor))
Expand All @@ -57,12 +59,13 @@ class LectureEvaluationRepositoryImpl(private val queryFactory: JPAQueryFactory)
.fetch()

override fun findEvaluationWithLectureByTagAndClassification(
userId: String,
tag: Tag,
classification: LectureClassification,
cursor: Long?,
pageSize: Int
pageSize: Int,
): List<EvaluationWithLectureDto> = queryFactory
.selectEvaluationWithLectureDto()
.selectEvaluationWithLectureDto(userId)
.where(getMainTagPredicate(tag))
.where(semesterLecture.classification.eq(classification))
.where(lectureEvaluation.isHidden.isFalse)
Expand Down Expand Up @@ -101,7 +104,7 @@ class LectureEvaluationRepositoryImpl(private val queryFactory: JPAQueryFactory)
)
}

private fun JPAQueryFactory.selectEvaluationWithSemesterDto() = select(
private fun JPAQueryFactory.selectEvaluationWithSemesterDto(userId: String) = select(
QEvaluationWithSemesterDto(
lectureEvaluation.id,
lectureEvaluation.userId,
Expand All @@ -114,6 +117,7 @@ class LectureEvaluationRepositoryImpl(private val queryFactory: JPAQueryFactory)
lectureEvaluation.likeCount,
lectureEvaluation.isHidden,
lectureEvaluation.isReported,
CaseBuilder().`when`(evaluationLike.id.isNull).then(false).otherwise(true).`as`("isLiked"),
lectureEvaluation.fromSnuev,
semesterLecture.year,
semesterLecture.semester,
Expand All @@ -122,8 +126,9 @@ class LectureEvaluationRepositoryImpl(private val queryFactory: JPAQueryFactory)
)
.from(lectureEvaluation)
.innerJoin(lectureEvaluation.semesterLecture, semesterLecture)
.leftJoin(lectureEvaluation.evaluationLikes, evaluationLike).on(evaluationLike.userId.eq(userId))

private fun JPAQueryFactory.selectEvaluationWithLectureDto() = select(
private fun JPAQueryFactory.selectEvaluationWithLectureDto(userId: String) = select(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

쿼리가 많이 빡세네요.. 추후에 개선 포인트로 두면 좋을 것 같습니다

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ㅋㅋ 좋습니다. 쿼리가 빡세다고 판단하신 이유는 join 3개 붙어서인가요?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵. 근데 무엇보다 where 절에서 tag 구하려고 avg 들어가는게 있는데 풀스캔이 돌 것 같아서 요것만 잘 분리해도 좋을 것 같아용.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

select le.id                             as col_0_0_,
       le.user_id                        as col_1_0_,
       le.content                        as col_2_0_,
       le.grade_satisfaction             as col_3_0_,
       le.teaching_skill                 as col_4_0_,
       le.gains                          as col_5_0_,
       le.life_balance                   as col_6_0_,
       le.rating                         as col_7_0_,
       le.like_count                     as col_8_0_,
       le.is_hidden                      as col_9_0_,
       le.is_reported                    as col_10_0_,
       IF(elike.id is null, false, true) as col_11_0_,
       le.from_snuev                     as col_12_0_,
       sl.year                           as col_13_0_,
       sl.semester                       as col_14_0_,
       sl.lecture_id                     as col_15_0_,
       lecture.title                     as col_16_0_,
       lecture.instructor                as col_17_0_
from lecture_evaluation le
         inner join semester_lecture sl on le.semester_lecture_id = sl.id
         left outer join evaluation_like elike
                         on le.id = elike.lecture_evaluation_id and elike.user_id = '61f55ab5b7a1c400103aed78'
         inner join lecture on sl.lecture_id = lecture.id
where (
        sl.lecture_id in (select sl_tmp.lecture_id
                          from lecture_evaluation le_tmp
                                   inner join
                               semester_lecture sl_tmp on le_tmp.semester_lecture_id = sl_tmp.id
                          where le_tmp.is_hidden = false
                          group by sl_tmp.lecture_id
                          having avg(le_tmp.rating) >= 4.0)
    )
  and sl.classification = '교양'
  and le.is_hidden = false
order by le.id desc
limit 20;

참고로 현재 쿼리... ㅋㅋㅋ

where subquery 로 실행되는 부분을 따로 query 해서 page size (20) 개수만 가져온 다음,
in 이용한 query 하도록,
application 에서 이렇게 2개로 분리하는 식을 생각하신 건가요? (즉, in에 들어가는 개수를 줄일 수 있어서..?)

avg() 쓰는 이상 기본적으로 lectureEvaluation 대상으로 풀 스캔 도는 거 자체는 어쩔 수가 없지 않나 궁금해서 질문입니다.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@PFCJeong 질문이 많아 죄송해요! 요거 시간 나실 때 답해주시면 추후 PR 에 참고하겠습니다. (요것도 #79 (comment)?)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

칼럼을 추가해서 강의평이 추가, 삭제, 수정될 때마다 미리 통계를 내서 태그 달아두면 좋을 것 같았어요.. 대대적인 공사가 필요하겠지만..

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아하 ㅋㅋㅋ 비정규화해서 저장해두는 거네요. 그렇게 하면 정말 대공사가 필요하긴 하겠군요.

QEvaluationWithLectureDto(
lectureEvaluation.id,
lectureEvaluation.userId,
Expand All @@ -136,6 +141,7 @@ class LectureEvaluationRepositoryImpl(private val queryFactory: JPAQueryFactory)
lectureEvaluation.likeCount,
lectureEvaluation.isHidden,
lectureEvaluation.isReported,
CaseBuilder().`when`(evaluationLike.id.isNull).then(false).otherwise(true).`as`("isLiked"),
lectureEvaluation.fromSnuev,
semesterLecture.year,
semesterLecture.semester,
Expand All @@ -146,6 +152,7 @@ class LectureEvaluationRepositoryImpl(private val queryFactory: JPAQueryFactory)
)
.from(lectureEvaluation)
.innerJoin(lectureEvaluation.semesterLecture, semesterLecture)
.leftJoin(lectureEvaluation.evaluationLikes, evaluationLike).on(evaluationLike.userId.eq(userId))
.innerJoin(semesterLecture.lecture, lecture)

private fun getEvaluationCursorPredicate(cursor: EvaluationCursor?) = BooleanBuilder(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.wafflestudio.snuttev.core.domain.evaluation.service
import com.wafflestudio.snuttev.core.common.dto.common.CursorPaginationResponse
import com.wafflestudio.snuttev.core.common.error.EvaluationAlreadyExistsException
import com.wafflestudio.snuttev.core.common.error.EvaluationLikeAlreadyExistsException
import com.wafflestudio.snuttev.core.common.error.EvaluationLikeAlreadyNotExistsException
import com.wafflestudio.snuttev.core.common.error.EvaluationReportAlreadyExistsException
import com.wafflestudio.snuttev.core.common.error.LectureEvaluationNotFoundException
import com.wafflestudio.snuttev.core.common.error.LectureNotFoundException
Expand Down Expand Up @@ -186,12 +187,23 @@ class EvaluationService internal constructor(
val classification = LectureClassification.LIBERAL_EDUCATION

var evaluationWithLectureDtos = cache.withCache(
CacheKey.EVALUATIONS_BY_TAG_CLASSIFICATION_PAGE.build(
builtCacheKey = CacheKey.EVALUATIONS_BY_TAG_CLASSIFICATION_PAGE.build(
tagId, classification, evaluationIdCursor, DEFAULT_PAGE_SIZE + 1,
),
postHitProcessor = { dtos ->
val evaluationsIds = dtos.map { it.id }
val likes = evaluationLikeRepository.findAllByLectureEvaluationIdIn(evaluationsIds)
dtos.map { dto ->
dto.copy(
likeCount = likes.count { it.lectureEvaluation.id == dto.id }.toLong(),
isLiked = likes.any { it.lectureEvaluation.id == dto.id && it.userId == userId },
)
}
},
Comment on lines +193 to +202
Copy link
Member Author

@davin111 davin111 Nov 30, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cache hit 해놓고도 DB 에 접근하는 게 슬프지만, 일단 너무 많은 걸 당장 바꾸지 않고 기능이 완벽하게 동작하는 것에 집중했습니다. single thread 인 Redis 없이 DB 로만 해결하는 방법도 일부러 해보고 싶었고요.

더 좋은 방식에 대한 의견은 남겨놔주세요. ex> like_count 를 Redis 로 관리하자 등. (그런다면 is_liked 도 그렇게 해야할까?)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

like 관련 로직은 새로 서비스를 하나 파서 분리하면 좋을 것 같습니다.

제 개인적인 의견으로는 둘 다 인메모리 캐싱을 해도 될 거 같아요.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 캐시 키는 user id 와 evaluation id 로 생각하시나요?

유저가 공감/공감 취소할 때마다 캐시 날리구요?

Redis 가 아닌 인 메모리로 보시는 이유도 궁금합니다.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

인메모리가 더 빠르고, api 스펙 자체가 리소스 부담을 줄이기 위해 캐시 size를 작게 잡아도 miss가 많이 생길것 같지 않아서요.

) {
val tag = tagRepository.findByIdOrNull(tagId) ?: throw TagNotFoundException
lectureEvaluationRepository.findEvaluationWithLectureByTagAndClassification(
userId,
tag,
classification,
evaluationIdCursor,
Expand Down Expand Up @@ -282,7 +294,7 @@ class EvaluationService internal constructor(
val evaluation = lectureEvaluationRepository.findByIdAndIsHiddenFalse(lectureEvaluationId) ?: throw LectureEvaluationNotFoundException

val deletedRowCount = evaluationLikeRepository.deleteByLectureEvaluationAndUserId(evaluation, userId)
if (deletedRowCount == 0L) return
if (deletedRowCount == 0L) throw EvaluationLikeAlreadyNotExistsException

evaluation.likeCount--
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.wafflestudio.snuttev.core.domain.evaluation.service

import com.wafflestudio.snuttev.core.common.error.EvaluationAlreadyExistsException
import com.wafflestudio.snuttev.core.common.error.EvaluationLikeAlreadyExistsException
import com.wafflestudio.snuttev.core.common.error.EvaluationLikeAlreadyNotExistsException
import com.wafflestudio.snuttev.core.common.error.NotMyLectureEvaluationException
import com.wafflestudio.snuttev.core.common.type.LectureClassification
import com.wafflestudio.snuttev.core.common.type.Semester
Expand Down Expand Up @@ -643,7 +644,9 @@ class EvaluationServiceTest @Autowired constructor(
assertThat(evaluation.likeCount).isEqualTo(0)

// duplicated
evaluationService.cancelLikeEvaluation(userId = likeUserId, lectureEvaluationId = evaluationId)
assertThatThrownBy {
evaluationService.cancelLikeEvaluation(userId = likeUserId, lectureEvaluationId = evaluationId)
}.isInstanceOf(EvaluationLikeAlreadyNotExistsException::class.java)
assertThat(evaluation.likeCount).isEqualTo(0)
}

Expand Down