본문 바로가기

우아한테크코스/미션 정리

Level2. atdd-subway-map 정리

2021-06-11글

중복되는 테스트 메서드

  • 중복되는 테스트 메서드는 추출하자
  • 테스트코드도 유지보수의 대상이며 하나의 문서이기 때문에 가독성을 고려하자
private ExtractableResponse<Response> addSection(String content) {
  return RestAssured.given().log().all()
    .body(content)
    .contentType(MediaType.APPLICATION_JSON_VALUE)
    .when()
    .post("/lines/{id}/sections", 1L)
    .then().log().all()
    .extract();
}

예외 처리는 Service에서

  • DAO에서 예외를 체크해주고 있었음
  • DAO는 말그대로 DB에 어세스하는 역할만 할 뿐
  • 이 결과에 대한 예외는 서비스가 알아서 하도록 하자

@Valid와 @Validated로 DTO 검증

  • @Valid 를 통해 DTO에서 요청에 대한 검증을 하게 해줌
  • Validation을 group화 할 수 있는 점을 이용해서 각기 요청마다 group을 지어 한 DTO를 사용해도 각각 요청에 맞는 검증을 진행

정리

Valid로 잡은 예외 메시지 처리

  • Valid 어노테이션으로 잡은 MethodArgumentNotValidException의 메시지를 제대로 출력하고 있지 않았음
  • 게이츠가 제안해준 방법을 적용하니 BindingResult에 있는 예외 메시지들을 추출하여 던져줄 수 있었음

Service 레이어에서 도메인이 아닌 DTO를 반환하자

  • Controller에서 도메인을 가져다 써서 화면에 필요한 데이터를 표현하기 위해 도메인 자체나 도메인의 getter를 노출시키고 있었음
  • 도메인의 정보를 외부(view)에 노출하는 경우, interface인 DTO를 통해 서비스 레이어에서 반환하도록 리팩토링
  • 참고

정리

RestAssured에 대해

  • 전 단계까지는 MockMVC 등으로 컨트롤러단을 단위테스트로 진행했음
  • E2E 테스트를 한다는 것은 말 그대로 끝부터 끝까지,요청부터 내가 원하는 응답을 테스트 하는 것인데
    이렇게되면 단위 테스트를 하는 이유가 있을까라는 고민을 했음
  • 고민한 결과 "단위 테스트는 구현 단계에서 내가 구현한 레이어(단위)가 정상 동작하는지를 테스트하기 위해,
    이후 모든 단위들이 조합되었을 때 통합테스트를 통해 애플리케이셔이 원하는 기능을 잘 수행하는지 테스트한다"
    이렇게 결론을 내림

구간 추가 로직

  • 구간을 추가할 때 상행역, 하행역이 존재하는지 또 이를 찾고 수정해주기 위한 많은 로직을 작성해야했음
  • 그런데 사실 비슷한 로직인데 상행인지 하행인지 대상만 달랐음
  • 아래와 같이 일단 구현을 목적으로 했을 때는 엄청난 분기가 생겨버림
@Transactional
public void addSection(final Long lineId, final SectionRequest sectionRequest) {
  Line line = lineRepository.findById(lineId);
  Section toAddSection = sectionRequest.toSection(lineId);
  Station targetStation = line.registeredStation(toAddSection);
  if (toAddSection.hasUpStation(targetStation)) {
    Section targetSection = line.findSectionWithUpStation(targetStation);
    checkAddableByDistance(toAddSection, targetSection);
    lineRepository.updateSection(lineId,
                                 new Section(targetSection.id(), lineId, toAddSection.downStation(), targetSection.downStation(), targetSection.subtractDistance(toAddSection)));
  }
  if (toAddSection.hasDownStation(targetStation)) {
    Section targetSection = line.findSectionWithDownStation(targetStation);
    checkAddableByDistance(toAddSection, targetSection);
    lineRepository.updateSection(lineId,
                                 new Section(targetSection.id(), lineId, targetSection.upStation(), toAddSection.upStation(), targetSection.subtractDistance(toAddSection)));
  }
  lineRepository.addSection(lineId, sectionRequest.getUpStationId(), sectionRequest.getDownStationId(), sectionRequest.getDistance());
}
  • 이는 상행구간 찾기, 하행구간 찾기를 전략패턴을 이용해 리팩토 해버렸음
public class LineService {
    private final LineRepository lineRepository;
    private final StationDao stationDao;
    private final List<FindSectionStrategy> findSectionStrategies;
// ...
  • 일단 LineService가 구간을 찾는 전략을 가지고 있음
@Transactional
public void addSection(final Long lineId, final SectionRequest sectionRequest) {
  Line line = lineRepository.findById(lineId);
  Section toAddSection = sectionRequest.toSection(lineId);
  Station targetStation = line.registeredStation(toAddSection);
  Section targetSection = line.findSectionWithStation(targetStation, findSectionStrategies);
  lineRepository.updateSection(lineId, targetSection.updateToAdd(toAddSection));
  lineRepository.addSection(lineId, sectionRequest.getUpStationId(), sectionRequest.getDownStationId(), sectionRequest.getDistance());
}
  • 대상 구간을 찾을 때 전략들을 주입하고
public Section findSectionWithStation(Station targetStation, List<FindSectionStrategy> findSectionStrategies) {
  return findSectionStrategies.stream()
    .map(findSectionStrategy -> findSectionStrategy.findSection(sections, targetStation))
    .filter(Optional::isPresent)
    .map(Optional::get)
    .findAny()
    .orElse(EMPTY);
}
  • Sections에서 다음과 같이 해당 구간을 찾음

PR 링크