๐Ÿ“š atdd-subway-fare ๋ฏธ์…˜ ์ •๋ฆฌ


์š”๊ธˆ ์ •์ฑ…

  • ๋กœ๊ทธ์ธ ์‚ฌ์šฉ์ž์™€ ๋น„๋กœ๊ทธ์ธ ์‚ฌ์šฉ์ž๋ฅผ ๊ตฌ๋ณ„ํ•˜์—ฌ ์š”๊ธˆ ์ •์ฑ…์„ ์ ์šฉ
  • PathController์—์„œ๋Š” ํ† ํฐ์ด ์—†์–ด๋„ ์˜ˆ์™ธ๊ฐ€ ์•„๋‹ˆ์—ˆ๊ณ 
  • MemberController์—์„œ๋Š” ํ† ํฐ์ด ์—†์œผ๋ฉด ์˜ˆ์™ธ์˜€๊ธฐ์— Resolver์—์„œ๋Š” ๊ฐ๊ฐ์— ๋งž๋Š” ์ฒ˜๋ฆฌ๋ฅผ ํ•ด์ฃผ์–ด์•ผ ํ–ˆ์Œ
  • ๋งจ ์ฒ˜์Œ ํŽ˜์–ด๋ž‘ ๊ธฐ๋Šฅ๋งŒ ๋Œ์•„๊ฐ€๊ฒŒ ๊ตฌํ˜„ํ–ˆ์„ ๋•Œ๋Š” ๊ธฐ์กด์— ์žˆ๋Š” ๋กœ๊ทธ์ธ๋œ ์‚ฌ์šฉ์ž์— ๋Œ€ํ•œ ์ธ๊ฐ€ ๊ณผ์ •์„ ๊ฑฐ์น˜๋Š” Resolver๋ฅผ ์žฌ์‚ฌ์šฉํ•˜๋„๋ก ๊ตฌํ˜„
  • ์ด๋Ÿฌ๊ธฐ ์œ„ํ•ด์„œ๋Š” ์ด ๊ฐ๊ฐ์— ๋งž๋Š” ์ฒ˜๋ฆฌ๋ฅผ ์ผ๊ฐ„ Controller๋กœ ์˜ฎ๊ฒจ์•ผ ํ–ˆ์Œ

AuthenticationPrincipalArgumentResolver

@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
    String credentials = AuthorizationExtractor.extract(webRequest.getNativeRequest(HttpServletRequest.class));
    LoginMember member = authService.findMemberByToken(credentials);
    if (member.getId() == null) {
        throw new AuthorizationException();
    }
    return member;
}

MemberController์—์„œ LoginMember์— ๋Œ€ํ•œ ์ฒ˜๋ฆฌ

private void unAuthorizedThrowException(@AuthenticationPrincipal LoginMember loginMember) {
    if (loginMember.getId() == null) {
        throw new AuthorizationException();
    }
}
  • id๊ฐ€ ์—†์œผ๋ฉด (์กด์žฌํ•˜์ง€ ์•Š๋Š” ์‚ฌ์šฉ์ž์ด๋ฉด) ์˜ˆ์™ธ

PathController์—์„œ LoginMember์— ๋Œ€ํ•œ ์ฒ˜๋ฆฌ

private LoginMember unLoginMemberWrapper(@AuthenticationPrincipal LoginMember loginMember) {
  if (loginMember.getId() == null) {
    return new LoginMember(null, null, -1);
  }
  return loginMember;
}
  • id๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์œผ๋ฉด age๋ฅผ -1 ๋กœ wrap
๊ทธ๋Ÿฐ๋ฐ ์ด๋ ‡๊ฒŒ ๊ตฌํ˜„ํ•˜๊ณ  ์ƒ๊ฐํ•œ ๋ฌธ์ œ์ ์ดโ€ฆ
  • Controller์—์„œ ์ด๋ฏธ ๋ฆฌ์กธ๋น™๋œ LoginMember์— ๋Œ€ํ•œ ์ฒ˜๋ฆฌ๋ฅผ ๋˜ ๊ฑด๋“œ๋ฆฌ๊ณ  ์žˆ๋‹ค๋Š” ๋Š๋‚Œ
  • Controller์—์„œ LoginMember์— ๋Œ€ํ•œ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ์•Œ๊ณ  ์žˆ๋Š” ๋Š๋‚Œ์ด๋ผ ๊ฐœ์„ ์ด ํ•„์š”ํ•˜๋‹ค๊ณ  ๋Š๋‚Œ
  • age๋ฅผ -1๋กœ ๊ฐ์‹ธ์„œ ์ด ๊ฒฝ์šฐ ๋น„๋กœ๊ทธ์ธ์ž๋กœ ๊ฐ€์ •ํ•˜๋Š” ๋กœ์ง์ด ๊ดœ์ฐฎ์€์ง€ ์˜๋ฌธ์„ ํ’ˆ๊ณ  ์žˆ์—ˆ์Œ

ํŽ˜์–ด์˜ ๋ฆฌ๋ทฐ์–ด์—๊ฒŒ ์˜จ ํ”ผ๋“œ๋ฐฑ

  • ์ผ๋‹จ AuthenticationPrincipalArgumentResolver ์™€ AgeAuthenticationPrincipalArgumentResolver ๋‘๊ฐœ๋กœ ๋ถ„๋ฆฌํ•˜์˜€์Œ
  • AgeAuthenticationPrincipalArgumentResolver ์—์„œ๋Š” ์š”๊ธˆ ์ •์ฑ…์— ํ•„์š”ํ•œ ๊ฐ’์ธ age ๋งŒ ๋ฐ˜ํ™˜ํ•˜๋„๋ก ๋ฆฌํŒฉํ† ๋ง

AgeAuthenticationPrincipalArgumentResolver

public class AgeAuthenticationPrincipalArgumentResolver implements HandlerMethodArgumentResolver {
    private AuthService authService;

    public AgeAuthenticationPrincipalArgumentResolver(AuthService authService) {
        this.authService = authService;
    }

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(AgeAuthenticationPrincipal.class);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
        String credentials = AuthorizationExtractor.extract(Objects.requireNonNull(webRequest.getNativeRequest(HttpServletRequest.class)));
        LoginMember memberByToken = authService.findMemberByToken(credentials);
        return memberByToken.getAge();
    }
}
  • Integer ํ˜•์ธ age๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋„๋กโ€ฆ.
    • ๋ฐ˜ํ™˜ํƒ€์ž…์„ ๋ช…์‹œ์ ์œผ๋กœ ์ ์–ด์ค„๊ฑธ ๊ทธ๋žฌ๋„ค

PathService์˜ findPath()

public PathResponse findPath(Integer age, Long source, Long target) {
    try {
        List<Line> lines = lineService.findLines();
        Station sourceStation = stationService.findStationById(source);
        Station targetStation = stationService.findStationById(target);
        SubwayPath subwayPath = pathFinder.findPath(lines, sourceStation, targetStation);
        int distance = subwayPath.calculateDistance();
        int lineFare = subwayPath.getMaxLineFare();
        SubwayPathFare subwayPathFare = new SubwayPathFare(age, distance, lineFare);
        return PathResponseAssembler.assemble(subwayPath, subwayPathFare);
    } catch (Exception e) {
        throw new InvalidPathException();
    }
}

SubwayPathFare

public class SubwayPathFare {
    private final Integer age;
    private final int distance;
    private final int lineFare;

    public SubwayPathFare(Integer age, int distance, int lineFare) {
        this.age = wrapAge(age);
        this.distance = distance;
        this.lineFare = lineFare;
    }

    private int wrapAge(Integer age) {
        if (age == null) {
            return -1;
        }
        return age;
    }

    public int getFare() {
        int fareByDistance = FareCalculatorByDistance.from(distance);
        return FareAdjusterByAge.of(age, fareByDistance, lineFare);
    }
}
  • ์š”๊ธˆ ์ •์ฑ…์„ ํ•ด๋‹น ๋„๋ฉ”์ธ์—์„œ Enum์ธ FareCalculatorByDistance ์™€ FareAdjusterByAge ๋ฅผ ํ†ตํ•ด ๊ณ„์‚ฐํ•˜๊ณ  ์žˆ์—ˆ์Œ
  • wrapAge() ๋ฅผ ํ†ตํ•ด null ๊ฐ’์œผ๋กœ age๊ฐ€ ๋“ค์–ด์˜ค๋ฉด FareAdjusterByAge.of()์—์„œ ์ŠคํŠธ๋ฆผ์„ ๋Œ๋ฆฌ๋Š”๋ฐ, ์ด์— ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š๋„๋ก -1๋กœ ํฌ์žฅ
์ฐธ๊ณ  - FareAdjusterByAge.of()
public static int of(int age, int fare, int lineFare) {
    return Arrays.stream(values())
            .filter(ageRange -> ageRange.minInclusive <= age && age < ageRange.maxExclusive)
            .map(calculator -> calculator.adjuster.apply(fare, lineFare))
            .findAny()
            .orElse(fare + lineFare);
}

FareCalculatorByDistance - ๊ฑฐ๋ฆฌ๋ณ„ ์š”๊ธˆ ์ •์ฑ…

public enum FareCalculatorByDistance {
    BASE(0, Constants.BASE_MAX_BOUNDARY, distance -> Constants.BASE_FARE),
    FIRST_ADDITIONAL(Constants.BASE_MAX_BOUNDARY, Constants.FIRST_ADDITIONAL_MAX_BOUNDARY,
            (distance) -> Constants.BASE_FARE + (int) ((Math.ceil((distance - Constants.BASE_MAX_BOUNDARY - 1) / 5)) + 1) * Constants.EXTRA_FARE),
    SECOND_ADDITIONAL(Constants.FIRST_ADDITIONAL_MAX_BOUNDARY, Integer.MAX_VALUE,
            (distance) -> Constants.BASE_FARE + 800 + (int) ((Math.ceil((distance - Constants.FIRST_ADDITIONAL_MAX_BOUNDARY - 1) / 8)) + 1) * Constants.EXTRA_FARE);


    private final int minExclusive;
    private final int maxInclusive;
    private final UnaryOperator<Integer> calculator;

    FareCalculatorByDistance(int minExclusive, int maxInclusive, UnaryOperator<Integer> calculator) {
        this.minExclusive = minExclusive;
        this.maxInclusive = maxInclusive;
        this.calculator = calculator;
    }

FareAdjusterByAge - ๋‚˜์ด๋ณ„ ์š”๊ธˆ ์ •์ฑ…

public enum FareAdjusterByAge {
    PRE_SCHOOLED(0, Constants.PRE_SCHOOL_MAX_BOUNDARY, getIntegerBinaryOperator(0)),
    SCHOOL_AGED(Constants.PRE_SCHOOL_MAX_BOUNDARY, Constants.SCHOOL_AGED_MAX_BOUNDARY, getIntegerBinaryOperator(Constants.SCHOOL_AGED_RATE)),
    ADOLESCENT(Constants.SCHOOL_AGED_MAX_BOUNDARY, Constants.ADOLESCENT_MAX_BOUNDARY, getIntegerBinaryOperator(Constants.ADOLESCENT_RATE));

    private final int minInclusive;
    private final int maxExclusive;
    private final BinaryOperator<Integer> adjuster;

    FareAdjusterByAge(int minInclusive, int maxExclusive, BinaryOperator<Integer> adjuster) {
        this.minInclusive = minInclusive;
        this.maxExclusive = maxExclusive;
        this.adjuster = adjuster;
    }

    private static BinaryOperator<Integer> getIntegerBinaryOperator(double rate) {
        return (fare, lineFare) -> (int) (((fare + lineFare) - 350) * rate);
    }

๋ฆฌ๋ทฐ์–ด์—๊ฒŒ ๋ฐ›์€ ํ”ผ๋“œ๋ฐฑ

  • ๋ฆฌ๋ทฐ์–ด์˜ ์˜๋„๋Š” Member๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๋„๋ฉ”์ธ์—์„œ age๋ฅผ nullableํ•˜๊ฒŒ ๋‘๊ณ  ์ด๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒƒ์ด ๋” ๋ช…ํ™•ํ•  ๊ฒƒ ๊ฐ™๋‹คํ•˜์‹ฌ

์•„์‰ฌ์šด ์ 

๋‚˜์ด๋ณ„ ์š”๊ธˆ์ •์ฑ…์„ ์ ์šฉํ•  ๋•Œ Member ๋„๋ฉ”์ธ์„ ์‚ฌ์šฉํ•ด๋ณผ๊ฑธ!

ํ˜„์žฌ ๊ฒฝ๋กœ ์กฐํšŒ๋ฅผ ํ•  ๋•Œ๋Š” member ๋„๋ฉ”์ธ์ด ๋„˜์–ด์˜ค์ง€ ์•Š๊ณ  age ๊ฐ’๋งŒ ๋„˜์–ด์˜ค๋„๋ก ๊ตฌํ˜„ํ–ˆ์—ˆ๋‹ค.
๊ทธ๋Ÿฐ๋ฐ Age๋Š” Member์— ์ข…์†๋˜๋Š” ํ•„๋“œ์ธ๋ฐ ์ด๋ฅผ ๋นผ์„œ ๋…๋ฆฝ์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๊ณ  ์žˆ๋Š”๊ฒŒ ์ง€๊ธˆ๋ณด๋‹ˆ ๋งˆ์Œ์— ๋“ค์ง€ ์•Š๋Š”๋‹ค.
๋งŒ์•ฝ Member์˜ ๋‹ค๋ฅธ ํ•„๋“œ๋“ค๋„ ์š”๊ธˆ์ •์ฑ…์— ์˜ํ–‰์„ ์ฃผ๊ฒŒ ๋œ๋‹ค๋ฉด ์œ ์ง€๋ณด์ˆ˜๊ฐ€ ์–ด๋ ค์šด ์ฝ”๋“œ๊ฐ€ ๋œ๋‹ค.
ํฌ๋ฃจ๋“ค์ด๋ž‘ ์ด์•ผ๊ธฐ๋ฅผ ํ•ด๋ณด๋‹ˆ ํ•œ ๋ฆฌ์กธ๋ฒ„์—์„œ Member๋ฅผ ๋ฐ˜ํ™˜ํ•ด์ฃผ๋Š”๋ฐ, ์ด ๋ฐ˜ํ™˜ ๋„๋ฉ”์ธ์„ ํ•œ ์ธํ„ฐํŽ˜์ด์Šค(์˜ˆ๋ฅผ ๋“ค๋ฉด Member)๋ฅผ ๋งŒ๋“ค๊ณ  ์ด๋ฅผ ๊ตฌํ˜„ํ•œ Guest, LoginMember ๋กœ ๋กœ๊ทธ์ธ, ๋น„๋กœ๊ทธ์ธ ์‚ฌ์šฉ์ž๋ฅผ ๊ตฌ๋ณ„ํ•˜์—ฌ ๋กœ์ง์„ ์ˆ˜ํ–‰ํ•˜๋Š” ๋ฐฉ๋ฒ•๋„ ์žˆ๋‹ค๊ณ  ํ•œ๋‹ค.

์ด๋Ÿฌ๋ฉด ๋ฆฌ์กธ๋ฒ„๋ฅผ ๋‚˜๋ˆŒ ํ•„์š”๋„ ์—†๊ณ  Member๊ฐ€ age๋ฅผ ๊ฐ€์ง€๊ณ  ๋‚˜์ด์— ๊ด€ํ•œ ์š”๊ธˆ ์ •์ฑ…์„ ์ˆ˜ํ–‰ํ•  ๋•Œ ํ•ด๋‹น ๋„๋ฉ”์ธ์—๊ฒŒ ๋ฌผ์–ด์„œ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์„ ๊ฒƒ ๊ฐ™๋‹ค.

Enum์— Predicate๋ฅผ ๊ฐ€์ง ํ• ๊ฑธ!

๋˜ํ•œ ๊ฐ๊ฐ ์š”๊ธˆ ์ •์ฑ…์„ Enum์œผ๋กœ ๊ตฌํ˜„ํ•˜์˜€๋Š”๋ฐ, ์—ฌ๊ธฐ์„œ ์ƒ์ˆ˜๋ฅผ ์“ฐ๋‹ˆ ๊ฐ€๋…์„ฑ์ด ๋งค์šฐ ๋–จ์–ด์ง€๋Š” ๋Š๋‚Œ์ด๋‹ค.

์ƒ์ˆ˜๋ฅผ ์“ด ์ด์œ ๋Š” ์œ„์˜ ํ”ผ๋“œ๋ฐฑ์œผ๋กœ ๋งค์ง ๋„˜๋ฒ„์— ์˜๋ฏธ๋ฅผ ๋ถ€์—ฌํ•˜๊ณ  ์‹ถ์—ˆ์œผ๋‚˜โ€ฆ
์ด๋ ‡๊ฒŒ ํ•˜๋‹ˆ ์˜คํžˆ๋ ค ๊ฐ€๋…์„ฑ์„ ํ—ค์น˜๋Š” ๋Š๋‚Œ์ด์—ˆ๊ณ  ๋งˆ์ง€๋ง‰ ๋ฆฌ๋ทฐ๋Œ€๋กœ Enum ์•ˆ์— ๋ฒ”์œ„๋ฅผ ํŒ๋‹จํ•˜๋Š” Predicate ๋ฅผ ๊ฐ€์ง€๋„๋ก ํ• ๊ฑธ ๊ทธ๋žฌ๋‹ค.

ํ”„๋ก ํŠธ์—”๋“œ์™€์˜ ํ˜‘์—…

  • ๋ฐฑ์—”๋“œ 4, ํ”„๋ก ํŠธ์—”๋“œ 2๋ช… ์ •๋„์”ฉ ํŒ€์„ ์ด๋ฃจ์–ด ํ˜‘์—…ํ•˜๋Š” ๋ฏธ์…˜์ด์—ˆ์Œ
  • ํ•ด๋‹น ๋ฌธ์„œ ์— ์žˆ๋Š” ํ”„๋ก ํŠธ์—”๋“œ ์š”๊ตฌ์‚ฌํ•ญ์„ ๋ผˆ๋Œ€์ฝ”๋“œ์— ๋ฐ˜์˜ํ•ด ๋‚˜๊ฐ

API ๋ฌธ์„œํ™”

  • Spring Rest Docs๋ฅผ ํ†ตํ•ด API ๋ฌธ์„œํ™” ์ž‘์—…์„ ์ง„ํ–‰
  • ๊ธฐ์กด์— ์žˆ๋Š” RestAssured ํ…Œ์ŠคํŠธ๋ฅผ ์ด์šฉํ•ด API๋ฅผ ๋งŒ๋“ค์ˆ˜๋„ ์žˆ๋Š”๋ฐ, ํŽ˜์–ด๋ž‘ ๊ฐ™์ด ์‹œ๋„ํ•˜๋‹ค๊ฐ€ ๊ณ„์† ์˜ค๋ฅ˜๋‚˜์„œโ€ฆ.
  • ๊ฒฐ๊ตญ MockMvc๋กœ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ๋‹ค์‹œ์งœ์„œ API ๋ฌธ์„œ๋ฅผ ๋งŒ๋“ค์—ˆ์Œ
  • ๋‹ค๋ฅธ ํฌ๋ฃจ๊ฐ€ RestAssured๋กœ ๋ฌธ์„œ๋ฅผ ๋งŒ๋“ค์—ˆ๋˜๋ฐ ๊ทธ๊ฑฐ ๋ณด๊ณ  ์‹œ๋„ํ•ด๋ด์•ผ์ง€โ€ฆ
  • ๋˜ ๋ฆฌ๋ทฐ์–ด๊ฐ€ ์ œ์‹œํ•œ ๋ฐฉํ–ฅ์œผ๋กœ API ๋ฌธ์„œ ๋งŒ๋“œ๋Š” ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์กฐ๊ธˆ ์ˆ˜์ •ํ–ˆ๋”๋‹ˆ ๋” ์ข‹์€ ๋ฌธ์„œ๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์—ˆ์Œ

ControllerTest

@ActiveProfiles("test")
@AutoConfigureRestDocs
public class ControllerTest {
    @Autowired
    public MockMvc mockMvc;

    @Autowired
    public ObjectMapper objectMapper;

    protected static OperationRequestPreprocessor getDocumentRequest() {
        return preprocessRequest(
                modifyUris()
                        .scheme("https")
                        .host("newwisdom-subway.p-e.kr")
                        .removePort(),
                prettyPrint());
    }

    protected static OperationResponsePreprocessor getDocumentResponse() {
        return preprocessResponse(prettyPrint());
    }
}
  • API ๋ฌธ์„œ ๋งŒ๋“œ๋Š”๋ฐ ํ•„์š”ํ•œ MockMvc ํ…Œ์ŠคํŠธ๋“ค์€ ์œ„ ํด๋ž˜์Šค๋ฅผ ์ƒ์†๋ฐ›๊ณ  ์žˆ์Œ
  • ๋ฆฌ๋ทฐ์–ด์˜ ์ œ์•ˆ์„ ํ†ตํ•ด getDocumentRequest()์™€ getDocumentResponse() ์„ ์ถ”๊ฐ€

ํ…Œ์ŠคํŠธ ์ฝ”๋“œ์˜ ๊ฐ€๋…์„ฑ

  • ์ด๋ฒˆ ๋ฆฌ๋ทฐ์–ด๊ฐ€ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ์—์„œ ๊ฐ€๋…์„ฑ์„ ์ง€ํ‚ค์ง€ ๋ชปํ•œ ๋ถ€๋ถ„๋“ค์„ ๋งŽ์ด ํ”ผ๋“œ๋ฐฑ ํ•ด์ฃผ์—ˆ์Œ
  • ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋„ ํ•˜๋‚˜์˜ ๋ฌธ์„œ์ด๋ฉฐ ์œ ์ง€๋ณด์ˆ˜ ๋Œ€์ƒ์ด๊ธฐ ๋•Œ๋ฌธ์— ๊ฐ€๋…์„ฑ์„ ๊ณ ๋ คํ•ด์•ผ ํ•œ๋‹ค๋Š” ๊ฒƒ์„ ๋Š๊ผˆ์Œ
  • ์„œ๋กœ ๋‹ค๋ฅธ ํ…Œ์ŠคํŠธ์—์„œ ๊ณตํ†ต์œผ๋กœ ์“ฐ์ด๋Š” ๋ฉ”์„œ๋“œ๋“ค์€ ์ƒ์œ„ ํด๋ž˜์Šค์ธ AcceptanceTest๋กœ ๋Œ์–ด ์˜ฌ๋ฆผ
  • ํ…Œ์ŠคํŠธ์— ์“ฐ์ด๋Š” ๋ณ€์ˆ˜๋ช…๋“ค๋„ ๋งฅ๋ฝ์„ ํŒŒ์•…ํ•˜๊ธฐ ์‰ฝ๋„๋ก ๋ณ€๊ฒฝ
  • ์ƒ์ˆ˜์ฒ˜๋ฆฌ๋„ ์‹ ๊ฒฝ์”€

์˜ˆ์™ธ ์ฒ˜๋ฆฌ

  • โ€œ์ผ๋‹จ ํ”„๋ก ํŠธ์˜ ์š”๊ตฌ์‚ฌํ•ญ ๋‹ค ๋ฐ˜์˜ํ•˜๋„๋ก ๊ตฌํ˜„ํ•ด๋ณด์ž!โ€๋กœ ์žก๊ณ  ํ–ˆ๋”๋‹ˆ ๋ฌด์ˆ˜ํ•œ ์ปค์Šคํ…€ ์˜ˆ์™ธ๊ฐ€ ๋งŒ๋“ค์–ด์กŒ๋‹คโ€ฆ^ใ… ^
  • ์ด๋ฅผ ๊ฐœ์„ ํ•  ๋ฐฉ๋ฒ•์„ ์ฐพ๋‹ค๊ฐ€โ€ฆ ํฌ๋ฃจ๋“คํ•œํ…Œ ๋“ค์€ Spring Guide - Exception ์ „๋žต ์„ ์ ์šฉํ•ด๋ณด๊ธฐ๋กœ~
  • RuntimeException์„ ์ƒ์†๋ฐ›๋Š” BuisnessException์„ ๋งŒ๋“ค๊ณ  ํ•„๋“œ์— ์ƒํƒœ์ฝ”๋“œ์™€ ErrorMessage(DTO)๋ฅผ ๊ฐ€์ง€๋„๋ก ๊ตฌํ˜„
  • ์ด๋ฅผ ์ƒ์†ํ•˜๋Š” ์ƒํƒœ์ฝ”๋“œ 400์ธ BusinessException๊ณผ ์ƒํƒœ์ฝ”๋“œ 401์ธ AuthorizationException์„ ๋งŒ๋“ฆ
  • ๋˜ ์ด๋ฅผ ์ƒ์†๋ฐ›๋Š” ๊ฐ๊ฐ์˜ ํด๋ผ์ด์–ธํŠธ์˜ ์š”๊ตฌ์‚ฌํ•ญ์— ๋งž๋Š” ์ปค์Šคํ…€ ์˜ˆ์™ธ ๋งŒ๋“ฆ
  • ControllerAdvice์—์„œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์ด BuisnessException์„ ์žก๊ณ  ์ด ์˜ˆ์™ธ์—์„œ ์ƒํƒœ์ฝ”๋“œ์™€ ์˜ˆ์™ธ ๋ฉ”์‹œ์ง€๋ฅผ ๊บผ๋‚ด๋„๋ก ๊ตฌํ˜„

  • ์ด๋ ‡๊ฒŒ ๋˜๋ฉด ์–ด๋“œ๋ฐ”์ด์Šค์—์„œ ์ˆ˜๋งŽ์€ ์ปค์Šคํ…€ ์˜ˆ์™ธ๋“ค์„ BuisnessException ํ•˜๋‚˜๋งŒ ์žก๊ณ  ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์Œ
  • ์ƒํƒœ์ฝ”๋“œ์— ๋”ฐ๋ฅธ ํด๋ž˜์Šค๋ฅผ ๋‘์–ด ์ด๊ฐ€ ๋ณ€๊ฒฝ๋˜์–ด๋„ ์œ ์ง€๋ณด์ˆ˜๊ฐ€ ์‰ฌ์›€
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ErrorMessage> handleRuntimeException(BusinessException e) {

  return ResponseEntity.status(e.getHttpStatus()).body(e.getErrorMessage());
}

BusinessException

public class BusinessException extends RuntimeException {
    private final HttpStatus httpStatus;
    private final ErrorMessage errorMessage;

    public BusinessException(HttpStatus httpStatus, String errorMessage) {
        this(httpStatus, new ErrorMessage(errorMessage));
    }

    public BusinessException(HttpStatus httpStatus, ErrorMessage errorMessage) {
        this.httpStatus = httpStatus;
        this.errorMessage = errorMessage;
    }

    public HttpStatus getHttpStatus() {
        return httpStatus;
    }

    public ErrorMessage getErrorMessage() {
        return errorMessage;
    }
}

AuthorizationException

public class AuthorizationException extends BusinessException {
    public AuthorizationException(String message) {
        super(HttpStatus.UNAUTHORIZED, message);
    }
}
  • BusinessException์„ ์ƒ์†
  • 401 ์ƒํƒœ์ฝ”๋“œ

BadRequestException

public class BadRequestException extends BusinessException {
    public BadRequestException(String message) {
        super(HttpStatus.BAD_REQUEST, message);
    }
}
  • BusinessException์„ ์ƒ์†
  • 400 ์ƒํƒœ์ฝ”๋“œ

๋ฐฐํฌ ์Šคํฌ๋ฆฝํŠธ

  • ์‹ค์ œ API ์‚ฌ์šฉ์ด ๊ฐ€๋Šฅํ•˜๋„๋ก EC2์— ๋ฐฐํฌํ•˜์—ฌ์•ผ ํ–ˆ์Œ
  • ํ”„๋กœ์ ํŠธ๊ฐ€ ์—…๋ฐ์ดํŠธ๋˜๋ฉด ํ”„๋กœ์„ธ์Šค๋ฅผ ์žฌ์‹œ๋™ํ•ด์•ผํ–ˆ์Œ
  • ์ด๋Ÿฌ๋ฉด ์—ฌ๋Ÿฌ๊ฐ€์ง€ ๋ช…๋ น์–ด๋ฅผ ์‹คํ–‰ํ•ด์•ผ ํ•จโ€ฆ.
  • ์ด๋ฅผ ์Šคํฌ๋ฆฝํŠธ๋กœ ๋งŒ๋“ค์–ด ํ•œ๋ฒˆ์— ์ฒ˜๋ฆฌํ•˜๋„๋ก ๊ตฌํ˜„!

deploy-prod.sh

#!/bin/bash
#!/bin/bash in the top of your scripts then you are telling your system to use bash as a default shell.

REPOSITORY=/home/ubuntu/ # ๊ฒฝ๋กœ ์„ค์ •
PROJECT_NAME=atdd-subway-fare

cd $REPOSITORY/$PROJECT_NAME/ # ๊ฒฝ๋กœ๋กœ ์ ‘๊ทผ

echo "> git reset --hard" 

git reset --hard # ๊นƒํ—ˆ๋ธŒ ์ดˆ๊ธฐํ™”

echo "> git pull origin step1"

git pull origin step1 # pull ๋•ก๊ฒจ์˜ค๊ธฐ

echo "> ํ”„๋กœ์ ํŠธ Build ์‹œ์ž‘"

./gradlew clean build # ๋นŒ๋“œ

echo "> Build ํŒŒ์ผ ๊ฒฝ๋กœ ๋ณต์‚ฌ"

JAR_LOCATION=$(find ./* -name "*jar" | grep atdd-subway-fare)

echo "> ํ˜„์žฌ ๊ตฌ๋™์ค‘์ธ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ pid ํ™•์ธ"

CURRENT_PID=$(pgrep -f ${PROJECT_NAME}[-A-z0-9.]*.jar$) # ์‹คํ–‰์‹œ์ผœ์ ธ์žˆ๋Š” jar pid ๋ฐ›๊ธฐ 


if [ -z "$CURRENT_PID" ]; then # -z ํ”Œ๋ž˜๊ทธ๋Š” null์ธ๊ฒƒ์„ ์ฒดํฌํ•จ, PID๊ฐ€ null์ธ ๊ฒฝ์šฐ if์ ˆ ์•ˆ์œผ๋กœ ๋“ค์–ด๊ฐ
    echo "> ํ˜„์žฌ ๊ตฌ๋™ ์ค‘์ธ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ์—†์œผ๋ฏ€๋กœ ์ข…๋ฃŒํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."
else
    echo "> ํ˜„์žฌ ๊ตฌ๋™์ค‘์ธ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ข…๋ฃŒ(pid : $CURRENT_PID)"
    echo "> kill -15 $CURRENT_PID"
    kill -15 $CURRENT_PID
    sleep 5
fi

echo "> ์ƒˆ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋ฐฐํฌ"
echo "> JAR Location: $JAR_LOCATION" ํ•ด๋‹น jarํŒŒ์ผ ์‹คํ–‰

#nohup java -jar ์‹คํ–‰
nohup java -jar -Dspring.profiles.active=prod ${JAR_LOCATION} 1> log-prod.md 2>&1  &