[회고] 일정 API (플러스) 과제 회고

wannabeing·2025년 4월 4일
2

SPARTA-회고

목록 보기
4/5
post-thumbnail

✅ 개요

  • 개발기간: 2025.03.27(목) ~ 2025.04.04(금)
  • 자바버전: OpenJDK 17
  • 사용기술: Spring Boot, JPA, MySQL
  • RESTful한 일정 관리 백엔드 API 서버 (JPA를 곁들인)
  • 프로젝트 링크: GitHub Repository

✅ 요구사항 정의

공통

  • 모든 테이블은 고유 식별자(ID)를 가진다.
  • 3 Layer Architecture를 지키며 개발한다.
  • JPA 를 사용하여 개발한다.
  • 인증/인가 절차는 Cookie/Session을 사용하여 개발한다.
  • JPA 연관관계는 단방향 이다.

💪🏻 필수 기능

  • [Lv1]. Schedule(일정) CRUD 기능
    • Schedule: 제목, 내용, 작성일, 수정일, 작성유저를 저장
    • 작성일, 수정일 필드는 JPA Auditing을 사용
  • [Lv2]. User(사용자) CRUD 기능
    • User: 유저명, 이메일, 비밀번호, 작성일 , 수정일를 저장
    • 작성일, 수정일 필드는 JPA Auditing을 사용
  • Schedule(일정)과 User(사용자) 연관관계 설정
    • 1:N 관계
    • 사용자는 여러개의 일정을 가질 수 있다.
    • 일정은 반드시 사용자에게 속한다.
  • [Lv3]. 회원가입 기능
  • [Lv4]. 로그인/로그아웃 기능
    • Cookie/Session을 활용해 로그인 기능을 구현
    • 이메일비밀번호를 활용해 로그인 기능을 구현
    • 회원가입, 로그인 요청은 인증 처리에서 제외

🚀 도전 기능

  • [Lv5]. 다양한 예외처리 적용
    • 프로젝트를 분석하고 예외사항들을에 대하여 예외처리 적용
  • [Lv6]. 비밀번호 암호화
    • PasswordEncoder를 사용하여 비밀번호 해쉬암호화
  • [Lv7]. 댓글 CRUD 기능
    • Comment: 댓글, 작성일, 수정일, 작성유저, 댓글단 일정을 저장
    • 작성일, 수정일 필드는 JPA Auditing을 사용
  • [Lv8]. 일정/댓글 페이징 기능
    • JPA가 제공하는 Pageable 인터페이스로 페이징 기능 적용
    • 디폴트 페이지크기는 10
    • 일정은 수정일 기준으로 내림차순 정렬
    • 댓글은 작성일 기준으로 내림차순 정렬

✅ Keep

현재 프로젝트에서 만족했고, 앞으로의 훈련기간에서 지속하고 싶은 부분을 작성했다.

  • 스프링 컨테이너, IoC와 DI, Bean에 대해 이해하고자 노력했다.
  • 엔티티 클래스와 DTO에 구성방식에 대해 이해하고자 노력했다.
  • 스트림/제네릭을 적절하게 적용해보았다고 생각한다.
  • 요구사항을 깊이 고민하고 구현하는 과정을 반복하고 있다.
  • 매일 코드카타를 통해 논리적인 접근을 계속해서 하고자 한다.
  • 앞으로 남은 훈련 과정에서도 주어진 과제에 적극적으로 임하며, 더 완성도 높은 결과물을 만들고싶다.

✅ Problem

현재 프로젝트에서 어려웠던 점과 아쉬웠던 점을 작성했다.


🙋‍♂️1) DTO는 왜 final로 설정할까? 그게 맞을까?

A) final로 설정하자!

DTO(Data Transfer Object)의 뜻처럼 데이터를 전달하는데 사용되기 때문이다.
객체의 불변성이 보장되야 해당 객체를 신뢰할 수 있고,
멀티쓰레드 환경에서도 안전하게 사용할 수 있기 때문이다.

해당 사실을 구글링하면서 재밌는 역직렬화 흐름을 알게 되었다.

역직렬화 흐름

  • Jackson 라이브러리(=ObjectMapper)는 @r@RequestBody로 받은 데이터로 객체를 생성할 때, 기본전략으로 기본생성자를 이용한다.
    그리고, 이 과정을 역직렬화라고 한다.
  • 기본생성자가 없고 다른 생성자(=유일 생성자)가 있을 경우,
    Jackson 라이브러리가 자동으로 그 생성자를 이용해 생성한다.
    *단, 매개변수가 1개인 생성자라면 @JsonCreator을 명시적으로 사용하는게 좋다.
  • 따라서 기본생성자가 있는 클래스라면 무분별하게 기본생성자로 인스턴스화 되지 않도록 접근 제한을 두어야한다. → @NoArgsConstructor
  • DTO의 경우, 보통 필드가 2개 이상이기 때문에 해당 필드를 초기화하는 생성자가 반드시 필요하다. → @RequiredArgsConstructor
  • 만약 생성자가 2개 이상이라면, Jackson 라이브러리에게 어떠한 생성자로 역직렬화를 진행할건지 알려줘야한다. → @JsonCreator 를 생성자 위에 붙이자.

💡 자바14버전+의 경우, record 클래스로 생성하는 쉬운 방법도 있다.

[TIL] DTO는 final로 설정하는 것이 맞을까?


🙋‍♂️2) 엔티티 클래스에는 왜 기본생성자가 필수일까?

위에 말처럼
기본생성자가 아닌 다른 유일생성자가 있으면 Jackson 라이브러리가 자동으로 매핑해서 생성자를 통해 객체를 생성해줄텐데, 엔티티(@Entity) 클래스에는 왜 기본생성자가 필수일까??

A) JPA가 "리플렉션"을 써서 그렇다!

JPA는 DB값으로 엔티티 객체를 생성할 때 리플렉션(Reflection)이라는 기술을 사용한다.
Jackson 라이브러리도 값을 주입할 때, 리플렉션을 사용한다고 한다.

JPA가 엔티티클래스를 객체화 하는 방법은
기본생성자를 통해서 객체를 생성하고 값을 주입하여 객체를 생성하는데,
Setter가 없어도 자동으로 필드에 값을 주입하여 생성한다고 한다.

Q2-Q1) Jackson처럼 유일 생성자 + Setter만 있으면 JPA도 괜찮은 거 아냐?

  • Jackson 라이브러리가 역직렬화를 통해 인스턴스화하는 것과
    JPA가 DB에 접근해 값을 갖고 엔티티 클래스를 인스턴스화할 때,
    리플렉션이라는 기술을 쓰는건 동일하다.
  • JPA는 프록시 또는 LazyLoading 기술을 써서
    "반드시" 기본생성자로 가짜객체를 만들고, 값을 주입하는 것이기 때문에
    무조건 기본생성자가 필요하다!

💡 안써도 프로젝트가 원활하게 돌아간다면?

그건 "하이버네이트"라는 녀석이 도와준 것이라고한다.
IDE에서도 기본생성자 만들라고 경고하니 왠만하면 만들자! → @NoArgsConstructor

[TIL] 엔티티클래스는 왜 기본생성자가 필수일까?


🙋‍♂️3) 테이블 컬럼 정렬이 안되는 건에 대하여

spring.jpa.hibernate.ddl-auto=create를 사용해서
편하게 DDL 작성을 했는데 이게 모야

컬럼이 뒤죽박죽이 되어버렸다..

구글링해보니, 방법들이 있었지만 애초에 테스트용도로 사용하는 옵션이기에
많은 분들이 굳이 하지 않는 것 같았다.

A) DDL은 직접 작성하자!

하이버네이트가 자동으로 생성해주는 DDL은 신뢰성이 떨어지기 때문에
실무에서는 JPA로 생성된 테이블의 DDL을 참고하여
개발자가 직접 스키마를 정의하고 관리한다고 한다!

[TIL] DDL 자동 생성 등...


🙋‍♂️4) LocalDateTime을 지원안해요?

com.fasterxml.jackson.databind.exc.InvalidDefinitionException:
Java 8 date/time type java.time.LocalDateTime not supported by default

LoginFilter를 사용하는데 위와 같은 에러를 만났다.
Jackson 라이브러리가 LocalDateTime을 역직렬화하지 못해서 생기는 문제라고 한다.

💡 역직렬화: JSON 데이터 → Java 클래스

A4) 도와줄게요~

따라서 아래 모듈을 설치하여 우리가 도와주자.

implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'

🙋‍♂️5) [예외] UnrecognizedPropertyException를 은 어떻게 핸들링하나요?

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) {
    	// 예외 핸들링
    }
    
    // ...
}

🙋‍♂️6) JSON 응답이 너무 TMI..?

{
    "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 정보가 담겨 있다...
해당 댓글의 작성자는 달라질 수 있지만, 일정의 정보는 똑같다.


A) 응답객체를 세분화해보자.

// 내가 바라는 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 명세서 작성...

포스트맨으로 작성한 API 명세서
작성 시간이 너무 오래 걸렸다..
POSTMAN으로 작성하고 있는데 다른 좋은 방법이 있는지 찾아봐야될 것 같다..


✅ Try

다음 프로젝트에서 시도해볼 점들을 작성했다.

  • API명세서를 작성한다면 자동화된 프로그램을 찾아서 써보자.
  • 예외처리에 대해 더 공부하고, 제대로 처리해보자.
  • 테스트코드 작성을 해보자.
  • 공부할게 산더미라서 천천히, 차근차근 해보자.
  • 내 코드의 구조와 동작원리를 명확히 이해하고, 설명하기

인프런 회고 문화

profile
wannabe---ing

0개의 댓글