Skip to content

Commit de2ceec

Browse files
authored
feat: 질문 생성 rate limiting (#65)
* feat: 질문 dto validation 추가 * feat: 질문 rate limit 적용 * feat: bucket 조건 변경 * feat: enum type 이름 변경 * feat: valid 추가
1 parent 01394fa commit de2ceec

File tree

10 files changed

+54
-37
lines changed

10 files changed

+54
-37
lines changed

src/main/java/com/oronaminc/join/global/exception/ErrorCode.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ public enum ErrorCode {
3939
NOT_FOUND_QUESTION("QUESTION-002", "질문을 찾을 수 없습니다.", NOT_FOUND),
4040
UNAUTHORIZED_EDIT_QUESTION("QUESTION-003", "작성자만 질문을 수정할 수 있습니다.", UNAUTHORIZED),
4141
UNAUTHORIZED_DELETE_QUESTION("QUESTION-004", "작성자 및 관리자만 질문을 삭제할 수 있습니다.", UNAUTHORIZED),
42+
TOO_MANY_REQUESTS_QUESTION("QUESTION-005", "잠시 후 다시 시도해주세요.", TOO_MANY_REQUESTS),
4243

4344
UNAUTHORIZED_ROLE_ANSWER("ANSWER-001", "팀원 또는 발표자만 댓글을 작성할 수 있습니다.", UNAUTHORIZED),
4445
NOT_FOUND_EXIST_ANSWER("ANSWER-002", "해당 질문에 대한 답변이 존재하지 않습니다.", NOT_FOUND),

src/main/java/com/oronaminc/join/global/ratelimit/RateLimitType.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@
66
import lombok.Getter;
77

88
public enum RateLimitType {
9+
CREATE_QUESTION(
10+
"CREATE_QUESTION:{}:{}",
11+
Bandwidth.builder()
12+
.capacity(3)
13+
.refillIntervally(3, Duration.ofSeconds(15))
14+
.build()
15+
),
916
EMOJI(
1017
"EMOJI:{}:{}:{}",
1118
Bandwidth.builder()

src/main/java/com/oronaminc/join/question/domain/Question.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import com.oronaminc.join.global.entity.BaseEntity;
44
import com.oronaminc.join.member.domain.Member;
5-
import com.oronaminc.join.question.dto.QuestionCreateRequest;
5+
import com.oronaminc.join.question.dto.QuestionRequest;
66
import com.oronaminc.join.room.domain.Room;
77
import jakarta.persistence.Entity;
88
import jakarta.persistence.FetchType;
@@ -50,7 +50,7 @@ public class Question extends BaseEntity {
5050
@Version
5151
private Integer version;
5252

53-
public static Question create(Room room, Member member, QuestionCreateRequest requestDto) {
53+
public static Question create(Room room, Member member, QuestionRequest requestDto) {
5454
return Question.builder()
5555
.room(room)
5656
.member(member)

src/main/java/com/oronaminc/join/question/dto/QuestionCreateRequest.java

Lines changed: 0 additions & 11 deletions
This file was deleted.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.oronaminc.join.question.dto;
2+
3+
import io.swagger.v3.oas.annotations.media.Schema;
4+
import jakarta.validation.constraints.NotBlank;
5+
import jakarta.validation.constraints.Size;
6+
7+
@Schema(description = "질문 생성/수정 요청 DTO")
8+
public record QuestionRequest(
9+
@Schema(description = "질문 내용", example = "질문있습니다. 질문생성DTO가 맞나요?")
10+
@NotBlank(message = "질문 내용을 입력해주시기 바랍니다.")
11+
@Size(max = 500, message = "질문 내용은 최대 500자까지 입력할 수 있습니다.")
12+
String content
13+
) {
14+
15+
}

src/main/java/com/oronaminc/join/question/service/QuestionService.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import com.oronaminc.join.question.domain.Question;
1313
import com.oronaminc.join.question.domain.QuestionSort;
1414
import com.oronaminc.join.question.dto.QuestionAssembleResponse;
15-
import com.oronaminc.join.question.dto.QuestionCreateRequest;
15+
import com.oronaminc.join.question.dto.QuestionRequest;
1616
import com.oronaminc.join.question.dto.QuestionFlatResponse;
1717
import com.oronaminc.join.question.util.QuestionMapper;
1818
import com.oronaminc.join.room.domain.Room;
@@ -39,7 +39,7 @@ public class QuestionService {
3939
private final ParticipantReader participantReader;
4040

4141
@Transactional
42-
public Question create(Long roomId, Long memberId, QuestionCreateRequest requestDto) {
42+
public Question create(Long roomId, Long memberId, QuestionRequest requestDto) {
4343

4444
Member member = memberReader.getById(memberId);
4545

@@ -78,8 +78,7 @@ public Slice<QuestionAssembleResponse> getQuestions(
7878
}
7979

8080
@Transactional
81-
public Question update(Long memberId, Long roomId, Long questionId,
82-
QuestionCreateRequest request) {
81+
public Question update(Long memberId, Long roomId, Long questionId, QuestionRequest request) {
8382
Question question = questionReader.getByIdAndRoomId(questionId, roomId);
8483

8584
// 참여자가 아님

src/main/java/com/oronaminc/join/question/util/QuestionMapper.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import com.oronaminc.join.global.dto.WriterDto;
55
import com.oronaminc.join.member.domain.Member;
66
import com.oronaminc.join.question.domain.Question;
7-
import com.oronaminc.join.question.dto.QuestionCreateRequest;
7+
import com.oronaminc.join.question.dto.QuestionRequest;
88
import com.oronaminc.join.question.dto.QuestionCreateResponse;
99
import com.oronaminc.join.question.dto.QuestionDeleteResponse;
1010
import com.oronaminc.join.question.dto.QuestionFlatResponse;
@@ -19,7 +19,7 @@
1919
@NoArgsConstructor(access = AccessLevel.PRIVATE)
2020
public class QuestionMapper {
2121

22-
public static Question toQuestion(Room room, Member member, QuestionCreateRequest request) {
22+
public static Question toQuestion(Room room, Member member, QuestionRequest request) {
2323
return Question.create(room, member, request);
2424
}
2525

src/main/java/com/oronaminc/join/websocket/api/EmojiWebsocketController.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public EmojiResponse createEmoji(
4747
@SendTo("/topic/rooms/{roomId}/emojis")
4848
public EmojiResponse deleteEmoji(
4949
@DestinationVariable Long roomId,
50-
@Payload EmojiRequest emojiRequest,
50+
@Payload @Valid EmojiRequest emojiRequest,
5151
Principal principal
5252
) {
5353
Long memberId = Long.valueOf(principal.getName());

src/main/java/com/oronaminc/join/websocket/api/QuestionWebsocketController.java

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,25 @@
11
package com.oronaminc.join.websocket.api;
22

3-
import com.fasterxml.jackson.databind.ObjectMapper;
4-
import com.oronaminc.join.member.security.MemberDetails;
3+
import com.oronaminc.join.global.exception.ErrorCode;
4+
import com.oronaminc.join.global.exception.ErrorException;
5+
import com.oronaminc.join.global.ratelimit.RateLimitService;
6+
import com.oronaminc.join.global.ratelimit.RateLimitType;
57
import com.oronaminc.join.question.domain.Question;
6-
import com.oronaminc.join.question.dto.QuestionCreateRequest;
78
import com.oronaminc.join.question.dto.QuestionCreateResponse;
89
import com.oronaminc.join.question.dto.QuestionDeleteResponse;
10+
import com.oronaminc.join.question.dto.QuestionRequest;
911
import com.oronaminc.join.question.dto.QuestionUpdateResponse;
10-
import com.oronaminc.join.question.util.QuestionMapper;
1112
import com.oronaminc.join.question.service.QuestionService;
13+
import com.oronaminc.join.question.util.QuestionMapper;
14+
import io.github.bucket4j.Bucket;
15+
import jakarta.validation.Valid;
1216
import java.security.Principal;
1317
import lombok.RequiredArgsConstructor;
1418
import lombok.extern.slf4j.Slf4j;
1519
import org.springframework.messaging.handler.annotation.DestinationVariable;
1620
import org.springframework.messaging.handler.annotation.MessageMapping;
1721
import org.springframework.messaging.handler.annotation.Payload;
1822
import org.springframework.messaging.handler.annotation.SendTo;
19-
import org.springframework.security.core.Authentication;
2023
import org.springframework.stereotype.Controller;
2124

2225
@Slf4j
@@ -25,18 +28,23 @@
2528
public class QuestionWebsocketController {
2629

2730
private final QuestionService questionService;
28-
private final ObjectMapper objectMapper;
31+
private final RateLimitService rateLimitService;
2932

3033
@MessageMapping("/rooms/{roomId}/questions/create")
3134
@SendTo("/topic/rooms/{roomId}/questions")
32-
public QuestionCreateResponse create(
35+
public QuestionCreateResponse createQuestion(
3336
@DestinationVariable Long roomId,
34-
@Payload QuestionCreateRequest request,
37+
@Payload @Valid QuestionRequest request,
3538
Principal principal
3639
) {
37-
3840
Long memberId = Long.valueOf(principal.getName());
3941

42+
Bucket bucket = rateLimitService.getBucket(RateLimitType.CREATE_QUESTION, roomId, memberId);
43+
44+
if (!bucket.tryConsume(1)) {
45+
throw new ErrorException(ErrorCode.TOO_MANY_REQUESTS_QUESTION);
46+
}
47+
4048
Question question = questionService.create(roomId, memberId, request);
4149

4250
log.info("수신한 메시지 = {}", request.content());
@@ -46,13 +54,12 @@ public QuestionCreateResponse create(
4654

4755
@MessageMapping("/rooms/{roomId}/questions/{questionId}/update")
4856
@SendTo("/topic/rooms/{roomId}/questions")
49-
public QuestionUpdateResponse update(
57+
public QuestionUpdateResponse updateQuestion(
5058
@DestinationVariable Long roomId,
5159
@DestinationVariable Long questionId,
52-
@Payload QuestionCreateRequest request,
60+
@Payload @Valid QuestionRequest request,
5361
Principal principal
5462
) {
55-
5663
Long memberId = Long.valueOf(principal.getName());
5764

5865
Question updated = questionService.update(memberId, roomId, questionId, request);
@@ -62,12 +69,11 @@ public QuestionUpdateResponse update(
6269

6370
@MessageMapping("rooms/{roomId}/questions/{questionId}/delete")
6471
@SendTo("/topic/rooms/{roomId}/questions")
65-
public QuestionDeleteResponse delete(
72+
public QuestionDeleteResponse deleteQuestion(
6673
@DestinationVariable Long roomId,
6774
@DestinationVariable Long questionId,
6875
Principal principal
6976
) {
70-
7177
Long memberId = Long.valueOf(principal.getName());
7278

7379
Long deletedId = questionService.delete(memberId, roomId, questionId);

src/test/java/com/oronaminc/join/question/service/QuestionServiceTests.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@
2121
import com.oronaminc.join.question.domain.Question;
2222
import com.oronaminc.join.question.domain.QuestionSort;
2323
import com.oronaminc.join.question.dto.QuestionAssembleResponse;
24-
import com.oronaminc.join.question.dto.QuestionCreateRequest;
2524
import com.oronaminc.join.question.dto.QuestionFlatResponse;
25+
import com.oronaminc.join.question.dto.QuestionRequest;
2626
import com.oronaminc.join.room.domain.Room;
2727
import com.oronaminc.join.room.domain.RoomStatus;
2828
import com.oronaminc.join.room.service.RoomReader;
@@ -66,7 +66,7 @@ class QuestionServiceTests {
6666

6767
private Room mockRoom;
6868
private Member mockMember;
69-
private QuestionCreateRequest request;
69+
private QuestionRequest request;
7070
private QuestionFlatResponse mockQ1;
7171
private QuestionFlatResponse mockQ2;
7272

@@ -93,7 +93,7 @@ void setUp() {
9393
.roomStatus(RoomStatus.STARTED)
9494
.build();
9595

96-
request = new QuestionCreateRequest("질문입니다");
96+
request = new QuestionRequest("질문입니다");
9797

9898
mockQ1 = QuestionFlatResponse.builder()
9999
.questionId(1L)

0 commit comments

Comments
 (0)