의존성
- Lombok
- Web
- HATEOAS
- REST Docs
- JPA
- H2
- PostgresQL
tip
- h2와 PostgresQL 둘다 스카프나 별도 설정없이 추가시 h2 인메모리 먼저 사용됨
도메인 구현 및 유닛 테스트
1 | public class EventTest { |
1 |
|
- 설명
- 빌더 패턴 적용여부 테스트 : @Builder로 해결
- javaBean 스펙 준수 테스트
- 스펙
- 기본 생성자로 인스턴스 생성 가능해야 한다
- getter, setter 사용 가능해야한다
- Builder 추가시 default 생성자가 추가가 안된다
–> @NoArgsConstructor로 해결 - Builder 추가시 모든 아규먼트의 생성자가 public이 아니다(deafult임)
–>다른 곳에서 해당 클래스 인스턴스 생성이 애매해진다
–> @AllArgsConstructor로 해결 - getter,setter -> @Getter , @Setter로 해결
- 스펙
- @EqualsAndHashCode를 id로만 하는 까닭?
- 엔티티 필드에 다른 엔티티가 들어가는 등의 상호 참조시 stack 에러 발생 가능
- @Data를 쓰지 않는 이유
- EqualsAndHashCode를 모든 프로퍼티를 다 사용 하기 때문
1 | ``` |
- 설명
- 특정 객체를 만들고 요청을 주면
그에 따른 응답이 201 create + json body의 id까지 확인하는 테스트
- 특정 객체를 만들고 요청을 주면
1 |
|
- ResponseEntity.created(URI uri)
- 201응답과 함께 주어진 URI를 Location헤더로 가지는 빌더
- URI를 만들기 위해 LinkTo()사용
- linkTo()
- 컨트롤러나 핸들러 메소드에서 URI정보를 읽어올 떄 쓰는 메소드
- Spring HATEOAS 프로젝트에서 제공하는 유틸리티
- @RequestBody로 받는 부분때문에 methodOn에 createEvent(null)로 처리
- 이게 귀찮거나 싫으면 다음과 같이 처리할 수도 있다
1 |
|
설명
- mapaping url value가 클래스에 붙어 있으므로 이제 methodOn 필요없이
바로 클래스에서 이렇게 긁어올 수도 있음 - 테스트 성공
- mapaping url value가 클래스에 붙어 있으므로 이제 methodOn 필요없이
id를 아직 DB에서 가져오지 않음
DB Repository 구현
1 |
|
- 설명
- 엔티티 클래스로 만들기 위해 @Entity를 붙임
- Enumerated로 EnumType을 String으로
- 기본값은 EnumType.ORDINAL으로 순서대로 숫자값 0,1,2로 저장
- 이 경우 나중에 Enum의 순서가 바뀐 경우 기존 데이터가 완전히 꼬일 수가 있음
- EventStatus의 기본값은 EventStatus.DRAFT
1 | import org.springframework.data.jpa.repository.JpaRepository; |
EventController에서 JpaRepository 주입 받는 방법
- Autowired
- 생성자 방법
- 생성자 주입 방법시, 파라미터가 이미 빈으로 등록되어있고, 생성자가 1개라면
@Autowired 생략이 가능1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class EventController {
private final EventRepository eventRepository;
//@Autowired 생략
public EventController(EventRepository eventRepository) {
this.eventRepository = eventRepository;
}
public ResponseEntity createEvent( { Event event)
Event resultEvent = this.eventRepository.save(event);
URI uri = linkTo(EventController.class)
.slash(resultEvent.getId()).toUri();
//event.setId(10);
return ResponseEntity.created(uri)
.body(resultEvent);
}
} - 설명
- 위에서 말했던 생성자 주입시 조건을 만족해서 @Autowired를 생략 가능
- eventRepository 리턴인 resultEvent는 id를 가지고 있다
테스트 실패 이유
- @WebMvcTest는 웹관련 빈들만 등록
- @MockBean을 이용해서 EventRepository를 Mocking할 수 있다
- 기존 빈을 MockBean이 대체하므로 동작 지정(stubbing)을 해주어야한다
1 |
|
- 설명
- mockito.when().thenReturn으로 stubbing을 했다
- id없는 event를 넣을시 id 있는 resultEvent 가 반환되도록
- 문자열보다는 이미 정의된 상수릃 써서 헤더나 미디아타입을 쓰면 오타위험도 적음
입력값 제한
생성되어야하는 id, 그리고 특정값의 계산에 듸한 상태값인 free,offline등의
boolean값이 getter,setter전체 세팅하였으므로 다 값이 입력가능함
1 |
|
DTO를 사용하는 이유
엔티티에 너무 과한 애노테이션
- 현재도 롬복과 persistence 관련 애노테이션
- JSR303 Validation까지 추가된다면 너무 과해짐
DTO에선 @Data 사용 가능
ModelMapper
- 도메인과 DTO끼리의 값 복사를 손쉽게
- 리플렉션을 이용해서 미세하게 성능 저하
- 빈 등록 해서 공용으로 쓰는게 좋을 듯
ModelMapper 빈 등록 1
2
3
4
5//@Configuration있는 곳 App.java등
public ModelMapper modelMapper() {
return new ModelMapper();
}
1 |
|
- 설명
- 빈으로 등록된 modelMapper 받아 옴
- @RequestBody로 EventDto로 받아옴.
- EventDto->Entity로 값 복사. 이제 DTO에 없는 값들은 Entity로 안들어감
1 |
|
설명
- 더이상 Mocking이 불가능
- Mocking Stubbing에 event객체가 들어왔을시 그 event를 return하도록 stubbing
- 실제 Respository에서 save된것은 DTO를 거쳐서 만들어진 새로운 객체이므로
event가 아니라고 판단되서 thenReturn에서 null반환해서 null예외 발생 - 슬라이싱 테스트가 안됨
- @SpringbootTest 적용
- @SpringbootTest 애노테이션 정의를 열어보면 WebEnvironment라는 파라미터가
있는데 기본값이 SpringBootTest.WebEnvironment.MOCK임 - Mocking을 한 DispatcherServlet이 만들어짐 ->계속해서 MockMvc 작성 가능
- @SpringbootTest 애노테이션 정의를 열어보면 WebEnvironment라는 파라미터가
- @AutoConfigureMockMvc
- 더이상 Mocking이 불가능
입력값 이외의 에러 발생
- 위 Test에서는 그냥 들어오는 값을 무시했지만 만약 DTO에서 정의한 이외의 값이
들어왔을때 에러로 처리 하고 싶은 경우
- 위 Test에서는 그냥 들어오는 값을 무시했지만 만약 DTO에서 정의한 이외의 값이
1 | spring.jackson.deserialization.fail-on-unknown-properties=true |
설명
- 모르는 프로퍼티가 오는 경우 실패처리 -> 스프링은 400에러 처리
어느게 낫나
- 받기로 한 값이외에 무시하는 방법
- BadRequest(400)에러 처리하는 방법
상황에 따라서 그때 그때 맞춰서 ?
- 개발시에 느슨하게? –> 클라이언트가 잘못된 값도 받을수 있나 오해의 소지가 있음
- 400으로 하는게 좀더 견고하게
Validation
1 |
|
1 |
|
값 이상 처리
- 타입은 맞으나 값이 이상한 경우
- 예를들어 시작가(basePrice)가 상한가(maxPrice)보다 큰 경우
- 이벤트 시작(beginEventDateTime)이 종료(endEventDateTime)보다 뒤
- 이런 경우 어떻게 검증? 애노테이션으로는 불가능
–> 따로 커스텀 Validator가 필요
1 |
|
- 설명
- Validator 를 상속받지 않은 Validator
- 각 조건시 errors에 에러를 담았다
- 이 빈을 컨트롤러에서 주입받은 후 validate후 errors안에 error가 있으면
BadRequest로 리턴한다
BadRequest 응답 본문 생성
- 현재 BadRequest이지만 응답본문엔 비어있음
- 응답본문에 에러를 실어서 보낼 수 있는가?
- Event객체처럼 ResponseEntity.의 body에 실어서? 불가능
- 현재 스프링 부트가 기본적으로 등록하는 jackson과 objectMapper 이용 중
- Event는 자바 빈 스펙을 준수했기 때문에 BeanSerializer로 serialization 가능
- Errors는 자바 빈 스펙을 준수하지 않음 -> 커스텀 Serializer 필요
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class ErrorsSerializer extends JsonSerializer<Errors> {
public void serialize(Errors errors, JsonGenerator gen, SerializerProvider serializerProvider) throws IOException {
gen.writeStartArray();
//field 에러 처리
errors.getFieldErrors().forEach(e -> {
try {
gen.writeStartObject();
gen.writeStringField("field",e.getField());
gen.writeStringField("objectName",e.getObjectName());
gen.writeStringField("code",e.getCode());
gen.writeStringField("defaultMessage",e.getDefaultMessage());
//reject된 value가 있을수도 없을수도 있으면 받아온다
Object rejectedValue = e.getRejectedValue();
if(rejectedValue!=null){
gen.writeStringField("rejectedValue", rejectedValue.toString());
}
gen.writeEndObject();
} catch (IOException e1) {
e1.printStackTrace();
}
});
//gobal error
//테스트에서 에러가 날 수 있으므로 일단 주석 처리
errors.getGlobalErrors().forEach(e -> {
try {
gen.writeStartObject();
gen.writeStringField("objectName",e.getObjectName());
gen.writeStringField("code",e.getCode());
gen.writeStringField("defaultMessage",e.getDefaultMessage());
gen.writeEndObject();
} catch (IOException e1) {
e1.printStackTrace();
}
});
gen.writeEndArray();
}
}
비지니스 로직
Location 값이 있으면 offline 상태는 true여야함
basePrice와 maxPrice가 둘다 0이면 상태 free 상태는 true여야함
수정
- Event.update()에서 위의 로직 반영
- Controller에서 ModelMapper에서 DTO에서 값을 엔티티 매핑 후 update()실행
- 로직이 많다면 update()와 JPA의 save까지 서비스 레이어 만들어서 이전
String isBlank()
- 자바 11부터 추가
- 이전 버전에선 trim 이후 isEmpty()를 사용
- 스페이스 외의 공백 문자열까지 확인 해줌
매개변수를 이용한 테스트
테스트 리팩토링 필요성
- 같은 테스트 코드에 특정 값에 따라 기대하는 true,false만 다를 때 꼼꼼한 테스트를
위해 많은 코드들이 반복됨 - JUnitParams - 매개변수만 교체 가능
- 원래 JUnit은 메소드에 파라미터를 가질 수가 없음
- 사용시 파라미터 사용해서 가능
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class EventTest {
public void testFree(int basePrice, int maxPrice, boolean Expected ) {
//Given
Event event = Event.builder()
.basePrice(basePrice)
.maxPrice(maxPrice)
.build();
//When
event.update();
// Then
assertThat(event.isFree()).isEqualTo(Expected);
}
}
- 설명
- Runner로 JUnitParamsRunner를 사용
- @Parameters 밑의 값이 메소드의 매개변수와 매칭
- 3개의 반복될 코드를 훌륭하게 제거
타입 세이프가능?: 현재 파라미터가 문자열이므로 타입 세이프하지 않음
1 |
|
- 설명
- @Parameters에 method 이름 설정 가능
- 컨벤션:
parametersFor
접두어가 컨벤션으로 컨벤션뒤 메소드명과 자동매칭