프로젝트 Git Hub 링크

개발 기간 : 2024-08-13 ~ 2024-08-16


요구 사항

깃허브

개인용 개인 캘린더 프로젝트

API를 직접 구현하고, Spring의 3 layer architecture를 준수하면서 개발을 진행한다.

핵심 기능은 일정 작성, 선택한 일정 조회, 일정 목록 조회, 선택한 일정 수정, 선택한 일정 삭제이며, 요구사항은 각각 다음과 같다.

일정 작성

  • 할일, 담당자명담당자 ID, 비밀번호, 작성/수정일 을 저장한다.
  • 각 일정의 고유 식별자를 자동으로 생성하여 관리
  • 최초 입력시 수정일과 작성일은 동일하다
  • 등록된 일정의 정보를 반환한다.

    선택한 일정 조회

  • 선택한 일정 단건의 정보를 조회할 수 있습니다.
  • 일정의 고유 식별자(ID)를 사용하여 조회합니다.

    일정 목록 조회

  • 조건 : 수정일, 담당자명담당자 ID을 입력 받아 조회한다.
  • 수정일을 기준으로 내림차순 정렬한다.

    선택한 일정 수정

  • 할일, 담당자명담당자 ID만 수정한다.
  • 비밀번호도 함께 전달해서 일치할 경우만 수정한다.
  • 수정한 내용은 반환한다.

    선택한 일정 삭제

  • 선택한 일정을 삭제한다.
  • 비밀번호도 함께 전달해서 일치할 경우만 수정한다.

API

위 요구사항을 통해 API를 작성할 수 있다.

기능 Method URL request response 상태코드
일정 작성 POST /events Body 등록 정보 200 : 정상작성,404 : 조회불가
선택한 일정 조회 GET /events/{eventId} Param 단건 응답 정보 200 : 정상조회,404 : 조회불가
일정 목록 조회 GET /events?manId&updateDay query 다건 응답 정보 200 : 정상조회, 404 : 조회불가
선택한 일정 수정 PUT /events/{eventId} Body 수정 정보 200 : 정상수정,403: 비밀번호 입력 오류
선택한 일정 삭제 DELETE /events/{eventId} Body 삭제 id 200 : 정상삭제, 403: 비밀번호 입력 오류

ERD

ERD

이를 이용해서 API를 구현한다.


API 구현

3 layer architecture에 맞춰서 Controller - Service - Repository로 Class를 구분한다.

IoC

DI를 위해서 Controller , Service , Repository은 각각 Service, Repository, JdbcTemplate 객체를 Bean을 이용해 생성자 주입 한다.

Controller

Controller는 Clinet의 입력을 마주하는 곳이다. 즉, Controller에서 API를 작성하게 된다.

Controller의 Componet는 @Controller@RestController로 볼 수 있는데, 현 프로젝트에선 View를 구현하지 않고 PostMan으로만 구동을 체크하기 때문에 @RestController만 사용한다.

또한, 각 Entity 마다 URL의 통일을 위해 @RequestMapping("/managers")와 같이 설정한다.

각 API는 HTTP Method에 맞춰서 Mapping하고, 각 입력들을 Dto를 이용해서 Service에 전달한다. 또한 각 반홥 값들을 Dto를 이용해서 Client에게 반환한다.

Service

Service는 비즈니스 로직을 구현하는 곳이다. 그러나 본 프로젝트는 CRUD를 위주로 구현하기 때문에 별도의 구현할 비즈니스 로직이 없다.

그래서 본 Service는 대부분 Repository의 메소드를 호출하거나, 객체가 null인지 판단하는 역할만 한다.

이곳에서 도출된 결돠는 Controller로 반환된다.

Repository

Repository는 DB와 소통하면서 DB의 데이터를 추가, 삭제, 수정, 조회 등을 담당한다.

따라서 JDBC를 사용하는 본 프로젝트에선 Repository에 직접 sql문을 사용해서 DB 내부 데이터를 조작한다.

이곳에서 도출된 결과는 Service로 반환된다.


추가 테이블 생성

기존 Event 테이블만 존재하던 Table에 Manager 테이블이 추가 되었다.

Manager의 primary key는 Event의 ManID와 외래키 관계를 맺으며, 앞으로 Event를 생성할 때 manID는 이미 db에 존재하는 Manager만 대상으로 할 수 있기 때문에 Manager 생성이 우선되어야 한다.

Manager도 Event와 동일하게 CRUD를 지원한다. 다만, Controller-Service-Repository는 Event와 구별해서 작성해야하기 때문에 {table명}Controller과 같이 table명~ 네이밍 규칙을 사용한다.


페이지네이션

Event가 많아진다면, 한번에 많은 Evnet를 출력시 내가 원하는 Event를 찾기 힘들 수 있다. 혹은 한 페이지에 너무 많은 Event를 출력하면서 사용자 편의성이 떨어질 수 있다.

이를 방지하기 위해서 데이터의 양을 한 페이지에 몇개씩 출력하는 방식을 페이지네이션 이라고 한다.

현재는 JDBC를 사용하기 때문에 sql의 LIMIT 문을 사용해서 조절을 진행한다.

우선, 페이지에 출력될 형태는 Event와 Manager의 name이므로 새로운 타입을 생성해야 한다. 나는 이를 Page클래스을 생성해서 반환 클래스로 사용한다.

또한 한 페이지에 몇 개의 Event를 포함할지, 현 페이지는 몇 페이지인지 받기위한 PageSpec클래스도 생성한다.

String sql = "SELECT e.eventId, e.todo, e.manId, e.createDay, e.updateDay, m.name " +
                "from event e join manager m on e.manId = m.manId " +
                "ORDER BY e.eventId LIMIT ?, ?";

다음과 같은 sql문을 이용해서 Event의 출력 대상과 manager의 이름까지 출력대상으로 하고, Limit로 시작 페이지, 한 페이지당 출력 개수를 매개변수로 사용하게 된다.

Long offset = pageSpec.getStartNum();
Long pageSize = pageSpec.getPageSize();

return jdbcTemplate.query(sql, (resultSet, rowNum) -> {
            Page page = new Page();
            page.setEventId(resultSet.getLong("e.eventId"));
            page.setTodo(resultSet.getString("e.todo"));
            page.setManId(resultSet.getString("e.manId"));
            page.setCreateDay(resultSet.getDate("e.createDay"));
            page.setUpdateDay(resultSet.getDate("e.updateDay"));
            page.setName(resultSet.getString("m.name"));
            return page;
        }, offset, pageSize);

위 처럼 offset을 현 페이지, pageSize를 한 페이지당 출력 개수를 지정해서 반환한다.

이는 Service-> Controller를 거쳐서 Client에게 전달되는데, 전달되는 결과는 다음과 같다. 나의 경우 한 페이지에 3개를 출력하게 했고, 앞 번호들 중 일부는 삭제 및 업데이트를 통해 뒷 페이지로 밀려났기 때문에 정상적으로 출력된다고 볼 수 있다.


예외 발생 처리

본 프로젝트에서 예상할 수 있는 예외는 다음과 같다.

  1. 수정, 삭제시 비밀번호 불일치
  2. 선택한 일정 정보 조회 불가일 경우 예외 발생

준비

예외 처리관련 클래스들을 관리하기 편하도록 exception 패키지를 통해서 관리한다.

수정, 삭제시 비밀번호 불일치

이 예외를 처리하기 위해선 IncorrectPasswordException이라는 에외 클래스를 생성한다.

package com.kcm.demo.exception;

public class IncorrectPasswordException extends RuntimeException {
    public IncorrectPasswordException(String message) {
        super(message);
    }
}

일정 정보 미확인

이 예외를 처리하기 위해선 IncorrectEventException라는 클래스를 생성한다.

package com.kcm.demo.exception;

public class IncorrectEventException extends RuntimeException{
    public IncorrectEventException(String message) {
        super(message);
    }
}

RuntimeException

위 두 클래스를 보면 RuntimeException를 상속하고있다. RuntimeException은 JVM의 작동 중에 발생할 수 있는 예외의 슈퍼 클래스이다. 이를 상속해서 개인화된 예외를 발생시킬 수 있다.

GlobalExceptionController

이제 내가 만든 예외들을 처리하기 위해서 커스텀 핸들러를 구현한다.

@ControllerAdvice //예외 처리 클래스
public class GlobalExceptionController {
~~~
}

와 같이 커스텀 핸들러를 구현하는데, @ControllerAdvice어노테이션을 사용하면, 본 Spring 전역에서 발생하는 예외를 이곳에서 핸들링 할 수 있다.

이후 구현한 2개의 예외를

  @ExceptionHandler(IncorrectPasswordException.class)
    public ResponseEntity<String> handledIncorrectPasswordException(IncorrectPasswordException x) {
        return ResponseEntity.status(HttpStatus.FORBIDDEN).body(x.getMessage());
    }

  @ExceptionHandler(IncorrectEventException.class)
    public ResponseEntity<String> handledIncorrectEventException(IncorrectEventException x) {
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(x.getMessage());
    }

와 같이 ResponseEntity.status(반환코드).body(내용)으로 반환을 하면된다.

반환 코드는 우리가 아는 403, 404 .. 등등이며 Spring은 import org.springframework.http.HttpStatus;를 통해 제공해주고 있다. 이후

      Event selectEvent = eventRepository.findById(eventId);
        if(selectEvent!=null){
            return new EventResponseDto(selectEvent);
        }
        else{
            throw new IncorrectEventException("존재하지 않는 일정입니다.");
        }

와 같이 try-catch 문으로 예외를 catch하면 해당 메시지가 x.getMessage()를 통해 body에 담겨서 사용자에게 전달된다.


Valid

null 체크 및 특정 패턴에 대한 검증을 하기위해서 사용하는 어노테이션인 @Valid이다.

이 기술은 내 경험상 dependencies설정이 전부다..

우선 나의 경우 첫 환경설정에서는 Valid가 작동을 안했다. 그 이유는 먼저 dependencies설정에 implementation 'javax.validation:validation-api:2.0.1.Final'로 설정을하고, javax를 통해서 @Valid을 사용하고자 했는데, 전혀 동작을 안했다.

인프런을 둘러보니 Validation 모듈은 implementation 'org.springframework.boot:spring-boot-starter-validation'이걸 사용하며 javax가 아닌 jakarta를 사용해야한다는 것이다..

수정하니 바로 @Valid가 정상작동하는걸 경험할 수 있었다..

@Valid

Client가 body를 통해 값을 전달하면 Server에서는 ‘이 값에 내가 필요로 하는게 다 있나?’를 검증을 해야한다. 이걸 해주는게 @Valid이다. 검증은 Controller에서만 이루어지거나, 다른 위치에서도 이루어지는것으로 구분되는데, 본 프로젝트는 Controller에서만 검증했다.

이를 위해선 Client의 RequestDto와 Controller에 작업이 필요하다.

RequestDto

RequestDto에선 검증 대상 필드를 작업해야한다.

public class EventRequestDto {

    @NotNull(message = "Todo cannot be null")
    @Size(max =200)
    private String todo;
    private String manId;

    @NotNull(message = "password cannot be null")
    private String password;
    private Date createDay;
    private Date updateDay;
}

위 코드를 보면 @NotNull어노테이션과 @Size 어노테이션을 확인할 수 있다.

나의 경우 todo와 password는 필수적으로 입력받아야 하며, todo의 크기는 200이 넘어선 안된다는 조건이 있기 때문에 위와 같이 설정했다.

Controller

@RestController
@RequestMapping("/events")
@Validated
public class EventController {~~}

위와 같이 @Validated를 추가해서 현 Controller에서 검증을 해달라는 표시를 한다. 이후 내가 검증을 해야하는 기능에

public EventResponseDto createEvent(@Valid @RequestBody EventRequestDto eventRequestDto) {

        return eventService.createEvent(eventRequestDto);
}
    ...
    
public EventResponseDto updateEvent(@PathVariable Long eventId, @Valid @RequestBody EventRequestDto eventRequestDto) {

        return eventService.updateEvent(eventId, eventRequestDto);
}

위와 같이 @Valid 어노테이션을 사용한다.

이러면 RequestDto에서 @NotNull, @Size등 내가 설정한 조건들을 이용해서 입력값을 검사하고, 해당 조건에 충족하지 않으면

다음과 같이 메시지를 전달한다.