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

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

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

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

 

오늘은 협업 미션 2단계 나머지 요구 사항을 구현하려고 노력했습니다. 

 

 

협업 미션 에러 핸들링

어제에 이어서 오늘도 에러 핸들링을 하였습니다. 멤버와 인증 부분이었는데, 이메일과 패스워드 입력 값 검증을 어떻게 할지가 관건이었습니다.

 

 

    @NotBlank(message = "이메일에 공백만 있을 수 없습니다.")
    @Length(min = 4, max = 20, message = "이메일은 4글자 이상 20글자 이하여야합니다.")
    @Email(message = "이메일 형식에 맞춰서 작성해야 합니다. (ex. eamil@email.com)")
    private String email;

    @NotEmpty(message = "패스워드에 공백만 있을 수 없습니다.")
    @Length(min = 4, max = 20, message = "패스워드는 4글자 이상 20글자 이하여야합니다.")
    @Pattern(regexp = "^[a-z|A-Z|0-9]*$", message = "패스워드는 영어 또는 숫자여야 합니다.")
    private String password;

    @NotNull
    @Positive(message = "나이는 양수여야 합니다.")
    private Integer age;

 

 

다행히 이메일은 @Email 어노테이션을 제공하고 있어서 이를 사용하면 되고, 디테일한 부분은 정규표현식을 사용했습니다. 또한, 나이는 양수여야하므로 @Positive를 사용했습니다. 마지막으로 문자열이 아닌 자료형에 대해서는 @NotEmpty나 @NotBlank를 사용할 수 없습니다.

 

 

javax.validation.UnexpectedTypeException: HV000030: No validator could be found for constraint 'javax.validation.constraints.NotEmpty' validating type 'java.lang.Long'. Check configuration for 'personNo'

 

 

위와 같이 올바르지 않은 타입에 어노테이션을 붙였다는 에러가 뜨므로 @NotNull정도만 쓰시길 바랍니다.

 

 

협업 미션 인터셉터 적용

프론트 측에서 조회는 비로그인 유저도 가능하되, 생성, 수정, 삭제에 대해서는 로그인 유저만 가능하도록 기능을 구현해 달라고 요청하셨습니다. 이것은 인터셉터를 적용함으로써 해결할 수 있었습니다.

 

 

public class LoginInterceptor implements HandlerInterceptor {

    private final AuthService authService;

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

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        if ("OPTIONS".equals(request.getMethod()) || "GET".equals(request.getMethod())) {
            return true;
        }
        final String accessToken = AuthorizationExtractor.extract(request);
        if (Objects.isNull(accessToken)) {
            throw new AuthorizationException("로그인을 먼저 해야 합니다.");
        }
        authService.validateToken(accessToken);
        return true;
    }
}

 

 

다만, 조회에 대해서는 이 인터셉터에 잡혀도 항상 true가 나와야 합니다. 그래서 OPTIONS 메소드 외에 GET 메소드에 대해서도 프리 패스할 수 있도록 처리했습니다. 이후에, 액세스 토큰이 null이라면 비로그인이므로 로그인을 먼저 하라고 요청하면 됩니다.

 

 

중복된 이메일인지 확인하는 API 구현

사실 이 기능 자체는 이미 구현되어있었습니다. 다만, API 명세가 없었기에 GET 방식인지, POST 방식인지도 정하지 않았고, url도 존재하지 않았습니다. 그래서 이를 규정해야했는데, 'GET 방식 + 쿼리스트링"을 할지, 'POST 방식 + 바디"을 할지 고민이 있었습니다. 처음에, 저는 POST 방식은 새 리소스 생성 시에만 사용하는 것으로 이해하고 전자를 택했으나, 제리의 조언을 통해 후자로 바꿨습니다.

 

 

 

 

새 리소스 생성 시에 POST 방식을 사용하는 것은 맞지만, 꼭 그럴 때만 POST 방식을 사용하는 것은 아닙니다. 2번째와 같이 "조회"에 성격이 아니고, 요청 데이터를 처리할 경우에도 POST 방식을 사용한다고 합니다. 즉, 이메일 중복 여부를 검사해달라는 것은 조회보다는 요청 데이터 처리에 가까우므로 POST 방식이 적합하다는 것이죠.

 

 

    @PostMapping("/members/email-check")
    public ResponseEntity<Void> checkDuplicatedEmail(@Valid @RequestBody EmailRequest emailRequest) {
        memberService.checkDuplicatedMemberEmail(emailRequest);
        return ResponseEntity.ok().build();
    }

 

 

그래서 위와 같이 POST 방식에다가 바디에는 Email을 넣어주는 구조로 기능을 구현했습니다.

 

 

환승 정보가 포함된 지하철역 API 구현

프론트 요구 사항에는 환승 정보가 포함된 지하철역에 대해서 몇호선이 있는지 보여주라는 것이 있었습니다. 그래서 백엔드에서 이에 관련된 API를 구현해 주기로 하였습니다.

 

 

[
    {
        "id": 1,
        "name": "강남역",
        "transfer": ["1호선", "2호선"]
    }
    , ...
]

 

 

응답은 위와 같고, transfer 항목에는 환승 가능한 노선의 이름이 들어가야 합니다. StationDao가 LineDao와 SecDao를 가져서 쿼리를 여러 번 보내는 방법도 있으나, 저는 Join을 통해 한 번의 쿼리만으로 노선의 이름을 얻도록 만들었습니다.

 

 

select distinct(LINE.name) from SECTION 
inner join LINE on LINE.id = SECTION.line_id 
where SECTION.up_station_id = ? OR SECTION.down_station_id = ?

 

 

위와 같이 LINE과 SECTION을 join한 뒤, 특정 지하철역의 ID와 같은 데이터만 걸러주고 LINE.name을 중복 제거해서 반환하는 것입니다.

 

 

    public List<StationTransferResponse> findAllStationWithTransfer() {
        List<Station> stations = stationDao.findAllAscById();

        List<StationTransferResponse> stationTransferResponses = new ArrayList<>();
        for (final Station station : stations) {
            List<String> transfer = stationDao.findTransfer(station.getId());
            stationTransferResponses.add(StationTransferResponse.from(station, transfer));
        }
        return stationTransferResponses;
    }

 

 

그리고 모든 지하철역을 가져와서 하나씩 환승 정보를 구합니다. 이 방식은 구현은 편하나, DB를 수백 번에서 수천 번 찔러야하므로 비효율적입니다. 다만, 한 번의 쿼리만으로 모든 환승 정보를 가져오는 방법을 아직 생각하지 못해서 추후 리팩토링할 예정입니다.

 

 

정리

프론트의 요청을 받아서 새로운 기능을 구현하고, 성공하는 결과를 눈으로 직접 보는 과정이 즐거웠습니다. 그리고 그 안에서도 새로운 개념을 배울 수 있었습니다. 오늘은 웬만한 요구 사항을 다 끝냈으므로 내일은 스프링 강의를 들을 계획입니다.

댓글

추천 글