Programming/Spring

Spring - 클래스 분리, 데이터 저장 (Builder, ObjectMapper, 상황 별Controller 반환 값 등...)

잇(IT) 2023. 8. 29. 13:45
- 데이터 DB에 저장

- 일반적으로 Controller -> (DTO) -> Service -> (DTO) -> Repository -> (Entity, Domain) -> DB 와 같이 클라이언트의 요청이 들어오고 DB에 데이터가 저장되는 흐름이다.

 

- Post.java

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PUBLIC)
public class Post {

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

    private String title;

    @Lob
    private String content;

    @Builder
    public Post(String title, String content) {
        this.title = title;
        this.content = content;
    }
}

- @Lob 어노테이션의 경우 java에서 입력된 String을 DB에 저장할 때 Long text로 변환해서 저장하게 도와주는 어노테이션이다.

- @Builder 어노테이션을 이용하여 Builder 패턴을 사용할 수 있다.

- Builder의 장점

 1. 가독성에 좋다.

 2. 필요한 값만 받을 수 있다.

 3. 객체의 불변성

- 생성자를 이용한 객체 생성 방식은 필드의 수가 많고, 때에 따라 필요한 생성자 파라미터 값이 계속해서 변할 경우 번거롭다. 하지만 builder를 사용하게 되면 1. 원하는 필드만 가져와서 사용할 수 있게 되고, 2. 파라미터의 순서도 신경쓰지 않아도 되며, 3. 어떤 필드에 대한 값인지에 대한 가독성이 좋아진다.

 

- PostRepository.java

public interface PostRepository extends JpaRepository<Post, Long> {
}

- JPA를 이용한 Repository를 작성한 코드이다. 엔티티 클래스의 타입은 Post이고, 기본값의 타입은 Long이다.

 

- PostService.java

@Slf4j
@Service
@RequiredArgsConstructor
public class PostService {

    private final PostRepository postRepository;

    public void write(PostCreate postCreate) {
        Post post = Post.builder()
                .title(postCreate.getTitle())
                .content(postCreate.getContent())
                .build();
        postRepository.save(post);
    }
}

- Service를 통해 Repository의 메서드를 호출해 DB에 데이터를 저장하게 된다.

- DTO에 해당하는 PostCreate 클래스를 통해 데이터를 받아오고, 해당 데이터를 엔티티에 해당하는 Post 객체에 넣어 Repository를 통해 DB에 저장하게 된다.

- Reposiytory를 통해 DB에 저장될 때는 엔티티가 파라미터로 넘어가야되고, Controller, Service, Repositroy에서 데이터를 주고 받을 때는 DTO를 통해 데이터를 주고 받는 것이 좋다. 


* 기존의 코드들 builder 패턴으로 변경

 

-PostCreate.java

@Getter @Setter
@ToString
public class PostCreate {

    @NotBlank(message = "title을 입력해주세요")
    private String title;

    @NotBlank(message = "content를 입력해주세요")
    private String content;

    @Builder
    public PostCreate(String title, String content) {
        this.title = title;
        this.content = content;
    }
}

 

- ErrorResponse.java

@Getter
public class ErrorResponse {

    private final String code;
    private final String message;
    private final Map<String, String> validation = new HashMap<>();

    @Builder
    public ErrorResponse(String code, String message) {
        this.code = code;
        this.message = message;
    }

    public void addValidation(String field, String errorMessage) {
        this.validation.put(field, errorMessage);
    }
}

 

- ExceptionController.java

@Slf4j
@ControllerAdvice
public class ExceptionController {

    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseBody // controller는 rest이기 때문에 body로 넘어가는데
    // controllerAdvice는 viewResolver로 넘어가기 때문에 body 어노테이션을 달아서
    // body로 넘겨준다.
    public ErrorResponse exceptionHandler(MethodArgumentNotValidException e) {
        ErrorResponse response = ErrorResponse.builder()
                .code("400")
                .message("잘못된 요청입니다.").build();

        for (FieldError fieldError : e.getFieldErrors()) {
            response.addValidation(fieldError.getField(), fieldError.getDefaultMessage());
        }
        return response;
    }
}

- Controller에 요청이 들어오게 되면 해당 데이터를 DB에 저장하기 위한 코드로 변경시켜준다.

 

- PostController.java

@RestController
@Slf4j
@RequiredArgsConstructor
public class PostController {

    private final PostService postService;

    @PostMapping("/posts")
    public void post(@RequestBody @Valid PostCreate request) {
        postService.write(request);
    }
}

- Test 코드

- PostControllerTest.java

@SpringBootTest
@AutoConfigureMockMvc

class PostControllerTest {

    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private PostRepository postRepository;

    @Autowired
    private MockMvc mockMvc;

    @BeforeEach
    void clean() {
        postRepository.deleteAll();
    }
    
@Test
    @DisplayName("/posts 요청시 DB에 값이 저장된다.")
    void saveToDb() throws Exception {

        PostCreate request = PostCreate.builder()
                .title("제목입니다.")
                .content("내용입니다.")
                .build();

        String json = objectMapper.writeValueAsString(request);

        //expected
        mockMvc.perform(MockMvcRequestBuilders.post("/posts")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(json)
                )
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andDo(MockMvcResultHandlers.print());

        assertEquals(1L, postRepository.count());

        Post post = postRepository.findAll().get(0);
        assertEquals("제목입니다.", post.getTitle());
        assertEquals("내용입니다.", post.getContent());
    }
}

 

//@WebMvcTest
@SpringBootTest
@AutoConfigureMockMvc
//MockMvc를 주입받기 위해서 사용한다.

- 기존에 @WebMvcTest 어노테이션을 사용했지만 간단한 Controller 요청이 아닌 Service, Repository까지 웹 전체에 대한 테스트를 하기 위해선 @SpringBootTest 어노테이션이 필요하다.

- 또한 @SpringBootTest 어노테이션을 사용하게 되면 MockMvc를 주입 받을 수 없고, @WebMvcTest를 같이 사용할 수 없기 때문에, @AutoConfigureMockMvc 어노테이션을 주입 받음으로서 MockMvc를 주입 받는다.

 

    @Autowired
    private ObjectMapper objectMapper;

- ObjectMapper는 Jackson 라이브러리에서 제공하는 클래스로, 자바 객체와 JSON 데이터 간의 변환을 처리하는 데 사용되는 도구이다.

- writeValue() : 자바 객체 -> JSON

- readValue() : JSON -> 자바 객체

 

		.......
        
        PostCreate request = PostCreate.builder()
                .title("제목입니다.")
                .content("내용입니다.")
                .build();

        String json = objectMapper.writeValueAsString(request);

        //expected
        mockMvc.perform(MockMvcRequestBuilders.post("/posts")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(json)
                )
                
                .......

- builder를 통해 객체를 생성해주고, MockMvcRequestBuilders의 content에는 String 형식만 들어 갈 수 있기 때문에 writeValueAsString을 이용하여 JSON 형식의 String을 반환해준다.


- 상황 별 Controller 반환 값

 

- PostController.java

@RestController
@Slf4j
@RequiredArgsConstructor
public class PostController {

    private final PostService postService;

    @PostMapping("/posts")
    public void post(@RequestBody @Valid PostCreate request) {
        postService.write(request);
    }
}

- Controller를 보게 되면 요청에 대한 응답으로 아무런 데이터를 전송하지 않는 것을 볼 수 있다.


- 상황에 따라 요청에 대해 여러가지 응답을 줄 수 있다.


1. Entity를 response로 응답하기

 

- PostService.java

@Slf4j
@Service
@RequiredArgsConstructor
public class PostService {

    private final PostRepository postRepository;

    public Post write(PostCreate postCreate) {
        Post post = Post.builder()
                .title(postCreate.getTitle())
                .content(postCreate.getContent())
                .build();
        return postRepository.save(post);
    }
}

- postRepository의 save() 메서드는 Post 객체를 저장하고 반환하기 때문에 반환값을 Post로 받을 수 있다.

 

- PostController.java

@RestController
@Slf4j
@RequiredArgsConstructor
public class PostController {

    private final PostService postService;

    @PostMapping("/posts")
    public Post post(@RequestBody @Valid PostCreate request) {
        return postService.write(request);
    }
}

- 요청에 대한 응답으로 Post 엔티티를 반환하게 되는데, @RestController에 의해 엔티티의 데이터가 JSON 형태로 반환된다.


2. primary_id를 response로 응답하기

 

- PostService.java

@Slf4j
@Service
@RequiredArgsConstructor
public class PostService {

    private final PostRepository postRepository;

    public Long write(PostCreate postCreate) {
        Post post = Post.builder()
                .title(postCreate.getTitle())
                .content(postCreate.getContent())
                .build();
        postRepository.save(post);

        return post.getId();
    }
}

 

- PostController.java

@RestController
@Slf4j
@RequiredArgsConstructor
public class PostController {

    private final PostService postService;

    @PostMapping("/posts")
    public Map post(@RequestBody @Valid PostCreate request) {
        Long id = postService.write(request);
        return Map.of("Id", id);
    }
}

- Entity를 응답하는 방식과 같이, Id를 반환 할 수 있다.

728x90