각종 후기/우아한테크코스

[우아한 테크코스 3기] LEVEL 2 회고 (92일차)

제이온 (Jayon) 2021. 5. 4.

안녕하세요? 제이온입니다.

 

오늘은 페어 프로그래밍하느라 바빴어서 핵심만 간단히 회고하려고 합니다.

 

 

지하철 노선도 관리 미션 소개

이번 미션은 지하철 노선도 관리입니다. 총 3단계 + 선택 미션이 있는데, 1, 2단계까지 페어 프로그래밍을 합니다. 1단계 요구 사항은 다음과 같습니다.

 

 

 

 

기본적으로 전체적인 틀은 잡혀 있고, API 명세에 따라 개발하는 것이 목표입니다. 저는 처음부터 도메인부터 설계할 줄 알았는데, 그건 아니고 스프링에 초점이 맞춰져 있었습니다. 다만, 한 가지 어려운 점이 있었죠.

 

 

 

 

바로 스프링 빈 등록을 하면 안 된다는 것입니다. @Controller 정도까지만 허용하고 그 외에는 객체를 직접 생성하여 의존 관계를 맺어 주어야 합니다. 또한, DB는 H2나 MySQL 같은 것이 아니라 메모리 DB를 사용합니다. 메모리 DB 말은 어려워보이는데, 그냥 static 타입의 자료 구조를 만들고 거기에 런타임동안 저장하는 것입니다.

 

 

public class StationDao {
    private static Long seq = 0L;
    private static List<Station> stations = new ArrayList<>();

    public static Station save(Station station) {
        Station persistStation = createNewObject(station);
        stations.add(persistStation);
        return persistStation;
    }

    public static List<Station> findAll() {
        return stations;
    }

    public static void deleteById(Long id) {
        stations.removeIf(it -> it.getId().equals(id));
    }

    public static Optional<Station> findById(Long id) {
        return stations.stream()
                .filter(it -> it.getId() == id)
                .findFirst();
    }

    private static Station createNewObject(Station station) {
        Field field = ReflectionUtils.findField(Station.class, "id");
        field.setAccessible(true);
        ReflectionUtils.setField(field, station, ++seq);
        return station;
    }
}

 

 

위와 같이 static으로 id와 stations를 관리합니다. static이란 특성때문에 테스트를 할 때, @BeforeEach를 통해 테스트 전에 매번 DB를 초기화해야 독립적인 테스트를 만들 수 있습니다.

 

 

페어 프로그래밍

이번 미션의 페어는 '중간곰'이었습니다. 중간곰에 대한 흥미로운 이야기도 참 많지만, 이 부분은 제가 시간이 좀 남을 때 하도록 하고, 같이 고민하거나 배운 점을 중심으로 기록하려고 합니다.

 


(1) 스프링 빈에 등록하지 않고, 의존 관계를 맺어주는 방법

가장 먼저 Station 쪽 코드를 익히고 나서, 프로그래밍 제약 사항을 어떻게 지킬 지 고민이 생겼습니다. 저는 그전까지 스프링 빈을 통해 직접적으로 의존 관계를 맺어 주는 방법만 수행해 보았고, StationController의 필드로 StationService를 만들어서 초기화해 두면 되지 않을까라는 생각을 하였습니다.

 

중간곰도 결론적으로는 저와 비슷하나, 한 가지 의문을 제기했습니다. 만약, StationService를 StationController말고 다른 곳에도 쓰고 싶다면, 새롭게 StationService를 할당해야하므로 이 두 가지 StationService는 서로 다른 서비스가 아니냐는 것이죠. 이를 고민하던 중, 검프가 Service 객체를 싱글톤으로 만들면 되지 않겠냐는 대답을 해 주었습니다. 그리고 저와 중간곰은 그것이 합리적이겠다고 판단하여 실행에 옮겼습니다.

 

 

@RestController
public class StationController {

    private final StationService stationService = StationService.getInstance();

    // ...
}

 

 

위와 같이 필드에 바로 생성과 동시에 초기화를 하고, API 로직에서 사용하면 됩니다.

 

 

(2) 정적 팩토리 메소드의 사용

서비스 객체에서 Dto 객체를 반환하려고 할 때, 한 가지 불편한 점이 있었습니다. LineReponse라는 Dto가 있었는데, 코드는 다음과 같습니다.

 

 

public class LineResponse {
    private Long id;
    private String name;
    private String color;
    private List<StationResponse> stations;

    public LineResponse() {
    }

    public LineResponse(Long id, String name, String color, List<StationResponse> stations) {
        this.id = id;
        this.name = name;
        this.color = color;
        this.stations = stations;
    }
	
    // ...
}

 

 

그리고 서비스에서는 다음과 같이 노선을 생성하고 LineResponse 객체를 반환합니다.

 

 

    public LineResponse createLine(final LineRequest lineRequest) {
        final String name = lineRequest.getName();
        final String color = lineRequest.getColor();

        validateDuplicatedLineName(name);
        final Line line = LineDao.save(new Line(name, color));
        return new LineResponse(line.getId(), lineRequest.getName(), lineRequest.getColor(), // List<Station>을 어떻게 만들지?)
    }

 

 

이때, Line 객체의 코드는 아래와 같습니다.

 

 

public class Line {
    private Long id;
    private String name;
    private String color;
    private List<Station> stations;

    public Line(String name, String color) {
        this(null, name, color, new ArrayList<>());
    }

    public Line(String name, String color, List<Station> stations) {
        this(null, name, color, stations);
    }

    public Line(Long id, String name, String color, List<Station> stations) {
        this.id = id;
        this.name = name;
        this.color = color;
        this.stations = stations;
    }

   // ...
}

 

 

주석을 보면 아시다시피, Line에는 List<StationResponse>를 반환하는 메소드는 없으므로 따로 이 형태로 만들어 주는 메소드가 필요합니다. 그렇다고 LineSerive 내의 createLine()이 이 역할을 수행하는 것은 너무 많은 역할을 가진다고 생각했습니다.

 

이때, 중간곰은 LineResponse에 정적 팩토리 메소드를 만들면 어떻겠냐고 제안했습니다. 아래와 같은 형태로 LineResponse에 정적 팩토리 메소드를 만들면 코드를 한결 줄여줄 수 있습니다.

 

 

public class LineResponse {
    private Long id;
    private String name;
    private String color;
    private List<StationResponse> stations;

    public LineResponse() {
    }

    public LineResponse(Long id, String name, String color, List<StationResponse> stations) {
        this.id = id;
        this.name = name;
        this.color = color;
        this.stations = stations;
    }

    public static LineResponse from(Line line) {
        final Long id = line.getId();
        final String name = line.getName();
        final String color = line.getColor();
        final List<StationResponse> stations = line.getStations().stream()
            .map(StationResponse::from)
            .collect(Collectors.toList());
        return new LineResponse(id, name, color, stations);
    }

    // ...
}

 

 

위와 같이 다음에도 Dto 객체를 서비스에서 반환할 때 코드가 길다면 정적 팩토리 메소드를 이용하는 것이 좋을 것 같습니다.

 

 

(3) 그 외의 학습해야할 점

이번 미션에서 생소한 용어가 있었습니다. 바로, E2E 테스트라는 것으로 브라우저 위에서 제대로 작동하는지 테스트하는 기법이라고 합니다. 저와 중간곰은 이것이 아예 처음이라서, 우선 오늘은 기능의 맛만 보기로 했습니다.

 

 

    @DisplayName("지하철역을 생성한다.")
    @Test
    void createStation() {
        // given
        Map<String, String> params = new HashMap<>();
        params.put("name", "강남역");

        // when
        ExtractableResponse<Response> response = RestAssured.given().log().all()
                .body(params)
                .contentType(MediaType.APPLICATION_JSON_VALUE)
                .when()
                .post("/stations")
                .then().log().all()
                .extract();

        // then
        assertThat(response.statusCode()).isEqualTo(HttpStatus.CREATED.value());
        assertThat(response.header("Location")).isNotBlank();
    }

 

 

위에서 중요한 것은 when 부분인 듯합니다. ExtractableResponse, RestAssured가 뭔진 모르겠지만, 뭔가 통신 이후 응답을 받는 것으로 보입니다. 그리고 그 응답의 상태 코드와 헤더를 확인하는 테스트 코드가 있는 것을 확인했습니다.

 

 

    @DisplayName("노선을 생성한다.")
    @Test
    void createLine() {
        // given
        Map<String, Object> params = new HashMap<>();
        params.put("name", "2호선");
        params.put("color", "grey darken-1");
        params.put("upStationId", 1);
        params.put("downStationId", 2);
        params.put("distance", 2);
        params.put("extraFare", 500);

        // when
        ExtractableResponse<Response> response = RestAssured.given().log().all()
            .body(params)
            .contentType(MediaType.APPLICATION_JSON_VALUE)
            .when()
            .post("/lines")
            .then().log().all()
            .extract();

        // then
        assertThat(response.statusCode()).isEqualTo(HttpStatus.CREATED.value());
        assertThat(response.header("Location")).isNotBlank();
        assertThat(response.body().as(LineResponse.class).getId()).isEqualTo(1L);
        assertThat(response.body().as(LineResponse.class).getName()).isEqualTo("2호선");
        assertThat(response.body().as(LineResponse.class).getColor()).isEqualTo("grey darken-1");
    }

 

 

포츈의 도움을 받아서 비슷하게 작성해 보았습니다. when까지는 유사하나, then 부분에서 바디를 검증하는 방식을 새롭게 알게 되었습니다. as() 통해서 어떤 클래스인지 체크하고, isEqualTo()를 통해 원하는 값이 맞는지 확인하는 것입니다. 내일은 E2E를 어느 정도 학습을 제대로 한 후에 다시 적용하려고 합니다.

 

 

다음으로, CQRS를 고려하는 것입니다. CQRS는 시스템의 상태를 변경하는 작업과 시스템의 상태를 반환하는 작업의 책임을 분리하는 것으로, 이를 어긴 예시를 보겠습니다.

 

 

    public static Station save(final Station station) {
        final Station persistStation = createNewObject(station);
        stations.add(persistStation);
        return persistStation;
    }

 

 

위는 DAO 단의 코드인데, save를 할 때 Station 객체가 반환되는 것을 알 수 있습니다. 저는 이것이 해당 메소드의 반환값을 통해 코드를 줄이는 효율적인 개발이 가능하다고 생각했었습니다. 하지만, 이에 대해 중간곰은 CQRS를 이야기하면서 save를 위한 쿼리문이 void가 아니고 특정 객체를 굳이 반환할 필요가 있겠냐고 이야기해 주었습니다.

 

이 부분에 대해서는 제가 아예 처음이다보니까 내일 1, 2단계 미션을 구현한 후, 따로 토론해 보기로 하였습니다.

 

 

정리

이번에도 훌륭한 페어와 함께 미션을 진행하게 되어서 참 다행이라고 생각합니다. 첫 날부터 배울 점이 굉장히 많았고, 아이스 브레이킹 때나 밥을 같이 먹으면서 해 준 이야기를 통해 상당히 노력하고 끈기가 있는 크루라는 점을 알게 되었습니다. 이번 미션동안 페어의 장점을 잘 배우는 기회가 되었으면 합니다.

댓글

추천 글