Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

2단계 - 경로 조회 기능 #10

Open
wants to merge 9 commits into
base: gmelon
Choose a base branch
from
3 changes: 2 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ repositories {
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
implementation 'org.jgrapht:jgrapht-core:1.5.2'

implementation 'net.rakugakibox.spring.boot:logback-access-spring-boot-starter:2.7.1'

Expand All @@ -24,4 +25,4 @@ dependencies {

test {
useJUnitPlatform()
}
}
17 changes: 17 additions & 0 deletions src/main/java/subway/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
## 2단계 리팩토링 요구사항
- [x] SectionService 부정형 조건문 !(a || b) 형태로 바꾸기
- [x] Station에서 id를 꺼내 외부에서 비교하지 말고 직접 물어보기
- SectionService 개선
- [x] Section 도메인 생성자 체이닝 적용
- [x] test에서 필드끼리 비교할 때 usingRecursiveComparison() 사용해보기
- [x] LineDaoTest - hasSize(0) 을 isEmpty() 로 개선

## 2단계 기능 요구사항
- [x] 프로덕션, 테스트용 profile 다르게 설정하기
- 프로덕션 DB는 로컬에 저장, 테스트 DB는 인메모리로 동작하도록 설정
- [x] 경로 조회 API 구현
- 1. 최단 거리 경로, 2. 거리 정보, 3. 해당 거리에 대한 요금 응답
- 다른 노선으로의 환승도 고려해야 함
- Controller & Test 수정
- Section, Station Controller에서 PathService 로직 호출하도록 하기
- PathIntegrationTest 는 기존 데이터 없이 Section/Station Controller 호출만으로 데이터 구성해서 테스트하기
25 changes: 17 additions & 8 deletions src/main/java/subway/application/SectionService.java
Original file line number Diff line number Diff line change
Expand Up @@ -59,17 +59,16 @@ private boolean isLineEmpty(Long lineId) {
}

private boolean isUpStationDoesNotEqualToLastDownStation(List<Section> sections, List<Station> distinctStations, Long upStationId) {
return !sections.isEmpty() && !getLastStationId(distinctStations).equals(upStationId);
return !(sections.isEmpty() || getLastStation(distinctStations).isIdEquals(upStationId));
}

private Long getLastStationId(List<Station> stations) {
return stations.get(stations.size() - 1).getId();
private Station getLastStation(List<Station> stations) {
return stations.get(stations.size() - 1);
}

private boolean isStationAlreadyExists(List<Station> distinctStations, Long downStationId) {
return distinctStations.stream()
.map(Station::getId)
.anyMatch(id -> id.equals(downStationId));
.anyMatch(station -> station.isIdEquals(downStationId));
}

public void deleteSectionById(Long lineId, Long sectionId) {
Expand All @@ -86,10 +85,20 @@ private void validateDeleteRequest(Long lineId, Long sectionId) {
}

private boolean isNotLastSection(List<Section> sections, Long sectionId) {
return !sections.isEmpty() && !getLastSectionId(sections).equals(sectionId);
return !(sections.isEmpty() || getLastSection(sections).isIdEquals(sectionId));
}

private Long getLastSectionId(List<Section> sections) {
return sections.get(sections.size() - 1).getId();
private Section getLastSection(List<Section> sections) {
return sections.get(sections.size() - 1);
}

public SectionResponse findSectionResponseById(Long id) {
Section persistSection = findSectionById(id);
return SectionResponse.of(persistSection);
}

private Section findSectionById(Long id) {
return sectionDao.findById(id)
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 Section 입니다. id = " + id));
}
}
39 changes: 39 additions & 0 deletions src/main/java/subway/application/path/PathPrice.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package subway.application.path;

import java.util.Arrays;

public enum PathPrice {

DEFAULT(0, 10, 1_250, 0),
FIRST_STEP(10, 50, 1_250, 5),
SECOND_STEP(50, Integer.MAX_VALUE, 2_050, 8);

private final int distanceUnderLimit;
private final int distanceUpperLimit;
private final int defaultPrice;
private final int billingUnit;

PathPrice(int distanceUnderLimit, int distanceUpperLimit, int defaultPrice, int billingUnit) {
this.distanceUnderLimit = distanceUnderLimit;
this.distanceUpperLimit = distanceUpperLimit;
this.defaultPrice = defaultPrice;
this.billingUnit = billingUnit;
}

public static int calculate(int totalDistance) {
PathPrice pathPrice = Arrays.stream(values())
.filter(value -> totalDistance <= value.distanceUpperLimit)
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("경로의 전체 거리 값이 잘못되었습니다."));

return pathPrice.defaultPrice
+ calculateOverPrice(totalDistance - pathPrice.distanceUnderLimit,pathPrice.billingUnit);
}

private static int calculateOverPrice(int distance, int billingUnit) {
if (billingUnit == 0) {
return 0;
}
return 100 * ((distance - 1) / billingUnit + 1);
}
}
98 changes: 98 additions & 0 deletions src/main/java/subway/application/path/PathService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package subway.application.path;

import org.jgrapht.GraphPath;
import org.jgrapht.alg.shortestpath.DijkstraShortestPath;
import org.jgrapht.graph.DefaultWeightedEdge;
import org.jgrapht.graph.WeightedPseudograph;
import org.springframework.stereotype.Service;
import subway.dao.SectionDao;
import subway.dao.StationDao;
import subway.domain.Section;
import subway.domain.Station;
import subway.dto.PathResponse;
import subway.dto.StationResponse;

import javax.annotation.PostConstruct;
import java.util.List;
import java.util.stream.Collectors;

@Service
public class PathService {

static final WeightedPseudograph<Long, DefaultWeightedEdge> GRAPH = new WeightedPseudograph<>(DefaultWeightedEdge.class);

private final StationDao stationDao;
private final SectionDao sectionDao;

public PathService(StationDao stationDao, SectionDao sectionDao) {
this.stationDao = stationDao;
this.sectionDao = sectionDao;
}

@PostConstruct
void addExistingDataToGraph() {
List<Station> stations = stationDao.findAll();
for (Station station : stations) {
addVertex(station.getId());
}

List<Section> sections = sectionDao.findAll();
for (Section section : sections) {
addEdge(section.getUpStation().getId(), section.getDownStation().getId(), section.getDistance());
}
}

public PathResponse findShortestPath(Long departureStationId, Long arrivalStationId) {
validateStationsAreNotSame(departureStationId, arrivalStationId);

GraphPath<Long, DefaultWeightedEdge> path = getPath(departureStationId, arrivalStationId);
List<StationResponse> stationResponses = path.getVertexList().stream()
.map(stationId -> stationDao.findById(stationId)
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 Station 입니다.")))
.map(StationResponse::of)
.collect(Collectors.toList());
int totalDistance = (int) path.getWeight();

return new PathResponse(stationResponses, totalDistance, PathPrice.calculate(totalDistance));
}

private void validateStationsAreNotSame(Long departureStationId, Long arrivalStationId) {
if (departureStationId.equals(arrivalStationId)) {
throw new IllegalArgumentException("출발역과 도착역이 동일합니다.");
}
}

private GraphPath<Long, DefaultWeightedEdge> getPath(Long departureStationId, Long arrivalStationId) {
DijkstraShortestPath<Long, DefaultWeightedEdge> dijkstra = new DijkstraShortestPath<>(GRAPH);

GraphPath<Long, DefaultWeightedEdge> path;
try {
path = dijkstra.getPath(departureStationId, arrivalStationId);
} catch (IllegalArgumentException exception) {
throw new IllegalArgumentException("출발역 또는 도착역이 존재하지 않습니다.");
}

if (path == null) {
throw new IllegalArgumentException("출발역과 도착역 사이에 경로가 존재하지 않습니다.");
}

return path;
}

public void addVertex(Long vertex) {
GRAPH.addVertex(vertex);
}

public void removeVertex(Long vertex) {
GRAPH.removeVertex(vertex);
}

public void addEdge(Long startVertex, Long endVertex, int weight) {
GRAPH.setEdgeWeight(GRAPH.addEdge(startVertex, endVertex), weight);
}

public void removeEdge(Long startVertex, Long endVertex) {
GRAPH.removeEdge(startVertex, endVertex);
}

}
8 changes: 5 additions & 3 deletions src/main/java/subway/domain/Section.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,7 @@ public class Section {
private Integer distance;

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

public Section(Long id, Station upStation, Station downStation, Integer distance) {
Expand All @@ -41,6 +39,10 @@ public Integer getDistance() {
return distance;
}

public boolean isIdEquals(Long id) {
return this.id.equals(id);
}

public static List<Station> distinctStations(List<Section> sections) {
return sections.stream()
.flatMap(section -> Stream.of(section.getUpStation(), section.getDownStation()))
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/subway/domain/Station.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ public String getName() {
return name;
}

public boolean isIdEquals(Long id) {
return this.id.equals(id);
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
Expand Down
27 changes: 27 additions & 0 deletions src/main/java/subway/dto/PathResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package subway.dto;

import java.util.List;

public class PathResponse {
private List<StationResponse> path;
private int totalDistance;
private int price;

public PathResponse(List<StationResponse> path, int totalDistance, int price) {
this.path = path;
this.totalDistance = totalDistance;
this.price = price;
}

public List<StationResponse> getPath() {
return path;
}

public int getTotalDistance() {
return totalDistance;
}

public int getPrice() {
return price;
}
}
25 changes: 25 additions & 0 deletions src/main/java/subway/ui/PathController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package subway.ui;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import subway.application.path.PathService;
import subway.dto.PathResponse;

@RestController
@RequestMapping("/path")
public class PathController {

private final PathService pathService;

public PathController(PathService pathService) {
this.pathService = pathService;
}

@GetMapping
public PathResponse findShortestPath(@RequestParam Long departureStationId, @RequestParam Long arrivalStationId) {
return pathService.findShortestPath(departureStationId, arrivalStationId);
}

}
11 changes: 10 additions & 1 deletion src/main/java/subway/ui/SectionController.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import subway.application.SectionService;
import subway.application.path.PathService;
import subway.dto.SectionRequest;
import subway.dto.SectionResponse;

Expand All @@ -13,20 +14,28 @@
public class SectionController {

private final SectionService sectionService;
private final PathService pathService;

public SectionController(SectionService sectionService) {
public SectionController(SectionService sectionService, PathService pathService) {
this.sectionService = sectionService;
this.pathService = pathService;
}

@PostMapping
public ResponseEntity<SectionResponse> createSection(@PathVariable Long lineId, @RequestBody SectionRequest request) {
SectionResponse response = sectionService.saveSection(lineId, request);
pathService.addEdge(request.getUpStationId(), request.getDownStationId(), request.getDistance());

return ResponseEntity.created(URI.create("/lines/" + lineId + "/sections/" + response.getId())).body(response);
}

@DeleteMapping("/{sectionId}")
public ResponseEntity<Void> deleteSection(@PathVariable Long lineId, @PathVariable Long sectionId) {
SectionResponse sectionResponse = sectionService.findSectionResponseById(sectionId);

sectionService.deleteSectionById(lineId, sectionId);
pathService.removeEdge(sectionResponse.getUpStationId(), sectionResponse.getDownStationId());

return ResponseEntity.noContent().build();
}

Expand Down
11 changes: 9 additions & 2 deletions src/main/java/subway/ui/StationController.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import subway.application.StationService;
import subway.application.path.PathService;
import subway.dto.StationRequest;
import subway.dto.StationResponse;
import subway.application.StationService;

import java.net.URI;
import java.sql.SQLException;
Expand All @@ -14,14 +15,18 @@
@RequestMapping("/stations")
public class StationController {
private final StationService stationService;
private final PathService pathService;

public StationController(StationService stationService) {
public StationController(StationService stationService, PathService pathService) {
this.stationService = stationService;
this.pathService = pathService;
}

@PostMapping
public ResponseEntity<StationResponse> createStation(@RequestBody StationRequest stationRequest) {
StationResponse station = stationService.saveStation(stationRequest);
pathService.addVertex(station.getId());

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Section, Station Controller에서 PathService의 로직을 호출하는데 이에 대한 검증을 PathController 통합 테스트에서만 진행해도 괜찮을까요?? 아니면 Section과 Station 통합 테스트에서도 어떻게든 검증을 하는게 좋을까요??

통합 테스트 이기 때문에 Path 통합 테스트에서 Section/Station API를 호출하고 제대로된 경로 정보를 반환하는지 확인하는게 더 적절한가 싶기도 하고 고민이 되네요🥲🥲

return ResponseEntity.created(URI.create("/stations/" + station.getId())).body(station);
}

Expand All @@ -44,6 +49,8 @@ public ResponseEntity<Void> updateStation(@PathVariable Long id, @RequestBody St
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteStation(@PathVariable Long id) {
stationService.deleteStationById(id);
pathService.removeVertex(id);

return ResponseEntity.noContent().build();
}

Expand Down
4 changes: 3 additions & 1 deletion src/main/resources/application.properties
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
spring.h2.console.enabled=true

spring.datasource.url=jdbc:h2:mem:subway;MODE=MySQL
spring.datasource.url=jdbc:h2:~/subway;MODE=MySQL
spring.datasource.driver-class-name=org.h2.Driver

spring.sql.init.mode=always
Loading