-
- Spring Web
- Spring Data JPA
- Spring Security
- Spring validation
- Lombok
- https://learned-kumquat-0dc.notion.site/1-2cfa7c10fe3c487481582bdd7cfb1bef
- https://learned-kumquat-0dc.notion.site/2-b87b26761db34bb28c0a4e693d495364
- https://learned-kumquat-0dc.notion.site/3-b9994eb8dbef459095553fa52d224e50
- https://learned-kumquat-0dc.notion.site/4-f8b06cc1ecf74badb4bb63023cc24257
Rest API μλ²λ‘μ Http Method μ URL μ ν΅ν΄ μλ²μκ² μμ², JSON ννμ μλ΅ μμ
- Controller: μΉ MVCμ 컨νΈλ‘€λ¬ μν (REST controller) + ν΄λΌμ΄μΈνΈμκ² μ λ¬ν DTOλ‘μ λ³ν
- Service: λλ©μΈμ λ€λ£¨λ λΉμ¦λμ€ λ‘μ§
- Repository: λ°μ΄ν° λ² μ΄μ€μ μ κ·Όνμ¬ λλ©μΈ λ°μ΄ν°λ₯Ό κ°μ Έμ€κ³ , μ μ₯ν¨
- Domain: μ건, μ¬μ©μ, ν¬ν λλ©μΈ κ°μ²΄
1. μμ€ν
μ μΈμ¦μ ν΅ν΄ μΈκ°λ μ¬μ©μλ§ μ κ·Όν μ μμ΄μΌ νκ³ ,
μ£Όμ£Όμ κ΄λ¦¬μκ° μν μ κΈ°λ°μΌλ‘ ν μ μλ νλμ΄ λ¬λΌμ ΈμΌ νλ€
http
.requestMatchers("/api/sign-up").permitAll()
.requestMatchers("/api/login").permitAll()
.requestMatchers(HttpMethod.POST, "/api/agendas").hasRole(Role.ADMIN.name())
.requestMatchers(HttpMethod.DELETE, "/api/agendas/**").hasRole(Role.ADMIN.name())
.requestMatchers("/api/agendas/*/terminate").hasRole(Role.ADMIN.name())
.requestMatchers(HttpMethod.POST, "/api/votes").hasRole(Role.USER.name())
.anyRequest().authenticated()
μ μνλ¦¬ν° μ€μ μ ν΅ν΄ μΈμ¦λμ§ μμ μ¬μ©μλ νμ κ°μ
κ³Ό λ‘κ·ΈμΈ URLμλ§ μμ²μ λ³΄λΌ μ μμΌλ©°,
μΈμ¦λ μ¬μ©μλ μΈκ°λμ§ μμ URLμλ μ κ·Ό λΆν
2. μμ€ν
μ μ건μ΄λΌκ³ λΆλ¦¬λ νμμ λν΄ μ°¬μ±, λ°λ λλ κΈ°κΆ μμ¬λ₯Ό νλͺ
ν μ μλ ν¬ν κΈ°λ₯μ μ 곡ν΄μΌ νλ€.
μ건μ κ΄λ¦¬μκ° μμ±νκ±°λ μμ ν μ μλ€
@ResponseStatus(HttpStatus.CREATED)
@PostMapping("/agendas")
public SimpleAgendaResponseDto create(@RequestBody @Valid AgendaCreateRequestDto agendaCreateRequestDto) {
Agenda savedAgenda = agendaService.createAgenda(agendaCreateRequestDto);
return AgendaResponseFactory.getDto(Role.ADMIN, savedAgenda);
}
@DeleteMapping("/agendas/{id}")
public void delete(@PathVariable("id") Long agendaId) {
agendaService.removeAgenda(agendaId);
}
@PostMapping("/votes")
public SimpleAgendaResponseDto vote(@RequestBody @Valid VoteRequestDto voteRequestDto) {
Agenda agenda = agendaService.vote(
SecurityUtil.getCurrentUserId(),
voteRequestDto.getAgendaId(),
voteRequestDto.getType(),
voteRequestDto.getQuantity());
return AgendaResponseFactory.getDto(Role.USER, agenda);
}
μ건μ μμ±, μμ λ° ν¬ν κΈ°λ₯μ μ 컨νΈλ‘€λ¬λ₯Ό ν΅ν΄ μμ² λ° μνν μ μμ
@Transactional
public Agenda createAgenda(AgendaCreateRequestDto agendaCreateRequestDto) {
return agendaRepository.save(Agenda.of(agendaCreateRequestDto));
}
@Transactional
public void removeAgenda(long agendaId) {
agendaRepository.delete(agendaRepository.findById(agendaId).orElseThrow());
}
@Transactional
public Agenda vote(String userId, Long agendaID, VoteType type, int quantity) {
User user = userService.getUser(userId);
Agenda agenda = agendaRepository.findByIdWithLock(agendaID).orElseThrow();
VotingSystem votingSystem = votingSystemFactory.makeVotingSystem(agenda);
votingSystem.vote(user, agenda, type, quantity);
return agenda;
}
컨νΈλ‘€λ¬μμ μλΉμ€λ‘ λ‘μ§ μνμ μμνκ³ , μλΉμ€μμ JPA Repositoryλ₯Ό ν΅ν΄ μ건μ μμ±, μμ ν¨
ν¬ν λν μ건μ ν¬ν λ°©μμ λ°λ₯Έ ν¬ν μμ€ν
μ ν΅ν΄ ν¬νλ₯Ό μνν μ μμ
3. μμ€ν
μ μ¬μ©μλ€μκ² μ건 λͺ©λ‘μ μ‘°νν μ μλ APIλ₯Ό μ 곡ν΄μΌ νκ³ ,
ν΄λΉ μκ±΄μ΄ νμ¬ ν¬ν μ€μΈμ§ μ¬λΆμ μμ§ μ§νλμ§ μμ κ²½μ° λ€μ ν¬ν μΌμ μ νμΈν μ μμ΄μΌ νλ€
@GetMapping("/agendas")
public Page<AllAgendaResponseDto> agendas(
@PageableDefault(sort = "startsAt", direction = Sort.Direction.ASC) Pageable pageable) {
return agendaService.getAgendas(pageable).map(AllAgendaResponseDto::from);
}
μ건 λͺ©λ‘μ μ‘°νν κ²½μ° νμ΄μ§ λ μ건 λͺ©λ‘μ λ°μ μ μμΌλ©°,
ν΄λΌμ΄μΈνΈκ° μνλ μ λ ¬ λ°©μκ³Ό λ°©ν₯μ κΈ°μ€μΌλ‘ μ λ ¬ν μ μμ (ν¬ν μμ μκ° λΉ λ₯Έ μ κΈ°λ³Έ)
λν ν¬ν λ³ ID λ° μ λͺ©, ν¬ν κ°λ₯ μκ°μ νμΈν μ μμ (ν¬ν μμ μκ° ~ ν¬ν μ’ λ£ μκ°)
4. ν¬νλ κ΄λ¦¬μκ° κ²μνκ±°λ μ’
λ£ν μ μλ€.
ν¬νλ κ΄λ¦¬μκ° μ§μ μ’
λ£ν μλ μμ§λ§ ν¬νλ₯Ό κ²μνλ μμ μ μ’
λ£ μκ°μ ν΅λ³΄νμ¬ μμ€ν
μ΄ ν΄λΉ μκ°μ΄ μ§λ νμ ν¬νλ₯Ό μ’
λ£ μν¬ μ μμ΄μΌ νλ€
@Column(nullable = false)
private LocalDateTime startsAt;
@Setter
@Column(nullable = false)
private LocalDateTime endsAt;
μ건λ μ§νμ€
νΉμ μ’
λ£
μ κ°μ μνλ₯Ό κ°μ§κ³ μμ§ μκ³ ,
ν¬ν νΉμ μ‘°νλ₯Ό μμ²ν μμ κ³Ό μ§μ ν μ’
λ£ μκ°μ λΉκ΅νμ¬ μ ν¨μ± λ° μ’
λ£ μ¬λΆλ₯Ό κ²°μ ν¨
@PatchMapping("/agendas/{id}/terminate")
public SimpleAgendaResponseDto end(@PathVariable("id") Long agendaId) {
Agenda agenda = agendaService.terminate(agendaId);
return AgendaResponseFactory.getDto(Role.ADMIN, agenda);
}
@Transactional
public Agenda terminate(long agendaId) {
Agenda agenda = agendaRepository.findById(agendaId).orElseThrow();
agenda.setEndsAt(LocalDateTime.now());
return agenda;
}
λλ¬Έμ μ’ λ£ μμ²μ ν΄λΉ μ건μ μ’ λ£ μκ°μ μ’ λ£λ₯Ό μμ²ν μκ°μΌλ‘ λ³κ²½νλ λ°©μμΌλ‘ λμ
5. μκ²°κΆμ μ건μ ν¬νν μ μλ ν¬νκΆμ κ°μλ‘ ν λͺ μ μ£Όμ£Όλ μ¬λ¬ κ°μ μκ²°κΆμ κ°μ§ μ μλ€
private Integer voteRights;
μ¬μ©μ μν°ν° λ΄ μ½λλ‘, μ¬μ©μλ μ§μ λ κ°μλ§νΌμ μκ²°κΆμ κ°μ§ μ μμΌλ©°,
μ¬μ©μ μμ± μ μ§μ λλ κ°
6. μ§ν μ€μΈ ν¬νμ μκ²°κΆμ νμ¬ν λ, μ£Όμ£Όλ 보μ ν μκ²°κΆλ³΄λ€ μ κ² νμ¬ν μ μλ€
private void validateUser(User user, int quantity) {
if (user.getVoteRights() < quantity) {
throw new InvalidVoteException(UserErrorCode.EXCEED_VOTE);
}
}
ν¬ν μμ€ν
μ ꡬν체 μ½λ λ΄ μ ν¨μ±μ κ²μ¦νλ κ³Όμ μ€,
μ¬μ©μκ° κ°μ§ μκ²°κΆμ κ°μμ νμ¬νλ €λ μκ²°κΆμ κ°μλ₯Ό λΉκ΅νμ¬ μ ν¨νμ§ κ²μ¬νλ λΆλΆμ΄
ν¬ν¨λμ΄ μμ
7. μ건μ κ²½μμ§μ μꡬμ λ°λΌ μ΄ 2 κ°μ§ ν¬ν λ°©μμ μ§μν΄μΌ νλ€. 첫 λ²μ§Έλ μκ²°κΆ μ μ°©μ μ ν κ²½μμ΄κ³ λλ¨Έμ§λ μ νμ΄ μλ λ°©μμ΄λ€
public enum AgendaType {
NORMAL,
LIMITED
}
ν¬ν λ°©μ(μΌλ°, μ μ°©μ)μ Enum μΌλ‘ μ μνκ³ μ건 μν°ν°μ ν¬ν¨μμΌ°μ
8. μκ²°κΆ μ μ°©μ μ ν κ²½μμ ν¬νμ μ°Έμ¬νλ μ μ°©μμΌλ‘ 10κ°μ μκ²°κΆλ§ ν¬νμ λ°μνλ λ°©μμ΄λ€.
μλ₯Ό λ€λ©΄ A μ£Όμ£Όλ 3κ°μ μκ²°κΆμ΄ μκ³ , B μ£Όμ£Όλ 8κ°μ μκ²°κΆμ΄ μμ λ,
Aμ Bκ° μμλλ‘ ν¬νμ μ°Έμ¬νλ€λ©΄ Aλ 3κ°μ μκ²°κΆμ λͺ¨λ νμ¬ν μ μκ³ , Bλ 8κ° μ€ 7κ°μ μκ²°κΆλ§ νμ¬ν μ μλ€.
μ΄νμ μ°Έκ°ν μ£Όμ£Όλ μκ²°κΆ νμ¬κ° λΆκ°λ₯νλ€
@Override
public void vote(User user, Agenda agenda, VoteType type, int quantity) {
quantity = Math.min(quantity, MAX_VOTE_COUNT - agenda.getTotalRights());
super.vote(user, agenda, type, quantity);
if (agenda.getTotalRights() >= MAX_VOTE_COUNT) {
agenda.setEndsAt(LocalDateTime.now());
}
}
μλ μ μ°©μ ν¬ν μμ€ν
μ ν¬ν μν λ©μλλ‘,
10κ°κ° λλ ν¬ν μμ²μ΄ λ€μ΄μ¬ κ²½μ°, 10κ°κΉμ§λ§ λ°μλλ κ°μμ μκ²°κΆλ§ λ°μλ€μ΄λλ‘ μ€κ³νμμ
λν, μ μ°©μ ν¬νμ λ°μλ νκ° 10νκ° λμμ λ, ν¬νκ° μ’ λ£λλλ‘ κ΅¬ν
9. μ ν μλ λ°©μμ μκ²°κΆμ μ ν μμ΄ λͺ¨λ μ£Όμ£Όκ° μμ μ΄ κ°μ§ λͺ¨λ μκ²°κΆμ μ건μ ν¬νν μ μλ€
private void validateUser(User user, int quantity) {
if (user.getVoteRights() < quantity) {
throw new InvalidVoteException(UserErrorCode.EXCEED_VOTE);
}
}
μꡬμ¬ν 6λ²κ³Ό λμΌν λ°©μμΌλ‘, μΌλ° ν¬νμ κ²½μ° μ¬μ©μκ° μμ§ν μκ²°κΆμ μ΄κ³Όλμ§ μμΌλ©΄ μ ν¨ν ν¬νλ‘ μΈμ λ¨
10. μμ€ν
μ ν¬ν κ²°κ³Όλ₯Ό ν¬λͺ
νκ² νμΈν μ μλλ‘ ν¬νκ° μλ£λ μ건μ λν΄ κ·Έ λͺ©λ‘κ³Ό μ°¬μ±, λ°λ, κΈ°κΆμ μ«μλ₯Ό νμΈν μ μλ APIλ₯Ό μ 곡ν΄μΌ νλ€.
κ΄λ¦¬μλ ν΄λΉ APIλ₯Ό ν΅ν΄ μ΄λ€ μ¬μ©μκ° ν΄λΉ μ건μ μ°¬μ±, λ°λ, κΈ°κΆ μμ¬ νλͺ
μ νλμ§ μ¬λΆμ μΌλ§λ λ§μ μκ²°κΆμ νμ¬νλμ§ νμΈν μ μμ΄μΌ νλ€
public class AgendaResponseFactory {
public static SimpleAgendaResponseDto getDto(Role role, Agenda agenda) {
if (LocalDateTime.now().isAfter(agenda.getEndsAt())) {
return new ResultAgendaResponseDto().from(agenda);
}
return switch (role) {
case USER -> new SimpleAgendaResponseDto().from(agenda);
case ADMIN -> new ResultAgendaResponseDto().from(agenda);
};
}
public static SimpleAgendaResponseDto getDto(Agenda agenda, List<Vote> votes) {
return new DetailResultAgendaResponse(votes).from(agenda);
}
}
λ¨μΌ μ건 μ‘°νλ 쑰건μ λ°λΌ ν¬κ² 3κ°μ§λ‘ λΆλ₯ν μ μμ
- ν¬νκ° μ§νμ€μΌ κ²½μ°
- μ¬μ©μ:
μ건 ID, μ건 μ λͺ©, ν¬ν κ°λ₯ μκ°
- κ΄λ¦¬μ:
μ건 ID, μ건 μ λͺ©, ν¬ν κ°λ₯ μκ°, νμ¬ ν μ’ λ₯ λ³ κ°μ(μ°¬μ±, λ°λ, 무ν¨)
- μ¬μ©μ:
- ν¬νκ° μ’
λ£λμμ κ²½μ°
- μ¬μ©μ:
μ건 ID, μ건 μ λͺ©, ν¬ν κ°λ₯ μκ°, κ²°κ³Ό ν μ’ λ₯ λ³ κ°μ(μ°¬μ±, λ°λ, 무ν¨)
- κ΄λ¦¬μ:
μ건 ID, μ건 μ λͺ©, ν¬ν κ°λ₯ μκ°, κ²°κ³Ό ν μ’ λ₯ λ³ κ°μ(μ°¬μ±, λ°λ, 무ν¨), μ¬μ©μ λ³ ν¬ν κΈ°λ‘
- μ¬μ©μ:
κ° μλ΅μ μ€λ³΅λλ λΆλΆμ΄ μμΌλ―λ‘ μμμ νμ©νμ¬ κ° μλ΅μ ꡬννμμ
public class ResultAgendaResponseDto extends SimpleAgendaResponseDto {
@JsonProperty
private int positiveRights;
@JsonProperty
private int negativeRights;
@JsonProperty
private int invalidRights;
public void setFrom(Agenda agenda) {
super.setFrom(agenda);
this.positiveRights = agenda.getPositiveRights();
this.negativeRights = agenda.getNegativeRights();
this.invalidRights = agenda.getInvalidRights();
}
@Override
public ResultAgendaResponseDto from(Agenda agenda) {
setFrom(agenda);
return this;
}
}
@RequiredArgsConstructor
public class DetailResultAgendaResponse extends ResultAgendaResponseDto {
private final List<Vote> votes;
@JsonProperty
private List<VoteResponseDto> voteResults;
@Override
public DetailResultAgendaResponse from(Agenda agenda) {
this.voteResults = votes.stream().map(VoteResponseDto::from).toList();
super.setFrom(agenda);
return this;
}
}
11. μμ€ν
μ ν¬ν κ²°κ³Όκ° μ‘°μλμ§ μμμ μ¦λͺ
νκΈ° μν΄ λ‘κ·Έλ₯Ό ν΅ν κ°μ¬λ₯Ό μ§μν΄μΌ νλ€.
μ΄λ₯Ό μν΄ νΉμ μ¬μ©μκ° ν¬νν κ²°κ³Όλ₯Ό μ€μκ°μΌλ‘ κΈ°λ‘ν΄μΌ νλ€
public void vote(User user, Agenda agenda, VoteType type, int quantity) {
validate(user, agenda, quantity);
agenda.vote(type, quantity);
log.info("ν¬νμ={}, μ건={}, ν μ’
λ₯={}, ν κ°μ={}", user.getName(), agenda.getTitle(), type.name(), quantity);
voteRepository.save(Vote.create(user, agenda, type, quantity));
}
ν¬ν μμ€ν
μ ν΅ν΄ ν¬νλ₯Ό μνν κ²½μ°, νλ₯Ό νμ¬νλ μ¬μ©μμ μ΄λ¦κ³Ό μ건μ μ λͺ©, ν μ’
λ₯(μ°¬μ±, λ°λ, 무ν¨), νμ¬ν μκ²°κΆ κ°μκ°
νμμ€ν¬νμ ν¨κ» μ½μμ κΈ°λ‘λ¨
12. μꡬ μ¬ν 8λ²μ ν΄λΉνλ μκ²°κΆ μ μ°©μ μ ν κ²½μ λ°©μμ μ¬λ¬ μ£Όμ£Όκ° λμμ μ ν κ²½μμ μ°Έμ¬νλλΌλ μ μ λμν¨μ 보μ₯ν΄μΌ νλ€.
μ΄λ₯Ό ν
μ€νΈ μ½λλ₯Ό ν΅ν΄ κ²μ¦μ΄ κ°λ₯ν΄μΌ νλ€
public interface AgendaRepository extends JpaRepository<Agenda, Long> {
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("select a from Agenda a where a.ID=:agendaId")
Optional<Agenda> findByIdWithLock(@Param("agendaId") Long agendaId);
}
μΌλ° ν¬νμ μ μ°©μ ν¬ν λͺ¨λ λμμ μμ²μ΄ μ λ¬λ μ μμΌλ―λ‘,
ν¬νλ₯Ό μν΄ μλΉμ€μμ ν΄λΉ μ건μ μ‘°νν΄μ¬ λ λ€λ₯Έ μμ²μ ν΄λΉ μ건μ μμ μ΄ λλ λ κΉμ§ κΈ°λ€λ¦¬λλ‘
λΉκ΄μ μ°κΈ°λ½μ κ±Έλλ‘ κ΅¬ννμμ
@DisplayName("λμμ μ μ°©μ ν¬νλ₯Ό μνν΄λ κ°μ₯ λΉ λ₯Έ 10νλ§ λ°μλλ€.")
@Test
void concurrentLimitedVote() throws InterruptedException {
// given
AgendaCreateRequestDto agendaDto = new AgendaCreateRequestDto("μ¬λ΄ ν΄κ²μμ€ μ¦μ§", AgendaType.LIMITED, LocalDateTime.now(), LocalDateTime.now().plusDays(5));
Agenda agenda = agendaService.createAgenda(agendaDto);
for (int i = 0; i < 5; i++) {
UserJoinRequestDto userDto = new UserJoinRequestDto("test" + i, "1234", "name" + i, Role.USER, 10);
userService.create(userDto);
}
// when
ExecutorService executorService = Executors.newFixedThreadPool(5);
CountDownLatch countDownLatch = new CountDownLatch(5);
CountDownLatchT tt = new CountDownLatchT();
for (int i = 1; i <= 5; i++) {
executorService.execute(() -> {
try {
tt.vote(agendaService, "test", agenda.getID(), VoteType.POSITIVE, 3);
} catch (Exception e) {
assertThat(e).isInstanceOf(InvalidVoteException.class);
} finally {
countDownLatch.countDown();
}
});
}
// then
countDownLatch.await();
assertThat(agendaService.getAgenda(agenda.getID()).getTotalRights()).isEqualTo(10);
}
public static class CountDownLatchT {
int count = 0;
public void vote(AgendaService agendaService, String userId, Long agendaId, VoteType type, int quantity) {
agendaService.vote(userId + count++, agendaId, type, quantity);
}
}
μ μ°©μ ν¬νμ λμμ± ν
μ€νΈλ₯Ό μν΄ μλ°μμ μ 곡νλ executorService
λ₯Ό νμ©νμ¬ λ³λ ¬ μν ν
μ€νΈλ₯Ό ꡬννμκ³ ,
λμμ μμ²μλ κ²°κ³Όμ μΌλ‘ 10κ°μ νλ§ λ°μμ΄ λκ³ μ΄νμ μμ²μ ν¬ν μ’ λ£λ‘ μΈν μμΈλ₯Ό λμ§λ λΆλΆλ νμΈνμμ
13. μꡬ μ¬ν 9λ²μ ν΄λΉνλ ν¬ν μμ€ν μ ν μ€νΈ μ½λλ₯Ό ν΅ν΄ κ²μ¦ν μ μμ΄μΌ νλ€
@DisplayName("ν¬νλ₯Ό μννλ©΄ ν¬ν μ’
λ₯μ λ°λΌ μ건μ νκ° λμ λλ€.")
@Test
void voteNormalAgenda() {
// given
Agenda agenda = agendaRepository.save(Agenda.create("μ¬λ΄ ν΄μμμ€ μ¦μ§", AgendaType.NORMAL,
LocalDateTime.now().minusDays(1),
LocalDateTime.now().plusDays(5)));
User user = userRepository.save(User.builder()
.userId("test")
.password("1234")
.name("νκΈΈλ")
.role(Role.USER)
.voteRights(50)
.build()
);
VoteType type = VoteType.POSITIVE;
int quantity = 15;
// when
Agenda agendaResult = agendaService.vote(user.getUserId(), agenda.getID(), type, quantity);
// then
assertThat(agendaResult.getPositiveRights()).isEqualTo(15);
}
ν΄λΉ ν
μ€νΈλ₯Ό ν΅ν΄ μΌλ° ν¬νλ μ¬μ©μκ° κ°μ§ μκ²°κΆ λ΄μμ μκ²°κΆμ νμ¬ν μ μκ³ ,
μ΄λ μ건μ ν¬ν νν©μ λμ λλ κ²μ νμΈνμμ
@DisplayName("μ¬μ©μκ° κ°μ§ ν¬νμ μ΄νμ ν¬νκΆλ§ νμ¬ν μ μλ€.")
@Test
void overVote() {
// given
Agenda agenda = agendaRepository.save(Agenda.create("μ¬λ΄ ν΄μμμ€ μ¦μ§", AgendaType.NORMAL,
LocalDateTime.now(),
LocalDateTime.now().plusDays(5)));
User user = userRepository.save(User.builder()
.userId("test")
.password("1234")
.name("νκΈΈλ")
.role(Role.USER)
.voteRights(10)
.build()
);
VoteType type = VoteType.POSITIVE;
int quantity = 11;
// when&then
assertThatThrownBy(() -> {
agendaService.vote(user.getUserId(), agenda.getID(), type, quantity);
}).isInstanceOf(InvalidVoteException.class);
}
λν νμ¬ κ°μ§ μκ²°κΆ μ΄μμΌλ‘ νμ¬ν κ²½μ°, μκ²°κΆ μ΄κ³Ό μμΈκ° λ°μνλ κ³Όμ λ νμΈνμμ
spring:
jpa:
open-in-view: false
OSIV μ΅μ μ λμΌλ‘μ¨ DB connectionμ ν¨μ¨μ μΌλ‘ κ΄λ¦¬ν μ μλλ‘ νμμ
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(indexes = {
@Index(columnList = "agenda_id"),
@Index(columnList = "agenda_id, user_id")
})
public class Vote extends CreatedAtBaseEntity {
...
}
ν¬ν ν
μ΄λΈμ λν΄ λ³΅ν© μΈλ±μ€μ λ¨μΌ μΈλ±μ€λ₯Ό ν΅ν΄ μ‘°ν μ±λ₯μ ν₯μμμΌ°μ
100000κ°μ ν¬ν + 5κ° μ건μ λν νκ· μ‘°ν μκ° (154ms -> 41ms) νμΈ
μμΈκ° λ°μνμ κ²½μ° μ λ¬ν μ½λμ λ©μμ§λ₯Ό Enum type μΌλ‘ μ μ
μ°μ μλ¬ μ½λ μΈν°νμ΄μ€λ₯Ό μ μνκ³ ,
보νΈμ μΈ μμΈμ λ°λ₯Έ μ½λμΈμ§, μ¬μ©μκ° μ μν μμΈμ λ°λ₯Έ μ½λμΈμ§λ₯Ό ꡬλ³ν΄μ ꡬννμμ
public interface ErrorCode {
HttpStatus getHttpStatus();
String getMessage();
}
@Getter
@RequiredArgsConstructor
public enum CommonErrorCode implements ErrorCode {
INVALID_PARAMETER(HttpStatus.BAD_REQUEST, "Invalid parameter included"),
RESOURCE_NOT_FOUND(HttpStatus.NOT_FOUND, "Resource not exists"),
NO_ELEMENT(HttpStatus.NOT_FOUND, "Element not exists"),
;
private final HttpStatus httpStatus;
private final String message;
}
@Getter
@RequiredArgsConstructor
public enum UserErrorCode implements ErrorCode {
EMPTY_CLAIM(HttpStatus.UNAUTHORIZED, "No authentication information in claim"),
DUPLICATED_ID(HttpStatus.CONFLICT, "ID already exists"),
WRONG_PERIOD(HttpStatus.BAD_REQUEST, "Not voting period"),
EXCEED_VOTE(HttpStatus.BAD_REQUEST, "Exceeding the number of possible votes"),
DUPLICATED_VOTE(HttpStatus.BAD_REQUEST, "Duplicate voting is not allowed")
;
private final HttpStatus httpStatus;
private final String message;
}
곡ν΅μ μΈ μμΈλ‘λ
- μ€νλ§ μ ν¨μ± κ²μ¦ λ¨κ³μμ (Spring validation) λ°μμν€λ μμΈ
- μλͺ»λ ID λ₯Ό ν΅ν μμ μ‘°νμ λν΄ Optional μμ κΈ°λ³Έμ μΌλ‘ λ°μμν€λ μμΈ
λ κ°μ§ μμΈλ₯Ό λ€λ£¨λλ‘ μ€κ³ νμκ³ ,
μ¬μ©μ μ μ μμΈλ‘λ
- ν ν°μ ν΄λ μ μ λ³΄κ° μμ λ μμΈ
- μ€λ³΅λ ID λ‘ νμ κ°μ μ μμ²ν λ μμΈ
- μ¬λ°λ₯΄μ§ λͺ»ν ν¬ν μμ²μ 보λμ λ μμΈ
μΈ κ°μ§ μμΈλ₯Ό λ€λ£¨λλ‘ μ€κ³νμμ
@Getter
@RequiredArgsConstructor
public class RestApiException extends RuntimeException {
private final ErrorCode errorCode;
}
public class InvalidVoteException extends RestApiException {
public InvalidVoteException(ErrorCode errorCode) {
super(errorCode);
}
}
public class DuplicateUserException extends RestApiException {
public DuplicateUserException(ErrorCode errorCode) {
super(errorCode);
}
}
μ°μ μμΈ λ΄ νλΌλ―Έν°λ‘ λ£μ μλ¬μ½λλ₯Ό RestApiException μΌλ‘ μ μνκ³ ,
ꡬ체μ μΈ μμΈλ₯Ό ν΄λΉ μμΈ μμ λ°λ κ΅¬μ‘°λ‘ μ μνμμ
μμμ μ μν μλ¬μ½λλ₯Ό ν리미ν°λ‘ μ λ¬νλ©΄, μμΈ κ°μ²΄ λ΄ μλ¬ μ½λλ₯Ό κ°μ§κ² λκ³ ,
ν΄λΉ μμΈλ₯Ό μ²λ¦¬ν λ λ©μμ§μ μνμ½λλ₯Ό νμΈ κ°λ₯
@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler(RestApiException.class)
public ResponseEntity<Object> handleCustomException(RestApiException e) {
ErrorCode errorCode = e.getErrorCode();
return new ResponseEntity<>(errorCode.getMessage(), errorCode.getHttpStatus());
}
@ExceptionHandler(EntityNotFoundException.class)
public ResponseEntity<Object> handleEntityNotFound(EntityNotFoundException e) {
ErrorCode errorCode = CommonErrorCode.RESOURCE_NOT_FOUND;
return new ResponseEntity<>(errorCode.getMessage(), errorCode.getHttpStatus());
}
@ExceptionHandler(NoSuchElementException.class)
public ResponseEntity<Object> handleNoSuchElement(NoSuchElementException e) {
ErrorCode errorCode = CommonErrorCode.NO_ELEMENT;
return new ResponseEntity<>(errorCode.getMessage(), errorCode.getHttpStatus());
}
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<Object> handleIllegalArgument(IllegalArgumentException e) {
ErrorCode errorCode = CommonErrorCode.INVALID_PARAMETER;
return new ResponseEntity<>(errorCode.getMessage(), errorCode.getHttpStatus());
}
}
μ΅μ’ μ μΌλ‘ μλ²μμ λ°μνλ μμΈλ₯Ό λ°μμ μ²λ¦¬νλ νΈλ€λ¬ ν΄λμ€
μλ¨μμ μ μ νλ RestApiException κ³Ό μΌλ°μ μΌλ‘ λ°μν μ μλ μμΈμ λν΄ μ²λ¦¬ κ³Όμ μ μμ±νλ©΄,
μ μ νλ μλ¬ μ½λλ₯Ό ν΅ν΄ μ¬μ©μμκ² μλ΅μ μ λ¬ν μ μκ² λ¨