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

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

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

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

 

오늘은 시드와 만나서 지하철 경로 조회 미션 1, 2, 3단계를 모두 수행하였습니다.

 

 

지하철 경로 조회 / 로그인 미션 소개

어제는 1, 2단계를 소개하였는데, 오늘은 3단계도 진행하였으므로 3단계 미션도 간략하게 소개하겠습니다.

 

 

 

 

구현 기능은 딱 2가지입니다. 최단 경로와 최소 거리를 계산하는 로직을 만들고, 경로를 조회하는 api를 만드는 것이죠. 물론, 프론트의 api와 관련된 코드도 작성해야 합니다.

 

우리는 하나의 정점에서 여러 개의 정점 중 최단 경로를 택하는 것이므로 다익스트라 알고리즘을 사용해야 합니다. 다만, 다익스트라 알고리즘을 제공하는 라이브러리가 강의 자료에 존재했습니다. jgrapht라는 라이브러리인데, 자세한 설명은 이곳을 참고하시길 바랍니다. 아래는 해당 라이브러리를 사용하여 최단 경로를 구하는 예제 코드입니다.

 

 

@Test
public void getDijkstraShortestPath() {
    WeightedMultigraph<String, DefaultWeightedEdge> graph
            = new WeightedMultigraph(DefaultWeightedEdge.class);
    graph.addVertex("v1");
    graph.addVertex("v2");
    graph.addVertex("v3");
    graph.setEdgeWeight(graph.addEdge("v1", "v2"), 2);
    graph.setEdgeWeight(graph.addEdge("v2", "v3"), 2);
    graph.setEdgeWeight(graph.addEdge("v1", "v3"), 100);

    DijkstraShortestPath dijkstraShortestPath
            = new DijkstraShortestPath(graph);
    List<String> shortestPath 
            = dijkstraShortestPath.getPath("v3", "v1").getVertexList();

    assertThat(shortestPath.size()).isEqualTo(3);
}

 

 

정점을 addVertex()로 정해주고, setEdgeWeight()와 addEdge()를 통해 간선과 가중치를 입력해 주면 끝입니다. 결국, 특정한 알고리즘 없이 정점과 간선, 가중치를 구현한 그래프만 만들면 됩니다.

 

 

페어 프로그래밍

어제 인증과 인가와 관련된 프로덕션 코드를 작성해 두었기 때문에 오늘은 TODO에 있는 프론트 코드를 작성하는 데 대부분의 시간을 보냈습니다. 이 부분은 제가 예전에 투두리스트 2단계 미션에서 배웠던 fetch를 이용한 api 호출이므로 어렵지 않았습니다. 다만, 구현된 vue 코드에서 일부분은 코드를 해독하여 추가로 작성해야하는 부분은 있었으나 시드가 옆에서 잘 캐치해줘서 성공적으로 해낼 수 있었습니다. 그래서 1, 2단계를 후딱 구현해서 pr을 날렸고 나머지는 3단계를 수행하였습니다. 다만, 1, 2단계를 구현하면서 문제점도 몇가지 있었습니다.

 

첫 번째는 새로고침할 때마다 로그아웃된다는 것입니다. 아마, 쿠키에 토큰을 저장하지 않아서 그런 것 같은데 내일 시드와 함께 이야기해 보려고 합니다.

 

두 번째는 회원 정보를 수정한 이후 로그아웃하지 않으면 수정 사항이 반영되지 않는 것입니다. 이것은 위의 문제를 해결하여 새로 고침하면 괜찮지 않을까 생각은 해 봤지만, 바로 수정 사항이 반영되게끔 만들려면 프론트 코드도 살펴봐야겠습니다.

 

마지막으로, 해당 미션에는 DataLoader라는 것이 존재하여 서버를 구동할 때 몇 가지 테스트용 지하철역, 노선, 구간을 만들어 줍니다. 하지만, 이것들을 조작하려고 할 때 몇몇 에러가 발생했습니다. 지하철역을 지울 때나 특정 노선에 새로운 구간을 추가할 때 에러가 발생했는데, DB 테이블의 참조 때문에 그런 것이 아닌가 추측은 하고 있습니다. 이 부분은 리뷰어님께도 질문하였습니다.

 

 

이후에는 경로 조회 코드를 작성하였습니다. 핵심은 그래프를 만드는 것이었습니다.

 

 

    private WeightedMultigraph<Station, DefaultWeightedEdge> drawGraph() {
        final WeightedMultigraph<Station, DefaultWeightedEdge> graph = new WeightedMultigraph<>(DefaultWeightedEdge.class);
        final List<Station> stations = addVertices(graph);
        setEdgeWeights(graph, stations);
        return graph;
    }

    private List<Station> addVertices(WeightedMultigraph<Station, DefaultWeightedEdge> graph) {
        final List<Station> stations = stationDao.findAll();
        for (final Station station : stations) {
            graph.addVertex(station);
        }
        return stations;
    }

    private void setEdgeWeights(WeightedMultigraph<Station, DefaultWeightedEdge> graph, List<Station> stations) {
        final List<Section> sections = sectionDao.findAll(stations);
        for (final Section section : sections) {
            graph.setEdgeWeight(graph.addEdge(section.getUpStation(), section.getDownStation()), section.getDistance());
        }
    }

 

 

이렇게 모든 지하철역을 불러와서 지하철 리스트를 만든 뒤, 정점에 등록합니다. 이후에는 모든 구간을 불러와서 구간 리스트를 만든 뒤, 간선과 가중치를 설정합니다. 그리고 나서 라이브러리에 존재하는 다익스트라 메소드를 호출합니다.

 

 

    public PathResponse shortenPath(final Long sourceId, final Long targetId) {
        final WeightedMultigraph<Station, DefaultWeightedEdge> graph = drawGraph();
        final DijkstraShortestPath<Station, DefaultWeightedEdge> dijkstraShortestPath = new DijkstraShortestPath<>(graph);

        final Station source = stationDao.findById(sourceId);
        final Station target = stationDao.findById(targetId);
        final List<StationResponse> shortestPath = makeShortestPath(dijkstraShortestPath, source, target);
        int distance = (int) dijkstraShortestPath.getPathWeight(source, target);
        return new PathResponse(shortestPath, distance);
    }

    private List<StationResponse> makeShortestPath(DijkstraShortestPath<Station, DefaultWeightedEdge> dijkstraShortestPath,
        Station source, Station target) {
        return dijkstraShortestPath.getPath(source, target).getVertexList()
            .stream()
            .map(StationResponse::of)
            .collect(Collectors.toList());
    }

 

 

큰 틀로 보면 전혀 어렵지 않습니다. 다만, 이미 존재하던 SectionDao에서 findAll()을 하기 위해서 애를 먹었습니다. 이것은 Section 테이블 구조와 실제 Section 도메인 객체의 구조가 달랐기 때문입니다.

 

 

create table if not exists SECTION
(
    id bigint auto_increment not null,
    line_id bigint not null,
    up_station_id bigint not null,
    down_station_id bigint not null,
    distance int not null,
    primary key(id),
    foreign key (up_station_id) references station(id),
    foreign key (down_station_id) references station(id)
);

 

 

위는 Section의 테이블 구조입니다. id, line_id, up_station_id, down_station_id, distance가 있음을 알 수 있습니다.

 

 

public class Section {
    private Long id;
    private Station upStation;
    private Station downStation;
    private int distance;

    public Section() {
    }

    public Section(Long id, Station upStation, Station downStation, int distance) {
        this.id = id;
        this.upStation = upStation;
        this.downStation = downStation;
        this.distance = distance;
    }

    public Section(Station upStation, Station downStation, int distance) {
        this.upStation = upStation;
        this.downStation = downStation;
        this.distance = distance;
    }

    // getter문
}

 

 

하지만, Section 도메인 객체는 상행역과 하행역의 id를 갖고 있는 것이 아니라 Station 자체를 필드로 갖고 있습니다. 따라서, 두 개의 구조 차이로 인하여 mapper를 만들기 쉽지 않았습니다. 다행히도, 이 부분은 시드가 해결해 주었습니다.

 

 

    private static RowMapper<Section> rowMapper(List<Station> stations) {
        return (rs, rowNum) -> {
            final long id = rs.getLong("id");
            final long upStationId = rs.getLong("up_station_id");
            final long downStationId = rs.getLong("down_station_id");
            final int distance = rs.getInt("distance");
            final Station upStation =
                stations.stream().filter(station -> station.getId().equals(upStationId))
                    .findAny().orElseThrow(() -> new IllegalArgumentException("없음"));
            final Station downStation =
                stations.stream().filter(station -> station.getId().equals(downStationId))
                    .findAny().orElseThrow(() -> new IllegalArgumentException("없음"));
            return new Section(id, upStation, downStation, distance);
        };
    }

 

 

rowMapper()를 변수가 아닌 메소드로 설정하고, 인자로 지하철 리스트를 받습니다. 그리고 평소에 변수 형태의 mapper와는 다르게 Section 도메인의 스펙에 맞게 데이터를 일부 조작을 해 준뒤 Section을 반환합니다.

 

 

    private final RowMapper<Station> rowMapper = (rs, rowNum) ->
            new Station(
                    rs.getLong("id"),
                    rs.getString("name")
            );

 

 

저는 그전까지 위와 같은 변수 형태의 mapper만 알고 있었는데, 이렇게 메소드로 mapper를 익히는 방법을 처음 알게 되었습니다. 앞으로도 DB와 도메인의 스펙이 다른 경우 메소드 형태의 mapper를 사용해 봐야겠습니다.

 

이 DB 부분만 해결하면 사실상 3단계는 끝이 납니다.

 

 

정리

내일까지 3단계 끝내면 좋겠다고 생각했는데, 시드와 합이 잘맞아서 오늘 오후 7시도 안 되어서 미션을 끝낼 수 있었습니다. 덕분에 저녁에는 시드 집에 가서 간단히 술도 먹고 즐거운 하루를 보낸 것 같습니다. 다만, 인증과 인가를 통해 로그인하고 멤버를 관리하는 과정이 아직도 어설퍼서 시드와 코드를 보면서 이것저것 물어봐야겠습니다. 또한, 앞서 이야기한 문제점을 해결해 보았으면 좋겠습니다.

댓글

추천 글