아래 코드는 피드백을 토대로 수정한 내용입니다. 자세한 소스코드는 다음 링크에서 확인 가능합니다.
다양한 예외처리 적용하기(@Valid)
package com.sparta.nuricalendaradvanced.domain.schedule.dto;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class ScheduleRequestDto {
@Pattern(regexp = "^(\\d{4})-(\\d{2})-(\\d{2})$")
@Size(max = 10)
private String date;
@Size(max = 50)
private String title;
@Size(max = 255)
private String contents;
@Size(max = 10)
private String username;
}
예외 일관적으로 처리
package com.sparta.nuricalendaradvanced.common.exception;
import com.fasterxml.jackson.core.JsonProcessingException;
import jakarta.servlet.ServletException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
@Slf4j(topic = "exception:")
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(value = ResponseException.class)
public ResponseEntity<String> responseException(ResponseException e) {
return ResponseEntity
.status(e.getResponseStatus().getHttpStatus())
.body(e.getErrMessage());
}
@ExceptionHandler(IOException.class)
public ResponseEntity<String> responseException(IOException e) {
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(e.getMessage());
}
@ExceptionHandler(ServletException.class)
public ResponseEntity<String> responseException(ServletException e) {
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(e.getMessage());
}
@ExceptionHandler(UnsupportedEncodingException.class)
public ResponseEntity<String> responseException(UnsupportedEncodingException e) {
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(e.getMessage());
}
@ExceptionHandler(JsonProcessingException.class)
public ResponseEntity<String> responseException(JsonProcessingException e) {
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(e.getMessage());
}
}
package com.sparta.nuricalendaradvanced.common.exception;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
@Getter
@RequiredArgsConstructor
public enum ResponseStatus {
USER_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 회원은 존재하지 않습니다."),
USER_NAME_DUPLICATED(HttpStatus.BAD_REQUEST, "이미 가입한 유저 입니다."),
USER_EMAIL_DUPLICATED(HttpStatus.BAD_REQUEST, "중복된 Email 입니다."),
USER_PASSWORD_NOT_MATCH(HttpStatus.UNAUTHORIZED, "암호가 일치하지 않습니다."),
ADMIN_PASSWORD_NOT_MATCH(HttpStatus.UNAUTHORIZED, "관리자 암호가 틀려 등록이 불가능합니다."),
SCHEDULE_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 일정이 존재하지 않습니다."),
COMMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 댓글이 존재하지 않습니다."),
TOKEN_NOT_FOUND(HttpStatus.NOT_FOUND, "토큰을 찾을 수 없습니다."),
TOKEN_INVALID(HttpStatus.UNAUTHORIZED, "토큰이 유효하지 않습니다.");
private final HttpStatus httpStatus;
private final String errMessage;
}
필터에서 로그 추가
package com.sparta.nuricalendaradvanced.common.filter;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Slf4j(topic = "LoggingFilter")
@Component
@Order(1)
public class LoggingFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
// 전처리
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String url = httpServletRequest.getRequestURI();
String userAgent = httpServletRequest.getHeader("user-agent"); // Header 로그 추가
log.info("url: " + url);
log.info("userAgent: " + userAgent); // Header 로그 추가
chain.doFilter(request, response);
// 후처리
log.info("비즈니스 로직 완료");
}
}
Early return
`if` 문의 조건이 길어지게 될 경우 해당 조건이 어떤 것을 의도하는지 코드를 읽는사람으로 하여금 파악하는데 어렵게 만듭니다. 이럴 경우 별도 메소드로 분리해서 의도를 설명해주는게 해결책이 될 수 있습니다.
package com.sparta.nuricalendaradvanced.common.filter;
import com.sparta.nuricalendaradvanced.common.exception.ResponseException;
import com.sparta.nuricalendaradvanced.common.exception.ResponseStatus;
import com.sparta.nuricalendaradvanced.common.jwt.JwtUtil;
import com.sparta.nuricalendaradvanced.domain.user.entity.User;
import com.sparta.nuricalendaradvanced.domain.user.repository.UserRepository;
import io.jsonwebtoken.Claims;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.io.IOException;
@Slf4j(topic = "AuthFilter")
@Component
@Order(2)
public class AuthFilter implements Filter {
private final UserRepository userRepository;
private final JwtUtil jwtUtil;
public AuthFilter(UserRepository userRepository, JwtUtil jwtUtil) {
this.userRepository = userRepository;
this.jwtUtil = jwtUtil;
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ResponseException, ServletException, IOException {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String url = httpServletRequest.getRequestURI();
// Early return으로 수정
if (StringUtils.hasText(url) &&
(url.startsWith("/api/user/signup") || url.startsWith("/api/user/signin"))
) {
chain.doFilter(request, response);
return;
}
String tokenValue = jwtUtil.getTokenFromRequest(httpServletRequest);
// Early return으로 수정
if (!StringUtils.hasText(tokenValue)) {
throw new ResponseException(ResponseStatus.TOKEN_NOT_FOUND);
}
String token = jwtUtil.substringToken(tokenValue);
// Early return으로 수정
if (!jwtUtil.validateToken(token)) {
throw new ResponseException(ResponseStatus.TOKEN_INVALID);
}
Claims info = jwtUtil.getUserInfoFromToken(token);
User user = userRepository.findById(Long.valueOf(info.getSubject())).orElseThrow(() ->
new ResponseException(ResponseStatus.USER_NOT_FOUND)
);
request.setAttribute("user", user);
chain.doFilter(request, response);
}
}
RESTful API에서 URL에 동사 넣지 않기 & RequestParam 사용
package com.sparta.nuricalendaradvanced.domain.schedule.controller;
@RestController
@RequestMapping("/api/schedule")
@RequiredArgsConstructor
public class ScheduleController {
private final ScheduleService scheduleService;
@PostMapping() // /submit url을 삭제
public ResponseEntity<ScheduleResponseDto> submitSchedule(@RequestBody @Valid ScheduleRequestDto requestDto,
HttpServletRequest req) throws JsonProcessingException {
User user = (User) req.getAttribute("user");
return ResponseEntity
.status(HttpStatus.CREATED)
.body(scheduleService.submitSchedule(requestDto, user));
}
@PutMapping() // /update url을 삭제
public ResponseEntity<ScheduleResponseDto> updateSchedule(@RequestParam Long id,
@RequestBody ScheduleRequestDto requestDto) throws ResponseException, JsonProcessingException {
return ResponseEntity
.status(HttpStatus.OK)
.body(scheduleService.updateSchedule(id, requestDto));
}
@DeleteMapping() // /delete url을 삭제
public ResponseEntity<Void> deleteSchedule(@RequestParam Long id) {
scheduleService.deleteSchedule(id);
return ResponseEntity
.status(HttpStatus.NO_CONTENT)
.build();
}
}
정적 팩토리를 이용하여 entity 객체 생성
package com.sparta.nuricalendaradvanced.domain.schedule.entity;
@Entity
@Getter
@NoArgsConstructor
public class Schedule extends Timestamped {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 10)
private String date;
@Column(nullable = false, length = 50)
private String title;
@Column(nullable = false)
private String contents;
@Column(nullable = false)
private String weather;
@ManyToOne
@JoinColumn(name = "user_id")
private User user;
@OneToMany(mappedBy = "schedule", cascade = {CascadeType.PERSIST, CascadeType.REMOVE})
private List<Comment> commentList = new ArrayList<>();
public static Schedule from(ScheduleRequestDto requestDto, User user, String weather) {
Schedule schedule = new Schedule();
schedule.initData(requestDto, user, weather);
return schedule;
}
private void initData(ScheduleRequestDto requestDto, User user, String weather) {
this.date = requestDto.getDate();
this.title = requestDto.getTitle();
this.contents = requestDto.getContents();
this.weather = weather;
this.user = user;
}
public Schedule update(ScheduleRequestDto requestDto, String weather) {
this.date = requestDto.getDate();
this.title = requestDto.getTitle();
this.contents = requestDto.getContents();
this.weather = weather;
return this;
}
}
IoC를 위반하지 않도록 코드 수정
`Schedule` Entity 가 RequestDTO 에 대해서 알고있는 것은 클린코드 관점에서 의존성 규칙을 위배했다고 볼 수 있습니다. 즉, 더 높은 수준의 객체인 Entity 가 더 낮은 수준의 객체인 DTO 를 의존하고 있어 변경에 취약한 구조가 만들어졌다고 볼 수 있습니다.
package com.sparta.nuricalendaradvanced.domain.schedule.service;
import java.time.format.DateTimeFormatter;
@Slf4j(topic = "Schedule Logic")
@Service
@RequiredArgsConstructor
public class ScheduleService {
private final ScheduleRepository scheduleRepository;
private final WeatherApiService weatherApiService;
public ScheduleResponseDto submitSchedule(ScheduleRequestDto requestDto, User user) throws JsonProcessingException {
Schedule schedule = Schedule.from(requestDto, user, findWeatherData());
Schedule savedSchedule = scheduleRepository.save(schedule);
// 변경 전
return savedSchedule.to();
// 변경 후
return ScheduleResponseDto.of(savedSchedule);
}
}
외부 API 조회 기능 추가
Weather 외부 API를 사용하여 날씨 정보를 조회하는 기능을 추가했습니다.
package com.sparta.nuricalendaradvanced.common.api;
@Slf4j(topic = "WeatherApi")
@Service
public class WeatherApiService {
private final RestTemplate restTemplate;
public WeatherApiService(RestTemplateBuilder builder) {
restTemplate = builder.build();
}
public List<WeatherDto> searchWeather() throws JsonProcessingException {
URI uri = UriComponentsBuilder
.fromUriString("https://f-api.github.io")
.path("/f-api/weather.json")
.encode()
.build()
.toUri();
log.info("uri: " + uri);
ResponseEntity<String> responseEntity = restTemplate.getForEntity(uri, String.class);
log.info("statusCode: " + responseEntity.getStatusCode());
log.info("Body: " + responseEntity.getBody());
return fromJSONtoWeather(responseEntity.getBody());
}
public List<WeatherDto> fromJSONtoWeather(String responseEntity) throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper();
return objectMapper.readValue(responseEntity, new TypeReference<List<WeatherDto>>() {
});
}
public Optional<String> getWeatherByDate(List<WeatherDto> weatherDtoList, String date) {
return weatherDtoList.stream()
.filter(weatherDto -> weatherDto.getDate().equals(date))
.map(WeatherDto::getWeather)
.findFirst();
}
}
'Spring' 카테고리의 다른 글
[Spring] LazyInitializationException 해결 방법 (3) | 2024.11.08 |
---|---|
[Spring] BeanDefinitionStoreException: Failed to parse configuration class 에러 해결 (0) | 2024.11.08 |
[Spring] a foreign key constraint fails 에러 발생 (0) | 2024.10.17 |
[Spring] Ambiguous handler methods mapped 에러 해결 (1) | 2024.10.04 |
[Spring] 3 Layer Architecture (0) | 2024.10.02 |