BootCamp

[Error] 일정 트러블 슈팅

limitation01 2025. 11. 5. 08:39

어려웠던 부분

1.JPA 관계 설정

ERD도 그려봤고 엔티티에도 다대일 일대다 양방향 관계 설정도 했다. 문제는 DTO. 사실 그냥 내 문제

잘못된 설계

Comment comment = new Comment(
    request.getContent(),
    request.getUsername(),
    request.getPassword(),
    request.getTodo()   // ❌ request에서 Todo를 직접 받음
);
  • 이건 request DTO에 Todo 엔티티가 직접 들어있다는 전제인데, 클라이언트는 Todo 엔티티 구조를 모름 Todo의 id만 알고있음
  • DTO는 엔티티를 몰라야함!!
  • 또한 JPA가 관리하지 않는(영속되지 않은) Todo 객체가 들어가면 연관관계가 깨질 수 있음
  • DTO는 단순히 데이터 전달용이라는 사실을 망각, 그리고 DTO안에 또다른 DTO를 의존할 수 있다는 것도 모르고 지금 내가 DTO안에서 작성하고 있으니까 아무생각 없었던거 같음
  • todoId로 Todo를 찾아 존재하지 않으면 예외 발생 , 있으면 실질적인 객체를 넘겨줘야함

수정

Todo todo =  findTodoOrException(todoId);
        Comment comment = new Comment(
                request.getContent(),
                request.getUsername(),
                request.getPassword(),
                todo // 실제 엔티티를 찾아 연결
        );

2. DTO간의 의존

DTO는 Entity를 직접 알아서는 안 된다.
Controller ↔ Service ↔ Repository 사이에서의 데이터 전달자
이 부분은 단순한 문법 문제가 아니라, 설계시 책임의 경계를 정해야 하는 부분이라 어렵게 느껴졌다.

특히 다음과 같은 의문이 들었다.

“DTO가 다른 DTO를 포함해도 될까?”

아래는 Todo와 Comment의 일대다 관계 예시 JSON response이다.

{
  "id": 1,
  "title": "오늘 일정",
  "comments": [
    {"id": 10, "content": "응원합니다!"},
    {"id": 11, "content": "화이팅!"}
  ]
}

이런식의 중첩 구조를 만들려면 responseDTO가 또다른 responseDTO를 포함해야 만들 수 있다. 포함해야 하는 구조가 필요한 이유는 response JSON의 구조 표현을 위해서라고 생각한다. 

3. 조회시 내림차순 정렬

JpaRepository.findAll()은 정렬 조건이 없는 단순 SELECT 쿼리를 실행하며 JPA는 기본적으로 정렬 순서를 보장하지 않아 명시적으로 OrderBy를 지정해줘야 한다고 한다.

해결 방법을 찾던중 커스텀 JPA 쿼리 메소드 라는 것을 찾아 적용해보았다.

List<Todo> findAllByOrderByCreatedAtDesc();
  • SELECT t FROM Todo t ORDER BY t.created_at DESC 로 자동 JPQL을 생성해준다.

 

4. 중복 URL prefix 처리

과제 구현 완료후 최대한 재사용해보자 해서 파일을 훑어보던 중 url prefix가 중복된다는 사실을 발견했다. 너무오래전이라 가물가물 하지만 django에선 환경변수로 urls.py에 참조 했던게 기억나 관련된 스프링 기능을 찾아보게 되었다.

@RequestMapping("/경로")

나는 지금까지 @PostMapping같은 매서드 단위의 어노테이션만 사용했다. 경로도 해당 어노테이션 안에서 처리해주고 있었는데 클래스 단위 공통 경로용으로 사용하는 어노테이션 @RequestMapping을 알게되었다. 지금 프로젝트는 prefix로 "/todos"를 사용하고 있는데 이 경로를 맵핑해줄 수 있었다.

물론 나중에 이 URL prefix를 환경변수로 참조할 수도 있다. @RequestMapping("${api.base-path}/todo-list") 이런식으로! 지금은 그럴필요까지는 없는 경로라 간단하게 처리

@RestController
@RequiredArgsConstructor
@RequestMapping("/todos")
public class CommentController {
    private final CommentService commentService;

    @PostMapping("/{todoId}/comments")
    public ResponseEntity<CreateCommentResponse> createComment(
            @PathVariable Long todoId,
            @Valid @RequestBody CreateCommentRequest request) {
        return ResponseEntity.status(HttpStatus.CREATED).body(commentService.save(todoId,request));
    }
}

5. N+1 문제

이건 일단 트러블슈팅 글로 다룰 예정

도전과제 단건조회시 Todo 엔티티 조회, 해당 Todo의 Comment 조회 이렇게 2번 쿼리를 날린다. JPA 관계 설정시 붙여줬던 어노테이션 fetch=Lazy 때문. Lazy(지연로딩)로 두는 이유는 성능, 필요한 시점에만 쿼리를 날릴 수 있도록 하기 위함

처음에는 다건조회에서 쿼스텀 쿼리 메소드 적었던 것 처럼 레포지토리에 @Query 어노테이션달고 적었다. 그 후 @EntityGraph를 알게되어 간단한 SQL문이므로 표현방식이 더 좋다고 생각해 변경했다. 결과 SQL은 동일햇음

# 전
select * from todos where id=?;
select * from comments where todo_id=?;

# 후
select t..., c... 
from todos t left join comments c on t.id = c.todo_id 
where t.id=?;

 

도전과제..어려웠음..입문인데..익숙해지겟지..레고조립하는거같다..근데 그 설명서가 아랍어로된... 방향은 알겟는데 정확히 뭔지는 모르는..눈물죽죽..반복하자..