본문 바로가기
프로그래밍/Project

[Spring] ToyProject 2단계(3) - 정리

by 코딩중독 2024. 2. 4.

목차

     

    일주일이 순식간에 지나가고 프로젝트 제출하고 정리의 시간이 되었습니다!😁

     

    모두가 각자의 역할에 책임을 다 해준 아름다운 결과물이 나왔습니다!

     

    프로젝트를 진행하면서 기억하고 넘어가면 좋은 내용들을 정리합니다.

     

    LocalDateTime 직렬화/역직렬화를 위한 준비

    jackson 라이브러리에서 objectmapper가 남모르게😅 직렬화와 역직렬화를 해줍니다.

    implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.2'

     

     

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    @Builder
    class Trip {
        private String name;
        private String startingPoint;
        private String destination;
        private LocalDateTime departureDateTime;
        private LocalDateTime arrivalDateTime;
    }
    
    @RestController
    @RequestMapping("/api")
    public class TestController {
    
        private final Trip trip;
    
        public TestController() {
            this.trip = Trip.builder()
                .name("오사카")
                .startingPoint("인천공항")
                .destination("간사이공항")
                .departureDateTime(LocalDateTime.of(2024, 1, 13, 9, 30))
                .arrivalDateTime(LocalDateTime.of(2024, 1, 13, 11, 30))
                .build();
        }
    
        @GetMapping("/trip")
        public Trip getTrip() {
            return trip;
        }
    
    }

     

    Trip 모델에 간단한 데이터를 넣고 출력!

    하지만 Java에서 LocalDateTime타입을 데이터로 출력했을 때 당황스러운 결과가 나온다?!

    {"name":"오사카","startingPoint":"인천공항","destination":"간사이공항","departureDateTime"}

     

    결과도 아니고 그냥 비정상 데이터가 출력됩니다. LocalDateTime 타입을 가지고 있는데 departureDateTime에서 그냥 잘려버리고 뒤에 있는 arrivalDateTime은 존재 자체도 모르게 된다.

     

    의존성 추가

    implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.15.2'

     

    추가하고 JavaTimeModule이 포함되어 있고 까보면 다음과 같이 직렬화/역직렬화에 포함이 되어 있다.

    JavaTimeModule

     

    그럼 의존성만 추가한 상태로 다시 출력을 해본다면..

     

    {
      "name":"오사카",
      "startingPoint":"인천공항",
      "destination":"간사이공항",
      "departureDateTime":[
        2024,
        1,
        13,
        9,
        30
      ],
      "arrivalDateTime":[
        2024,
        1,
        13,
        11,
        30
      ]
    }

     

    jackson 친구가 추가된 의존성으로 LocalDateTime에 대해 학습은 한것같지만 저렇게 생긴 데이터는 사용자에게 보여주기 위해서는 한번 더 손을 봐줘야 하는 번거로움이 있다.

     

    어노테이션 추가

        @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", shape = JsonFormat.Shape.STRING)
        private LocalDateTime departureDateTime;
    
        @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", shape = JsonFormat.Shape.STRING)
        private LocalDateTime arrivalDateTime;

     

    @JsonFormat 어노테이션을 추가한다면

    이것봐 너도 할 수 있잖아...

    {
      "name":"오사카",
      "startingPoint":"인천공항",
      "destination":"간사이공항",
      "departureDateTime":"2024-01-13 09:30:00",
      "arrivalDateTime":"2024-01-13 11:30:00"
    }

     

     

    여정 타입을 위한 enum과 inner class 사용

    • 하나의 여행에는 n개의 여정이 포함되어 있습니다.
    • 각 여정 정보는 이동정보, 체류정보, 숙박정보 등의 종류를 포함할 수 있습니다.

    즉 하나의 여행정보는 여러 개의 여정정보를 포함할 수 있고 각 여정은 이동, 체류, 숙박 세 가지의 타입을 가질 수 있다.

     

    프로젝트에서 요청(입력)은 RequestDTO, 응답(출력)은 ResponseDTO를 사용하고 있다.

    여정정보를 입력할 때에 여정의 타입은 필수 입력 값이지만 타입에 따라 입력해야 하는 정보가 다르다.

    뷰 페이지가 있었다면 타입을 선택할 때 입력하는 폼을 각각 다르게 줘서 데이터를 받을 수 있었겠지만 뷰 페이지가 없는 상태에서 RequestDTO는 모든 타입을 입력받을 수 있게 방치(?)했다.

    하지만 응답값을 줄 때 null 값이 보이는 상황은 용납할 수 없어서 입력된 타입에 따라 Response이동, Response숙박, Response체류 3개의 inner class를 갖는 ResponseDTO를 만들어서 사용했다.

    enum의 용도는 입력받을 때에도 3가지 타입 이외의 입력값을 받지 않기 위함인데 시간관계상 입력 후에 출력데이터를 분류하는 용도로만 사용하게 됐다. 하루의 시간이 더 있었다면...

     

    Converter 사용

    요청과 응답의 흐름에 따라 데이터를 변환해줘야 하는 상황이 있다.

    요청으로 RequestDTO가 입력된다면 데이터베이스로 전달하기 전에 Entity로 변환, 데이터베이스에서 Entity를 전달해 준다면 ResponseDTO로 변환.

    여행정보를 응답해 주는 DTO에 시간정보가 String으로 만들어진 내용을 확인했지만 학습을 하는 단계이기에 LocalDateTime으로 바꾸지 않고 Converter 단계에서 LocalDateTime을 원하는 포맷의 문자열 형태로 변환하는 부분을 포함했다.

    public class TripConverter {
        public Trip toEntity(TripDTO dto) {
            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    
            String startDate = dto.getStartDate();
            String endDate = dto.getEndDate();
    
            return new Trip(dto.getId(), dto.getUserId(), dto.getName(),
                LocalDateTime.parse(startDate, formatter),
                LocalDateTime.parse(endDate, formatter),
                dto.getStatus());
        }
    
    }

     

    응답 데이터 처리를 위한 ResponseEntity 사용

    ResponseEntity는 사용자의 요청에 대한 응답 데이터를 포함하는 클래스로 HttpEntity를 상속받고 HttpStatus, HttpHeaders, HttpBody를 포함하고 있다.

    public class ResponseEntity<T> extends HttpEntity<T> {

     

    status와 body만 사용했다.

    제네릭에 데이터 타입을 담아서 사용하면 끝!

    데이터를 보내줄 때는 DTO를, 성공/실패를 알려줄 때는 String을 사용했다.

     

    public ResponseEntity<List<TripDTO>> getAllTrip(@PathVariable Long userId) {
    }
    
    public ResponseEntity<String> editTrip(@RequestBody TripDTO dto) {
    }

     

     

    예외처리

    사용자가 요청한 응답이 null일 수 있는 상황에 대한 예외처리

    (사용자가 로그인을 요청했을 때 입력한 이메일이 없거나 비밀번호가 틀렸을 때, 등록되지 않은 여행아이디를 조회했을 때, 수정하려는 여정의 아이디가 없을 때 등..)

    public TripDTO getTrip(Long id) {	// 여행아이디로 여행데이터 조회
    	// 데이터베이스에서 전달받은 객체 null 체크
        return Optional.ofNullable(tripMapper.getTrip(id))
            .map(trip -> {
            	// null이 아니라면 여정 데이터 setting
                settingPlanIntoTrip(trip, id);
                return trip;
                // null이라면 Exception message 전달
            }).orElseThrow(() -> new RuntimeException("Trip Search Failed"));
    }

     

    위 예시에도 적혀 있지만, 사용자와 여행, 여정 등 여러 개의 Controller에서 예외를 처리해줘야 하는 상황이라 모든 컨트롤러의 예외를 잡아줄 클래스 생성!

    @RestControllerAdvice
    public class GlobalExceptionHandler {
    
        @ExceptionHandler(RuntimeException.class)
        public ResponseEntity<String> runtimeExceptionHandler(RuntimeException e) {
            return ResponseEntity.status(400).body(e.getMessage());
        }
    
        @ExceptionHandler(IllegalArgumentException.class)
        public ResponseEntity<String> illegalExceptionHandler(IllegalArgumentException e) {
            return ResponseEntity.status(400).body(e.getMessage());
        }
    
    }

     

    스프링부트 프로젝트로 학습하던 때와 헷갈려서 component 스캔을 환경설정에서 추가했던 내용을 잊고 다른 패키지에 GlobalExceptionHandler 클래스를 생성하고 왜 잡아주지??? 왜???

     

    <context:component-scan base-package="exception"/>

     

    해당 패키지를 등록하거나 이미 등록된 패키지 내에 클래스를 생성하면 된다.

     

    이렇게 Spring Legacy를 이용한 토이프로젝트가 끝났다.

     

    다음 프로젝트는 SpringBoot로 진행하겠지?????