깃허브 링크: https://github.com/Jaehyeon-Han/TaskManagementProject
Lv. 3의 요구사항은 할일 테이블에 작성자 이름 대신 작성자 id를 저장하는 것이었다. 이는 작성자 이름 값이 작성자를 고유하게 식별할 수 없기 때문이다.
하지만 할일을 등록하는 과정에서 사용자가 id를 제공해야 한다면, 어떤 식으로던 id를 받을 수 있는 방법이 존재해야 한다고 생각했다. 이를 위해 단순히 Controller-Service-Repository를 하나 더 만드는 방법을 생각했고, 학습 내용에서 벗어나지 않아 추가로 구현하였다.
사용자는 /users에 먼저 등록과정을 거쳐야 하며, 반환받은 id로 글을 작성할 수 있다. 자세한 내용은 깃허브 루트 디렉토리의 README.md와 openapi.yml에서 확인할 수 있다.
DTO는 두 계층 간에만 사용돼야 한다고 알고 있었다. 하지만 강의자료와 온라인의 일부 예제에서는 컨트롤러의 폼 객체에 가까운 요청 객체를 리포지토리까지 가져가는 경우도 있었다. 이에 대해 튜터님에게 질문하였는데, DTO 변환과 관리에 대한 작업량과 DTO 객체에의 의존성에 대한 트레이드오프를 생각해보라고 답변해주셨다. 이와 관련하여 더 조사하여 글을 작성할 계획이다.
이 과제에서는 편의상 컨트롤러의 요청 객체와 응답 객체를 서비스 계층에서 동일하게 사용하였고, 리포지토리는 교체되기 쉬워야 한다고 생각하여 리포지토리에는 별도의 DTO(TaskDto, UserDto)를 정의하여 넘겨주었다.
API 명세를 작성하는 방식에 대한 규정은 없었지만, 임의의 항목을 넣어놓은 표가 아니라 많은 사람이 동의하는 내용과 구조로 작성하고 싶었다. 그래서 API 명세 작성법에 대해 찾아보던 중 OpenAPI 명세를 알게되었고, 사실상 표준에 가깝다고 생각되어 OpenAPI 명세를 따라 작성해보기로 하였다.
명세는 분량 상 글에는 포함하지 않았고, 깃허브 루트 디렉토리의 openapi.yml에서 확인할 수 있다.
다음과 같이 tasks 테이블을 만들었다.
CREATE TABLE tasks
(
task_id BIGINT AUTO_INCREMENT PRIMARY KEY,
task VARCHAR(200) NOT NULL,
author_id BIGINT REFERENCES USERS (user_id),
password VARCHAR(20) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_modified_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
그리고 다음 코드를 실행했을 때, created_at 칼럼과 last_modified_at 칼럼의 기본값이 설정되지 않는 문제가 발생했다.
SimpleJdbcInsert simpleJdbcInsert = new SimpleJdbcInsert(jdbcTemplate);
simpleJdbcInsert.withTableName("tasks")
.usingGeneratedKeyColumns("task_id");
Map<String, Object> parameters = new HashMap<>();
parameters.put("task", taskDto.getTask());
parameters.put("author_id", taskDto.getAuthorId());
parameters.put("password", taskDto.getPassword());
return simpleJdbcInsert.executeAndReturnKey(parameters).longValue();
Jdbc의 로깅 기능을 활성화하니 다음처럼 명시적으로 NULL을 넣는 것을 확인할 수 있었다.
o.s.jdbc.core.simple.SimpleJdbcInsert :
The following parameters are used for call INSERT INTO tasks
(task, author_id, password, created_at, last_modified_at)
VALUES(?, ?, ?, ?, ?) with: [task, 1, password, null, null]
반면 usingColumns를 사용하니 지정한 칼럼에 대해서만 값을 넣어주었다.
o.s.jdbc.core.simple.SimpleJdbcInsert :
The following parameters are used for call INSERT INTO tasks
(task, author_id, password)
VALUES(?, ?, ?) with: [task2, 1, password]
더불어 일부러 매개변수 이름을 틀리게 넣어보니 해당 값(출력의 두 번째 인자)도 NULL로 설정되는 것도 확인할 수 있었다.
// usingColumns 미사용
// ...
parameters.put("author_ids", taskDto.getAuthorId()); // wrong parameter name
// ...
o.s.jdbc.core.simple.SimpleJdbcInsert :
The following parameters are used for call INSERT INTO tasks
(task, author_id, password, created_at, last_modified_at)
VALUES(?, ?, ?, ?, ?) with: [task, null, password, null, null]
처음에는 편의를 위해 예외 응답값을 ResponseEntity<String>으로 설정하였다. 하지만 JSON의 필드 에러를 검증하려다 보니 ResponseEntity<Map<String, String>>도 반환하게 되었고, 결과적으로 한 엔드포인트에 대해 text/plain과 application/json 두 형태의 오류 메시지가 올 수 있는 형태가 되어버렸다. 바꾸고 검증할 시간이 부족하여 그냥 두었는데, 다음에는 API 명세를 작성할 때부터 오류 응답 형식을 정해두고 작성해야겠다는 생각이 든다.
schema.sql와 data.sql을 이용해서 DB 초기화 작업까지는 자동화해두었지만, 여전히 하나의 계층에서라도 수정이 일어나면 전체 API를 Postman으로 실행해야 하는 점이 번거로웠다. 테스트가 없다면 변경을 쉽게 할 수가 없다는 점을 절실하게 느꼈다. 시간을 내어 계층 간에 단위 테스트를 할 수 있는 방법이나 통합 테스트의 자동화 방법이 있는지 학습하려 한다.
처음에는 tasks 테이블만 두었기에 테이블명과 관련 DTO 필드명을 모두 id와 password로 두었다. 하지만 users 테이블이 추가되며 두 테이블 간에 동일한 이름을 갖는 칼럼이 등장했다. 또한 JOIN을 해서 값을 불러오는 경우, DTO 필드명도 모호하게 해석되기 시작했다. 처음부터 task_id라고 설정했으면 좋았을 거라는 생각이 든다.