[Java/Spring] End Point 개발

Subeen·2025년 3월 30일
0

Spring

목록 보기
4/4

Spring에서 엔드 포인트(end point)는 클라이언트의 요청을 처리하는 URL 경로와 그에 해당하는 메소드를 의미한다.

ERD

간단한 게시판의 ERD를 작성해보면 게시판(board), 게시물(post), 댓글(reply)의 테이블이 필요함
각 테이블의 ERD에서 관계를 설정하면
board : post = 1:N 관계 ➡️ 하나의 게시판에는 여러개의 게시물이 포함될 수 있지만 하나의 게시물은 하나의 게시판에 속해야 함
post : reply = 1:N 관계 ➡️ 하나의 게시물에는 여러 개의 댓글이 달릴 수 있지만 하나의 댓글은 하나의 게시물에 속해야 함

Post

클래스간의 관계

  • PostApiController : 사용자의 요청을 받아 PostService에 전달
  • PostService : 요청을 처리하고 필요한 데이터를 PostRepository에서 가져오며 ReplyService를 호출하여 댓글을 가져올 수도 있음
  • PostRepository : 데이터베이스에서 게시글 데이터를 조회/저장/삭제
  • ReplyService : 특정 게시글에 달린 댓글 리스트를 조회

Controller

HTTP 요청을 받아서 적절한 service 메소드를 호출함

@RestController
@RequestMapping("/api/post")
@RequiredArgsConstructor // final 필드(postService)에 대한 생성자를 자동으로 생성
public class PostApiController {

    private final PostService postService;

    @PostMapping("")
    public PostEntity create(
            @Valid
            @RequestBody PostRequest postRequest
    ) {
        return postService.create(postRequest);
    }

    @PostMapping("/view")
    public PostEntity view(
            @Valid
            @RequestBody PostViewRequest postViewRequest
    ) {
        return postService.view(postViewRequest);

    }

    @GetMapping("/all")
    public List<PostEntity> list(

    ) {
        return postService.all();
    }

    @PostMapping("/delete")
    public void delete(
            @Valid
            @RequestBody PostViewRequest postViewRequest
    ) {
        postService.delete(postViewRequest);
    }

}

DTO

요청을 받을 때 사용하는 객체

  • @NotBlank : 필수 입력 값
  • @Size(min = 4, max = 4) : 4자리만 가능하도록 설정
  • @Email : 이메일 형식 검사
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@ToString
@Builder
@JsonNaming(value = PropertyNamingStrategies.SnakeCaseStrategy.class)
public class PostRequest {
    @NotBlank
    private String userName;
    @NotBlank
    @Size(min = 4, max = 4)
    private String password;
    @NotBlank
    @Email
    private String email;
    @NotBlank
    private String title;
    @NotBlank
    private String content;
}

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@ToString
@Builder
@JsonNaming(value = PropertyNamingStrategies.SnakeCaseStrategy.class)
public class PostViewRequest {
    @NotNull
    private Long id;
    @NotBlank
    private String password;
}

Entity

post 테이블과 매핑되는 클래스

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Builder
@ToString
@Entity(name = "post") // post 테이블과 매핑 
public class PostEntity {

    @Id // 기본키
    @GeneratedValue(strategy = GenerationType.IDENTITY) // 자동 증가 
    private Long id;

    private Long boardId;
    private String userName;
    private String password;
    private String email;
    private String status;
    private String title;

    @Column(columnDefinition = "TEXT") // TEXT 타입 
    private String content;
    private LocalDateTime postedAt; // 작성 시간

	// DB에 저장되지 않는 필드로 게시물을 조회할 때만 사용함 
    @Transient
    private List<ReplyEntity> replyList = List.of();
}

Repository

Spring Data JPA를 사용하여 데이터베이스와 직접 소통하는 계층
Spring Boot JPA에서는 메소드 이름을 기반으로 자동으로 SQL 쿼리를 생성함

Optional<PostEntity> findFirstByIdAndStatusOrderByIdDesc(Long id, String status);
// 위의 메소드는 내부적으로 아래와 같은 SQL을 실행함 
SELECT * FROM post WHERE id = ? AND status = ? ORDER BY id DESC LIMIT 1;
// @Query를 사용해서 같은 기능을 구현할 수 있음
@Query("SELECT p FROM post p WHERE p.id = :id AND p.status = :status ORDER BY p.id DESC")
Optional<PostEntity> findLatestPost(@Param("id") Long id, @Param("status") String status);
public interface PostRepository extends JpaRepository<PostEntity, Long> { // 기본적인 CRUD를 제공함 

    //select * from post where id = ? and status = ? order by id desc limit 1
    // id, status가 주어진 값과 일치하는 게시물을 최신순으로 조회 
    Optional<PostEntity> findFirstByIdAndStatusOrderByIdDesc(Long id, String status);

}

Service

실제 비즈니스 로직을 수행하며 데이터베이스에서 데이터를 가져오거나 수정하는 로직을 포함

@Service // 서비스 역할을 하는 클래스 
@RequiredArgsConstructor // postRepository, replyService의 생성자를 자동으로 생성
public class PostService {

    private final PostRepository postRepository;
    private final ReplyService replyService;

    public PostEntity create(
            PostRequest postRequest
    ) {
        var entity = PostEntity.builder()
                .boardId(1L) // 임시 데이터
                .userName(postRequest.getUserName())
                .password(postRequest.getPassword())
                .email(postRequest.getEmail())
                .status("REGISTERED")
                .title(postRequest.getTitle())
                .content(postRequest.getContent())
                .postedAt(LocalDateTime.now())
                .build();

        return postRepository.save(entity);
    }

    public PostEntity view(@Valid PostViewRequest postViewRequest) {
        return postRepository.findFirstByIdAndStatusOrderByIdDesc(postViewRequest.getId(), "REGISTERED")
                .map(it -> {
                    if (!it.getPassword().equals(postViewRequest.getPassword())) { // Corrected check
                        throw new RuntimeException(
                                String.format("패스워드가 맞지 않습니다. 입력값: %s, 실제값: %s",
                                        postViewRequest.getPassword(), it.getPassword())
                        );
                    }
                    // 답변도 같이
                    var replyList = replyService.findAllByPostId(it.getId());
                    it.setReplyList(replyList);
                    return it;
                })
                .orElseThrow(() ->
                        new RuntimeException("해당 게시글이 존재하지 않습니다. ID: " + postViewRequest.getId())
                );
    }


    public List<PostEntity> all() {
        return postRepository.findAll();
    }

    public void delete(@Valid PostViewRequest postViewRequest) {
        postRepository.findById(postViewRequest.getId())
                .map(it -> { // map은 값이 존재하는 경우에만 동작함 
                    if (it.getPassword().equals(postViewRequest.getPassword())) { // 비밀번호가 같지 않으면 예외를 발생 시킴  
                        throw new RuntimeException(
                                String.format("패스워드가 맞지 않습니다. 입력값: %s, 실제값: %s",
                                        postViewRequest.getPassword(), it.getPassword())
                        );
                    }
                    it.setStatus("UNREGISTERED");
                    postRepository.save(it);
                    return it;
                }).orElseThrow( // 값이 없을 경우 예외를 발생 시킴
                        () -> new RuntimeException("해당 게시글이 존재하지 않습니다. : " + postViewRequest.getId())
                );
    }
}
profile
개발 공부 기록 🌱

0개의 댓글