이번에는 Board API에 관해 정리할 예정이다.
이번 글은 전반적인 코드 설명과 여러 시행착오에 대한 설명을 할 예정이다.
Board는 일반적인 게시판과 일치한다.
하지만 코드를 작성할때 컨벤션 및 클린코드를 적용하려 노력하였다.
또한 여러 테스트를 통해 동작을 검증하였고, 그 과정에서 발생한 시행착오를 기록하겠다.
Board는 현재 예외에 대한 문서화도 진행되어 있다.
Board
@Entity
public class Board {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String title;
@Column(nullable = false)
@Lob
private String content;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
@OnDelete(action = OnDeleteAction.CASCADE)
private Member member;
protected Board() {
}
private Board(final Long id, final String title, final String content, final Member member) {
this.id = id;
this.title = title;
this.content = content;
this.member = member;
}
public static Board from(final String title, final String content, final Member member) {
validationBoard(title, content);
return new Board(null, title, content, member);
}
public static Board from(final Long id, final String title, final String content, final Member member) {
validationBoard(title, content);
return new Board(id, title, content, member);
}
private static void validationBoard(final String title, final String content) {
if (isEmpty(title)) {
throw new BoardTitleBlankException(title);
}
if (isEmpty(content)) {
throw new BoardContentBlankException(content);
}
}
private static boolean isEmpty(final String text) {
return text == null || text.isBlank();
}
public void checkWriter(final Member member) {
if (!this.member.equals(member)) {
throw new MemberNotEqualsException();
}
}
public void update(final String title, final String content) {
validationBoard(title, content);
this.title = title;
this.content = content;
}
public Long getId() {
return id;
}
public String getTitle() {
return title;
}
public String getContent() {
return content;
}
public Member getMember() {
return member;
}
}
BoardController
@RequestMapping("/api/boards")
@RestController
public class BoardController {
private final BoardService boardService;
public BoardController(final BoardService boardService) {
this.boardService = boardService;
}
@PostMapping
public ResponseEntity<BoardResponse> create(@JwtLogin final Member member, @RequestBody @Valid final BoardRequest boardRequest) {
return ResponseEntity.status(HttpStatus.CREATED)
.body(boardService.create(member, boardRequest));
}
@GetMapping("/{boardId}")
public ResponseEntity<BoardResponse> findBoard(@PathVariable("boardId") final Long boardId) {
return ResponseEntity.ok()
.body(boardService.findBoard(boardId));
}
@GetMapping
public ResponseEntity<BoardAllResponse> findAllBoards(@PageableDefault final Pageable pageable) {
return ResponseEntity.ok()
.body(boardService.findAllBoards(pageable));
}
@PatchMapping("/{boardId}")
public ResponseEntity<BoardResponse> update(@PathVariable("boardId") final Long boardId,
@JwtLogin final Member member,
@RequestBody @Valid final BoardRequest boardRequest) {
return ResponseEntity.ok()
.body(boardService.update(boardId, member, boardRequest));
}
@DeleteMapping("/{boardId}")
public ResponseEntity<Void> delete(@PathVariable("boardId") final Long boardId,
@JwtLogin final Member member) {
boardService.delete(boardId, member);
return ResponseEntity.status(HttpStatus.NO_CONTENT)
.build();
}
}
원래 통일성을 위해 ResponseEntity.ok()를 Response.status(HttpStatus.Ok)식으로 작성하려 하였다.
하지만 조금 더 간단하고자 만든 메서드를 굳이 통일성을 위해 길게 작성할 필요가 없다고 느껴 이대로 진행하였다.
BoardService
@Service
public class BoardService {
private static final String BOARD_ID = "id";
private final BoardRepository boardRepository;
public BoardService(final BoardRepository boardRepository) {
this.boardRepository = boardRepository;
}
@Transactional
public BoardResponse create(final Member member, final BoardRequest boardRequest) {
Board board = Board.from(boardRequest.getTitle(), boardRequest.getContent(), member);
boardRepository.save(board);
return BoardResponse.from(board);
}
@Transactional(readOnly = true)
public BoardResponse findBoard(final Long boardId) {
Board board = boardRepository.findById(boardId)
.orElseThrow(() -> new BoardNotFoundException(boardId));
return BoardResponse.from(board);
}
@Transactional(readOnly = true)
public BoardAllResponse findAllBoards(final Pageable pageable) {
Page<Board> sortedBoards = boardRepository.findAll(sortByIdWithDesc(pageable));
BoardPageInfo boardPageInfo = BoardPageInfo.from(sortedBoards);
List<BoardResponse> boards = sortedBoards.stream()
.map(BoardResponse::from)
.collect(collectingAndThen(toList(), Collections::unmodifiableList));
return BoardAllResponse.from(boards, boardPageInfo);
}
@Transactional
public BoardResponse update(final Long boardId, final Member member, final BoardRequest boardRequest) {
Board board = boardRepository.findById(boardId)
.orElseThrow(() -> new BoardNotFoundException(boardId));
board.checkWriter(member);
board.update(boardRequest.getTitle(), boardRequest.getContent());
return BoardResponse.from(board);
}
@Transactional
public void delete(final Long boardId, final Member member) {
Board board = boardRepository.findById(boardId)
.orElseThrow(() -> new BoardNotFoundException(boardId));
board.checkWriter(member);
boardRepository.delete(board);
}
private PageRequest sortByIdWithDesc(final Pageable pageable) {
return PageRequest.of(pageable.getPageNumber(), pageable.getPageSize(), Sort.by(BOARD_ID).descending());
}
}
로직자체에서 특별한 것은 없고, 기존에는 예외를 던질 때 BoardNotFoundException::new로 만들었는데, 예외 로깅을 쉽게 하기 위해
BoardId를 넘겨서 이 또한 보여주도록 수정하였다.
Test
BoardController
통합테스트
public class BoardControllerIntegrationTest extends IntegrationTest {
private String token;
@Autowired
private BoardService boardService;
@Autowired
private AuthService authService;
@Autowired
private MemberRepository memberRepository;
@BeforeEach
void setUp() {
MemberResponse register = authService.register(new MemberCreateRequest("a@a.com", "1234"));
Member member = memberRepository.findByEmail(register.getEmail()).get();
MemberLoginRequest memberLoginRequest = new MemberLoginRequest("a@a.com", "1234");
TokenResponse tokenResponse = authService.login(memberLoginRequest);
token = tokenResponse.getAccessToken();
boardService.create(member, new BoardRequest("initTitle", "initContent"));
}
@DisplayName("게시글을 작성한다.")
@Test
void create_board() {
// given
BoardRequest boardRequest = new BoardRequest("title", "content");
// when
Response response = RestAssured.given()
.contentType(ContentType.JSON)
.auth().preemptive().oauth2(token)
.body(boardRequest)
.when()
.post("/api/boards");
// then
response.then()
.statusCode(HttpStatus.CREATED.value());
}
@DisplayName("게시글을 단건 조회한다.")
@Test
void find_board() {
//given
Long boardId = 1L;
//when
Response response = RestAssured.given()
.contentType(ContentType.JSON)
.auth().preemptive().oauth2(token)
.pathParam("boardId", boardId)
.when()
.get("/api/boards/{boardId}");
//then
response.then()
.statusCode(HttpStatus.OK.value());
}
@DisplayName("게시글을 전부 조회한다.")
@Test
void findAll_board() {
//given
Pageable pageable = PageRequest.of(10, 10);
//when
Response response = RestAssured.given()
.contentType(ContentType.JSON)
.auth().preemptive().oauth2(token)
.body(pageable)
.when()
.get("/api/boards");
//then
response.then()
.statusCode(HttpStatus.OK.value());
}
@DisplayName("게시글을 수정합니다.")
@Test
void update_board() {
//given
Long boardId = 1L;
BoardRequest boardRequest = new BoardRequest("title", "content");
//when
Response response = RestAssured.given()
.contentType(ContentType.JSON)
.auth().preemptive().oauth2(token)
.pathParam("boardId", boardId)
.body(boardRequest)
.when()
.patch("/api/boards/{boardId}");
//then
response.then()
.statusCode(HttpStatus.OK.value());
}
@DisplayName("게시글을 삭제합니다.")
@Test
void delete_board() {
//given
Long boardId = 1L;
//when
Response response = RestAssured.given()
.contentType(ContentType.JSON)
.auth().preemptive().oauth2(token)
.pathParam("boardId", boardId)
.when()
.delete("/api/boards/{boardId}");
//then
response.then()
.statusCode(HttpStatus.NO_CONTENT.value());
}
}
시행착오
현재 IntegrationTest를 extend 하고 있는데, 이때 BeforeEach가 각각 적용된다.
하지만 처음에 IntegrationTest의 BeforeEach가 적용되지 않았다.
이로 인해 디비가 정리되지 않았고, 테스트가 전부 터지는 일이 발생하였다.
이유는 간단하였다. 자식의 BeforeEach 메서드명과 부모의 BeforeEach 메서드명이 둘 다 init()으로 동일하였기 때문이다.
메서드명을 setUp으로 수정하니 정상적으로 동작하였다.
단위테스트
@WebMvcTest(BoardController.class)
@AutoConfigureRestDocs
public class BoardControllerUnitTest {
private MockTestHelper mockTestHelper;
@MockBean
private BoardService boardService;
@MockBean
private JwtAuthService jwtAuthService;
@MockBean
private ArgumentResolverConfig argumentResolverConfig;
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@BeforeEach
void init() {
mockTestHelper = new MockTestHelper(mockMvc);
}
@DisplayName("게시글 생성을 진행한다.")
@Test
void create_board() throws Exception {
// given
Member member = createMember();
BoardRequest boardRequest = new BoardRequest("title", "content");
Board board = BoardFixture.createBoard(member);
BoardResponse boardResponse = BoardResponse.from(board);
given(boardService.create(any(Member.class), any(BoardRequest.class))).willReturn(boardResponse);
// when & then
mockTestHelper.createMockRequestWithTokenAndContent(post(("/api/boards")), boardRequest)
.andExpect(status().isCreated())
.andExpect(jsonPath("$.boardId").value(board.getId()))
.andExpect(jsonPath("$.title").value(board.getTitle()))
.andExpect(jsonPath("$.content").value(board.getContent()))
.andDo(MockMvcResultHandlers.print())
.andDo(customDocument("create_board",
requestHeaders(
headerWithName(HttpHeaders.AUTHORIZATION).description("로그인 후 제공되는 Bearer 토큰")
),
requestFields(
fieldWithPath("title").description("게시판의 제목"),
fieldWithPath("content").description("게시판의 내용")
),
responseFields(
fieldWithPath("boardId").description("게시판 생성 후 반환된 board의 ID"),
fieldWithPath("title").description("게시판 생성 후 반환된 board의 제목"),
fieldWithPath("content").description("게시판 생성 후 반환된 board의 내용")
)
)).andReturn();
}
@DisplayName("게시글 단건을 조회한다.")
@Test
void find_board() throws Exception {
// given
Member member = createMember();
Board board = BoardFixture.createBoard(member);
BoardResponse boardResponse = BoardResponse.from(board);
given(boardService.findBoard(any(Long.class))).willReturn(boardResponse);
// when & then
mockTestHelper.createMockRequestWithTokenAndWithoutContent(get("/api/boards/{boardId}", "1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.boardId").value(board.getId()))
.andExpect(jsonPath("$.title").value(board.getTitle()))
.andExpect(jsonPath("$.content").value(board.getContent()))
.andDo(MockMvcResultHandlers.print())
.andDo(customDocument("find_board",
requestHeaders(
headerWithName(HttpHeaders.AUTHORIZATION).description("로그인 후 제공되는 Bearer 토큰")
),
responseFields(
fieldWithPath("boardId").description("게시판 조회 후 반환된 board의 ID"),
fieldWithPath("title").description("게시판 조회 후 반환된 board의 제목"),
fieldWithPath("content").description("게시판 조회 후 반환된 board의 내용")
)
)).andReturn();
}
@DisplayName("게시글 전체를 조회한다.")
@Test
void find_all_boards() throws Exception {
// given
Board board = BoardFixture.createBoard(createMember());
BoardAllResponse boardAllResponse = getBoardAllResponse(board);
given(boardService.findAllBoards(any(Pageable.class))).willReturn(boardAllResponse);
// when & then
mockTestHelper.createMockRequestWithTokenAndWithoutContent(get("/api/boards?page=0?size=10"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.boards[0].boardId").value(board.getId()))
.andExpect(jsonPath("$.boards[0].title").value(board.getTitle()))
.andExpect(jsonPath("$.boards[0].content").value(board.getContent()))
.andExpect(jsonPath("$.boardPageInfo.totalPage").value(boardAllResponse.getBoardPageInfo().getTotalPage()))
.andExpect(jsonPath("$.boardPageInfo.nowPage").value(boardAllResponse.getBoardPageInfo().getNowPage()))
.andExpect(jsonPath("$.boardPageInfo.numberOfElements").value(boardAllResponse.getBoardPageInfo().getNumberOfElements()))
.andExpect(jsonPath("$.boardPageInfo.hasNextPage").value(boardAllResponse.getBoardPageInfo().isHasNextPage()))
.andDo(MockMvcResultHandlers.print())
.andDo(customDocument("find_all_boards",
requestHeaders(
headerWithName(HttpHeaders.AUTHORIZATION).description("로그인 후 제공되는 Bearer 토큰")
),
responseFields(
fieldWithPath("boards[0].boardId").description("게시판 전체 조회 후 반환된 board의 ID"),
fieldWithPath("boards[0].title").description("게시판 전체 조회 후 반환된 board의 제목"),
fieldWithPath("boards[0].content").description("게시판 전체 조회 후 반환된 board의 내용"),
fieldWithPath("boardPageInfo.totalPage").description("게시판 전체 조회 후 반환된 전체 페이지"),
fieldWithPath("boardPageInfo.nowPage").description("게시판 전체 조회 후 반환된 현재 페이지"),
fieldWithPath("boardPageInfo.numberOfElements").description("게시판 전체 조회 후 반환된 갯수"),
fieldWithPath("boardPageInfo.hasNextPage").description("게시판 전체 조회 후 다음 페이지가 존재하는지 여부")
)
)).andReturn();
}
@Test
@DisplayName("게시글을 수정한다.")
void update_board() throws Exception {
// given
Member member = createMember();
BoardRequest boardRequest = new BoardRequest("title", "content");
Board board = BoardFixture.createBoard(member);
BoardResponse boardResponse = BoardResponse.from(board);
given(boardService.update(any(Long.class), any(Member.class), any(BoardRequest.class))).willReturn(boardResponse);
// when & then
mockTestHelper.createMockRequestWithTokenAndContent(patch("/api/boards/{boardId}", "1"), boardRequest)
.andExpect(status().isOk())
.andExpect(jsonPath("$.boardId").value(1))
.andExpect(jsonPath("$.title").value("title"))
.andExpect(jsonPath("$.content").value("content"))
.andDo(customDocument("update_board",
requestHeaders(
headerWithName(HttpHeaders.AUTHORIZATION).description("로그인 후 제공되는 Bearer 토큰")
),
requestFields(
fieldWithPath(".title").description("게시판의 제목"),
fieldWithPath(".content").description("게시판의 내용")
),
responseFields(
fieldWithPath(".boardId").description("게시판 생성 후 반환된 board의 ID"),
fieldWithPath(".title").description("게시판 생성 후 반환된 board의 제목"),
fieldWithPath(".content").description("게시판 생성 후 반환된 board의 내용")
)
));
}
@DisplayName("게시글 삭제")
@Test
void delete_board() throws Exception {
// when & then
mockTestHelper.createMockRequestWithTokenAndWithoutContent(delete("/api/boards/{boardId}", "1"))
.andExpect(status().isNoContent())
.andDo(customDocument("delete_board",
requestHeaders(
headerWithName(HttpHeaders.AUTHORIZATION).description("로그인 후 제공되는 Bearer 토큰")
)));
}
@DisplayName("게시글을 작성할때 제목이 빈칸이면 예외가 발생한다")
@ParameterizedTest(name = "{index} : {0}")
@MethodSource("boardRequestProviderWithNoTitle")
public void fail_to_create_board_with_blank_title(String text, BoardRequest boardRequest) throws Exception {
// given
Member member = createMember();
Board board = BoardFixture.createBoard(member);
BoardResponse boardResponse = BoardResponse.from(board);
given(boardService.create(any(Member.class), any(BoardRequest.class))).willReturn(boardResponse);
// when & then
mockTestHelper.createMockRequestWithTokenAndContent(post(("/api/boards")), boardRequest)
.andExpect(status().isBadRequest())
.andDo(MockMvcResultHandlers.print())
.andDo(RestDocsHelper.customDocument("fail_to_create_board_with_blank_title",
requestHeaders(
headerWithName(HttpHeaders.AUTHORIZATION).description("로그인 후 제공되는 Bearer 토큰")
),
requestFields(
fieldWithPath("title").description("게시판의 제목"),
fieldWithPath("content").description("게시판의 내용")
)
)).andReturn();
}
@DisplayName("게시글을 작성할때 내용이 빈칸이면 예외가 발생한다")
@ParameterizedTest(name = "{index} : {0}")
@MethodSource("boardRequestProviderWithNoContent")
public void fail_to_create_board_with_blank_content(String text, BoardRequest boardRequest) throws Exception {
// given
Member member = createMember();
Board board = BoardFixture.createBoard(member);
BoardResponse boardResponse = BoardResponse.from(board);
given(boardService.create(any(Member.class), any(BoardRequest.class))).willReturn(boardResponse);
// when & then
mockTestHelper.createMockRequestWithTokenAndContent(post(("/api/boards")), boardRequest)
.andExpect(status().isBadRequest())
.andDo(MockMvcResultHandlers.print())
.andDo(RestDocsHelper.customDocument("fail_to_create_board_with_blank_content",
requestHeaders(
headerWithName(HttpHeaders.AUTHORIZATION).description("로그인 후 제공되는 Bearer 토큰")
),
requestFields(
fieldWithPath("title").description("게시판의 제목"),
fieldWithPath("content").description("게시판의 내용")
)
)).andReturn();
}
@DisplayName("게시글을 다른 사람이 수정하면 예외가 발생한다.")
@Test
public void fail_to_update_board_with_different_member() throws Exception {
// given
Member member = createMember();
BoardRequest boardRequest = new BoardRequest("title", "content");
BoardFixture.createBoard(member);
given(boardService.update(any(Long.class), any(Member.class), any(BoardRequest.class))).willThrow(new MemberNotEqualsException());
// when & then
mockTestHelper.createMockRequestWithTokenAndContent(patch("/api/boards/{boardId}", "1"), boardRequest)
.andExpect(status().isBadRequest())
.andDo(MockMvcResultHandlers.print())
.andDo(RestDocsHelper.customDocument("fail_to_update_board_with_different_member",
requestHeaders(
headerWithName(HttpHeaders.AUTHORIZATION).description("로그인 후 제공되는 Bearer 토큰")
),
requestFields(
fieldWithPath(".title").description("게시판의 제목"),
fieldWithPath(".content").description("게시판의 내용")
)
)).andReturn();
}
@DisplayName("게시판을 단건조회할때 게시판 아이디가 올바르지 않으면 예외가 발생한다.")
@Test
public void fail_to_find_board_with_wrong_board_id() throws Exception {
// given
Member member = createMember();
Board board = BoardFixture.createBoard(member);
given(boardService.findBoard(any(Long.class))).willThrow(new BoardNotFoundException(board.getId()));
// when & then
mockTestHelper.createMockRequestWithTokenAndWithoutContent(get("/api/boards/{boardId}", "1"))
.andExpect(status().isNotFound())
.andDo(MockMvcResultHandlers.print())
.andDo(customDocument("fail_to_find_board_with_wrong_board_id",
requestHeaders(
headerWithName(HttpHeaders.AUTHORIZATION).description("로그인 후 제공되는 Bearer 토큰")
)
)).andReturn();
}
private static Stream<Arguments> boardRequestProviderWithNoTitle() {
return Stream.of(
Arguments.of("제목이 null인 경우", new BoardRequest(null, "content")),
Arguments.of("제목이 공백인 경우", new BoardRequest("", "content")),
Arguments.of("제목이 빈 칸인 경우", new BoardRequest(" ", "content"))
);
}
private static Stream<Arguments> boardRequestProviderWithNoContent() {
return Stream.of(
Arguments.of("내용이 null인 경우", new BoardRequest("title", null)),
Arguments.of("내용이 공백인 경우", new BoardRequest("title", "")),
Arguments.of("내용이 빈 칸인 경우", new BoardRequest("title", " "))
);
}
private BoardAllResponse getBoardAllResponse(final Board board) {
List<Board> boards = new ArrayList<>();
boards.add(board);
List<BoardResponse> responses = List.of(BoardResponse.from(board));
Page<Board> page = new PageImpl<>(boards);
BoardPageInfo pageInfo = BoardPageInfo.from(page);
return BoardAllResponse.from(responses, pageInfo);
}
}
코드가 상당히 길어졌는데, 이는 예외테스트도 문서화를 진행하였기에 이러한 결과나 나타났다.
아마 새로운 부분은 맨 아래 Provider메서드일 것이다. 이는 여러 개의 케이스를 쉽게 테스트하기 위해 사용되었다.
예시를 보면 아래와 같이 여러 케이스의 테스트가 적용되는 것을 볼 수 있다.
시행착오
예외를 문서화할 때 예외의 메시지가 안 담기는 일이 발생하였다.
이는 기존의 코드는 이러하였다.
given(boardService.update(any(Long.class), any(Member.class), any(BoardRequest.class))).willThrow(MemberNotEqualsException.class);
이는 단순히 예외가 발생하는데 끝나고, 새로운 인스턴스를 만드는 것이 아니기에 예외 메시지를 담을 수 없었다.
이를 아래와 같이 수정하니 예외 메시지가 담기게 되었다.
given(boardService.update(any(Long.class), any(Member.class), any(BoardRequest.class))).willThrow(new MemberNotEqualsException());
아래는 문서화된 모습이다.
시행착오
BoardService 통합테스트에서 분명 같은 멤버가 만든 게시판임에도 접근 시 다른 멤버라고 예외가 발생하는 일이 생겼다.
이를 해결하고자 처음에는 Member에서 Equals, HashCode를 재정의하였다.
우선 이러한 방법이 마음에 들지는 않았다.
이유는 프로덕션 코드에서는 Equals, HashCode의 재정의 없이도 올바르게 동작했다.
또한 테스트를 위해 프로덕션 코드를 수정한다는 것이 마음에 들지 않았다.
이를 해결하기 위해 오랜 시간 디버깅을 하며 발견한 사항은 다음과 같았다.
우선 아래와 같은 로직이 있다.
private Member member;
@BeforeEach
void setUp() {
MemberCreateRequest memberCreateRequest = new MemberCreateRequest("a@naver.com", "1234");
MemberResponse register = authService.register(memberCreateRequest);
member = memberRepository.findByEmail(register.getEmail()).get();
BoardRequest boardRequest = new BoardRequest("initTitle", "initContent");
boardService.create(member, boardRequest);
}
멤버가 Board를 만들고 아래 테스트에서 그 멤버가 업데이트를 시도하는 로직이다.
@DisplayName("게시판을 수정한다")
@Test
void update_board() {
//given
Long boardId = 1L;
BoardRequest boardRequest = new BoardRequest("updateTitle", "updateContent");
//when
BoardResponse board = boardService.update(boardId, member, boardRequest);
//then
assertAll(
() -> assertThat(board.getTitle()).isEqualTo("updateTitle"),
() -> assertThat(board.getContent()).isEqualTo("updateContent")
);
}
실제 프로덕션 코드는 이러하였다.
@Transactional
public BoardResponse update(final Long boardId, final Member member, final BoardRequest boardRequest) {
Board board = boardRepository.findById(boardId)
.orElseThrow(() -> new BoardNotFoundException(boardId));
board.checkWriter(member);
board.update(boardRequest.getTitle(), boardRequest.getContent());
return BoardResponse.from(board);
}
public void checkWriter(final Member member) {
if (!this.member.equals(member)) {
throw new MemberNotEqualsException();
}
}
걸리는 부분은 checkWriter에서 다른 멤버라고 뜨면서 예외가 발생하였다.(실제로는 같은 멤버임에도)
왜 이러한 현상이 발생했을까?
우선 이유는 테스트에 @Transactional을 붙이지 않았었다.
따라서 이러한 현상이 발생했는데, 그렇다면 왜 트랜잭션이 없다면 예외가 발생할까?(같은 객체임에도 다른 객체로 인식)
우선 여러 가지 이유가 있다.
이를 정리하면 다름과 같다.
- 캐싱: 데이터베이스에서 객체를 조회하거나 저장할 때, 특정 캐시 메커니즘에 의해 객체가 캐싱되어 재사용될 수 있다. 이 경우 트랜잭션이 없으면 객체 상태 변경이 데이터베이스에 바로 반영되어 캐시 또한 업데이트되지 않을 수 있다. 같은 객체를 조회하더라도 새로운 인스턴스를 만들어 사용하게 되므로, 같은 객체임에도 다른 객체로 인식하게 된다.
- 지연 로딩(Lazy Loading): Hibernate와 같은 ORM(Object Relational Mapping) 프레임워크에서 지연 로딩이 사용될 수 있다. 이는 객체의 연관 관계가 있는 경우 실제로 필요한 시점에 객체를 로딩하는 방식이다. 트랜잭션이 없는 경우 객체의 지연 로딩이 완료되기 전에 객체를 접근하거나 변경이 발생할 수 있으며, 이로 인해 실제 데이터베이스와 객체의 상태가 일치하지 않게 된다. 이 경우 다른 객체로 인식하게 될 수 있다.
- 객체 상태 변경: 트랜잭션이 없는 경우 객체의 상태 변경이 바로 데이터베이스에 반영되며, 이로 인해 객체의 상태를 신뢰할 수 없게 된다. 같은 객체를 조회하더라도 객체의 상태가 실시간으로 변경되어 이전 상태의 객체와 현재 상태의 객체가 서로 다르게 인식될 수 있다.
이러한 이유 때문에 제대로 동작하지 않았던 것이다.
이제 문제를 해결하고 다른 테스트들도 모두 통과하게 되었다.
이외에 다른 자세한 코드는 아래 깃허브를 통해 볼 수 있습니다.
https://github.com/kimtaesoo99/ChatUniv
'프로젝트 > ChatUniv' 카테고리의 다른 글
[프로젝트] ChatUniv 페이징 최적화 (2) | 2023.09.02 |
---|---|
[프로젝트] ChatUniv GPT 적용 (0) | 2023.08.26 |
[프로젝트] ChatUniv Comment API, 동적 테스트 #4 (0) | 2023.07.30 |
[프로젝트] ChatUniv Member API #2 (0) | 2023.07.16 |
[프로젝트] ChatUniv 전반적인 설계 및 Auth구조 #1 (0) | 2023.07.04 |