우아한테크코스 지하철 경로 조회 미션을 중에 최단 경로를 조회하기 위해 외부 라이브러리인 JGrapht를 사용하게 되었다. 이때 외부 라이브러리와 도메인을 분리하기 위해 Adapter pattern을 사용했다. 페어의 도움으로 알게된 지식이라 이 지식을 내 것으로 만들기 위해 학습하게 되었다.
Adapter pattern의 사전적 의미는 다음과 같다.
어댑터 패턴은 클래스의 인터페이스를 사용자가 기대하는 다른 인터페이스로 변환하는 패턴으로, 호환성이 없는 인터페이스 때문에 함께 동작할 수 없는 클래스들이 함께 작동하도록 해준다 - 위키백과
내가 Adapter pattern을 사용한 이유는 두 가지이유에서인데
1. 변경에 용이한 코드를 짜기 위해서
2. 외부 라이브러리인 jgrapht를 도메인과 분리하기 위해서
그럼 이제 코드를 보면서 이해를 해보자.
@Service
public class PathService {
private final StationRepository stationRepository;
private final SectionRepository sectionRepository;
public PathService(StationRepository stationRepository,
SectionRepository sectionRepository) {
this.stationRepository = stationRepository;
this.sectionRepository = sectionRepository;
}
@Transactional(readOnly = true)
public PathResponse searchPath(Long source, Long target) {
List<Station> stations = stationRepository.findAll();
List<Section> sections = sectionRepository.findAll();
Graph graph = new JGraphtAdapter(stations, sections);
FareCalculator fareCalculator = new FareCalculator();
List<Station> path = graph.findPath(source, target);
if (path.isEmpty()) {
throw new UnreachablePathException(source, target);
}
int distance = graph.findDistance(source, target);
int fare = fareCalculator.findFare(distance);
return new PathResponse(path, distance, fare);
}
}
위는 직접적으로 최단 경로를 조회하기 위해 JGrapt의 기능을 이용하는 service class이다.
Graph graph = new JGraphtAdapter(stations, sections);
List<Station> path = graph.findPath(source, target);
if (path.isEmpty()) {
throw new UnreachablePathException(source, target);
}
유의해서 볼 부분은 이 부분이다. Graph라는 추상화된 인터페이스를 구현하는 JGraptAdapter 객체를 이용하고 있음을 알 수 있다. 해당 service class는 Graph라는 객체만 알고 있으면 된다. 그 Graph객체가 다른 외부 라이브러리를 사용하고 있다는 것은 알 필요가 없다. 그저 Graph 인터페이스에 정의된 함수를 쓰고 원하는 정보를 얻으면 된다. 만약 Jgrapt 말고 다른 방식의 graph를 사용하더라도 추상화된 Graph를 사용하는 객체의 코드는 바뀌지 않는다. 이를 통해 OCP(개방 폐쇄 원칙)를 지킬 수 있게 된다.
import java.util.List;
import wooteco.subway.domain.Station;
public interface Graph {
List<Station> findPath(Long source, Long target);
int findDistance(Long source, Long target);
}
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.jgrapht.GraphPath;
import org.jgrapht.alg.shortestpath.DijkstraShortestPath;
import org.jgrapht.graph.DefaultWeightedEdge;
import org.jgrapht.graph.WeightedMultigraph;
import wooteco.subway.domain.path.Graph;
import wooteco.subway.domain.Section;
import wooteco.subway.domain.Station;
public class JGraphtAdapter implements Graph {
private final DijkstraShortestPath<Long, DefaultWeightedEdge> shortestPath;
private final Map<Long, Station> vertex;
public JGraphtAdapter(List<Station> vertex, List<Section> edge) {
this.shortestPath = createDijkstraShortestPath(vertex, edge);
this.vertex = createVertexMap(vertex);
}
private DijkstraShortestPath<Long, DefaultWeightedEdge> createDijkstraShortestPath(
List<Station> stations, List<Section> sections) {
WeightedMultigraph<Long, DefaultWeightedEdge> graph
= new WeightedMultigraph<>(DefaultWeightedEdge.class);
for (Station station : stations) {
graph.addVertex(station.getId());
}
for (Section section : sections) {
graph.setEdgeWeight(
graph.addEdge(section.getUpStationId(), section.getDownStationId()),
section.getDistance());
}
return new DijkstraShortestPath<>(graph);
}
private Map<Long, Station> createVertexMap(List<Station> vertex) {
return vertex.stream()
.collect(Collectors.toMap(Station::getId, value -> value));
}
@Override
public List<Station> findPath(Long source, Long target) {
GraphPath<Long, DefaultWeightedEdge> graph = shortestPath.getPath(source, target);
if (graph == null) {
return List.of();
}
return graph.getVertexList().stream()
.map(vertex::get)
.collect(Collectors.toList());
}
@Override
public int findDistance(Long source, Long target) {
return (int) shortestPath.getPathWeight(source, target);
}
}
Adapter pattern을 사용한 이유는 OCP를 지키기위해서 뿐만 아니라 외부 라이브러리인 JGrapht를 도메인과 분리하기 위함도 있다.
현재 Jgrapht 라이브러리를 사용해 Graph 객체를 사용하고 있다. 실제로 쓰고 있는 것은 Jgrapht이지만 도메인에 외부 라이브러리를 사용한 코드가 들어가는 것은 위험하다고 생각한다. 내가 짠 코드가 아닌 외부 라이브러를가 언제 어떻게 바뀔 지도 모르기 때문이다. 따라서 JgraptAdapter를 사용해 도메인과 외부 라이브러리의 관계를 맺는 동시에 의존은 하지 못하게 하는 효과도 낼 수 있다.
외부 라이브러를 직접적으로 사용해 본 적은 처음인데 이를 효과적으로 사용할 수 있는 방법을 알게 돼서 기쁘다. 앞으로 다른 유용한 디자인 패턴들도 학습해 보고 싶어졌다.