개발환경

  • MacBook Air (M1, 2020)
  • OpenJDK 11
  • IntelliJ IDEA Community Edition
  • Spring Boot 2.7.3
  • MySQL Workbench 8.0.28


기간

  • 2022.8.15 ~


주제

  • 오늘의 소비에 대한 기억과 감정을 기록으로 남기고 나중에 되돌아보는 시간이 있었으면 좋겠다는 생각에 진행하게 된 프로젝트이다.
  • 새로 산 물건의 사진을 찍고 그에 대한 감상을 함께 글로 남길 수 있는 기능을 메인으로 제작할 것이다.


진행상황

DiaryRepository

  • Repository에서 회원의 아이디로 일기 목록을 조회하는 메서드를 작성했다.
@Repository
@RequiredArgsConstructor
public class DiaryRepository {

    private final EntityManager em; // 의존성 자동 주입

    public List<Diary> findAll(Long id) {
        return em.createQuery("select d from Diary d where d.member.id = :member_id", Diary.class)
                .setParameter("member_id", id)
                .getResultList();
    }
}

DiaryService

  • 이제 Service에서는 아까 만든 Repository의 메서드를 호출해 주면 된다.
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class DiaryService {

    private final DiaryRepository diaryRepository;

    /**
     * 일기 전체 조회
     * @return List<Diary>
     */
    public List<Diary> findDiaries(Long id) {
        return diaryRepository.findAll(id);
    }
}
  • 파라미터로 조회하고자 하는 회원의 아이디를 넣어준다.

DiaryApiController

  • HTTP RequestURL 매핑을 해 준 다음 메서드를 작성했다.

일기 목록 리턴 메서드 v1

@RestController
@RequiredArgsConstructor
public class DiaryApiController {

    private final DiaryService diaryService;

    /**
     * 회원 한 명의 일기 목록을 응답
     * @param id
     * @return id에 해당하는 회원의 일기 전체 목록
     */
    @GetMapping("/diaries/{id}")
    public List<Diary> diaries(@PathVariable Long id) {
        return diaryService.findDiaries(id);
    }
}
  • 이제 완벽하다고 생각했는데 막상 실행하니까 또 동작이 제대로 되지 않는 것이었다.
  • 왜냐면 객체를 직접 반환하다 보니 Diary 객체에 있는 MemberCategory 객체 필드도 채워줘야 하는데 저것들은 @ManyToOne(fetch = FetchType.LAZY로 설정했기 때문에 일기 목록만 조회하는 시점에서는 프록시 객체로만 있는 상태지 실제 객체가 존재하는 것은 아니다. 그래서 사실상 빈 상태이다 보니 필드가 채워지지 않아
    No serializer found for class org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS)
  • 이런 에러가 나는 것이었다.

  • 이걸 해결하려면 @ManyToOne 어노테이션마다 @JsonIgnore를 붙여주던가 application.yml에서
spring:
  jackson:
    serialization:
      FAIL_ON_EMPTY_BEANS: false
  • 이런 식으로 빈 필드에 대한 예외처리를 해 주는 설정값을 추가해 주어야 했다. 하지만 일기 목록을 조회할 때 회원과 카테고리의 모든 정보가 필요한 것이 아니기 때문에 빈 필드에 대한 예외처리를 해 주는 것은 사실상 불필요한 작업이라고 할 수 있다. 회원 한 명의 일기 목록을 조회하는 것이기 때문에 요청하는 쪽에서 회원 정보는 이미 가지고 있는 상태이고, 카테고리에서 필요한 정보는 카테고리 이름 정도이다. 그래서 한마디로 하면 좋긴 하겠지만 굳이 할 필요가 없는 작업인 것이다.
  • 그래서 또 강의를 참고하여… 객체의 직접 리턴이 아닌 별도의 DTO를 만들어 리턴하는 방식으로 바꿨다.

일기 목록 리턴 메서드 v2

  • JSON 데이터로 만들 클래스
@Data
@AllArgsConstructor
public class Result<T> {

    private int count;
    private T diaryList;
}
  • 각 일기 하나가 담고 있을 정보를 담은 클래스
@Data
@AllArgsConstructor
public class DiaryDto {

    private String category;
    private String title;
    private String content;
    private LocalDateTime date;
    private String photo;
    private int price;
}
@RestController
@RequiredArgsConstructor
public class DiaryApiController {

    private final DiaryService diaryService;
    private final MemberService memberService;
    private final CategoryService categoryService;

    /**
     * 회원 한 명의 일기 목록을 응답
     * @param id
     * @return id에 해당하는 회원의 일기 전체 목록
     */
    @GetMapping("/diaries/{id}")
    public Result diaries(@PathVariable Long id) {
        List<Diary> findDiaries = diaryService.findDiaries(id);
        Member findMember = memberService.findMember(id);
        Category findCategory = categoryService.findCategory(1L);

        List<DiaryDto> collect = findDiaries.stream()
                .map(d -> new DiaryDto(
                        d.getCategory().getName(), 
                        d.getTitle(), 
                        d.getContent(), 
                        d.getDate(), 
                        d.getPhoto(), 
                        d.getPrice()))
                .collect(Collectors.toList());

        return new Result(collect);
    }
}
  • 화면에서 회원의 전체 일기 목록을 보여주는 데 필요한 파라미터만 넘기기 위해 별도의 DTO 클래스를 생성한 뒤 Result 객체에 담아 리턴했다.
{
    "count": 5,
    "diaryList": [
        {
            "category": "food",
            "title": "first buy",
            "content": "fiirst diary",
            "date": "2022-08-25T23:00:44.068405",
            "photo": null,
            "price": 1000
        },
        {
            "category": "food",
            "title": "second buy",
            "content": "second diary",
            "date": "2022-08-25T23:02:41.910958",
            "photo": null,
            "price": 2000
        },
        {
            "category": "food",
            "title": "third buy",
            "content": "third diary",
            "date": "2022-08-25T23:03:12.428172",
            "photo": null,
            "price": 3000
        },
        {
            "category": "food",
            "title": "fourth buy",
            "content": "fourth diary",
            "date": "2022-08-25T23:03:31.481156",
            "photo": null,
            "price": 4000
        },
        {
            "category": "food",
            "title": "fifth buy",
            "content": "fifth diary",
            "date": "2022-08-25T23:04:35.310611",
            "photo": null,
            "price": 5000
        }
    ]
}
  • 그리하여 응답 받은 데이터! 추후 추가로 필요해진 파라미터를 추가할 수 있게 확장에 열린 API가 되었다. Result 클래스의 필드값만 수정하면 되기 때문에 해당 API에서 응답해야 하는 데이터에 변경이 생겼을 경우 수정해야 하는 부분을 최소화 할 수 있다.


정리

  • 강의를 한 번 본 뒤 나의 프로젝트에 맞게 변형해 작성한 코드였는데 처음엔 에러가 참 많이 났다. 맞게 한 거 같은데 왜 나는 안 되는 걸까…
  • 문제는 강의를 한 번 본 것으로 어느정도 이해하고 코드를 작성할 수 있다고 착각한 것이었다. 그래서 빨리 만들자는 마음을 버리고 강의를 다시 한 번 복습하며 내 코드의 문제점을 깨닫고 처음에 작성했던 코드는 왜 동작하지 않았는지 원인을 파악하는 시간을 가졌다. 그리고 JPA의 동작을 좀 더 이해하는 데에 많은 도움이 되었다. 이제 같은 실수는 하지 않을 거 같다!


참고