GithubHelp home page GithubHelp logo

parkchu / atdd-with-kotlin Goto Github PK

View Code? Open in Web Editor NEW
3.0 3.0 1.0 385 KB

아들과 함께 ATDD를 경험하기 위한 프로젝트

Kotlin 45.91% JavaScript 5.84% Vue 20.13% HTML 0.25% SCSS 27.63% Shell 0.24%

atdd-with-kotlin's People

Contributors

javajigi avatar parkchu avatar pobiconan avatar

Stargazers

 avatar

Watchers

 avatar

atdd-with-kotlin's Issues

로그인 토큰 발급

미션 - 토큰 발급 기능 구현하기

  • 인증을 위해 토큰을 발급받는 기능을 구현하세요.

미션 방법

  • 기능 구현을 위한 테스트 메서드를 성공시키세요.
  • 신규로 구현하는 기능에 대해서는 TDD 사이클을 적용해서 구현해보세요.
  • 기존 로직에 대하여 테스트 작성 연습을 해도 좋습니다.

요구사항

  • AuthAcceptanceTest의 loginWithBasicAuth 테스트 메서드를 성공시키기
  • POST /login/token 요청 시 JWT 토큰 응답 받기

인수 조건

Feature: Basic Auth 로그인

  Scenario: Basic Auth를 통한 로그인 시도
    Given 회원 등록되어 있음
    When 로그인 요청
    Then 로그인 됨

요청/응답

request

POST /login/token HTTP/1.1
authorization: Basic ZW1haWxAZW1haWwuY29tOnBhc3N3b3Jk
accept: application/json
content-length: 0
host: localhost:57634
connection: Keep-Alive
user-agent: Apache-HttpClient/4.5.12 (Java/1.8.0_252)
accept-encoding: gzip,deflate

response

HTTP/1.1 200 
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
Content-Type: application/json
Content-Length: 245
Date: Tue, 14 Jul 2020 09:28:12 GMT
Keep-Alive: timeout=60
Connection: keep-alive

{
    "accessToken": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ7XCJpZFwiOjEsXCJlbWFpbFwiOlwiZW1haWxAZW1haWwuY29tXCIsXCJwYXNzd29yZFwiOlwicGFzc3dvcmRcIixcImFnZVwiOjIwfSIsImlhdCI6MTU5NDcxODg5MywiZXhwIjoxNTk0NzIyNDkzfQ.SMyb9RNrs5Uy5eqVZ0jZw3SEgWFsZaifnlslI-cEQ-c"
}

미션 수행 순서

  1. TokenAuthenticationInterceptorTest 생성
  2. TokenAuthenticationInterceptor의 기능에 대한 단위 테스트 작성
  3. 단위 테스트를 성공 시키기 위한 기능 구현
  4. WebMvcConfig에 등록하기

힌트


TokenAuthenticationInterceptor에서 구현할 기능

  • SessionAuthenticationInterceptor의 수행 순서는 아래와 같음
  1. HttpServletRequest에서 로그인 정보를 추출 하여 검증 객체(AuthenticationToken)를 생성
  2. AuthenticationToken을 통해 인증을 시도하여 성공하면 인증 객체(Authentication)를 생성
  3. Authentication가 정상적으로 생성되면 세션에 저장

로그인 정보 추출

  • SessionAuthenticationInterceptor에서는 Form을 통해 로그인 정보 추출
  • 신규 AuthenticationInterceptor에서는 Basic Auth를 통해 로그인 정보 추출해야함

인증

  • SessionAuthenticationInterceptor와 동일함

후처리

  • SessionAuthenticationInterceptor에서는 인증에 성공 후 세션에 저장
  • 신규 AuthenticationInterceptor에서는 TokenResponse를 응답해야함

MockHttpServletRequest와 MockHttpServletResponse

  • TokenAuthenticationInterceptor에 대한 단위 테스트를 작성 시 HttpServletRequest와 HttpServletResponse 필요
  • HttpServletRequest와 HttpServletResponse는 MockHttpServletRequest와 MockHttpServletResponse로 대체

MockHttpServletRequest request = new MockHttpServletRequest();
byte[] targetBytes = (EMAIL + REGEX + PASSWORD).getBytes();
byte[] encodedBytes = Base64.getEncoder().encode(targetBytes);
String credentials = new String(encodedBytes);
request.addHeader("Authorization", "Basic " + credentials);

MockHttpServletResponse response = new MockHttpServletResponse();

interceptor.preHandle(request, response, new Object());

  • JWT token을 생성할 때 JwtTokenProvider를 활용
JwtTokenProvider jwtTokenProvider = mock(JwtTokenProvider.class);
when(jwtTokenProvider.createToken(anyString())).thenReturn("jwtToken");

AuthorizationExtractor

  • Authorization 헤더의 형식은 아래와 같음
Authorization: <type> <credentials>

  • AuthorizationExtractor을 활용하여 HttpServletRequest에서 type별 credentials을 추출 가능
// basic auth
String credentials = AuthorizationExtractor.extract(request, AuthorizationType.BASIC);

// bearer auth
String credentials = AuthorizationExtractor.extract(request, AuthorizationType.BEARER);

HTTP 인증


JwtTokenProvider

토큰 생성

String payload = new ObjectMapper().writeValueAsString(authentication.getPrincipal());
String token = jwtTokenProvider.createToken(payload);
TokenResponse tokenResponse = new TokenResponse(token);

토큰 검증

jwtTokenProvider.validateToken(credentials)

토큰 추출

String payload = jwtTokenProvider.getPayload(credentials);

지하철 노선에 역을 제외하는 기능을 구현하기

미션 방법

  • 지하철 노선에 역을 제외하는 기능을 구현하기 위한 인수 조건이 제공됩니다.
  • 인수 조건을 기반으로 인수 테스트를 작성하시고, 인수 테스트를 성공 시키기 위해 기능을 구현하세요.
  • 기능이 잘 이해가 되지 않으신다면 기능 설명페이지를 참고해서 진행해주세요.
  • 아래의 미션 수행 순서에 따라서 진행하면 도움이 됩니다.

인수 조건

Feature: 지하철 노선에서 역 제외 기능

  Background:
    Given 지하철역이 등록되어 있음
    And 지하철 노선이 등록되어 있음
    And 지하철 노선에 지하철역 등록되어 있음

  Scenario: 지하철 노선에 등록된 마지막 지하철역을 제외한다.
    When 지하철 노선의 마지막에 지하철역 제외 요청
    Then 지하철 노선에 지하철역 제외됨
    When 지하철 노선 상세정보 조회 요청
    Then 지하철 노선에 지하철역 제외 확인됨
    And 지하철 노선에 지하철역 순서 정렬됨
    
  Scenario: 지하철 노선에 등록된 중간 지하철역을 제외한다.
    When 지하철 노선의 중간 지하철역 제외 요청
    Then 지하철 노선에 지하철역 제외됨
    When 지하철 노선 상세정보 조회 요청
    Then 지하철 노선에 지하철역 제외 확인됨
    And 지하철 노선에 지하철역 순서 정렬됨
    
  Scenario: 지하철 노선에서 등록되지 않는 역을 제외한다.
    When 지하철 노선에 등록되지 않은 역 제외 요청
    Then 지하철 노선에 지하철역 제외 실패됨

기능 설명

지하철 노선에 역 제거

  • 출발역이 제거될 경우 출발역 다음으로 오던 역이 출발역으로 됨
  • 중간역이 제거될 경우 재배치를 함
    • 노선에 A - B - D - C 역이 연결되어 있을 때 B역을 제거할 경우 A - B - C로 재배치 됨

image.png


미션 수행 순서

  • 인수 테스트 작성하기
  • 기능 구현하기
  • 예외상황 처리하기

로그인 토큰 인증

미션 - 토큰 인증 기능 구현하기

  • 토큰의 유효성 검사를 하고 토큰의 정보를 이용하여 사용자 인증 하기

미션 방법

  • 기능 구현을 위한 테스트 메서드를 성공시키세요.
  • 신규로 구현하는 기능에 대해서는 TDD 사이클을 적용해서 구현해보세요.
  • 기존 로직에 대하여 테스트 작성 연습을 해도 좋습니다.

요구사항

  • AuthAcceptanceTest의 myInfoWithBearerAuth 테스트 메서드를 성공시키기
  • 로그인이 필요한 요청 시 JWT token을 포함하여 요청 보내기
  • 요청에서 JWT 토큰의 유효성 검사를 하고 토큰에서 사용자 정보 추출하기

인수 조건

Feature: 내 정보 조회 기능

  Scenario: 로그인을 통해 내 정보 조회
    Given 회원 등록되어 있음
    And 로그인되어있음
    When 내 회원 정보 조회 요청
    Then 회원 정보 조회됨

미션 수행 순서

  1. TokenSecurityContextPersistenceInterceptorTest 생성
  2. TokenSecurityContextPersistenceInterceptor 기능에 대한 단위 테스트 작성
  3. 단위 테스트를 성공 시키기 위한 기능 구현
  4. WebMvcConfig에 등록하기
  5. 컨트롤러에서 Session이 아닌 SecurityContextHolder에서 LoginMember 정보 조회

힌트


TokenSecurityContextPersistenceInterceptor에서 구현할 기능

  1. HttpServletRequest에서 JWT 토큰을 추출 후 유효성 검사
  2. JWT 토큰에서 payload를 추출하여 SecurityContext 추출
  3. SecurityContext이 정상적으로 존재하면 SecurityContextHolder에 저장

JwtTokenProvider

토큰 검증

jwtTokenProvider.validateToken(credentials)

토큰 추출

String payload = jwtTokenProvider.getPayload(credentials);

spring boot 프로젝트 생성 및 jpa 적용

요구사항

  • https://spring.io/guides/tutorials/spring-boot-kotlin/ 문서를 참고
    • spring boot with kotlin 프로젝트 생성(빌드 도구는 gradle)
    • intellij로 import
    • 스프링부트와 AWS로 혼자 구현하는 웹 서비스 책의 39 ~ 49 페이지 참고해 github에 추가
  • 스프링부트와 AWS로 혼자 구현하는 웹 서비스 책 2장(51 ~ 78 페이지) 참고해 테스트 코드 작성
  • 스프링부트와 AWS로 혼자 구현하는 웹 서비스 책 3장(79 ~ 123페이지) 참고해 JPA로 데이터베이스 연동

즐겨찾기 리팩터링

미션 - 즐겨찾기 기능 변경

  • 즐겨 찾기 기능을 미션 방법에 맞춰 기능 변경을 하세요.

미션 방법

  • 현재 즐겨 찾기 기능은 모든 회원이 공유(?)하는 즐겨 찾기 입니다.
  • 회원 별로 즐겨찾기를 관리할 수 있도록 기능변경 하세요.
  • 기능 변경은 TDD 사이클을 적용해서 구현해보세요.
  • 기존 로직에 대하여 테스트 작성 연습을 해도 좋습니다.

인수 조건

Feature: 즐겨찾기를 관리한다.

  Background 
    Given 지하철역 등록되어 있음
    And 지하철 노선 등록되어 있음
    And 지하철 노선에 지하철역 등록되어 있음
    And 회원 등록되어 있음
    And 로그인 되어있음

  Scenario: 즐겨찾기를 관리
    When 즐겨찾기 생성을 요청
    Then 즐겨찾기 생성됨
    When 즐겨찾기 목록 조회 요청
    Then 즐겨찾기 목록 조회됨
    When 즐겨찾기 삭제 요청
    Then 즐겨찾기 삭제됨

페이지

프론트엔드 코드는 모두 구현이 되어 있습니다. API만 구현해주세요 :)

즐겨찾기 추가 및 목록 페이지

image.png

지하철 노선에 역을 추가하는 것이 가능해야 한다.

미션 방법

  • 지하철 노선에 역을 추가하는 기능을 구현하기 위한 인수 테스트가 제공됩니다.
  • 인수 테스트를 성공 시키기 위해 API와 기능을 완성시켜주세요.
  • 실제 지하철 노선처럼 구현할 경우 난이도가 어려워 질 수 있어서 기능 제약조건을 두었습니다.
  • 기능이 잘 이해가 되지 않으신다면 기능 설명페이지를 참고해서 진행해주세요.
  • 아래의 미션 수행 순서에 따라서 진행하면 도움이 됩니다.

인수 조건

Feature: 지하철 노선에 역 등록 기능
    
   Background:
    Given 지하철역이 등록되어 있음
    And 지하철 노선이 등록되어 있음

  Scenario: 지하철 노선에 역을 등록한다.
    When 지하철 노선의 마지막에 지하철역 등록 요청
    Then 지하철 노선에 지하철역 등록됨

  Scenario: 지하철 노선 상세정보 조회 시 역 정보가 포함된다.
    Given 지하철 노선에 지하철역 등록되어 있음
    When 지하철 노선 상세정보 조회 요청
    Then 지하철 노선 상세정보 응답됨

  Scenario: 지하철 노선에 역을 마지막에 등록한다.
    Given 지하철 노선에 지하철역 등록되어 있음
    When 지하철 노선의 마지막에 지하철역 등록 요청
    Then 지하철 노선에 지하철역 등록됨
    When 지하철 노선 상세정보 조회 요청
    Then 지하철 노선 상세정보 응답됨
    And 등록된 지하철역이 마지막에 위치됨

  Scenario: 지하철 노선에 역을 중간에 등록한다.
    Given 지하철 노선에 지하철역 등록되어 있음
    When 지하철 노선의 중간에  지하철역 등록 요청
    Then 지하철 노선에 지하철역 등록됨
    When 지하철 노선 상세정보 조회 요청
    Then 지하철 노선 상세정보 응답됨
    And 등록된 지하철역이 중간에 위치됨

기능 제약조건

  • 한 노선의 출발역은 하나만 존재하고 단방향으로 관리함
    • 실재 운행 시 양쪽 두 종점이 출발역이 되겠지만 관리의 편의를 위해 단방향으로 관리
    • 추후 경로 검색이나 시간 측정 시 양방향을 고려 할 예정
  • 한 노선에서 두 갈래로 갈라지는 경우는 없음

기능 설명

역 추가 방법

  • 마지막 역이 아닌 뒷 따르는 역이 있는경우 재배치를 함
    • 노선에 A - B - C 역이 연결되어 있을 때 B 다음으로 D라는 역을 추가할 경우 A - B - D - C로 재배치 됨

image.png


페이지

지하철 구간 관리 페이지


미션 수행 순서

  • 단순히 노선에 역이 등록되는 인수 테스트를 통해 단순히 노선에 역이 등록되는 기능 구현하기
  • 노선 조회 시 등록된 역도 함께 응답 되도록 수정하기
  • 마지막 역 다음에 추가되는 케이스에 대해 기능 구현하기
  • 중간 역 다음에 추가되는 케이스에 대해 기능 구현하기

힌트


JPA 관계 맵핑

  • 지하철역은 여러개의 지하철 노선에 포함될 수 있다.
    • ex) 강남역은 2호선에 등록되어 있는 동시에 신분당선에 등록되어 있음
  • 따라서 다대다 관계로 보아 @ManyToMany로 관계를 맺을 수 있음
  • 하지만 다대다 관계는 여러가지 예상치 못한 문제를 발생시킬 수 있어 추천하지 않음
  • 지하철역과 지하철 노선의 맵핑 테이블을 엔티티로 두는 방법을 추천
    • 기존에 Station과 Line이 있었다면 Line에 속하는 Station을 LineStation이라는 엔티티로 도출
    • Line과 LineStation을 @ManyToOne 관계로 설정
  • 참고할 코드:
    https://github.com/next-step/atdd-subway-map/blob/boorownie/src/main/java/nextstep/subway/line/domain/LineStations.java
    • intellij에서 kotlin 파일을 생성하고 java 코드를 복사하면 자동으로 kotlin 코드로 변경해 줌
    • 참고한 코드에서는 LineStation을 일급컬렉션을 묶어 LineStations로 둠
    • JPA @Embedded And @Embeddable을 참고하세요.

로그인 리팩터링

미션 - 인증 로직 리팩터링 및 기능 추가

  • 1,2단계에서 구현한 인증 로직에 대한 리팩터링을 진행하세요
  • 내 정보 수정 / 삭제 기능을 처리하는 기능을 구현하세요.
  • HandlerMethodArgumentResolver를 활용하여 Controller에서 Authentication에 접근해보세요.

TDD를 활용한 리팩터링 방법

1. 기존 코드는 그대로 두고 새로운 테스트를 만들기

  • 기존 코드나 기존 테스트를 먼저 제거한다면 엄청난 재앙이 시작됨...ㄷㄷ

2. 새로운 테스트를 만족하는 프로덕션 코드 만들기

  • 이때 불가피하게 코드 중복이 발생함

3. 기존 코드를 모두 대체했다면 그 때 기존 테스트와 함께 지우기

  • 이렇게 하면 리팩터링 하는 도중에 코드작업을 멈추거나 다른 개발을 하더라도 롤백해하는 일이 없음
  • 단, 코드 중복이 되어있는 상태를 짧게 가져가도록 해야함

리팩터링 힌트


AuthenticationConverter 추상화

  • Basic Auth와 FormLogin으로 나뉘어 있는 AuthenticationConverter를 추상화
public interface AuthenticationConverter {
    AuthenticationToken convert(HttpServletRequest request);
}

AuthenticationInterceptor 추상화

  • AuthenticationInterceptor의 후처리 로직을 추상화
public abstract void afterAuthentication(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException;

auth 패키지와 member 패키지에 대한 의존 제거

  • 현재 auth 패키지와 member 패키지는 서로 의존하고 있음
  • UserDetailsService를 추상화 하여 auth -> member 의존을 제거하기

우아한 객체지향 세미나 - 패키지 의존 문제 해결


SecurityContextInterceptor 추상화

public abstract class SecurityContextInterceptor implements HandlerInterceptor {

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        SecurityContextHolder.clearContext();
    }
}

힌트


HandlerMethodArgumentResolver

travis CI를 활용해 배포를 자동화한다.

Travis CI 도구를 활용해 배포를 자동화한다.

개발을 하면서 pr이 merge될 때마다 수동으로 배포하는 것은 여간 번거로운 작업이 아니다.
즉, 매번 터미널로 서버에 접속해 deploy.sh 파일을 배포해야 한다.
이런 단점을 보완하기 위해 CI 도구를 활용해 배포를 자동화한다.

참고자료

"스프링 부트와 AWS로 혼자 구현하는 웹 서비스" 책의 9장을 보면 "코드가 푸시되면 자동으로 배포해 보자 - Travis CI 배포 자동화"가 있다.
이 내용을 참고해 배포를 자동화한다.

지하철 미션을 aws ec2에 배포한다.

요구사항

  • 지금까지 완료한 지하철 미션을 aws ec2에 배포한다.
    • deploy.sh과 같은 파일을 이용해 자동 배포가 가능해야 하며, 서비스가 중단되지 않도록 한다.
  • DB는 RDS가 아닌 h2 DB로 배포해도 된다.

요금 조회

미션 - 경로 조회 시 요금 정보 포함하기

미션 방법

  • 경로 조회 결과에 요금 정보를 포함하세요.
  • 인수 테스트 -> 문서화 -> 기능 구현 순으로 진행하세요.

인수 조건

Feature: 지하철 경로 검색

  Scenario: 두 역의 최단 거리 경로를 조회
    Given 지하철역이 등록되어있음
    And 지하철 노선이 등록되어있음
    And 지하철 노선에 지하철역이 등록되어있음
    When 출발역에서 도착역까지의 최단 거리 경로 조회를 요청
    Then 최단 거리 경로를 응답
    And 총 거리와 소요 시간을 함께 응답함
    And 지하철 이용 요금도 함께 응답함

요금 계산 방법

  • 기본운임(10㎞ 이내) : 기본운임 1,250원
  • 이용 거리초과 시 추가운임 부과
    • 10km초과∼50km까지(5km마다 100원)
    • 50km초과 시 (8km마다 100원)
지하철 운임은 거리비례제로 책정됩니다. (실제 경로가 아닌 최단거리 기준)

힌트


5km 마다 100원 추가

    private int calculateOverFare(int distance) {
        return (int) ((Math.ceil((distance - 1) / 5) + 1) * 100);
    }

지하철 노선 관련 기능의 인수 테스트를 작성하기

실습 방법

  • 아래의 인수 조건을 참고하여 인수 테스트를 작성하세요.
  • 인수 테스트를 위한 기능은 구현되어 있습니다.
  • 기능의 이해를 돕기위해 페이지가 제공됩니다.

인수 조건

Feature: 지하철 노선 관련 기능

  Scenario: 지하철 노선을 생성한다.
    When 지하철 노선을 생성 요청한다.
    Then 지하철 노선이 생성된다.
    
  Scenario: 기존에 존재하는 지하철 노선 이름으로 지하철 노선을 생성한다.
    Given 지하철 노선이 등록되어 있다
    When 지하철 노선을 생성 요청한다.
    Then 지하철 노선 생성이 실패된다.
        
  Scenario: 지하철 노선 목록을 조회한다.
    Given 지하철 노선이 등록되어 있다
    When 지하철 노선 목록을 조회 요청한다.
    Then 지하철 노선 목록이 응답된다.
    And 응답에 등록된 목록이 포함된다.
        
  Scenario: 지하철 노선을 조회한다.
    Given 지하철 노선이 등록되어 있다
    When 지하철 노선을 조회 요청한다.
    Then 지하철 노선이 응답된다.
        
  Scenario: 지하철 노선을 수정한다.
    Given 지하철 노선이 등록되어 있다
    When 지하철 노선을 수정 요청한다.
    Then 지하철 노선이 수정된다.
        
  Scenario: 지하철 노선을 제거한다.
    Given 지하철 노선이 등록되어 있다
    When 지하철 노선을 삭제 요청한다.
    Then 지하철 노선이 삭제된다.

페이지

지하철 노선 관리 페이지

image.png


힌트


RestAssured

given

  • 요청을 위한 값을 설정 (header, content type 등)
  • body가 있는 경우 body 값을 설정 함

when

  • 요청의 url와 method를 설정

then

  • 응답의 결과를 관리
  • response를 추출하거나 response 값을 검증할 수 있음

자세한 사용법은 Usage Guide를 참고

로그인 인증 프로세스 실습

실습 - 로그인 인증 프로세스


실습 방법

  • 요구사항에 따라서 로그인 인증 프로세스를 실습하세요.
  • 원활한 실습을 위해 인수 테스트 뼈대 코드가 제공됩니다.
  • API(자신의 회원 정보 조회) 호출에 필요한 인증을 구현하고 이를 검증하는 테스트 작성이 목표입니다.

요구사항 1. 인수 테스트 통합

  • MemberAcceptanceTest의 manageMember 테스트 메서드를 완성 시키기
  • 아래의 인수 조건을 만족 하기

인수 조건

Feature: 회원 정보를 관리한다.

  Scenario: 회원 정보를 관리
    When 회원 생성을 요청
    Then 회원 생성됨
    When 회원 정보 조회 요청
    Then 회원 정보 조회됨
    When 회원 정보 수정 요청
    Then 회원 정보 수정됨
    When 회원 삭제 요청
    Then 회원 삭제됨

요구사항 2.

  • AuthAcceptanceTest의 myInfoWithSession 테스트 메서드를 성공 시키기
  • GET /members/me 요청을 처리하는 컨트롤러 메서드를 완성하기
  • 아래 뼈대 코드 설명을 참고

뼈대 코드 설명


SessionAuthenticationInterceptor

  • 세션 기반 로그인 요청을 처리하는 인터셉터

세션 기반 로그인 프로세스

  1. HttpServletRequest에서 로그인 정보를 추출 하여 검증 객체(AuthenticationToken)를 생성
  2. AuthenticationToken을 통해 인증을 시도하여 성공하면 인증 객체(Authentication)를 생성
  3. Authentication가 정상적으로 생성되면 세션에 저장

AuthenticationToken 추출하기

  • Form 형태로 username과 password가 포함되어 요청 옴
public AuthenticationToken convert(HttpServletRequest request) {
    Map<String, String[]> paramMap = request.getParameterMap();
    String principal = paramMap.get(USERNAME_FIELD)[0];
    String credentials = paramMap.get(PASSWORD_FIELD)[0];

    return new AuthenticationToken(principal, credentials);
}

인증 수행 및 Authentication 객체 추출하기

  • AuthenticationToken을 통해 Member 정보를 가지는 객체(LoginMember)를 조회
  • LoginMember를 통해 인증을 수행 후 Authentication 생성
  • LoginMember는 Member 정보에서 로그인 된 사용자의 정보를 담고 있는 객체
  • Authentication은 SecurityContextHolder에서 관리할 인증 정보 (추후 설명)
public Authentication authenticate(AuthenticationToken token) {
    String principal = token.getPrincipal();
    LoginMember userDetails = userDetailsService.loadUserByUsername(principal);
    checkAuthentication(userDetails, token);

    return new Authentication(userDetails);
}

세션에 인증 정보 저장

  • 세션에 SecurityContext 형태로 저장(SecurityContext에 Authentication을 포함)
HttpSession httpSession = request.getSession();
httpSession.setAttribute(SPRING_SECURITY_CONTEXT_KEY, new SecurityContext(authentication));
response.setStatus(HttpServletResponse.SC_OK);

SecurityContextHolder

  • 인증 정보를 관리하는 주체

SecurityContextHolder 구성

  • ThreadLocal을 활용하여 SecurityContext을 관리
  • 스레드 별로 다른 SecurityContext에 접근 할 수 있음
public class SecurityContextHolder {
    public static final String SPRING_SECURITY_CONTEXT_KEY = "SECURITY_CONTEXT";
    private static final ThreadLocal<SecurityContext> contextHolder;

    static {
        contextHolder = new ThreadLocal<>();
    }
    
    ...
}

SecurityContextHolder 활용

  • setContext
  • getContext
  • clearContext
public static void clearContext() {
    contextHolder.remove();
}

public static SecurityContext getContext() {
    SecurityContext ctx = contextHolder.get();

    if (ctx == null) {
        ctx = createEmptyContext();
        contextHolder.set(ctx);
    }

    return ctx;
}

public static void setContext(SecurityContext context) {
    if (context != null) {
        contextHolder.set(context);
    }
}  

  • 아래와 같이 사용할 수 있음
public ResponseEntity<MemberResponse> findMemberOfMine(HttpServletRequest request) {
    SecurityContext context = (SecurityContext) request.getSession().getAttribute(SPRING_SECURITY_CONTEXT_KEY);
    LoginMember loginMember = (LoginMember) context.getAuthentication().getPrincipal();
    ...
}

SecurityContext 객체

  • SecurityContextHolder가 관리하는 SecurityContext는 Authentication을 포함함
public class SecurityContext {
    private Authentication authentication;
    ...

SessionSecurityContextPersistenceInterceptor

  • 세션에서 SecurityContext를 추출하여 SecurityContextHolder에 보관
  • 요청에 대한 처리가 끝나면 초기화

SecurityContext 추출

public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
    SecurityContext securityContext = (SecurityContext) request.getSession().getAttribute(SPRING_SECURITY_CONTEXT_KEY);
    if (securityContext != null) {
        SecurityContextHolder.setContext(securityContext);
    }
    return true;
}

SecurityContext 초기화

  • Thread Pool을 사용할 경우 ThreadLocal에 보관한 정보가 공유될 수 있음
  • 따라서 요청을 모두 처리한 후 SecurityContext를 초기화 해주어야 함
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
    SecurityContextHolder.clearContext();
}

요금 정책 추가

미션 - 스펙 추가하기

미션 방법

  • 추가된 요금 정책을 반영하세요.
  • 인수 테스트 변경 -> 문서화 변경 -> 기능 구현 순으로 진행하세요.

추가된 요금 정책

노선별 추가 요금

  • 추가 요금이 있는 노선을 이용 할 경우 측정된 요금에 추가
    • ex) 900원 추가 요금이 있는 노선 8km 이용 시 1,250원 -> 2,150원
    • ex) 900원 추가 요금이 있는 노선 12km 이용 시 1,350원 -> 2,250원
  • 경로 중 추가요금이 있는 노선을 환승 하여 이용 할 경우 가장 높은 금액의 추가 요금만 적용
    • ex) 0원, 500원, 900원의 추가 요금이 있는 노선들을 경유하여 8km 이용 시 1,250원 -> 2,150원

로그인 사용자의 경우 연령별 요금으로 계산

  • 청소년: 운임에서 350원을 공제한 금액의 20%할인
  • 어린이: 운임에서 350원을 공제한 금액의 50%할인
- 청소년: 13세 이상~19세 미만
- 어린이: 6세 이상~ 13세 미만

스펙 변경에 따른 기능 추가 및 리팩터링

미션 방법

  • 리팩터링 방법에 맞춰 리팩토링을 하세요.
  • 리팩터링 후 인수 조건을 참고하여 최소 시간 경로 기능을 추가하세요.

리팩터링 방법

1. Domain으로 옮길 로직을 찾으세요

  • 스프링 빈을 사용하는 객체와 의존하는 로직을 제외하고는 도메인으로 옮길 예정
  • 객체지향 생활체조를 참고

2. Domain의 단위 테스트를 작성하세요.

  • 서비스 레이어에서 옮겨 올 로직의 기능을 테스트

3. 로직을 옮기세요.

  • 기존 로직을 지우지 말고 새로운 로직을 만들어 수행
  • 정상 동작 확인 후 기존 로직 제거

인수 조건

Feature: 지하철 경로 검색

  Scenario: 두 역의 최소 시간 경로를 조회
    Given 지하철역이 등록되어있음
    And 지하철 노선이 등록되어있음
    And 지하철 노선에 지하철역이 등록되어있음
    When 출발역에서 도착역까지의 최소 시간 경로 조회를 요청
    Then 최소 시간 경로를 응답
    And 총 거리와 소요 시간을 함께 응답함

스펙 변경 팁

프로덕션 코드를 먼저 수정하지 않기

  • 항상 테스트 코드를 먼저 수정한 후 프로덕션 코드를 수정

추가 검증이 필요한 부분을 새로운 테스트 작성으로 검증하기

  • 기존 테스트 코드의 수정이 필요하면 수정
  • 기존 테스트 코드로 검증하기 어려우면 새로운 테스트 코드 작성

TDD의 마지막 단계는 항상 리팩터링으로 끝내기

  • TDD의 Green 단계에서는 어떠한 악행도 허용
  • 단 리팩터링 단계를 거쳐서 이 부분을 수정해야 함

페이지

프론트엔드 코드는 모두 구현이 되어 있습니다. API만 구현해주세요 :)

지하철 경로 검색 페이지

maria db를 설치하고 연결한다.

maria db 설치

  • 새로운 ec2를 하나더 생성한 후 maria db를 설치한다.
  • 구글에서 "ec2 mariadb 설치"와 같은 키워드로 검색해 maria db를 설치한다.
  • maria db port를 3306이 아니라 13306과 같이 다른 포트로 변경해 오픈한다.

지하철 노선도 ec2에서 maria db 연결

  • 로컬에서 개발할 때는 h2, ec2에서 서비스할 때는 maria db를 연결해 서비스한다.
    • 실 서비스에서 maria db 연결하기 위한 설정 정보를 github을 통해 관리하면 안된다.
  • spring boot의 profile 기능을 적용해 설정 파일을 분리한다.

인수 테스트 리팩터링 후 예외 상황 처리 하기

미션 방법

  • 인수 테스트를 리팩터링 하여 중복을 제거하고 가독성을 높이세요.
  • 1단계 미션에서 고려하지 않은 예외 상황을 처리하기 위해 인수 조건을 참고하세요.
  • 예외 상황을 검증할 수 있는 인수 테스트를 작성하세요.
  • 인수 테스트를 성공 시키기 위한 기능을 완성시켜주세요.
  • 아래의 미션 수행 순서에 따라서 진행하면 도움이 됩니다.

인수 조건

Feature: 지하철 노선에 역 등록 기능
    
  Background:
    Given 지하철역이 등록되어 있음
    And 지하철 노선이 등록되어 있음

  Scenario: 이미 등록되어 있던 역을 등록한다.
    Given 지하철 노선에 지하철역이 등록되어 있음
    When 지하철 노선에 이미 등록되어있는 지하철역 등록 요청
    Then 지하철 노선에 지하철역 등록 실패됨
    
  Scenario: 존재하지 않는 역을 등록한다.
    Given 지하철 노선에 지하철역이 등록되어 있음
    When 지하철 노선에 존재하지 않는 지하철역 등록 요청
    Then 지하철 노선에 지하철역 등록 실패됨

인수 테스트 리팩터링 포인트

1. API 요청 부분 분리하기

2. @BeforeEach 활용하여 중복 제거하기

3. 테스트 메서드에는 흐름을 이해하기 쉽게 메서드만 남기기

미션 수행 순서

  • 리팩터링 하기
  • 인수 조건을 기반으로 인수 테스트 작성하기
  • 인수 테스트가 성공 할 수 있도록 기능 구현하기

지하철 노선도 전체 정보를 조회하는 기능을 구현하기

미션 방법

  • 인수 조건을 검증할 수 있는 인수 테스트를 작성하세요.
  • 이 후 인수 테스트를 성공 시키기 위해 API와 기능을 완성시켜주세요.
  • HTTP Cache 설정을 구현해주세요.

인수 조건

Feature: 지하철 노선도 조회 기능

  Scenario: 지하철 노선도를 조회한다.
    Given 지하철역이 등록되어 있음
    And 지하철 노선이 등록되어 있음
    And 지하철 노선에 지하철역 등록되어 있음
    When 지하철 노선도 조회 요청
    Then 지하철 노선도 응답됨
    And 지하철 노선도에 노선별 지하철역 순서 정렬됨

요청 / 응답

Request

GET /maps HTTP/1.1
accept: application/json
host: localhost:49254
connection: Keep-Alive
user-agent: Apache-HttpClient/4.5.12 (Java/1.8.0_252)
accept-encoding: gzip,deflate

Response

HTTP/1.1 200 
ETag: "0e14d3fbd586864ca048f7287c2e348e4"
Content-Type: application/json
Content-Length: 1240
Date: Mon, 13 Jul 2020 21:18:58 GMT
Keep-Alive: timeout=60
Connection: keep-alive

{
    "lineResponses": [
        {
            "id": 1,
            "name": "2호선",
            "color": "GREEN",
            "startTime": "05:30:00",
            "endTime": "23:30:00",
            "intervalTime": 5,
            "stations": [
                {
                    "station": {
                        "id": 1,
                        "name": "강남역",
                        "createdDate": "2020-07-14T06:18:58.754",
                        "modifiedDate": "2020-07-14T06:18:58.754"
                    },
                    "preStationId": null,
                    "distance": 0,
                    "duration": 0
                },
                {
                    "station": {
                        "id": 2,
                        "name": "역삼역",
                        "createdDate": "2020-07-14T06:18:58.775",
                        "modifiedDate": "2020-07-14T06:18:58.775"
                    },
                    "preStationId": 1,
                    "distance": 5,
                    "duration": 2
                },
                {
                    "station": {
                        "id": 3,
                        "name": "선릉역",
                        "createdDate": "2020-07-14T06:18:58.793",
                        "modifiedDate": "2020-07-14T06:18:58.793"
                    },
                    "preStationId": 2,
                    "distance": 5,
                    "duration": 2
                }
            ],
            "createdDate": "2020-07-14T06:18:58.617",
            "modifiedDate": "2020-07-14T06:18:58.617"
        },
        {
            "id": 2,
            "name": "신분당성",
            "color": "RED",
            "startTime": "05:30:00",
            "endTime": "23:30:00",
            "intervalTime": 5,
            "stations": [
                {
                    "station": {
                        "id": 1,
                        "name": "강남역",
                        "createdDate": "2020-07-14T06:18:58.754",
                        "modifiedDate": "2020-07-14T06:18:58.754"
                    },
                    "preStationId": null,
                    "distance": 5,
                    "duration": 2
                },
                {
                    "station": {
                        "id": 4,
                        "name": "양재역",
                        "createdDate": "2020-07-14T06:18:58.82",
                        "modifiedDate": "2020-07-14T06:18:58.82"
                    },
                    "preStationId": 1,
                    "distance": 5,
                    "duration": 2
                }
            ],
            "createdDate": "2020-07-14T06:18:58.723",
            "modifiedDate": "2020-07-14T06:18:58.723"
        }
    ]
}

페이지

지하철 구간 관리 페이지


미션 수행 순서

  • 인수 테스트 작성
  • 인수 테스트를 만족하는 기능 구현
  • 캐시 적용
LineService의 findAllLines 메서드는 노선의 목록을 조회하는 메서드 입니다. 지하철 노선도를 조회하는 새로운 비즈니스 로직을 구현해주세요 :)

힌트


ETag 인수 테스트

응답에 ETag header 존재 여부 확인

RestAssured.given().log().all().
        ...
        when().
        get(uri).
        then().
        header("ETag", notNullValue()).
        ...

요청에 header 추가

RestAssured.given().log().all().
        header("If-None-Match", eTag).
        accept(MediaType.APPLICATION_JSON_VALUE).
        ...

추가 자료 문서 https://www.baeldung.com/etags-for-rest-with-spring


MapService 단위 테스트 픽스쳐 예시

조금 더 가독성 좋게 리팩터링 해보세요 :)
@ExtendWith(MockitoExtension.class)
public class MapServiceTest {
    @Mock
    private LineService lineService;

    private List<LineResponse> lines;

    private MapService mapService;

    @BeforeEach
    void setUp() {
        StationResponse stationResponse1 = new StationResponse(1L, "교대역", LocalDateTime.now(), LocalDateTime.now());
        StationResponse stationResponse2 = new StationResponse(2L, "강남역", LocalDateTime.now(), LocalDateTime.now());
        StationResponse stationResponse3 = new StationResponse(3L, "양재역", LocalDateTime.now(), LocalDateTime.now());
        StationResponse stationResponse4 = new StationResponse(4L, "남부터미널역", LocalDateTime.now(), LocalDateTime.now());

        LineStationResponse lineStationResponse1 = new LineStationResponse(stationResponse1, null, 2, 2);
        LineStationResponse lineStationResponse2 = new LineStationResponse(stationResponse2, 1L, 2, 2);

        LineStationResponse lineStationResponse3 = new LineStationResponse(stationResponse2, null, 2, 2);
        LineStationResponse lineStationResponse4 = new LineStationResponse(stationResponse3, 2L, 2, 1);

        LineStationResponse lineStationResponse5 = new LineStationResponse(stationResponse1, null, 2, 2);
        LineStationResponse lineStationResponse6 = new LineStationResponse(stationResponse4, 1L, 1, 2);
        LineStationResponse lineStationResponse7 = new LineStationResponse(stationResponse3, 4L, 2, 2);

        LineResponse lineResponse1 = new LineResponse(1L, "2호선", "GREEN", LocalTime.now(), LocalTime.now(), 5, Lists.newArrayList(lineStationResponse1, lineStationResponse2), LocalDateTime.now(), LocalDateTime.now());
        LineResponse lineResponse2 = new LineResponse(2L, "신분당선", "RED", LocalTime.now(), LocalTime.now(), 5, Lists.newArrayList(lineStationResponse3, lineStationResponse4), LocalDateTime.now(), LocalDateTime.now());
        LineResponse lineResponse3 = new LineResponse(3L, "3호선", "ORANGE", LocalTime.now(), LocalTime.now(), 5, Lists.newArrayList(lineStationResponse5, lineStationResponse6, lineStationResponse7), LocalDateTime.now(), LocalDateTime.now());

        lines = Lists.newArrayList(lineResponse1, lineResponse2, lineResponse3);

        mapService = new MapService(lineService);
    }

    ...
}

단위 테스트 작성

실습 방법

  • 요구사항에 따라서 단위 테스트를 작성하세요.
  • nextstep.study.unit 패키지에 있는 테스트 클래스를 참고하세요.

요구사항

1. LineStationsTest

  • LineStations와 관련된 단위 테스트를 작성
  • 협력 객체를 사용한 단위 테스트를 작성

2. LineStationServiceTest

  • LineStationService와 관련된 단위 테스트를 작성
  • mockito의 MockitoExtension을 활용하여 가짜 협력 객체를 사용한 단위 테스트 작성

학습 테스트


단위 테스트

UnitTest

  • 객체의 단위 테스트를 작성
  • 협력 객체를 사용한 단위 테스트를 작성

MockitoExtensionTest

  • mockito의 MockitoExtension을 활용하여 가짜 협력 객체를 사용한 단위 테스트 작성

JGraphTest

  • 경로 탐색을 위한 라이브러리 JGraph의 학습 테스트

가장 빠른 도착 경로 조회

미션 - 가장 빠른 도착 경로 조회

미션 방법

  • 가장 빨리 도착할 수 있는 경로를 조회하는 기능을 구현하세요.
  • 인수 테스트 -> 문서화 -> 기능 구현 순으로 진행하세요.
뼈대 코드는 원활한 미션 진행을 위해 제공되는 코드입니다. 얼마든지 변경해서 사용하세요 :)
이번 미션의 목적은 완벽한 알고리즘을 구현하기 보다는 복잡한 로직을 TDD로 구현하는 경험을 하는 것 입니다. 모든 상황을 고려한 로직을 구현하려다보면 미션의 목적을 잃고 헤맬 수 있습니다. 고려해야 할 경우가 많다고 느껴 질 경우 제약사항을 통해 기능을 단순화 시켜보세요. (제약사항을 둘 경우 MD파일로 남겨주세요.)

요청 / 응답

Request

GET /paths?source=1&target=3&type=ARRIVAL_TIME&time=202007221800 HTTP/1.1
accept: application/json
host: localhost:50460
connection: Keep-Alive
user-agent: Apache-HttpClient/4.5.12 (Java/1.8.0_252)
accept-encoding: gzip,deflate

Response

HTTP/1.1 200 
Set-Cookie: JSESSIONID=DD307C8D09D4DD94A70311B8E41B3703; Path=/; HttpOnly
Content-Type: application/json
Transfer-Encoding: chunked
Date: Wed, 22 Jul 2020 09:40:29 GMT
Keep-Alive: timeout=60
Connection: keep-alive

{
    "stations": [
        {
            "id": 1,
            "name": "교대역"
        },
        {
            "id": 2,
            "name": "강남역"
        },
        {
            "id": 3,
            "name": "양재역"
        }
    ],
    "duration": 3,
    "distance": 4,
    "fare": 1250
}

힌트


가장 빠른 도착 경로 조회 방법

  • 모든 경로를 조회
  • 각 경로별로 도착 시간 계산 (추후 설명)
  • 도착 시간이 가장 빠른 경로 응답

모든 경로 조회

JgraphTest의 getKShortestPaths 테스트 메서드를 참고하여 모든 경로를 조회하세요.

    @Test
    public void getKShortestPaths() {
        String source = "v3";
        String target = "v1";

        Multigraph<String, DefaultWeightedEdge> graph = new Multigraph(DefaultWeightedEdge.class);
        graph.addVertex("v1");
        graph.addVertex("v2");
        graph.addVertex("v3");

        graph.addEdge("v1", "v2");
        graph.addEdge("v2", "v3");
        graph.addEdge("v1", "v3");

        List<GraphPath> paths = new KShortestPaths(graph, 1000).getPaths(source, target);

        assertThat(paths).hasSize(2);
        paths.forEach(it -> {
            assertThat(it.getVertexList()).startsWith(source);
            assertThat(it.getVertexList()).endsWith(target);
        });
    }

도착 시간 계산

A역 -> B역 -> C역

  • 요청 시간 기준으로 A역의 가장 빠른 정차시간을 조회
  • A역 정차시간 + A->B의 소요시간 기준으로 B역의 가장 빠른 정차시간을 조회
  • B역 정차시간 + B->C의 소요시간 기준으로 C역의 가장 빠른 정차시간을 조회

가장 빠른 정차 시간 조회

  • 모든역의 정차시간을 저장할 수 없기 때문에 종점의 첫차 출발 시간간격으로 계산해야 함
  • 첫차 시간 + (간격 X n) + 종점에서 정차역까지의 소요시간이 조회 기준 시간보다 커질 때 까지 n을 증가시키고, 커졌을 때의 시간이 해당 역의 가장 빠른 정차 시간

LineStation의 방향성

  • LineStation은 preStationId와 stationId를 가지고 있으며 단방향으로 저장됨
  • 실제 SubwayPath의 결과로 응답되는 LineStation은 방향성이 없음
  • 따라서 LineStation의 preStationId와 stationId를 가지고 방향성을 알 수 있음
    • 출발역이 preStationId라면 preStationId -> StationId 방향이므로 정방향
    • 출발역이 stationId라면 stationId -> preStationId 방향이므로 역방향
  • 정방향의 경우에는 LineStation의 첫번째 부터 정차역까지의 소요시간을 계산
  • 역방향의 경우에는 마지막 LineStation부터 정차역까지의 소요시간을 계산

최단 경로 조회 기능을 구현하기

미션 방법

  • 최단 경로 조회 기능을 구현하세요.
  • 인수 조건을 검증할 수 있는 인수 테스트를 작성하세요.
  • 이 후 인수 테스트를 성공 시키기 위해 API와 기능을 완성시켜주세요.
  • 테스트 비용을 고려하여 사이드 케이스는 인수 테스트가 아닌 단위 테스트를 통해 검증하세요.
  • 인수 테스트 작성 이후 단위 테스트를 작성하여 ATDD 사이클을 경험해보세요.
  • TDD의 흐름에 있어 방향성은 관계 없습니다. 테스트 코드를 작성하고 기능구현하고 리팩터링 하는 사이클에 집중해주세요 :)

인수 조건

Feature: 지하철 경로 검색

  Scenario: 두 역의 최단 거리 경로를 조회
    Given 지하철역이 등록되어있음
    And 지하철 노선이 등록되어있음
    And 지하철 노선에 지하철역이 등록되어있음
    When 출발역에서 도착역까지의 최단 거리 경로 조회를 요청
    Then 최단 거리 경로를 응답
    And 총 거리와 소요 시간을 함께 응답함

기능 제약조건

  • 최단 경로가 하나가 아닐 경우 어느 경로든 하나만 응답

요청 / 응답 포맷

Request

HTTP/1.1 200 
Request method:	GET
Request URI:	http://localhost:55494/paths?source=1&target=6
Headers: 	Accept=application/json
		Content-Type=application/json; charset=UTF-8

Response

HTTP/1.1 200 
Content-Type: application/json
Transfer-Encoding: chunked
Date: Sat, 09 May 2020 14:54:11 GMT
Keep-Alive: timeout=60
Connection: keep-alive

{
    "stations": [
        {
            "id": 5,
            "name": "양재시민의숲역",
            "createdAt": "2020-05-09T23:54:12.007"
        },
        {
            "id": 4,
            "name": "양재역",
            "createdAt": "2020-05-09T23:54:11.995"
        },
        {
            "id": 1,
            "name": "강남역",
            "createdAt": "2020-05-09T23:54:11.855"
        },
        {
            "id": 2,
            "name": "역삼역",
            "createdAt": "2020-05-09T23:54:11.876"
        },
        {
            "id": 3,
            "name": "선릉역",
            "createdAt": "2020-05-09T23:54:11.893"
        }
    ],
    "distance": 40,
    "duration": 40
}

페이지

프론트엔드 코드는 모두 구현이 되어 있습니다. API만 구현해주세요 :)

지하철 경로 검색 페이지


예외 상황 예시

  • 출발역과 도착역이 같은 경우
  • 출발역과 도착역이 연결이 되어 있지 않은 경우
  • 존재하지 않은 출발역이나 도착역을 조회 할 경우

미션 수행 순서

인수 테스트 성공 시키기

  • mock 서버와 dto를 정의하여 인수 테스트 성공 시키기

기능 구현

TDD의 방향보다 테스트를 통해 구현할 기능을 명세하는것과 리팩터링이 더 중요합니다!

Outside In 경우

  • 컨트롤러 레이어 구현 이후 서비스 레이어 구현 시 서비스 테스트 우선 작성 후 기능 구현
  • 서비스 테스트 내부에서 도메인들간의 로직의 흐름을 검증, 이 때 사용되는 도메인은 mock 객체를 활용
  • 외부 라이브러리를 활용한 로직을 검증할 때는 가급적 실제 객체를 활용
  • Happy 케이스에 대한 부분만 구현( Side 케이스에 대한 구현은 다음 단계에서 진행)

Inside Out 경우

  • 도메인 설계 후 도메인 테스트를 시작으로 기능 구현 시작
  • 해당 도메인의 단위 테스트를 통해 도메인의 역할과 경계를 설계
  • 도메인의 구현이 끝나면 해당 도메인과 관계를 맺는 객체에 대해 기능 구현 시작
ex) 경로 조회를 수행하는 도메인 구현 예시
  - 1. PathFinder 라는 클래스 작성 후 경로 조회를 위한 테스트를 작성
  - 2. 경로 조회 메서드에서 Line을 인자로 받고 그 결과로 원하는 응답을 리턴하도록 테스트 완성
  - 3. 테스트를 성공시키기 위해 JGraph의 실제 객체를 활용(테스트에서는 알 필요가 없음)
두 방향성을 모두 사용해보시고 테스트가 협력 객체의 세부 구현에 의존하는 경우(가짜 협력 객체 사용)와 테스트 대상이 협력 객체와 독립적이지 못하고 변경에 영향을 받는 경우(실제 협력 객체 사용)를 모두 경험해보세요 :)

힌트


최단 경로 라이브러리

  • jgrapht 라이브러리를 활용하면 간편하게 최단거리를 조회할 수 있음
  • 정점(vertext)과 간선(edge), 그리고 가중치 개념을 이용
    • 정점: 지하철역(Station)
    • 간선: 지하철역 연결정보(LineStaion)
    • 가중치: 거리 or 소요시간
  • 최단 거리 기준 조회 시 가중치를 거리로 설정
@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);
}

jgrapht graph-algorithms


외부 라이브러리 테스트

  • 외부 라이브러리의 구현을 수정할 수 없기 때문에 단위 테스트를 하지 않음
  • 외부 라이브러리를 사용하는 직접 구현하는 로직을 검증해야 함
  • 직접 구현하는 로직 검증 시 외부 라이브러리 부분은 실제 객체를 활용

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.