룸메이트 매칭 프로젝트 3주차

윤장원·2023년 6월 16일
0

3주차엔 게시글 찜 기능 구현을 했다. 유저가 게시글에 찜 버튼을 눌렀을 때 LikeArticle 테이블에 해당 정보가 있으면 데이터를 삭제하여 찜 삭제를 구현했고, LikeArticle 테이블에 해당 정보가 없으면 데이터를 저장하여 찜 등록을 구현했다. 그리고 유저가 해당 게시글에 찜을 한 상태인지 불러오는 기능도 구현했다.
찜 삭제 기능에서 팀원끼리 Soft Delete 방식을 사용할지 Hard Delete 방식을 사용할지 고민해보았는데 Soft Delete는 보존해야 할 데이터에 적용을 해야하고, 불필요한 데이터가 많이 쌓여서 오히려 안 좋을 수 있다는 생각을 하여 Hard Delete 방식을 사용하기로 했다

LikeArticle

@Entity
@Builder
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class LikeArticle extends BaseEntity {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Integer id;

  @ManyToOne
  @JoinColumn(name = "user_id")
  @ToString.Exclude
  private User user;

  @ManyToOne
  @JoinColumn(name = "article_id")
  @ToString.Exclude
  private Article article;
}

ArticleController

  @PostMapping("/favorites/{id}")
  public ResponseEntity<?> postArticleFavorite(
      @AuthenticationPrincipal User user,
      @PathVariable int id
  ) {
    var result = articleService.postArticleFavorite(user, id);
    return ApiResponse.builder().code(ResponseCode.RESPONSE_CREATED).data(result).toEntity();
  }

  @GetMapping("/favorites/{id}")
  public ResponseEntity<?> getArticleFavorite(
      @AuthenticationPrincipal User user,
      @PathVariable int id
  ) {
    var result = articleService.getArticleFavorite(user, id);
    return ApiResponse.builder().code(ResponseCode.RESPONSE_SUCCESS).data(result).toEntity();
  }

ArticleService

  @Override
  public String postArticleFavorite(User user, Integer id) {

    Article article = articleRepository.findById(id)
        .orElseThrow(() -> new CustomException(ErrorCode.ARTICLE_NOT_EXISTS));

    if (likeArticleRepository.existsByUserIdAndArticleId(user.getId(), id)) {
      LikeArticle likeArticle = likeArticleRepository.findByUserIdAndArticleId(user.getId(), id);

      likeArticleRepository.delete(likeArticle);

      return DELETE_LIKE_ARTICLE_SUCCESS;
    } else {
      LikeArticle likeArticle = LikeArticle.builder()
          .user(user)
          .article(article)
          .build();

      likeArticleRepository.save(likeArticle);

      return ADD_LIKE_ARTICLE_SUCCESS;
    }
  }

  @Override
  public boolean getArticleFavorite(User user, Integer id) {

    return likeArticleRepository.existsByUserIdAndArticleId(user.getId(), id);
  }

그리고 게시글 상세정보 보기 구현을 했다. Article Id 값을 받아 해당 Article의 정보를 불러오는 코드를 작성했다.

ArticleController

  @GetMapping("/{id}")
  public ResponseEntity<?> getArticle(
      @PathVariable int id
  ) {
    var result = articleService.getArticle(id);
    return ApiResponse.builder().code(ResponseCode.RESPONSE_SUCCESS).data(result).toEntity();
  }

ArticleService

  @Override
  public ArticlePageDto getArticle(Integer id) {
    Article article = articleRepository.findByIdAndIsDeletedFalse(id)
        .orElseThrow(() -> new CustomException(ErrorCode.ARTICLE_NOT_EXISTS));

    return ArticlePageDto.toDto(article);
  }

로그 아웃 기능 프론트엔드와 백엔드 연동 테스트하는 과정에서 User가 null 값으로 들어와도 Article 등록이 되는 문제를 발견했다. 컨트롤러에 @AuthenticationPrincipal 어노테이션을 사용해서 토큰이 없는 접근은 막을 수 있을 줄 알았지만 해당 어노테이션은 가져오는 User가 null 이어도 그대로 null로 보냈다. 이를 해결하기 위해 Article 엔티티 설정을 변경해줬고, SecurityConfig 수정을 통해 Article 관련 API에 대해서는 HTTP 요청 중 GET 메서드만 permitall 처리를 해줬다.

SecurityConfig

.antMatchers(HttpMethod.GET ,"/api/articles/**").permitAll()

이렇게 처리를 해주어서 로그인 없이 요청을 했을 때 Filter에서 처리를 해주어 403에러를 내도록 만들었다. 그 후 팀 회의를 통해 인증이 되지 않은 유저의 요청에 따른 403 에러를 백엔드에서 Handling을 하기로 했다. 검색을 통해 찾아보니 SecurityConfig 파일에 .exceptionHandling().authenticationEntryPoint() 를 통해 에러를 핸들링 할 수 있는 것을 알 수 있었다. 방법은 다음과 같다. AuthenticationException이 발생하게 되면 우리가 만든 Controller로 Redirect를 하는 과정을 거치게 되어 해당 주소에서 우리가 만든 에러를 throw시키면 된다.

CustomAuthenticationEntryPoint

public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {

  @Override
  public void commence(HttpServletRequest request, HttpServletResponse response,
      AuthenticationException authException) throws IOException, ServletException {

    response.sendRedirect("/api/exception");
  }
}

SecurityConfig

.exceptionHandling().authenticationEntryPoint(new CustomAuthenticationEntryPoint())

ExceptionController

@RestController
@RequestMapping("/api/exception")
public class ExceptionController {

  @GetMapping
  public ResponseEntity<?> getException() {
    throw new CustomException(ErrorCode.USER_IS_NULL);
  }
}

해당 에러 핸들링을 공부하는 과정에서 Security에 대해 더 학습하는 시간을 가질 수 있어서 좋았다.

이번주의 팀원들의 궁금했던 점들과 그에 대한 답안들은 다음과 같다.

  1. Soft Delete와 Hard Delete 중 어떤 것을 사용할지 정하는 기준은?
    Soft Delete는 데이터를 보존할 수 있다는 장점이 있다. 하지만 불필요한 데이터가 많이 쌓이게 되면 데이터 조회하는 과정에서 문제가 발생할 수 있다. 그래서 Soft Delete를 처리한지 N Day, N Year 이후 완전 삭제하는 방법을 사용할 수 있다. 그 과정에서 파일로 저장하거나 백업 데이터베이스를 생성할 수 있다.
    Hard Delete 했을 때 Delete & Insert 하는 비용 보다 상태를 Update 하는 비용이 싸다.
    둘 중 어느 방식을 사용할지 정하는 과정에서 어느 정도 기간을 두고 Soft Delete, Hard Delete를 바꿔 보면서 둘을 비교해서 검증하여 선택할 수 있다.

  2. 프로젝트를 진행하면서 서로 바빠지면서 코드 리뷰를 제대로 하지 못하고 PR에 대해 무지성 Approve를 하게 되는 것 같다. 이를 해결하기 위한 방안은?
    모든 PR에 대한 코드 리뷰가 이루어지기 힘든 건 당연하다. PR로 코드 리뷰하기 어려운 내용이나 변경되는 코드의 양이 매우 많다면 라이브 코드 리뷰를 통해 팀원들에게 자신의 코드를 설명하는 시간을 갖고, 2차로 PR에 대한 코드 리뷰를 받으면 좋을 것 같다. 라이브 코드 리뷰를 통해 자신의 코드를 남에게 설명하는 법을 기를 수 있고, 남의 코드를 보고 질문하는 힘을 기를 수 있어 좋다.

  3. 이미지, 채팅 로그 파일 저장하는 장소?
    서버가 한 대가 아니여서 filesystem은 사용하지 않는 쪽이 좋다. 이미지 업로드는 S3를 이용하고, 채팅은 작은 단위라 NoSQL을 사용할 계획이다.

  4. 데이터베이스에 Enum 타입은 어떻게 저장할까?
    Enum 타입의 데이터는 String 타입이나 Tinyint 타입으로 저장할 수 있다. 하지만 데이터베이스 관리, 조회하는 과정에서 String 타입을 쓰는 것이 좋다.

0개의 댓글