오늘은 클린 코드와 예외 처리의 중요성을 주제로 이야기하려고 합니다. 특히 중복 코드의 리팩토링과 사용자 권한 검증을 통해 코드의 가독성과 유지보수성을 높이는 방법에 대해 소개하겠습니다.
이번 프로젝트는 RESTful API를 사용해 게시판 서비스를 구현하는 것이 목적이었습니다. 게시글 작성, 수정, 삭제와 같은 기능을 구현하면서, 아래와 같은 문제점을 발견하게 되었습니다.
중복 코드의 반복 사용:
예외 처리의 일관성 부족:
ResponseStatusException
을 통해 명확한 에러 메시지와 상태 코드를 반환하도록 개선함.MemberLoginService
클래스중복된 로그인 유저 조회 로직을 MemberLoginService
클래스에 통합하여 각 컨트롤러에서 재사용할 수 있게 했습니다.
package com.moonBam.service.member;
import com.moonBam.dto.MemberDTO;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;
import java.security.Principal;
import java.util.Optional;
@Service
@RequiredArgsConstructor
public class MemberLoginService {
private final MemberService memberService;
public MemberDTO findByPrincipal(Principal principal) {
if (principal == null) {
return null;
}
return Optional.ofNullable(memberService.findByUserId(principal.getName()))
.orElseThrow(() -> new ResponseStatusException(HttpStatus.UNAUTHORIZED, principal.getName() + " not found"));
}
}
findByPrincipal
메서드:Principal
을 통해 MemberDTO
를 찾음.ResponseStatusException
을 발생시켜 UNAUTHORIZED
상태 코드를 반환.BoardAPIController
클래스권한 검증과 유효성 검사를 개선한 BoardAPIController
클래스입니다.
package com.moonBam.controller.board;
import com.moonBam.dto.MemberDTO;
import com.moonBam.dto.board.PostDTO;
import com.moonBam.dto.board.PostPageDTO;
import com.moonBam.dto.board.PostUpdateRequestDTO;
import com.moonBam.service.PostService;
import com.moonBam.service.ScrapService;
import com.moonBam.service.member.MemberLoginService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.security.Principal;
import java.util.Map;
import java.util.stream.Collectors;
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/post")
public class BoardAPIController {
private final PostService postService;
private final MemberLoginService memberLoginService;
private final ScrapService scrapService;
@PostMapping
public ResponseEntity<?> create(@RequestBody @Validated PostDTO postDTO,
BindingResult bindingResult, Principal principal) {
if (bindingResult.hasErrors()) {
Map<String, String> errors = getErrors(bindingResult);
String s = errors.values().stream().toList().get(0);
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(Map.of("message", s, "errors", errors));
}
// XSS 방지를 위해 입력값에서 스크립트 태그를 제거하는 로직 추가
String sanitizedPostText = sanitizeHtml(postDTO.getPostText());
String sanitizedPostTitle = sanitizeHtml(postDTO.getPostTitle());
postDTO.setPostText(sanitizedPostText);
postDTO.setPostTitle(sanitizedPostTitle);
MemberDTO loginUser = memberLoginService.findByPrincipal(principal);
postDTO.setUserId(loginUser.getUserId());
postDTO.setNickname(loginUser.getNickname());
Long postID = postService.save(postDTO);
return ResponseEntity.ok(Map.of("postID", postID)); // 동적으로 ajax success function에서 redirect 시킬 용도
}
@PatchMapping("/{postId}")
public ResponseEntity<?> update(@RequestBody @Validated PostUpdateRequestDTO postUpdateRequestDTO, @PathVariable("postId") Long postId,
BindingResult bindingResult, Principal principal) {
PostDTO postDTO = postService.findById(postId);
if (bindingResult.hasErrors()) {
Map<String, String> errors = getErrors(bindingResult);
String s = errors.values().stream().toList().get(0);
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(Map.of("message", s, "errors", errors));
}
// XSS 방지를 위해 입력값에서 스크립트 태그를 제거하는 로직 추가
String sanitizedPostText = sanitizeHtml(postUpdateRequestDTO.getPostText());
postUpdateRequestDTO.setPostText(sanitizedPostText);
postUpdateRequestDTO.setPostTitle(sanitizeHtml(postUpdateRequestDTO.getPostTitle()));
MemberDTO loginUser = memberLoginService.findByPrincipal(principal);
if (!loginUser.getUserId().equals(postDTO.getUserId())) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Map.of("message", "글을 수정할 권한이 없습니다."));
}
postService.update(postUpdateRequestDTO);
return ResponseEntity.ok(Map.of("postID", postId));
}
@DeleteMapping("{postId}")
public ResponseEntity<?> delete(@PathVariable("postId") Long postId, Principal principal) {
PostDTO postDTO = postService.findById(postId);
MemberDTO loginUser = memberLoginService.findByPrincipal(principal);
if (postDTO == null) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(Map.of("message", "글이 존재하지 않습니다."));
}
if (!loginUser.getUserId().equals(postDTO.getUserId())) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Map.of("message", "글을 삭제할 권한이 없습니다."));
}
scrapService.findAllByPostId(postId).forEach(scrapDTO -> scrapService.delete(scrapDTO.getScrapId()));
postService.delete(postId);
return ResponseEntity.ok(Map.of("message", "삭제가 완료되었습니다."));
}
@GetMapping("/{postId}")
public ResponseEntity<?> findOne(@PathVariable("postId") Long postId, Principal principal) {
PostPageDTO pDTO = postService.selectPagePost(postId);
if (pDTO == null) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(Map.of("message", "존재하지 않는 게시글입니다."));
}
MemberDTO loginUser = memberLoginService.findByPrincipal(principal);
boolean isAuthorized = loginUser != null && loginUser.getUserId().equals(pDTO.getUserId());
return ResponseEntity.ok(Map.of("pDTO", pDTO, "isAuthorized", isAuthorized));
}
private Map<String, String> getErrors(BindingResult bindingResult) {
return bindingResult.getFieldErrors().stream()
.collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage));
}
private String sanitizeHtml(String input) {
return input.replaceAll("(?i)<script.*?>.*?</script>", "") // 스크립트 태그 제거
.replaceAll("(?i)<.*?javascript:.*?>.*?</.*?>", "") // "javascript:" URI 사용 제거
.replaceAll("(?i)<.*?\\bon.*?>.*?</.*?>", ""); // 이벤트 핸들러 제거 (예: onclick)
}
}
중복 코드 제거:
MemberLoginService
로 통합함으로써 가독성 및 재사용성을 높였습니다.일관된 예외 처리:
ResponseStatusException
을 활용해 사용자 권한 검증과 요청 유효성 검사 시 명확한 상태 코드를 반환함으로써 예외 처리의 일관성을 확보했습니다.보안 강화:
sanitizeHtml
메서드를 통해 사용자 입력에 대한 XSS 방어 로직을 추가하여 보안을 강화했습니다.이번 프로젝트를 통해 클린 코드의 중요성과 예외 처리의 중요성을 다시 한번 깨닫게 되었습니다. 코드를 리팩토링하고 중복 로직을 제거함으로써 가독성과 유지보수성을 향상시키고, 일관된 예외 처리로 권한 검증 및 유효성 검사 로직을 개선했습니다.