Skip to content

Commit c172338

Browse files
authored
Merge pull request #110 from NMNB-Team/v1/feat/#109-report-block-duplication-check
[refactor] 신고/차단 중복 검증을 DB제약으로 변경
2 parents df98b07 + 25af14d commit c172338

12 files changed

Lines changed: 157 additions & 70 deletions

File tree

nmnb-application/src/main/kotlin/nmnb/application/domain/block/controller/BlockController.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,10 @@ class BlockController(
2727
summary = "사용자 차단 API",
2828
description = "사용자를 차단합니다. 차단한 사용자는 페이지에 조회되지 않습니다._숙희",
2929
)
30-
@ApiResponses(ApiResponse(responseCode = "COMMON200", description = "성공입니다."))
30+
@ApiResponses(
31+
ApiResponse(responseCode = "COMMON200", description = "성공입니다."),
32+
ApiResponse(responseCode = "BLOCK400", description = "이미 차단한적이 있습니다."),
33+
)
3134
@PostMapping("/users/blocks")
3235
fun blockUser(
3336
@Parameter(name = "user", hidden = true) @AuthUser user: User,
Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
package nmnb.application.domain.block.service
22

33
import nmnb.application.domain.block.service.dto.request.UserBlockServiceRequest
4+
import nmnb.common.response.exception.BlockException
45
import nmnb.common.response.exception.UserException
56
import nmnb.common.response.status.ErrorStatus
67
import nmnb.domain.block.Block
78
import nmnb.domain.block.repository.BlockRepository
89
import nmnb.domain.user.User
910
import nmnb.domain.user.repository.UserRepository
11+
import org.springframework.dao.DataIntegrityViolationException
1012
import org.springframework.stereotype.Service
1113
import org.springframework.transaction.annotation.Transactional
1214

@@ -18,20 +20,26 @@ class UserBlockService(
1820
) {
1921
@Transactional
2022
fun block(user: User, request: UserBlockServiceRequest) {
21-
val blockedUser = userRepository.findById(request.userId).orElseThrow {
22-
UserException(ErrorStatus.USER_NOT_FOUND)
23+
val blockedUser = getBlockedUser(request)
24+
25+
try {
26+
blockRepository.save(Block(blocker = user, blockedUser = blockedUser))
27+
} catch (e: DataIntegrityViolationException) {
28+
throw BlockException(ErrorStatus.ALREADY_BLOCKED)
2329
}
24-
blockRepository.save(Block(blocker = user, blockedUser = blockedUser))
2530
}
2631

2732
@Transactional
2833
fun unBlock(user: User, request: UserBlockServiceRequest) {
29-
val blockedUser = userRepository.findById(request.userId).orElseThrow {
30-
UserException(ErrorStatus.USER_NOT_FOUND)
31-
}
34+
val blockedUser = getBlockedUser(request)
3235
blockRepository.findByBlockerAndBlockedUser(user, blockedUser)
3336
?.let { block ->
3437
blockRepository.delete(block)
3538
}
3639
}
40+
41+
private fun getBlockedUser(request: UserBlockServiceRequest): User =
42+
userRepository.findById(request.userId).orElseThrow {
43+
UserException(ErrorStatus.USER_NOT_FOUND)
44+
}
3745
}

nmnb-application/src/main/kotlin/nmnb/application/domain/report/service/PostReportService.kt

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ class PostReportService(
2121
override fun validateReport(user: User, request: PostReportServiceRequest) {
2222
val post = fetchPost(request.targetId)
2323
validateNotSelfReport(user.id!!, post)
24-
validateNotDuplicateReport(user, request.targetId)
2524
}
2625

2726
private fun fetchPost(targetId: Long): Post {
@@ -39,15 +38,6 @@ class PostReportService(
3938
}
4039
}
4140

42-
private fun validateNotDuplicateReport(
43-
reporter: User,
44-
targetId: Long,
45-
) {
46-
if (reportRepository.existsPostReport(reporter, targetId)) {
47-
throw ReportException(ErrorStatus.ALREADY_REPORTED)
48-
}
49-
}
50-
5141
override fun createReport(user: User, request: PostReportServiceRequest): PostReport {
5242
return PostReport(request.targetId, user, request.contentType)
5343
}

nmnb-application/src/main/kotlin/nmnb/application/domain/report/service/ReportService.kt

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,23 @@
11
package nmnb.application.domain.report.service
22

3+
import nmnb.common.response.exception.ReportException
4+
import nmnb.common.response.status.ErrorStatus
35
import nmnb.domain.report.Report
46
import nmnb.domain.report.repository.ReportRepository
57
import nmnb.domain.user.User
8+
import org.springframework.dao.DataIntegrityViolationException
69
import org.springframework.transaction.annotation.Transactional
710

811
abstract class ReportService<R : Report, Request>(
912
private val reportRepository: ReportRepository,
1013
) {
11-
fun save(report: R) = reportRepository.save(report)
14+
fun save(report: R) {
15+
try {
16+
reportRepository.save(report)
17+
} catch (e: DataIntegrityViolationException) {
18+
throw ReportException(ErrorStatus.ALREADY_REPORTED)
19+
}
20+
}
1221

1322
@Transactional
1423
open fun report(user: User, request: Request) {

nmnb-application/src/main/kotlin/nmnb/application/domain/report/service/UserReportService.kt

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ class UserReportService(
2020
override fun validateReport(user: User, request: UserReportServiceRequest) {
2121
validateTarget(request.targetId)
2222
validateNotSelfReport(user.id!!, request.targetId)
23-
validateNotDuplicateReport(user, request.targetId)
2423
}
2524

2625
private fun validateTarget(targetId: String): User =
@@ -37,15 +36,6 @@ class UserReportService(
3736
}
3837
}
3938

40-
private fun validateNotDuplicateReport(
41-
reporter: User,
42-
targetId: String,
43-
) {
44-
if (reportRepository.existsUserReport(reporter, targetId)) {
45-
throw ReportException(ErrorStatus.ALREADY_REPORTED)
46-
}
47-
}
48-
4939
override fun createReport(user: User, request: UserReportServiceRequest): UserReport =
5040
UserReport(request.targetId, user, request.contentType)
5141
}

nmnb-application/src/test/kotlin/nmnb/application/domain/report/service/PostReportServiceTest.kt

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import nmnb.domain.user.User
1313
import nmnb.domain.user.repository.UserRepository
1414
import org.assertj.core.api.Assertions.assertThat
1515
import org.junit.jupiter.api.AfterEach
16+
import org.junit.jupiter.api.BeforeEach
1617
import org.junit.jupiter.api.DisplayName
1718
import org.junit.jupiter.api.Test
1819
import org.junit.jupiter.api.assertThrows
@@ -28,9 +29,13 @@ class PostReportServiceTest : IntegrationTestSupport() {
2829
@Autowired
2930
private lateinit var reportRepository: ReportRepository
3031

31-
@Autowired
3232
private lateinit var postReportService: PostReportService
3333

34+
@BeforeEach
35+
fun setUp() {
36+
postReportService = PostReportService(reportRepository, postRepository)
37+
}
38+
3439
@AfterEach
3540
fun tearDown() {
3641
reportRepository.deleteAllInBatch()
@@ -58,20 +63,16 @@ class PostReportServiceTest : IntegrationTestSupport() {
5863
assertThat(reportRepository.findAll()).hasSize(1)
5964
}
6065

61-
@DisplayName("중복 신고시 예외가 발생한다.")
66+
@DisplayName("본인의 게시글을 신고할 시 예외가 발생한다.")
6267
@Test
63-
fun postReportDuplicate() {
68+
fun postReportSelf() {
6469
// given
65-
val poster = User.fixture()
6670
val reporter = User.fixture()
67-
userRepository.saveAll(listOf(poster, reporter))
71+
userRepository.save(reporter)
6872

69-
val post = Post.fixture(user = poster)
73+
val post = Post.fixture(user = reporter)
7074
postRepository.save(post)
7175

72-
val report = PostReport.fixture(post.id!!, reporter)
73-
reportRepository.save(report)
74-
7576
val request = PostReportServiceRequest(post.id!!, ContentType.SEXUAL)
7677

7778
// when
@@ -80,28 +81,31 @@ class PostReportServiceTest : IntegrationTestSupport() {
8081
}
8182

8283
// then
83-
assertThat(exception.getCode()).isEqualTo(ErrorStatus.ALREADY_REPORTED)
84-
assertThat(reportRepository.findAll()).hasSize(1)
84+
assertThat(exception.getCode()).isEqualTo(ErrorStatus.CANNOT_REPORT_SELF)
8585
}
8686

87-
@DisplayName("본인의 게시글을 신고할 시 예외가 발생한다.")
87+
@DisplayName("중복 신고시 예외가 발생한다.")
8888
@Test
89-
fun postReportSelf() {
89+
fun postReportDuplicate() {
9090
// given
91+
val poster = User.fixture()
9192
val reporter = User.fixture()
92-
userRepository.save(reporter)
93+
userRepository.saveAll(listOf(poster, reporter))
9394

94-
val post = Post.fixture(user = reporter)
95+
val post = Post.fixture(user = poster)
9596
postRepository.save(post)
9697

97-
val request = PostReportServiceRequest(post.id!!, ContentType.SEXUAL)
98+
val report1 = PostReport.fixture(post.id!!, reporter)
99+
val report2 = PostReport.fixture(post.id!!, reporter)
100+
reportRepository.save(report1)
98101

99102
// when
100103
val exception = assertThrows<ReportException> {
101-
postReportService.validateReport(reporter, request)
104+
postReportService.save(report2)
102105
}
103106

104107
// then
105-
assertThat(exception.getCode()).isEqualTo(ErrorStatus.CANNOT_REPORT_SELF)
108+
assertThat(exception.getCode()).isEqualTo(ErrorStatus.ALREADY_REPORTED)
109+
assertThat(reportRepository.findAll()).hasSize(1)
106110
}
107111
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package nmnb.application.domain.report.service
2+
3+
import nmnb.application.IntegrationTestSupport
4+
import nmnb.domain.post.Post
5+
import nmnb.domain.post.repository.PostRepository
6+
import nmnb.domain.report.PostReport
7+
import nmnb.domain.report.repository.ReportRepository
8+
import nmnb.domain.user.User
9+
import nmnb.domain.user.repository.UserRepository
10+
import org.assertj.core.api.Assertions.assertThat
11+
import org.junit.jupiter.api.AfterEach
12+
import org.junit.jupiter.api.DisplayName
13+
import org.junit.jupiter.api.Test
14+
import org.springframework.beans.factory.annotation.Autowired
15+
import java.util.concurrent.CountDownLatch
16+
import java.util.concurrent.Executors
17+
import java.util.concurrent.TimeUnit
18+
19+
class ReportServiceConcurrencyTest : IntegrationTestSupport() {
20+
@Autowired
21+
private lateinit var userRepository: UserRepository
22+
23+
@Autowired
24+
private lateinit var postRepository: PostRepository
25+
26+
@Autowired
27+
private lateinit var reportRepository: ReportRepository
28+
29+
@Autowired
30+
private lateinit var postReportService: PostReportService
31+
32+
@AfterEach
33+
fun tearDown() {
34+
reportRepository.deleteAllInBatch()
35+
postRepository.deleteAllInBatch()
36+
userRepository.deleteAllInBatch()
37+
}
38+
39+
@DisplayName("동일 게시글 신고 요청 시 중복 등록되지 않는다.")
40+
@Test
41+
fun report() {
42+
// given
43+
val threadCount = 10
44+
45+
val poster = User.fixture()
46+
val reporter = User.fixture()
47+
userRepository.saveAll(listOf(poster, reporter))
48+
49+
val post = Post.fixture(user = poster)
50+
postRepository.save(post)
51+
52+
val report = PostReport.fixture(post.id!!, reporter)
53+
reportRepository.save(report)
54+
55+
// when
56+
val executorService = Executors.newFixedThreadPool(threadCount)
57+
val latch = CountDownLatch(threadCount)
58+
59+
try {
60+
repeat(threadCount) {
61+
executorService.submit {
62+
try {
63+
postReportService.save(report)
64+
} finally {
65+
latch.countDown()
66+
}
67+
}
68+
}
69+
70+
latch.await()
71+
} finally {
72+
executorService.shutdown()
73+
executorService.awaitTermination(1, TimeUnit.MINUTES)
74+
}
75+
76+
// then
77+
val allReport = reportRepository.findAll()
78+
assertThat(allReport).hasSize(1)
79+
}
80+
}

nmnb-application/src/test/kotlin/nmnb/application/domain/report/service/UserReportServiceTest.kt

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import nmnb.domain.user.User
1111
import nmnb.domain.user.repository.UserRepository
1212
import org.assertj.core.api.Assertions
1313
import org.junit.jupiter.api.AfterEach
14+
import org.junit.jupiter.api.BeforeEach
1415
import org.junit.jupiter.api.DisplayName
1516
import org.junit.jupiter.api.Test
1617
import org.junit.jupiter.api.assertThrows
@@ -23,9 +24,13 @@ class UserReportServiceTest : IntegrationTestSupport() {
2324
@Autowired
2425
private lateinit var reportRepository: ReportRepository
2526

26-
@Autowired
2727
private lateinit var userReportService: UserReportService
2828

29+
@BeforeEach
30+
fun setUp() {
31+
userReportService = UserReportService(reportRepository, userRepository)
32+
}
33+
2934
@AfterEach
3035
fun tearDown() {
3136
reportRepository.deleteAllInBatch()
@@ -70,20 +75,18 @@ class UserReportServiceTest : IntegrationTestSupport() {
7075
@DisplayName("사용자를 중복 신고할 시 예외가 발생한다.")
7176
@Test
7277
fun userReportDuplicate() {
73-
// given
7478
// given
7579
val poster = User.fixture()
7680
val reporter = User.fixture()
7781
userRepository.saveAll(listOf(poster, reporter))
7882

79-
val report = UserReport.fixture(poster.id!!, reporter)
80-
reportRepository.save(report)
81-
82-
val request = UserReportServiceRequest(poster.id!!, ContentType.SEXUAL)
83+
val report1 = UserReport.fixture(poster.id!!, reporter)
84+
val report2 = UserReport.fixture(poster.id!!, reporter)
85+
reportRepository.save(report1)
8386

8487
// when
8588
val exception = assertThrows<ReportException> {
86-
userReportService.validateReport(reporter, request)
89+
userReportService.save(report2)
8790
}
8891

8992
// then
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package nmnb.common.response.exception
2+
3+
import nmnb.common.response.base.BaseErrorCode
4+
5+
class BlockException(code: BaseErrorCode) : GeneralException(code)

nmnb-common/src/main/kotlin/nmnb/common/response/status/ErrorStatus.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ enum class ErrorStatus(
4747
INVALID_POST_TARGET_ID(HttpStatus.NOT_FOUND, "REPORT401", "유효한 Post 아이디가 아닙니다."),
4848
CANNOT_REPORT_SELF(HttpStatus.BAD_REQUEST, "REPORT402", "본인을 신고할 수 없습니다."),
4949
ALREADY_REPORTED(HttpStatus.BAD_REQUEST, "REPORT403", "이미 신고한적이 있습니다."),
50+
51+
// BLOCK
52+
ALREADY_BLOCKED(HttpStatus.BAD_REQUEST, "BLOCK400", "이미 차단한적이 있습니다."),
5053
;
5154

5255
override fun getReasonHttpStatus(): ErrorReasonDTO {

0 commit comments

Comments
 (0)