Skip to content

Conversation

@DongCaprio
Copy link

댓글 기능 추가 / 기존 기능 리팩토링 / 조회수 기능 추가

현재 진행중

- service에 댓글 조회 기능 추가
 - 전체 게시글 조회 시 내용 안보임(상세조회 해야 내용 보임)
 - 생성자 추가/변경으로 인한 테스트 변경
- boardApiController > ArticleController 등

refactor : 클래스명 더 알맞게 리팩토링
- boardApiController > ArticleController 등
 - 기존 List 반환에서 Page 커스텀 DTO로 반환으로 리팩토링
- 기존 기본 생성자에서 builder 방식으로 변경
- Builder 방식을 통해 좀 더 명확하게 게시글 부분에 null이 들어가는것을 표현
- 실제 삭제가 아닌 deleted로 인한 boolean 값으로 soft delete 상태/기능 추가
 - 현재
  - 게시글 : [최신순, 오래된순, 조회순] 정렬 가능
  - 댓글 : [최신순, 오래된순] 정렬 가능
 - soft delete를 통해 deleted 상태로 관리
 - 회원탈퇴 경우(soft delete) 값은 db에 보관되기 때문에 해당 id/nickName와 중복된 값으로는 회원가입 불가
Copy link
Member

@sosow0212 sosow0212 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수고하셨습니다~ 간단하게 사용 근거를 여쭤보고자 리뷰 남겨봤는데 확인하시고 의견 나눠주시면 좋을 것 같습니다! 연휴동안 수고 많으셨습니다!

Comment on lines 28 to 30
@SpringBootTest
@AutoConfigureMockMvc
@Transactional
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

테스트 격리를 위해 Transactional을 함께 쓰신 것 같은데, 제가 쓴 글이긴 한데 보시고 테스트 격리용 클래스나 어노테이션을 만들어보면 어떨까요?
https://blog.naver.com/sosow0212/223838063652

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

통합테스트용 커스텀 어노테이션을 만들고 사용했습니다!

재윤님 글처럼 truncate 하는 방식 검색해보고 그것으로 변경해보려고 했는데,,
이게 자꾸 오류가 나서 좀 더 확인해봐야될것 같습니다!

현재로써는 기존처럼 test.yml 에서 ddl-auto: create-drop 을 통해서
매 테스트마다 테이블이 재 생성되어
테스트를 한번에 돌려도 개별 테스트처럼 실행되게 설정하였습니다!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ddl-auto: create-drop 이렇게 사용하면 컨텍스트가 띄워지는 시점에서만 초기화 되는 거 아닌가요!?

만약 지금 구조라면 캐싱하지 않아서 하나의 통합 테스트 클래스에서 하나의 스프링 컨텍스트가 띄워질텐데 그러면 한 클래스 안에 있는 테스트 메서드들끼리는 실행 사이에 create-drop이 되지 않아서 격리가 되지 않을 것 같습니다!

Copy link
Author

@DongCaprio DongCaprio May 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

말씀해주신 격리에 대해 검색을 해보았는데
아직 헷갈리는 부분이 있어 여쭈어보고 싶습니다!

현재 구조에서 데이터 초기화의 핵심은 ddl-auto: create-drop 와 전체 테스트에 적용되어 있는 @Transactional 이라고 생각합니다.

컨텍스트가 띄워지는 시점에는 ddl-auto: create-drop 로 인해서 새롭게 테이블이 삭제 후 생성되니 초기화가 성공적으로 될것같고

하나의 통합 테스트 클래스의 메서드 끼리는 각 테스트마다 @Transactional 을 통해서 1번 테스트에서 insert,update,delete 등을 해도 2번 테스트에서는 롤백된 상황에서 진행되니 크게 문제가 없는게 아닌가? 라고 생각했었습니다
(실패하든, 성공하든, 에러발생하든 @Transactional 통해서 데이터는 롤백)

검색해보니 @transactional데이터베이스 스키마(테이블 구조, 제약조건)은 영향을 주지 못한다 그래서 문제가 될 수 있다는 글을 보았는데
저의 기능에는 현재 기능 테스트에서 스키마를 변경하는 부분은 없어서
이 경우도 문제가 안될 것이라고 생각했습니다.

혹시 실무에서는 테스트에서 스키마를 변경하는 테스트가 존재하고
이때문에 @Transactional을 넣어도 격리가 안될 가능성이 있기 때문에
격리 전용 어노테이션 등을 만드는건가요?
아니면 그것과 관계없이 only 데이터만 두고 봐도
현재 기준으로 저의 테스트 설정이 격리가 안될 문제가 있는것일까요..?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

찾아보니 컨텍스트를 초기화 할 수 있는 어노테이션인
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)가 있어서
해당 어노테이션을 사용해서 커스텀 어노테이션을 생생해보았습니다!

현재 특수하게 적용해야되는 경우는 없는것 같아서 적용은 안했지만
특정 케이스에는 위 어노테이션을 적용하면 될 것 같습니다!
(특정 케이스란?

- DB 트랜잭션으로 롤백되지 않는 외부 시스템과 연동되는 경우
- 스프링 컨텍스트 초기화 필요시
- 정적(static) 변수값이 공유되는데 초기화 필요시
- 비동기 처리(@Async, @Scheduled 등) 테스트 시

)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. 트랜잭션 사용 여부는 향로님 글에 잘 정리가 되어 있어서 보시면 좋을 것 같습니다! 말씀하신 것처럼 트랜잭션을 사용해서 격리를 해도 되지만, 위 글에도 나온 것처럼 예상치 못한 경우가 있을 수 있기 때문에 저는 DB Cleaner를 만드는 것을 선호합니다.
  2. @DirtiesContext는 테스트 수행마다 새로운 컨텍스트를 띄워서 테스트하는 방식입니다. 컨텍스트를 띄우는 데에는 비용이 많이 들어가서 전체적인 테스트 속도가 느려질 수 있습니다. (새로운 컨텍스트를 테스트 메서드마다 띄워서 테스트를 할 때 격리가 된다면, 기존에는 컨텍스트를 공유해서 쓰고있다는 말이겠죠 -> 그러면 공유하는 컨텍스트로 인해 테스트 격리가 필요하다는 것도 유추할 수 있습니다.) 따라서 해당 방식도 좋지 않다고 생각합니다! 차라리 지금 방식처럼 필요할 때 @transactional을 사용하거나 Cleaner를 쓰시는 게 좋을 것 같습니다. 만약 새로운 컨텍스트가 필요하면 그 시점에서만 쓰면 되니깐요 ㅎㅎ

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

통합테스트에서 전체적으로 적용되어있던 @transactional을 제거하고
모든 테이블을 truncate하는 DatabaseCleaner 클래스를 만들고
통합테스트에는 이 클래스를 사용하는 CleanDatabaseBeforeEachTest 추상클래스를 만들어서 상속하도록 하였습니다!

이제 매 테스트 시 db가 깔끔하게 지워지도록 만들어보았습니다!
변경한 뒤 통합테스트에서 실패하는 부분이 있었습니다.
예를 들면

@Test
    @DisplayName("삭제된 게시물을 제외하고 전체 조회 테스트")
    void findAllExcludingDeletedArticlesTest() throws Exception {
        // Given: 게시물 1개 생성
        MemberEntity member = createAndSaveMember("[email protected]", "nickname");
        ArticleEntity article1 = articleRepository.save(new ArticleEntity("Title 1", "Content 1", member));

        // 게시물 1개 삭제
        article3.softDelete();

        // When: 전체 조회 요청
        mockMvc.perform(get("/articles").param("page", "0").param("size", "10"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.content.size()").value(0)) // 여기서 실제 값이 1이라서 실패
    }

하면 오류가 났습니다
기존에는 @transactional 때문에 중간의 article3.softDelete(); 가 실제 db 값을 지워줬던거같은데
(로직은 아래와 같이 값만 변경)

public void softDelete() {
        this.isDeleted = true;
    }

이제는 트랜잭션이 없어서 그런지 정말 딱 값만 바꿔지고 delete하는 쿼리는 작동되지 않았고
db에 변화가 없어서 오류가 난 것이었습니다.

이제는

article3.softDelete();
articleRepository.save(article3);

이렇게 실제 db를 변경해주어야 될 것 같습니다!
아직은 조금 익숙하지 않은데
잘 익혀보도록 하겠습니다!
감사합니다!!

Copy link
Member

@sosow0212 sosow0212 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

몇 부분 추가 리뷰 남겼습니다! 확인해주세요~~ 수고하셨습니다!

import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

추가적으로 RefreshToken을 사용하는 이유가 뭔지 학습해보시고 적용해보시면 좋을 것 같습니다!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

리프레시 토큰 필요 이유

기존 엑세스 토큰만 사용하는 방식도 가능 / 엑세스 토큰은 서버가 상태저장 안하기 때문에 한번 발급되면 유효기간 동안은 무조건 유효

그에 따른 단점 발생 가능
엑세스토큰 유효시간 길면 : 토큰 탈취 시 위험 큼
엑세스토큰 유효시간 짧으면 : 사용자가 자꾸 로그인 해야됌(ux 나빠짐)

위 문제를 해결하기 위한 해결책 : 리프레시 토큰

리프레시 토큰 도입 시 결과 요약

  • 엑세스 토큰 : 짧게 유지 (탈취되더라도 피해 최소화)
  • 리프레시 토큰 : 길게 유지 (사용자 로그인 자꾸 안해도됌)

리프레시 토큰 사용되는 경우

  1. 액세스 토큰 만료시 리프레시 토큰 유무 확인해서 액세스 토큰 재발급
  2. 로그인 시 액세스/리프레시 토큰 발급
  3. 로그아웃 시 리프레시 토큰 삭제

위와 같이 리프레시 토큰 사용 이유와 장점 정리해보고
기능도 개발해보았습니다!!

DongCaprio added 13 commits May 13, 2025 18:14
 - 기존 member에서 존재하던 controller 분리(로그인, 회원가입 등)
- 주소정보 + 해당하는 httpMethod 종류들로 구성
 - "/comments" 에서 "/articles/{articleId}/comments"
 - 그에 맞게 articleId 관련 코드 전부 수정
 - 인증 시 전부 jwt + header 인증방식 사용하도록 변경
 - "/reissue" 요청시 리프레시 토큰 확인하여 엑세스 토큰 발행
 - 일반 로그인 시 액세스/리프레시 토큰 모두 발급
 - 둘다 고유값이지만 id로 하는것이 더 빠르고 위험 적음(더 짧고, 변경 가능성 낮음)
 - 리프레시 토큰 도입하면서 액세스/리프레스 토큰 전부 id를 통해서 발급하도록 변경
 - 사용 시 속도저하가 크기 때문에 특정 케이스에서만 적용
 - 통합테스트에 CleanDatabaseBeforeEachTest 상속하면 테스트 실행 시 모든 db table truncate
 - truncate 추가로 ddl-auto: create로 변경
 - 댓글에 대댓글 작성 가능
 - 대댓글에는 대댓글 작성 불가(즉 level 0, 1만 가능)
 - 댓글별 대댓글 조회 가능
 - 대댓글은 먼저 작성한게 먼저 보임(오래된순)
 - 5분에 1번씩 스케줄러로 Redis에 저장된 게시글 조회수 DB에 저장
 - 글 조회 시 Redis 사용해서 조회수 증가/조회 함
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants