본문 바로가기

기능이해

[JAVA | SPRING | MVC | CRUD] 게시물 작성 CREATE

게시물 작성

 



HTTP메서드 : POST

Endpoint URL : /api/user/posts

작업 내용 : 새 게시물 생성

 

CREATE는 CRUD(Create, Read, Update, Delete)에서 데이터를 생성하는 작업을 의미하며 웹 애플리케이션에서는 주로 사용자가 입력한 데이터를 데이터베이스에 저장하는 기능을 구현한다.

예를 들어, 게시물 작성, 회원가입, 상품 등록 등이 이에 해당 한다.

 

 


요구 사항

 

 

 

/usr/article/doAdd 요청에 대한 컨트롤러,서비스,리포지토리의 기능을 이해해 보자.
    • 요청 및 매개변수 처리 : 사용자에게 게시물의 제목과 내용을 입력. 제목,내용 외에 필요한 필드를 정의
    • 유효성 검사 : 입력값을 검증 null이나 빈문자 검사. 길이제한. 특수문자처리.
    • 데이터 저장 : 엔티티를 정의하고 post테이블 생성. 트랜잭션관리. ID자동증가문제
    • 에러 처리 및 예외 처리 : 유효성검사,DB저장,기능동작 중 실패시 처리.
    • 작성자 정보 저장
 

 


개념 정리

 

 



웹이나 프론트엔드에서 JSON이나 URL로 요청이 들어왔을때 MVC 모델에 따라 처리되는 과정을 정리해보자. 

클라이언트 요청:  프론트엔드에서 JSON이나 URL로 요청이 들어옴.

컨트롤러 계층: 요청을 받아 서비스 계층으로 데이터를 전달. 요청을 처리하기 전 유효성 검사 수행.

  • 클라이언트 요청 처리: 클라이언트(프론트엔드)로부터 들어오는 요청(예: HTTP 요청)을 수신하여 적절한 메서드와 연결한다.
    -URL 매핑: @RequestMapping, @PostMapping, @GetMapping 등의 어노테이션을 사용하여 특정 URL 요청과 컨트롤러 메서드를 매핑한다.
  • 요청 데이터 수집 및 변환: 클라이언트에서 보낸 데이터를 자바 객체로 변환하거나 파라미터를 받아 처리한다.
    -@RequestParam: URL에서 요청 파라미터를 받아오는 데 사용.
    -@RequestBody: JSON과 같은 형식으로 요청된 데이터를 객체로 변환.
  • 유효성 검사: 서비스 계층에 데이터를 전달하기 전에 데이터의 유효성을 확인한다. 유효성 검사는 주로 입력된 값이 null이거나 공백인지 확인하거나, 특정 패턴(이메일 형식, 숫자 범위 등)에 맞는지 확인하는 작업을 포함한다.
    -@Valid와 BindingResult를 통해 데이터 유효성을 검사할 수 있다.
  • 서비스 계층 호출: 유효한 데이터를 서비스 계층으로 전달하여, 비즈니스 로직을 처리할 수 있게 한다.
  • 응답 반환: 요청 처리가 끝나면 클라이언트에게 결과를 반환한다. 주로 JSON 형태로 데이터를 응답하거나, 특정 뷰(HTML, JSP 등)를 렌더링하여 반환할 수 있다.

서비스 계층: 비즈니스 로직을 처리하고, 레포지토리 계층에 데이터베이스 작업을 요청.

  • 비즈니스 로직 처리
    - 비즈니스 로직은 애플리케이션의 핵심 규칙이나 데이터 처리 로직을 말한다. 예를 들어, 게시물을 생성할 때 게시물 제목이 중복되지 않도록 처리하거나, 입력된 데이터를 가공하는 작업이 여기에 해당한다.
    - 컨트롤러에서 받은 데이터를 가공하거나 필요한 추가 작업(예: 날짜 설정, 값 변환 등)을 수행한 후 레포지토리 계층에 전달한다.
  • 데이터 검증 및 가공
    - 데이터가 레포지토리 계층에 전달되기 전에 추가적인 유효성 검사나 데이터 가공 작업을 수행할 수 있다.
    - 서비스 계층에서만 검증해야 하는 규칙이나 조건이 있는 경우 이를 처리한다.
  • 레포지토리 계층과의 상호작용
    - 데이터베이스 작업은 주로 레포지토리 계층에서 수행되지만, 서비스 계층이 그 작업을 요청하는 역할을 한다.
    - 데이터를 저장하거나 조회하는 등의 작업을 레포지토리 계층에 위임한다.
    - 트랜잭션 관리도 서비스 계층에서 수행할 수 있으며, 여러 레포지토리 작업이 함께 수행되는 경우 트랜잭션을 통해 작업을 하나의 단위로 처리한다.
  • 트랜잭션 관리
    - 서비스 계층에서 트랜잭션을 관리하여 여러 데이터베이스 작업이 모두 성공적으로 완료되거나, 실패 시 롤백하도록 한다. 이를 통해 데이터의 일관성을 보장할 수 있다.
    - Spring에서는 @Transactional 어노테이션을 사용하여 트랜잭션을 쉽게 관리할 수 있다.

레포지토리 계층: 데이터베이스에 데이터를 저장.

  • 데이터 저장: 데이터베이스에 새로운 게시물을 삽입하는 작업을 수행한다.
  • MyBatis와 JPA는 모두 데이터베이스와 상호작용하여 데이터를 저장, 조회, 수정, 삭제하는 ORM(Object-Relational Mapping) 기술이지만, 그 동작 방식과 사용법에서 많은 차이가 있다.
 

 


코드 설명

Post 엔티티

게시물 저장에 필요한 데이터를 정의한 엔티티 클래스.

작성자 정보를 저장하기 위해 Account 객체와 연관 관계를 설정한다.

@Entity
public class Post {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @NotBlank(message = "제목은 필수 입력 항목입니다.")
    private String title;

    @NotBlank(message = "내용은 필수 입력 항목입니다.")
    private String content;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "account_id") // 작성자 정보
    private Account author;

    @CreationTimestamp
    private LocalDateTime createdAt;

    @UpdateTimestamp
    private LocalDateTime updatedAt;

    // Getter, Setter, Constructor
}

 

PostRequestDto

클라이언트 요청 데이터를 수집하는 DTO 클래스.

이 클래스는 유효성 검사를 추가하여 잘못된 데이터가 서비스로 전달되지 않도록 한다.

public class PostRequestDto {

    @NotBlank(message = "제목을 입력하세요.")
    private String title;

    @NotBlank(message = "내용을 입력하세요.")
    private String content;

    // Getter, Setter
}

 

PostService

비즈니스 로직을 처리하는 서비스 계층.

로그인된 사용자의 정보를 작성자로 설정하고 DTO데이터를 set으로 엔티티로 변환하여 레파지토리 계층으로 보낸다.

@Service
@RequiredArgsConstructor
public class PostService {

    private final PostRepository postRepository;

    @Transactional
    public Long createPost(PostRequestDto requestDto, Account author) {
        Post post = new Post();
        post.setTitle(requestDto.getTitle());
        post.setContent(requestDto.getContent());
        post.setAuthor(author); // 작성자 설정
        return postRepository.save(post).getId();
    }
}

PostController

클라이언트 요청을 처리하는 컨트롤러 계층.

로그인된 사용자의 정보를 @AuthenticationPrincipal을 통해 가져와 작성자로 설정.

@RestController
@RequestMapping("/api/user/posts")
@RequiredArgsConstructor
public class PostController {

    private final PostService postService;

    @PostMapping
    public ResponseEntity<?> createPost(
            @Valid @RequestBody PostRequestDto requestDto,
            BindingResult bindingResult,
            @AuthenticationPrincipal PrincipalDetails principal) {
        if (bindingResult.hasErrors()) {
            return ResponseEntity.badRequest().body(bindingResult.getFieldError().getDefaultMessage());
        }

        Long postId = postService.createPost(requestDto, principal.getAccount());
        return ResponseEntity.ok(Map.of("status", "success", "postId", postId));
    }
}

 

 

PostRepository 데이터 저장 과정

  1. postRepository.save(post) 호출:
    • JPA는 Post 엔티티의 상태를 확인.
      • @Id 필드 값이 null이면 INSERT 쿼리를 생성.
      • @Id 필드 값이 존재하면 UPDATE 쿼리를 생성.
    • 이미 Post 엔티티 객체의 필드 값들이 설정되어 있으므로 별도의 set 호출이 필요하지않다.
  2. 엔티티 매핑 후 데이터베이스 작업:
    • save() 메서드 내부에서 JPA가 EntityManager.persist() 또는 EntityManager.merge()를 호출하여 데이터를 저장.
    • 엔티티 객체의 상태를 기반으로 적절한 SQL 쿼리를 생성하고 실행.

개발 중 생길 수 있는 기본적 의문들

Q: 로그인된 사용자가 작성자로 어떻게 확인하여 저장 되는건지??

A 로그인된 사용자는 Spring Security를 통해 인증되며, 컨트롤러에서@AuthenticationPrincipal로 가져온 PrincipalDetails 객체에 포함되어 있다. PrincipalDetails는 현재 로그인된 사용자의 계정 정보를 포함하고 있으며 이를 통해 Account 객체를 추출하여 Post 엔티티의 작성자로 설정한다.

 

Q: 게시물 작성시 보통 입력받는 데이터 외에 자동설정 해주는 데이터는 무엇인지?

A

  • 작성 시간 (createdAt) @CreationTimestamp 어노테이션을 사용해 게시물이 생성될 때 자동으로 현재 시간을 설정.
  • 수정 시간 (updatedAt) @UpdateTimestamp 어노테이션을 사용해 게시물이 수정될 때 자동으로 현재 시간을 설정.
  • 작성자 정보 (author) 로그인된 사용자의 정보를 작성자로 자동 설정.
  • 게시물 ID @GeneratedValue 어노테이션을 통해 데이터베이스에서 자동 생성.

Q: 작성 기능동작시 DTO의 사용이유와 변환 시점이 언제인지?

A

DTO 사용 이유:

  • 클라이언트 요청 데이터를 엔티티와 별도로 관리하여 보안을 강화하고, 불필요한 데이터를 차단.
  • DTO 클래스에 유효성 검사 어노테이션을 추가하여 잘못된 데이터를 초기에 걸러냄.
  • 비즈니스 로직과 클라이언트 요청 데이터 처리 로직을 분리하여 유지보수성을 높임.

변환 시점:

클라이언트 요청 데이터를 DTO로 수집한 뒤 서비스 계층으로 전달.

엔티티로의 변환은 서비스 계층에서 이루어짐.

 

Q: 엔티티 변환 로직에 빌더 패턴이 사용되는 이유와 사용방법은?

A: 엔티티 변환은 별도의 변환 메서드 또는 빌더 패턴을 사용하는 것이 유지보수성에 유리합니다.

public Post toEntity(PostRequestDto requestDto, Account author) {
    return Post.builder()
               .title(requestDto.getTitle())
               .content(requestDto.getContent())
               .author(author)
               .build();
}