ID
)를 가진다.3 Layer Architecture
를 지키며 개발한다.JPA
를 사용하여 개발한다.Cookie/Session
을 사용하여 개발한다.단방향
이다.제목
, 내용
, 작성일
, 수정일
, 작성유저
를 저장작성일
, 수정일
필드는 JPA Auditing
을 사용유저명
, 이메일
, 비밀번호
, 작성일
, 수정일
를 저장작성일
, 수정일
필드는 JPA Auditing
을 사용이메일
과 비밀번호
를 활용해 로그인 기능을 구현회원가입
, 로그인 요청
은 인증 처리에서 제외PasswordEncoder
를 사용하여 비밀번호 해쉬암호화댓글
, 작성일
, 수정일
, 작성유저
, 댓글단 일정
을 저장작성일
, 수정일
필드는 JPA Auditing
을 사용현재 프로젝트에서 만족했고, 앞으로의 훈련기간에서 지속하고 싶은 부분을 작성했다.
현재 프로젝트에서 어려웠던 점과 아쉬웠던 점을 작성했다.
DTO(Data Transfer Object)
의 뜻처럼 데이터를 전달하는데 사용되기 때문이다.
객체의 불변성이 보장되야 해당 객체를 신뢰할 수 있고,
멀티쓰레드 환경에서도 안전하게 사용할 수 있기 때문이다.
해당 사실을 구글링하면서 재밌는 역직렬화 흐름을 알게 되었다.
역직렬화 흐름
- Jackson 라이브러리(=ObjectMapper)는 @r@RequestBody로 받은 데이터로 객체를 생성할 때, 기본전략으로 기본생성자를 이용한다.
그리고, 이 과정을 역직렬화라고 한다.
- 기본생성자가 없고 다른 생성자(=유일 생성자)가 있을 경우,
Jackson 라이브러리가 자동으로 그 생성자를 이용해 생성한다.
*단, 매개변수가 1개인 생성자라면@JsonCreator
을 명시적으로 사용하는게 좋다.
- 따라서 기본생성자가 있는 클래스라면 무분별하게 기본생성자로 인스턴스화 되지 않도록 접근 제한을 두어야한다. →
@NoArgsConstructor
- DTO의 경우, 보통 필드가 2개 이상이기 때문에 해당 필드를 초기화하는 생성자가 반드시 필요하다. →
@RequiredArgsConstructor
- 만약 생성자가 2개 이상이라면, Jackson 라이브러리에게 어떠한 생성자로 역직렬화를 진행할건지 알려줘야한다. →
@JsonCreator
를 생성자 위에 붙이자.💡 자바14버전+의 경우,
record
클래스로 생성하는 쉬운 방법도 있다.
[TIL] DTO는 final로 설정하는 것이 맞을까?
위에 말처럼
기본생성자가 아닌 다른 유일생성자가 있으면 Jackson 라이브러리가 자동으로 매핑해서 생성자를 통해 객체를 생성해줄텐데, 엔티티(@Entity
) 클래스에는 왜 기본생성자가 필수일까??
JPA는 DB값으로 엔티티 객체를 생성할 때 리플렉션(Reflection)이라는 기술을 사용한다.
Jackson 라이브러리도 값을 주입할 때, 리플렉션을 사용한다고 한다.
JPA가 엔티티클래스를 객체화 하는 방법은
기본생성자를 통해서 객체를 생성하고 값을 주입하여 객체를 생성하는데,
Setter가 없어도 자동으로 필드에 값을 주입하여 생성한다고 한다.
Q2-Q1) Jackson처럼 유일 생성자 + Setter만 있으면 JPA도 괜찮은 거 아냐?
- Jackson 라이브러리가 역직렬화를 통해 인스턴스화하는 것과
JPA가 DB에 접근해 값을 갖고 엔티티 클래스를 인스턴스화할 때,
리플렉션
이라는 기술을 쓰는건 동일하다.
- JPA는 프록시 또는 LazyLoading 기술을 써서
"반드시" 기본생성자로 가짜객체를 만들고, 값을 주입하는 것이기 때문에
무조건 기본생성자가 필요하다!
그건 "하이버네이트"라는 녀석이 도와준 것이라고한다.
IDE에서도 기본생성자 만들라고 경고하니 왠만하면 만들자! → @NoArgsConstructor
spring.jpa.hibernate.ddl-auto=create
를 사용해서
편하게 DDL 작성을 했는데 이게 모야
컬럼이 뒤죽박죽이 되어버렸다..
구글링해보니, 방법들이 있었지만 애초에 테스트용도로 사용하는 옵션이기에
많은 분들이 굳이 하지 않는 것 같았다.
하이버네이트가 자동으로 생성해주는 DDL은 신뢰성이 떨어지기 때문에
실무에서는 JPA로 생성된 테이블의 DDL을 참고하여
개발자가 직접 스키마를 정의하고 관리한다고 한다!
com.fasterxml.jackson.databind.exc.InvalidDefinitionException:
Java 8 date/time type
java.time.LocalDateTime
not supported by default
LoginFilter를 사용하는데 위와 같은 에러를 만났다.
Jackson 라이브러리가 LocalDateTime
을 역직렬화하지 못해서 생기는 문제라고 한다.
💡 역직렬화: JSON 데이터 → Java 클래스
따라서 아래 모듈을 설치하여 우리가 도와주자.
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'
HttpMessageNotReadableException
└── cause: UnrecognizedPropertyException
스프링은 파싱 중에 UnrecognizedPropertyException
이 발생하면,
다양한 JSON 오류를 하나의 예외로 처리 가능하게 하기 위해서
HttpMessageNotReadableException
에 래핑해서 던진다고 한다.
따라서 아래와 같이 HttpMessageNotReadable
내부에서 핸들링 해야한다고 한다!
@ExceptionHandler(HttpMessageNotReadableException.class)
public ResponseEntity<?> handleException(HttpMessageNotReadableException ex) {
Throwable cause = ex.getCause(); // 예외가 발생한 이유
// 발생한 이유가 UnrecognizedPropertyException라면?
if (cause instanceof UnrecognizedPropertyException unrecognized) {
// 예외 핸들링
}
// ...
}
{
"timestamp": "2025-04-03 10:52:10",
"code": 200,
"status": "OK",
"path": "/schedule/1/comments",
"message": "일정의 전체 댓글을 조회합니다.",
"data": [
{
"comment": "test",
"commentId": 1,
"createdAt": "2025-04-03 10:51:28",
"updatedAt": "2025-04-03 10:51:28",
"user": {
"id": 1,
"name": "test",
"email": "123@123.com"
},
"schedule": {
"id": 1,
"title": "test",
"contents": "123123123"
}
},
{
"comment": "123123",
"commentId": 2,
"createdAt": "2025-04-03 10:52:05",
"updatedAt": "2025-04-03 10:52:05",
"user": {
"id": 1,
"name": "test",
"email": "123@123.com"
},
"schedule": {
"id": 1,
"title": "test",
"contents": "123123123"
}
},
]
}
모든 댓글 객체마다 user
, schedule
정보가 담겨 있다...
해당 댓글의 작성자는 달라질 수 있지만, 일정의 정보는 똑같다.
// 내가 바라는 JSON 응답
data: {
schedule: { ... }
comments: [ ... ]
}
├── CommentResponseDto.java // (기본) 댓글 응답객체
├── CommentDetailResponseDto.java // (확장) 댓글 상세정보 응답객체
└── PagedCommentResponseDto.java // (확장) 댓글리스트 응답객체
위와 같이 구성을 해보자!
// ✅ 기본 댓글응답 객체
@Getter
@RequiredArgsConstructor
public class CommentResponseDto {
private final Long commentId;
private final String comment;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
private final LocalDateTime createdAt;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
private final LocalDateTime updatedAt;
// ✅ 생성자 (Comment 객체를 받아 생성)
public CommentResponseDto(Comment comment)
{
this.commentId = comment.getId();
this.comment = comment.getComment();
this.createdAt = comment.getCreatedAt();
this.updatedAt = comment.getUpdatedAt();
}
}
기본 응답객체를 생성하고, 상세응답객체/리스트응답객체로 세분화하였다.
// ✅ 상세 응답객체 (기본 응답객체 상속받음)
@Getter
public class CommentDetailResponseDto extends CommentResponseDto {
// ✅ 기본 응답객체 + 유저/일정 정보 추가
private final UserInfoDto user;
private final ScheduleInfoDto schedule;
// ✅ 생성자 (Comment 객체를 받아 생성)
public CommentDetailResponseDto(Comment comment) {
super(comment); // 부모 생성자 호출
this.user = new UserInfoDto(comment.getUser());
this.schedule = new ScheduleInfoDto(comment.getSchedule());
}
}
상세응답객체는 기본응답객체+ 유저/일정
으로 확장한 응답객체이다.
// ✅ 리스트 응답 객체 (+ 리스트 기본 응답객체를 리스트)
@Getter
@JsonPropertyOrder({"page", "user", "schedule", "comments"})
public class PagedCommentResponseDto {
private final PageInfo page;
// ✅ 리스트<기본응답객체> + 작성자/일정 정보
private final UserInfoDto user;
private final ScheduleInfoDto schedule;
private final List<CommentResponseDto> comments;
// ✅ 생성자 (페이징댓글, 유저, 일정 객체를 받아 생성)
public PagedCommentResponseDto(
Page<Comment> pagedComment,
UserInfoDto user,
ScheduleInfoDto schedule)
{
this.page = new PageInfo(pagedComment);
this.user = user;
this.schedule = schedule;
this.comments = pagedComment.map(CommentResponseDto::new).getContent();
}
}
리스트응답객체는 리스트<기본응답객체> + 일정정보
로 확장한 응답객체이다.
아래와 같이 변했다!
{
"timestamp": "2025-04-04 17:24:20",
"code": 200,
"status": "OK",
"path": "/schedule/1/comments",
"message": "일정의 전체 댓글을 조회합니다.",
"data": {
"page": { ... }, // ✅ 페이지 정보
"user": { ... }, // ✅ 해당 Schedule의 작성자 정보
"schedule": { ... }, // ✅ 댓글단 Schedule 정보
"comments": [ ... ] // ✅ 해당 Schedule의 모든 댓글 정보
}
}
포스트맨으로 작성한 API 명세서
작성 시간이 너무 오래 걸렸다..
POSTMAN으로 작성하고 있는데 다른 좋은 방법이 있는지 찾아봐야될 것 같다..
다음 프로젝트에서 시도해볼 점들을 작성했다.