👇 [스프링 부트로 CRUD API 만들기 1편] 보러 가기 👇
오늘의 궁금증
1. 왜 createdAt, modifiedAt 값이 null로 전달될까?
위쪽은 Postman으로 POST 테스트했을 시 반환되는 값이고,
아래쪽은 h2 DB에서 확인한 값이다. DB에는 있는 날짜가 중간에 길을 잃고 클라이언트까지 닿지 못하였다..
현재 생성 날짜와 수정 날짜는 Timestamped 클래스에서 상속받아 구현되고 있다.
처음 ResponseDto가 아닌 Entity 자체를 반환할 때는 아무 문제없이 날짜들이 잘 전달되었는데,
ResponseDto를 정의하고 난 뒤부터 createdAt과 modifiedAt이 null로 전달되었다.
나는 Timestamped 클래스의 문제라고 생각하고 어노테이션도 변경해보고 별걸 다 해봤는데 답을 찾지 못했다.
그러다 오늘 나의 이 에러를 알고 있던 이진님께서 답을 찾았다며 관련 내용을 공유해주셨다!
해결 방법은 바로 ResponseDto에서 createdAt과 modifiedAt 필드를 추가하고 생성자에서 정의해주는 것..!
너무도 당연했다. Timestamped를 extends 해서 필드 값은 있을지 몰라도 생성자에서 정의해주지 않으면 값이 없는 게 당연한 건데 난 이걸로 이틀을 고민하고 있던 것이다...😇
생성자에 대한 이해가 부족했다... 하
package com.sparta.crud.dto;
import com.sparta.crud.entity.Board;
import lombok.Getter;
import java.time.LocalDateTime;
@Getter
public class BoardResponseDto {
private Long id;
private String title;
private String name;
private String content;
private LocalDateTime createdAt;
private LocalDateTime modifiedAt;
public BoardResponseDto(Board board) {
this.id = board.getId();
this.title = board.getTitle();
this.name = board.getName();
this.content = board.getContent();
this.createdAt = board.getCreatedAt();
this.modifiedAt = board.getModifiedAt();
}
}
단 4줄 추가로 에러가 깔끔하게 해결되었다! (이진님 감사해요❤)
2. @Transactional 없이 update 하기.. 가능할까?
@Transactional
public BoardResponseDto update(Long id, BoardRequestDto requestDto) {
Board board = boardRepository.findById(id).orElseThrow(
() -> new IllegalArgumentException("아이디가 존재하지 않습니다.")
);
board.update(requestDto); // 엥 이게 끝?
return new BoardResponseDto(board);
}
어제 기술 매니저님께서 주신 미션 아닌 미션. (CRUD API 만들기 1편의 내용 참고)
Service단에서 업데이트쪽 메소드를 보면 별도로 Repository에 접근하지 않더라도 업데이트가 잘 된다.
JPA가 수정 메소드를 제공하지 않는다는 건 저번 시간에 알게 되었는데, 트랜잭션과의 관계는 뭘까?
트랜잭션과 업데이트가 무슨 관련이 있는지부터 피부로 느끼기 위해 바로 @Transactional 어노테이션을 지워보았다.
테스트해보니 반환되는 객체는 업데이트가 되었는데 DB는 정말 업데이트가 되지 않았다. 오 흥미로운데?
관련 키워드들로 폭풍 검색에 들어갔다.
영속성 컨텍스트란?
- 엔티티를 영구적으로 저장하는 환경
- 애플리케이션과 데이터베이스 사이에서 객체를 보관하는 가상의 데이터베이스 같은 역할
- 객체 - 영속성 컨텍스트 매니저 (entity context manager) - DB
트랜잭션이란?
- 데이터 베이스 작업의 단위로써 하나 이상의 쿼리를 처리할 때 동일한 Connection 객체를 공유하여 에러가 발생한 경우 모든 과정을 되돌리기 위한 방법이다.
- 더 이상 쪼갤 수 없는 최소 작업 단위를 의미한다.
- 하나의 트랜잭션은 Commit 되거나 Rollback 된다.
JPA의 데이터 변경 로직
- 트랜잭션 시작
- 영속성 Entity 조회 (없으면 DB 조회 후 영속화)
- 조회한 영속성 Entity의 데이터 수정
- 트랜잭션 커밋
Dirty Checking이란?
- 여기에서 Dirty란 상태의 변화가 생긴 정도로 이해하면 된다. 즉, Dirty Checking이란 상태 변경 감지이다.
- 변경 감지는 트랜잭션 커밋 시 영속화된 Entity가 가지고 있었던 최초 정보(스냅샷)와 바뀐 Entity 정보를 비교해서 바뀐 부분을 update 해주는 기능이다.
정말 산 넘어 산이다🤦♀️ 키워드 하나를 찾으면 그 안에 또 모르는 키워드가 있고, 그걸 찾으면 또 모르는 게 있고..
일단 이 모든 키워드를 조합하여 내가 이해한 바로는 이런 흐름인 것 같다.
@Transactional
public BoardResponseDto update(Long id, BoardRequestDto requestDto) {
// 트랜잭션 시작
Board board = boardRepository.findById(id).orElseThrow(
() -> new IllegalArgumentException("아이디가 존재하지 않습니다.")
); // 해당 id를 찾아 영속성 컨텍스트 1차 캐시에 저장
board.update(requestDto); // 엔티티만 변경
return new BoardResponseDto(board);
// 트랜잭션 커밋
}
- @Transactional에 따라 트랜잭션 시작
- boardRepository에서 해당하는 id를 찾아 영속성 컨텍스트에 1차 캐시에 저장 (1차캐시 == DB)
- 해당 엔티티의 값을 변경
- 영속성 컨텍스트에서 보관하고 있는 객체도 수정한 값으로 변경 (1차캐시 != DB)
- @Transactional이 해당 코드 실행이 마무리될 때 자동으로 커밋
- 커밋이 진행될 때, 1차캐시에 저장된 값과 DB 값을 비교하여 다른 부분을 모두 update
이렇게 되면 save 메소드로 변경사항을 저장하지 않아도 update 쿼리가 실행된다.
이유는? 바로 Dirty Checking(상태 변경 감지) 덕분이다.
JPA에서는 트랜잭션이 끝나는 시점에 변화가 있는 모든 엔티티 객체를 데이터베이스에 자동으로 반영해준다.
(물론 영속성 컨텍스트가 관리하는 엔티티에만 적용된다.)
결론적으로 영속성 컨텍스트의 특징인 Dirty Checking 덕분에 save 없이 update가 가능하고, 또 Dirty Checking이 가능하려면 트랜잭션 커밋을 해주어야 하는데 그걸 @Transactional이 해주고 있는 것 같다.
그래서 @Transactional 없이 update 어떻게 하는 건데
public BoardResponseDto update(Long id, BoardRequestDto requestDto) {
Board board = boardRepository.findById(id).orElseThrow(
() -> new IllegalArgumentException("아이디가 존재하지 않습니다.")
); // 해당 id를 찾아 영속성 컨텍스트 1차 캐시에 저장
// board 객체를 입력 받은 requestDto 값으로 변경
savedBoard.update(requestDto);
// board를 저장
Board resultBoard = boardRepository.save(savedBoard);
return new BoardResponseDto(resultBoard);
}
- boardRepository에서 해당하는 id를 찾아 영속성 컨텍스트에 1차 캐시에 저장
- 해당 엔티티의 값을 변경
- save() 메소드를 이용하여 업데이트 처리
간단히 save() 메소드를 이용해 이런 식으로 작성해주었다.
일단 테스트는 통과했는데 모든 개념을 이해하려면 좀 더 시간이 필요할 것 같다😭
📚 참고자료
3. 성공 여부를 어떻게 반환하면 좋을까?
게시글 삭제 시 요구사항은,
'선택한 게시글을 삭제하고 Client로 성공했다는 표시 반환하기'이다.
JDBC에서는 쿼리를 업데이트하는 메소드 자체의 반환 값이 결과 성공 여부였는데 deleteById()는 반환 값이 없다.
음.. 그럼 일단 반환 메시지를 작성해보자.
@Transactional
public Map<String, Object> deleteBoard(Long id) {
Map<String, Object> response = new HashMap<>();
boardRepository.deleteById(id);
response.put("success", true);
return response;
}
처음에는 이렇게 HashMap을 이용하여 { "success" : true } 형태로 반환해주었다.
그러다 ResponseDto로 반환하면서 비슷한 형태를 만들 수 있지 않을까? 하는 생각이 들었다.
ResponseDto에 결과를 표현해줄 success 필드를 선언하고,
생성자와 함께 성공 여부를 전달해줄 수 있도록 수정하였다.
package com.sparta.crud.dto;
import com.sparta.crud.entity.Board;
import lombok.Getter;
import java.time.LocalDateTime;
@Getter
public class BoardResponseDto {
private Long id;
private String title;
private String name;
private String content;
private LocalDateTime createdAt;
private LocalDateTime modifiedAt;
private Boolean success;
public BoardResponseDto(Board board, Boolean success) {
this.id = board.getId();
this.title = board.getTitle();
this.name = board.getName();
this.content = board.getContent();
this.createdAt = board.getCreatedAt();
this.modifiedAt = board.getModifiedAt();
this.success = success;
}
public BoardResponseDto(Boolean success) {
this.success = success;
}
}
테스트해본 결과, 의도와는 다르게 success필드를 제외한 전체 필드가 null인 채로 전달되는 걸 확인할 수 있었다.
뭔가 다른 방법은 없을까?
혼자 고민하다가 기술 매니저님께 조언을 구하였다.
"결과 메시지나 상태 코드를 담을 클래스를 만들고, 상속을 이용해 보세요!"
세상에. 그런 방법이..!
일단 머리에는 대충 구조가 떠올랐는데 내일 직접 코드로 구현해봐야 더 와닿을 것 같다.
CRUD API 현재까지 진행 상황
게시글 조회, 작성, 수정, 삭제 API와 비밀번호 일치 확인 로직까지
요구사항은 거의 해결했고, 내가 더 해보고 싶은 것들은!
1) 성공 여부 깔끔하게 반환하기
2) 비밀번호 암호화 하기
3) AWS 서버에 올려서 Postman으로 테스트하기
요렇게 세 가지이다.
1번은 바로 시도해볼 수 있을 것 같고,
2번은 아직 검색을 더 해봐야 할 것 같고,
3번은 기술 매니저님이 시도해보라고 던져 주셨는데 큰 도전이 될 것 같다.
오늘의 나는
오전에는 자바 스터디를 하고, 오후에는 대부분의 시간을 과제로 보냈다.
얼핏 보면 간단해 보이는 과제인데 디테일을 파고들면 끝도 없기 때문에 많은 고민을 하게 되는 것 같다.
나도 아직 모르는 게 많지만 동기분들이 질문하러 와주실 때만큼은
내가 알고 있는 것들을 전부 꺼내서 최대한으로 도움을 드리고 싶은 마음이 크다.
분명 나도 모든 게 처음이던 때가 있었기에 그 답답한 마음들에 더 공감이 가는 것 같다.
다행히도 함께 해결해낸 경우가 많아서 덩달아 뿌듯하곤 하다😊
내일은 과제를 마무리하고, 관련 내용들을 정리해보는 시간을 가져야겠다.
'📝 TIL' 카테고리의 다른 글
[TIL] 3주차 주특기 입문ㅣSpring 시험 (0) | 2022.12.01 |
---|---|
[TIL] 3주차 주특기 입문ㅣCRUD API 만들기 3편 (0) | 2022.11.30 |
[TIL] 3주차 주특기 입문ㅣCRUD API 만들기 1편 (3) | 2022.11.28 |
[TIL] 2주차 주특기 입문ㅣJava 스터디 발표 (0) | 2022.11.26 |
[TIL] 2주차 주특기 입문ㅣ주간 시작 (0) | 2022.11.25 |