Projects/청하-청년을 위한 커뮤니티 서비스

[청하] 10. 게시글 리스트 조회 기능 구현 (feat. 정적 팩토리 메서드)

Lpromotion 2024. 10. 8. 23:32

1. 프로젝트 구조

src/main/java/com/example/withpeace/
│
├── domain/                 # 도메인 모델 (엔티티)
│   ├── Image.java          # 이미지 엔티티
│   └── Post.java           # 게시글 엔티티
│
├── repository/             # 데이터 접근 계층
│   ├── ImageRepository.java    # 이미지 레포지토리
│   └── PostRepository.java     # 게시글 레포지토리
│
├── dto/                    # 데이터 전송 객체
│   └── response/
│       └── PostListResponseDto.java # 게시글 리스트 조회 응답 DTO
│
├── controller/             # 컨트롤러 계층
│   └── PostController.java # 게시글 관련 API 엔드포인트
│
├── service/                # 비즈니스 로직 계층
│   └── PostService.java    # 게시글 관련 비즈니스 로직
│
└── util/                   # 유틸리티 클래스
    └── TimeFormatter.java  # 시간 포맷팅 유틸리티

 

2. DTO 구현

@Builder
public record PostListResponseDto(
        Long postId,
        String title,
        String content,
        ETopic type,
        String createDate,
        String postImageUrl) {

    // 정적 팩토리 메서드: Post 엔티티와 이미지 URL을 받아 DTO 객체 생성
    public static PostListResponseDto from(Post post, String postImageUrl) {
        return new PostListResponseDto(
                post.getId(),
                post.getTitle(),
                post.getContent(),
                post.getType(),
                TimeFormatter.timeFormat(post.getCreateDate()), // 날짜 포맷팅
                postImageUrl
        );
    }
}

게시글 리스트 조회의 응답 DTO 이다. 게시글의 정보와 게시글과 연관된 이미지 1개를 반환한다.

 

from 메서드

Post 엔티티와 이미지 URL을 받아 DTO 객체를 생성하는 *정적 팩토리 메서드 이다. 서비스 계층의 가독성을 높이기 위해 사용했다.

정적 팩토리 메서드 (Static Factory Method)

REF) https://inpa.tistory.com/entry/GOF-💠-정적-팩토리-메서드-생성자-대신-사용하자

정적 팩토리 메서드 패턴은 개발자가 구성한 Static Method를 통해 간접적으로 객체를 생성하는 디자인 패턴이다. 객체를 인스턴화 할 때 생성자를 호출하여 생성하는 것이 아닌, 메서드를 호출하여 객체를 생성하는 방법이다.

정적 팩토리 메서드는 생성자의 역할을 대신 이행하는 것 뿐만 아니라, 좀 더 가독성 좋은 코드를 작성하고 객체지향적으로 프로그래밍 할 수 있게 도와준다.

 

3. 레포지토리 구현

3.1. PostRepository

@Repository
public interface PostRepository extends JpaRepository<Post, Long> {
		
    @Query(value = "SELECT p FROM Post p WHERE p.type=:type ORDER BY p.createDate DESC")
    Page<Post> findByType(ETopic type, Pageable pageable);
}

findByType은 특정 타입의 게시글을 페이지네이션하여 조회하는 메서드이다. ORDER BY p.createDate DESC로 생성시간 기준으로 내림차순 정렬했다.

 

3.2. ImageRepository

@Repository
public interface ImageRepository extends JpaRepository<Image, Long> {
    // ...

    @Query(value = "SELECT i.url FROM images i WHERE i.post_id = :postId ORDER BY i.id ASC LIMIT 1",
            nativeQuery = true)
    Optional<String> findUrlsByPostIdOrderByIdAsc(@Param("postId") Long postId);

}

findUrlsByPostIdOrderByIdAsc은 특정 게시글의 첫 번째 이미지 URL을 조회하는 메서드이다.

LIMIT 1로 첫 번째 결과만 가져오기 위해 nativeQuery 속성을 사용하였다. JPA 쿼리 메서드로 표현하기 어렵기 때문이다.

Optional<String>을 반환 타입으로 설정했다.

  • NullPointerException 방지
  • 값의 부재 명시적으로 표현

 

4. 컨트롤러 구현

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/posts")
public class PostController {
    private final PostService postService;
    // ... (게시글 관련 메서드 생략)

    // 게시글 리스트 조회
    @GetMapping("")
    public ResponseDto<?> getPostList(@UserId Long userId, // 사용자 ID
                                      @RequestParam ETopic type, // 게시글 타입
                                      @RequestParam(defaultValue = "0") @Valid @NotNull @Min(0) Integer pageIndex, // 페이지 인덱스
                                      @RequestParam(defaultValue = "1") @Valid @NotNull @Min(1) Integer pageSize // 페이지 크기
                                      ) {
        return ResponseDto.ok(postService.getPostList(userId, type, pageIndex, pageSize));
    }
}

사용자 ID, 게시글 타입, 페이지 정보를 입력받아 서비스 계층에 전달하고, 결과를 반환한다.

  • @RequestParam: HTTP 요청의 파라미터를 메서드 파라미터에 바인딩한다.
  • @Valid: 해당 파라미터의 유효성 검사를 수행한다.
  • @NotNull: 파라미터가 null 이 아님을 검증한다.
  • @Min: 파라미터의 최소값을 지정한다.

 

5. 서비스 구현

@Service
@RequiredArgsConstructor
public class PostService {
    // ... (필드 선언 생략)
    // ... (게시글 관련 메서드 생략)
    
    @Transactional
    public List<PostListResponseDto> getPostList(Long userId, ETopic type, Integer pageIndex, Integer pageSize) {
        // 사용자 존재 여부 확인
        User user =
                userRepository.findById(userId).orElseThrow(() -> new CommonException(ErrorCode.NOT_FOUND_USER));

        Pageable pageable = PageRequest.of(pageIndex, pageSize); // 페이지네이션 객체 생성
        Page<Post> postPage = postRepository.findByType(type, pageable); // 게시글 조회
        
        // 게시글 목록을 DTO로 변환
        List<PostListResponseDto> postListResponseDtos = postPage.getContent().stream()
                .map(post -> {
                    // 각 게시글의 첫 번째 이미지 URL 조회
                    String postImageUrl = imageRepository.findUrlsByPostIdOrderByIdAsc(post.getId())
                            .orElse(null); // 이미지가 존재하지 않으면 null
                    // DTO 생성 및 반환
                    return PostListResponseDto.from(post, postImageUrl);
                })
                .collect(Collectors.toList()); // 게시글이 존재하지 않으면 빈 리스트

        return postListResponseDtos;
    }
}
  1. 사용자 ID를 통해 사용자를 조회하고 존재하지 않으면 에러를 반환한다.
  2. Pageable 객체를 생성해 페이지네이션 정보를 설정한다.
  3. 특정 타입의 게시글을 페이지네이션하여 조회한다.
  4. 조회된 게시글 목록을 stream 을 사용해 DTO로 변환한다.
    • 각 게시글에 대해 첫 번째 이미지 URL을 조회한다.
    • 게시글 정보와 이미지 URL을 사용해 DTO를 생성한다.
  5. 변환된 DTO 리스트를 반환한다.

 

6. API 응답 예시

요청 URL

[GET] http://cheongha.site/api/v1/posts?type=FREEDOM&pageIndex=0&pageSize=10 

 

Response Body

{
  "data": [
    {
      "postId": 8,
      "title": "가을이 온 것 같네요!",
      "content": "선선한 날씨가 너무 좋네요!\\n시원해진 날씨에 다들 하고싶은게 있으신가요?!",
      "type": "FREEDOM",
      "createDate": "2024/09/22 17:37:07",
      "postImageUrl": null
    },
    {
      "postId": 1,
      "title": "너무 덥네요ㅠ",
      "content": "ㅠㅠ",
      "type": "FREEDOM",
      "createDate": "2024/07/17 22:23:58",
      "postImageUrl": "https://storage.googleapis.com/cheong-ha-bucket/postImage/1/0_imageFile2488896157096969460.jpg"
    }
  ],
  "error": null
}
반응형