일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
- 자료구조와함께배우는알고리즘입문
- 알파회계
- 스프링 시큐리티
- 코드로배우는스프링부트웹프로젝트
- 티스토리 쿠키 삭제
- 스프링부트핵심가이드
- 친절한SQL튜닝
- 자바편
- 이터레이터
- network configuration
- d
- 데비안
- GIT
- 처음 만나는 AI 수학 with Python
- 구멍가게코딩단
- /etc/network/interfaces
- 선형대수
- 자료구조와 함께 배우는 알고리즘 입문
- ㅒ
- 페이징
- baeldung
- 처음 만나는 AI수학 with Python
- resttemplate
- 리눅스
- 서버설정
- 네트워크 설정
- 코드로배우는스프링웹프로젝트
- Kernighan의 C언어 프로그래밍
- 목록처리
- iterator
- Today
- Total
bright jazz music
blog06: 응답클래스 분리(req: PostCreate / res:PostResponse) 본문
blog06: 응답클래스 분리(req: PostCreate / res:PostResponse)
bright jazz music 2022. 12. 30. 11:33요청 클래스와 응답 클래스를 나눠야 할 일이 생겼다.
클라이언트로부터 요청이 있었기 때문이다.
요청: 타이틀을 10자로 끊어서 보내주세요.
이 요청은 사실 클라이언트 측에서 처리하는 것이 적합한 듯하다.
어쨌든 서버단에서 처리해 보기로 한다.
실제로는 PostResponse 클래스 먼저 작성하였지만 보기 쉽도록 아래와 같은 순서로 배치하였다.
- PostController => PostService => PostResponse => PostControllerTest => 결과
---
가장 중요한 PostResponse 클래스부터 먼저 보자. 나중에 똑같은 내용으로 다시 등장하기는 한다.
1. PostResponse 클래스 생성
클라이언트의 요청에 부합하는 응답을 보내기 위해 생성한 클래스이다.
//PostResponse.java
package com.endofma.blog.response;
import lombok.Builder;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
/**
* 서비스 정책에 맞는 응답 클래스 (10글자)
*/
@Getter
//@Builder
public class PostResponse {
private final Long id;
private final String title;
private final String content;
// 10글자 반환
// public String getTitle(){
// return this.title.substring(0, 10);
// //이것보다는 생성자를 만들어서 빌더를 달아주자
// }
@Builder
public PostResponse(Long id, String title, String content){
this.id = id;
// this.title = title.substring(0, 10); //10글자만 세팅된다. 입력값이 10자보다 적으면 오류가 발생한다.
this.title = title.substring(0, Math.min(title.length(), 10)); //Math.min():입력받은 두 인자 중 작은 값 리턴.
this.content = content;
}
}
2. PostController 수정
이전에는 Post 타입의 객체를 클라이언트에게 반환하였다.
그러나 이제 PostResponse 타입의 객체를 반환한다.
//PostController.java
package com.endofma.blog.controller;
//..
@Slf4j
@RestController
@RequiredArgsConstructor
public class PostController {
private final PostService postService;
@PostMapping("/posts")
public void post(@RequestBody @Valid PostCreate request) {
postService.write(request);
// return Map.of();
}
//단건 조회
@GetMapping("/posts/{postId}")
public PostResponse get(@PathVariable Long postId){
//현재 요청 클래스와 응답클래스가 같지 않고 두 개로 나뉘어 있다.
//Request 클래스 (PostCreate)
//Response 클래스 (PostResponse:서비스 정책에 맞는 클래스)
PostResponse response = postService.get(postId);
return response;
}
}
3. PostService 수정
응답 클래스 분리 작업을 여기서 수행한다.
DB에서 가져온 Post 객체의 값을 빼내어 PostResponse 클래스의 생성자에 넣을 파라미터로 사용한다.
생성된 PosResponse 객체를 생성하여 컨트롤러에 반환한다
//PostService.java
//..
@Slf4j
@Service
//@RequiredArgsConstructor
public class PostService {
private final PostRepository postRepository;
public PostService(PostRepository postRepository){
this.postRepository = postRepository;
}
public void write(PostCreate postCreate) {
//PostCreate 일반 클래스 ==> Post 엔티티
Post post = Post.builder()
.title(postCreate.getTitle())
.content(postCreate.getContent())
.build();
postRepository.save(post);
}
//단건 조회
public PostResponse get(Long id) {
// Post post = postRepository.findById(id); optional 데이터로 감싸져서 오기 때문에 아래와 같이 할 수 있다.
// Optional<Post> postOptional = postRepository.findById(id);
// if(postOptional.isPresent()){
// return postOptional.get();
// }
// return null; 그러나 가능한 한 옵셔널은 바로 꺼내서 변환해 주는 게 좋다.
Post post = postRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 글입니다.")); //있으면 post반환 없으면 에러 반환
/*응답 클래스 분리 작업*/
//어떻게 10글자만 반환할까? 서비스 정책에 맞는 응답 클래스를 만들어(변환해)주는 것이다.
PostResponse response = PostResponse.builder()
.id(post.getId())
.title(post.getTitle())
.content(post.getContent())
.build();
//그런데 이 서비스 레이어에서 변환작업을 해주는 것이 맞을까? 기호와 경우에 따라 다르다.
/**
* PostController => WebPostService(지금 이 클래스. 여기서 분류처리를 한다.) => Repository
* PostService(외부통신 하는 경우에만)
*/
return response;
}
}
다시 PostResponse의 내용을 다시 확인하자.(맨 위와 달라진 내용은 없음)
package com.endofma.blog.response;
import lombok.Builder;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
/**
* 서비스 정책에 맞는 응답 클래스 (10글자)
*/
@Getter
//@Builder
public class PostResponse {
private final Long id;
private final String title;
private final String content;
// 10글자 반환
// public String getTitle(){
// return this.title.substring(0, 10);
// //이것보다는 생성자를 만들어서 빌더를 달아주자
// }
@Builder
public PostResponse(Long id, String title, String content){
this.id = id;
// this.title = title.substring(0, 10); //10글자만 세팅된다. 입력값이 10자보다 적으면 오류가 발생한다.
this.title = title.substring(0, Math.min(title.length(), 10)); //Math.min():입력받은 두 인자 중 작은 값 리턴.
this.content = content;
}
}
Math.min()을 사용한 코드는 request로부터 전달받은 값이 10자가 되지 않는 경우를 대비해서 작성한 것이다.
이 메소드를 사용하지 않은 경우를 가정해 보자.
- 클라이언트가 타이틀 필드에 들어갈 값으로 "12345" 를 입력하여 요청했다.
- PostService레이어에서 DB에서 가져온 Post 객체의 타이틀 값을 꺼내서(post.getTitle())을 response의 .title()에 넣고, 그 값을 반환하려고 할 때 문제가 발생한다.
0번째(첫 번째) 문자에서 시작하여 10개의 문자를 잘라내야 하는데 입력값이 10개가 되지 않기 때문이다. 따라서 Math.min()함수를 사용하여 입력값과 10 가운데 작은 값을 기준으로 문자열을 잘라내도록 만든 것이다.
public class Math{
public static min(Long)
}
4. 테스트: test4()
//PostControllerTest.java
//..
//@WebMvcTest //간단한 웹테스트만 가능. 우린 스프링 전반에 걸쳐 여러 가지를 만들었기 때문에 @SpringBootTest가 필요
@AutoConfigureMockMvc //@WebMvcTest가 없어지면 기존 테스트가 안되므로 @WebMvcTest를 구성하는 애노테이션을 떼내어 붙였다.
@SpringBootTest
class PostControllerTest {
@Autowired
private MockMvc mockMvc; ////Could not autowire. No beans of 'MockMvc' type found.
@Autowired
private PostRepository postRepository; //DB저장 테스트를 위해 주입
@BeforeEach
void clean() {
postRepository.deleteAll();
}
@Test
@DisplayName("/posts 요청시 hello world를 출력한다")
void test() throws Exception {
//given
// PostCreate request = new PostCreate("제목입니다.", "내용입니다.");
PostCreate request = PostCreate.builder()
.title("제목입니다.")
.content("내용입니다.")
.build();
//jackson을 사용하여 객체를 Json 형태로 바꿔준다.(json을 처리해 줌)
ObjectMapper objectMapper = new ObjectMapper();
String json = objectMapper.writeValueAsString(request);
// @Autowired private ObjectMapper objectMapper; 이렇게 주입해서도 쓸 수 있다.
System.out.println(json);
//expected
//기본적으로 Content-Type을 application/json으로 보냄. 예전에는 application/x-www-form-urlencoded를 썼다.
mockMvc.perform(post("/posts")
.contentType(APPLICATION_JSON) //기본값이라 주석처리
// .content("{\"title\": \"제목입니다.\", \"content\": \"내용입니다.\"}") //json 형태로 값 넣어주기
//.content에는 byte나 String만 들어가진다. 따라서 jackson으로 Json변환처리를 하지 않은 request를 넣을 수는 없다.
.content(json) //jackson을 사용하여 json이 된 객체. 클래스에 게터가 존재해야 한다.
)
.andExpect(status().isOk()) //http response 가 200인지
.andExpect(content().string("")) // 내용이 hello world인지
.andDo(print()); //요청에 대한 전반적인 요약을 출력해준다.
}
@Test
@DisplayName("/posts 요청시 title 값은 필수다")
void test2() throws Exception {
//given
PostCreate request = PostCreate.builder()
//.title("제목입니다.")
.content("내용입니다.")
.build();
ObjectMapper objectMapper = new ObjectMapper();
String json = objectMapper.writeValueAsString(request);
//expected
mockMvc.perform(post("/posts")
.contentType(APPLICATION_JSON)
.content(json)
)
.andExpect(status().isBadRequest()) //.OK()
.andExpect(jsonPath("$.code").value("400")) //json 검증
.andExpect(jsonPath("$.message").value("잘못된 요청입니다!"))
// .andExpect(jsonPath("$.validation.title").value("타이틀을 입력하세요!"))
.andDo(print());
}
@Test
@DisplayName("/posts 요청시 DB에 값이 저장된다.")
void test3() throws Exception {
//given
PostCreate request = PostCreate.builder()
.title("제목입니다.")
.content("내용입니다.")
.build();
ObjectMapper objectMapper = new ObjectMapper();
String json = objectMapper.writeValueAsString(request);
//when
mockMvc.perform(post("/posts")
.contentType(APPLICATION_JSON)
.content(json)
)
.andExpect(status().isOk())
.andDo(print());
//then
assertEquals(1L, postRepository.count()); //하나의 값이 있을 거라고 예상. 일치
//DB에 잘 들어갔는지 확인
Post post = postRepository.findAll().get(0); //가장 처음 데이터 가져옴
assertEquals("제목입니다.", post.getTitle());
assertEquals("내용입니다.", post.getContent());
}
@Test
@DisplayName("글 1개 조회")
void test4() throws Exception{
//given
Post post = Post.builder()
.title("123456789012345")
.content("bar")
.build();
System.out.println("post.getId!!1="+ post.getId()); //==null
postRepository.save(post);
System.out.println("post.getId!!2="+ post.getId()); //==1
//expected
mockMvc.perform(get("/posts/{postId}", post.getId())
.contentType(APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(post.getId()))
.andExpect(jsonPath("$.title").value("1234567890"))
.andExpect(jsonPath("$.content").value("bar"))
.andDo(print());
}
}
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.7.6)
2022-12-30 11:56:28.107 INFO 32496 --- [ Test worker] c.e.blog.controller.PostControllerTest : Starting PostControllerTest using Java 11.0.12 on DESKTOP-8H1PTVG with PID 32496 (started by markany-hjcha in D:\personal\blog)
2022-12-30 11:56:28.108 INFO 32496 --- [ Test worker] c.e.blog.controller.PostControllerTest : No active profile set, falling back to 1 default profile: "default"
2022-12-30 11:56:28.684 INFO 32496 --- [ Test worker] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode.
2022-12-30 11:56:28.727 INFO 32496 --- [ Test worker] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 36 ms. Found 1 JPA repository interfaces.
2022-12-30 11:56:29.145 INFO 32496 --- [ Test worker] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
2022-12-30 11:56:29.342 INFO 32496 --- [ Test worker] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
2022-12-30 11:56:29.403 INFO 32496 --- [ Test worker] o.hibernate.jpa.internal.util.LogHelper : HHH000204: Processing PersistenceUnitInfo [name: default]
2022-12-30 11:56:29.461 INFO 32496 --- [ Test worker] org.hibernate.Version : HHH000412: Hibernate ORM core version 5.6.14.Final
2022-12-30 11:56:29.639 INFO 32496 --- [ Test worker] o.hibernate.annotations.common.Version : HCANN000001: Hibernate Commons Annotations {5.1.2.Final}
2022-12-30 11:56:29.766 INFO 32496 --- [ Test worker] org.hibernate.dialect.Dialect : HHH000400: Using dialect: org.hibernate.dialect.H2Dialect
2022-12-30 11:56:30.299 INFO 32496 --- [ Test worker] o.h.e.t.j.p.i.JtaPlatformInitiator : HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
2022-12-30 11:56:30.307 INFO 32496 --- [ Test worker] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2022-12-30 11:56:30.758 WARN 32496 --- [ Test worker] JpaBaseConfiguration$JpaWebConfiguration : spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning
2022-12-30 11:56:31.409 INFO 32496 --- [ Test worker] o.s.b.t.m.w.SpringBootMockServletContext : Initializing Spring TestDispatcherServlet ''
2022-12-30 11:56:31.410 INFO 32496 --- [ Test worker] o.s.t.web.servlet.TestDispatcherServlet : Initializing Servlet ''
2022-12-30 11:56:31.411 INFO 32496 --- [ Test worker] o.s.t.web.servlet.TestDispatcherServlet : Completed initialization in 1 ms
2022-12-30 11:56:31.443 INFO 32496 --- [ Test worker] c.e.blog.controller.PostControllerTest : Started PostControllerTest in 3.63 seconds (JVM running for 5.907)
post.getId!!1=null
post.getId!!2=1
MockHttpServletRequest:
HTTP Method = GET
Request URI = /posts/1
Parameters = {}
Headers = [Content-Type:"application/json;charset=UTF-8"]
Body = null
Session Attrs = {}
Handler:
Type = com.endofma.blog.controller.PostController
Method = com.endofma.blog.controller.PostController#get(Long)
Async:
Async started = false
Async result = null
Resolved Exception:
Type = null
ModelAndView:
View name = null
View = null
Model = null
FlashMap:
Attributes = null
MockHttpServletResponse:
Status = 200
Error message = null
Headers = [Content-Type:"application/json"]
Content type = application/json
Body = {"id":1,"title":"1234567890","content":"bar"}
Forwarded URL = null
Redirected URL = null
Cookies = []
2022-12-30 11:56:31.974 INFO 32496 --- [ionShutdownHook] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
2022-12-30 11:56:31.975 INFO 32496 --- [ionShutdownHook] .SchemaDropperImpl$DelayedDropActionImpl : HHH000477: Starting delayed evictData of schema as part of SessionFactory shut-down'
2022-12-30 11:56:31.980 INFO 32496 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown initiated...
2022-12-30 11:56:31.984 INFO 32496 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown completed.
BUILD SUCCESSFUL in 7s
4 actionable tasks: 1 executed, 3 up-to-date
AM 11:56:32: Execution finished ':test --tests "com.endofma.blog.controller.PostControllerTest.test4"'.
5. Gradle 탭에서 전체 테스트 수행하기
새로 만든 기능이 기존 기능과 충돌하는지 테스트 하기 위해 수행한다.
- 인텔리제이 오른쪽 끝 Gradle 탭 => Tasks => verification => test 실행하면 전체 테스트 실행.
성공
'Projects > blog' 카테고리의 다른 글
blog08: 페이징 처리 (0) | 2023.01.04 |
---|---|
blog07: 여러 글(list) 조회 (0) | 2023.01.02 |
blog05: 단건조회 (포스트 조회) (0) | 2022.12.29 |
blog04: 작성글 저장2 - ObjectMapper(jackson)사용, 클래스 분리 (0) | 2022.12.28 |
blog04: 작성글 저장1 - 게시글 저장 구현 (0) | 2022.12.28 |