이번 글에서는, 4호선톤에서 개발한 내용을 바탕으로 실제 서비스 오픈을 할 때 까지의 개발 내용을 다룰 것이다. 크게 Github, DB 구현, API 리팩토링, 배포에 관련된 내용으로 구성되어 있다.
[Github] gitmoji
깃허브에서, 다른 사람들의 레파지토리를 보면 커밋 메세지가 예쁘게 되어있는 것을 본 적이 있을 것이다. 사용 예시는 다음과 같다.
확실히 커밋메세지가 깔끔해 보인다. 뿐만 아니라 커밋메세지 맨앞의 이모지들이 다 특별한 의미가 있었다. https://gitmoji.dev/ 여기에 작성된 이모지들의 설명을 바탕으로 해당 커밋이 어떤 내용을 포함하고 있는지 알 수 있었다. https://inpa.tistory.com/entry/GIT-%E2%9A%A1%EF%B8%8F-Gitmoji-%EC%82%AC%EC%9A%A9%EB%B2%95-Gitmoji-cli 를 통해 깃모지를 사용하는 이유와 사용하는 방법을 배웠고, 이것을 알고 난 이후로는 커밋할 때 계속 깃모지를 쓰고 있다! 아래는 온라인 칠판 편지의 백엔드 레포의 커밋메세지이다.
[Github] Project, Issue, PR
개발을 하다 보면 내가 어떤 내용을 개발해야 하는지, 개발은 어디까지 진행했는지 관리하기 힘들 때가 있다. 노션에 기록하는 방법도 있지만 깃허브에서 프로젝트를 관리할 수 있는 기능도 제공해주기 때문에 이 기능을 사용해보기로 했다. 지금은 모두 Done 상태이지만, 개발 당시에는 해야할 일들을 Issue로 등록해놨다. 어떤 기능이 개발이 끝났는지, 어떤 기능이 추가 개발이 필요한지 등을 한 눈에 볼 수 있었다.
또한, Labels를 통해 기능 개발이면 enhancement, 어떤 것을 제거하는 것이면 delete 를 붙였다.
PR을 할 때, 급하지 않은 상황에서는 최대한 다음과 같은 양식을 사용하여 해당 커밋에서 어떤 Issue와 관련이 있는지, 어떤 변경사항이 있었는지 상세하게 기록하는 습관을 들였다. 이 때, close, refs 등의 키워드를 사용하여 연관된 Issue를 표시했다. close 키워드를 사용하여 이슈 번호를 지정하면 해당 이슈가 자동으로 closed 상태로 변경된다! 항상 이렇게 예쁘게 기록을 남겨놓으면 기분이 좋았다 ㅎㅎ!!
[DB 구현] 모든 클래스에 created_at, updated_at 작성
DB에 데이터가 저장될 때 기본적으로 저장되어야 하는 필드로, created_at, updated_at 을 포함시켜야 한다. 이전에 4호선톤에서는 일부 엔티티에 직접 필드를 넣었지만, 이번에는 BaseEntity를 만들어 모든 엔티티에서 BaseEntity를 상속받아 created_at, updated_at 필드가 포함되게 했다. created_at은 엔티티가 생성될 때 자동으로 값이 채워지며, updated_at은 엔티티에 변경이 일어났을 때 자동으로 값이 변경되도록 설정했다. BaseEntity에서는 @MappedSuperclass 와 @EntityListeners(AuditingEntityListener.class)를 사용하고 created_at에는 @CreatedDate를, updated_at에는 @LastModifiedDate 를 사용했다. Main 클래스에서는 해당 기능을 사용하기 위해 @EnableJpaAuditing을 붙였다.
[API] 공통 응답 형식 사용
4호선톤에서는, 데이터를 다음과 같은 형식으로 작성했다.
{
"blackboardCount" : 8
}
서비스 오픈을 준비하면서, 모든 API의 ResponseBody에 다음과 같은 응답 형식을 갖추도록 하였다.
{
"status":200,
"data":{
"blackboardCount":1
},
"message":"칠판 개수가 정상적으로 조회되었습니다."
}
이렇게 작성하니 프론트에서 응답을 더 직관적으로 확인할 수 있고, 응답의 유연성도 확장되었다. 이를 위해 공통 응답 클래스인 ApiResponse 클래스를 정의하였고, 해당 클래스에서는 모든 클래스들이 접근할 수 있도록 static 메소드를 작성했다. createSuccess는 status를 임의로 정할 수 있고, data는 어떠한 데이터도 포함될 수 있게 타입매개변수로 사용했다. message는 이 API를 통해 어떤 것이 이루어졌는지 보여질 수 있게 했다. createFail로도 status, message를 사용했고, data는 무조건 null로 반환하게 했다.
서비스 계층에서의 실제 사용 예시는 다음과 같았다.
[API] CustomErrorController 사용
오류가 발생할 시 프론트 측에서, 해당 오류가 서버 문제라는 것을 알려주기 위해 내가 작성한 ApiResponse 클래스의 형식에 맞게 응답을 내려주도록 했다. 이 Controller는 ErrorController를 구현하여 기본 에러 페이지 경로에 대한 처리를 커스터마이징한다. 이 경로를 통해 애플리케이션에서 발생하는 모든 에러를 처리할 수 있도록 했다. 이 Controller를 통해 내려오는 응답의 예시는 다음과 같다.
{
"status":500,
"data":null,
"message":"서버 에러가 발생하였습니다."
}
[API] GlobalExceptionHandler 사용
이 클래스로 다양한 유형의 예외를 효율적으로 관리하고 처리할 수 있었고, 코드의 중복을 줄이고 유지 보수성을 향상시킬 수 있었다. 여기에서 다루는 Exception 클래스는 MethodArgumentNotValidException.class, ConstraintViolationException.class, EntityNotFoundException.class, BlackboardEntityDuplicateException.class가 있다. EntityNotFoundException.class, BlackboardEntityDuplicateException.class는 내가 만든 Custom Exception 클래스이다.
[API] 서비스 계층에서 인터페이스와 해당 인터페이스를 구현하는 클래스로 분리 (ISP)
4호선톤에서는, Controller에서 Service 클래스를 바로 사용하도록 했다면, 이번에 리팩토링을 하면서는 Service를 위한 Interface를 만들고, 해당 Interface를 구현하는 SerivceImple 클래스를 만들었다. 설계패턴 수업에서 배운 SOLID 중 ISP를 적용할 수 있었다.
[API] dto 클래스의 사용
이전에는 dto 클래스의 사용 목적은 알았지만, 이 클래스를 잘 활용하는 방법은 몰랐다. 깃허브를 둘러보다가 추천으로 어떤 레파지토리가 떴는데, https://github.com/cheese10yun/spring-jpa-best-practices/blob/master/doc/step-01.md 바로 이 글이였다. API 개발의 초보라고 할 수 있었던 내가, 이 글을 보고 많은 것을 배웠고 이번 프로젝트 이후 계속 이 방법을 사용하게 되었다. 이 글과 완전 동일하다고는 말할 수 없지만, 내 나름대로 dto 클래스를 사용하는 방법을 정하기로 했다.
기능별로 dto 클래스를 나누고 (ex. createBlackBoardDto, readBlackBoardDto) 해당 기능을 구현하는 Controller 에서 @RequestBody 가 존재하는 경우, 해당 dto 클래스 내부에 static으로 Req 클래스를 static으로 만들고 또 그 내부에 필드를 정의하는 형식으로 작성했다. 여기저기 흩어져있어 정확히 어떤 dto 클래스인지 확인하기 어려웠던 것을 알아보기 쉽게 정리했다.
[API] 서비스 계층에서 공통되는 로직 분리 - Custom Exception 클래스 생성과 사용
서비스 계층에서 칠판을 찾을 때 없으면 응답을 생성해서 반환하는 로직을 다음과 같이 작성했다.
// 칠판 찾기, 없으면 404
Optional<BlackBoardEntity> findBlackBoard = blackBoardRepository.findBlackBoardsByUserId(userId);
if(findBlackBoard.isEmpty()) {
ApiResponse<Object> res = ApiResponse.createFailWithoutData(404, "칠판을 찾을 수 없습니다.");
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(res);
}
// 칠판 엔티티 가져오기
BlackBoardEntity foundBlackBoard = findBlackBoard.get();
서비스 계층에서 세 개의 메소드들이 해당 로직을 사용했다. 중복되는 코드의 사용을 줄이기 위해 Custom Exception을 만들어 처리하기로 했다. 아래와 같이 EntityNotFoundException을 정의하고
public class EntityNotFoundException extends RuntimeException {
public EntityNotFoundException(String message) {
super(message);
}
}
GlobalExceptionHandler 클래스에서 이 Exception을 다루도록 했다.
@ExceptionHandler(EntityNotFoundException.class)
public ResponseEntity<ApiResponse<?>> handleEntityNotFoundException(EntityNotFoundException e) {
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(ApiResponse.createFailWithoutData(HttpStatus.NOT_FOUND.value(), e.getMessage()));
}
모든 서비스 계층에서 이 로직을 사용할 수 있게 ServiceUtils 클래스를 만들어 다음과 같이 작성하였다. 타입매개변수를 활용해 특정 엔티티 뿐만 아니라 다른 엔티티도 이 메소드를 이용해 엔티티가 존재하는지 확인할 수 있게 했다.
public class ServiceUtils {
public static <T> T getEntityOrThrow(Optional<T> optional, String message) {
return optional.orElseThrow(() -> new EntityNotFoundException(message));
}
}
실제 서비스 계층에서는 최종적으로 다음과 같은 코드로 해당 엔티티가 존재하는지 확인할 수 있었다.
// 칠판 찾고 가져오기
BlackBoardEntity foundBlackBoard = ServiceUtils.getEntityOrThrow(
blackBoardRepository.findBlackBoardsByUserId(userId),
"칠판을 찾을 수 없습니다."
);
[테스트 코드] Jacoco
나는 만들어진 API를 최종적으로 점검하고, API를 호출하는 시간이 얼마나 걸리는지 확인하기 위해 테스트 코드를 작성하였다. 테스트 코드를 쓰고 문서를 만드는 데에는 Jacoco를 사용하였다. 처음 사용했는데, 사용 방법이 어렵지는 않았다. 테스트 코드의 결과 문서도 자동으로 생성해줘 편리했다. 테스트 코드를 작성하는 것은 따분한 일이라고 생각했지만 성능 개선과 배포 전에는 무조건 거쳐야 하는 단계라고 생각하고 테스트 코드를 작성하였다. 실패가 뜨면 정말 무서웠다.. 테스트 코드를 작성하며 내가 개발한 API를 최종 점검할 수 있었다!
[배포] 로깅
API 호출이 얼마나 되는지, 만약 문제가 발생한다면 어디에서 발생하는지 알기 위해 배포 서버에서 로그를 확인할 수 있도록 Interceptor를 작성하고 등록했다. 의존성은 logback을 사용하였다.
implementation 'ch.qos.logback:logback-classic:1.2.3'
implementation 'ch.qos.logback:logback-core:1.2.3'
resources/logback.xml 을 작성했다. 로그 파일은 하루 단위로 작성되게 했으며, 해당 파일은 ubuntu 서버의 루트(/) 경로의 logs 폴더에 저장되게 작성했다.
어떤 정보를 로그 파일에 찍을건지 LoggingInterceptor 클래스에 작성하고, LogConfig에 LoggingInterceptor 클래스를 등록했다.
내가 찍고싶은 정보는 다음과 같았다. 실무에서는 어떤 정보들을 로그로 남기는지 몰라 GPT의 도움을 받아 필요한 정보들을 추출했고 관련 정보를 로그로 남겼다.
logger.info("Request - Method: {}, URI: {}, Client IP: {}, User-Agent: {}, Authorization: {}",
request.getMethod(),
request.getRequestURI(),
request.getRemoteAddr(),
request.getHeader("User-Agent"),
request.getHeader("Authorization"));
로그에 찍히는 데이터의 예시는 다음과 같다.
Request - Method: POST, URI: /api/users/login, Client IP: 192.168.1.1, User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36, Authorization: Bearer exampleToken
[배포] 쉘 스크립트
ubuntu에 git 원격저장소와 연결된 폴더가 존재한다. 아직 CI/CD, Github Actions와 같은 것을 공부하지 못해 변경사항이 있으면 처음에는 cd folder, gradlew build ~, nohup java -jar ~.jar 처럼 명렁어를 사용해 배포를 했다. 하지만 이렇게 명령어를 일일이 치면 그 시간 동안 사용자들이 서비스를 제대로 이용하지 못할 것 같아 최대한 재배포 시간을 줄이는 방법을 찾다가 쉘 스크립트를 작성하는 법을 알아냈다. 쉘 스크립트를 작성하는 방법은 https://coding-orange.tistory.com/115 여기에 자세히 기록해 두었다.
서비스 오픈을 준비하며 배운 내용은 여기까지이다. 개발을 진행하면서 기록해 둔 내용을 이렇게 글로 풀어내니 정말 많은 것을 배운 것이 느껴진다. 앞으로도 기록하는 습관을 꾸준히 들여야겠다. 아직 CI/CD, 무중단 배포, docker를 공부하지 않은 상태였기에 다음에 실제 서비스를 내거나 해커톤에 참여하여 배포를 한다면 인프라적인 부분에서 더 힘을 쏟고 싶다.
서비스 개발 총 후기
처음에 4호선톤에 참여하면서, 이 프로젝트를 실제 서비스로 만든다는 것은 상상하지도 못했다. 팀원들도 너무 좋은 사람들을 만났고, 각자의 위치에서 모두 최선을 다했기 때문에 이런 결과를 낼 수 있었다고 생각한다. 앞으로도 다양한 사람들과 함께 프로젝트를 해보고 싶고, 성장을 이루도록 끊임없는 노력과 배움이 필요하다는 것을 느꼈다. 백엔드 개발자로 더 성장하여 최고가 되는 그날까지!! 열심히 해야겠다 ㅎㅎ
+) 모든 백엔드 소스코드는 https://github.com/balckBoard-4line/blackBoard-server 여기에 있습니다. (시간 날 때 swagger로 API 문서화도 할 예정이다!)
+) 졸업은 아니였지만, 서비스를 써보고 싶어서 나도 칠판을 만들어 보았다!! ^_^
+ GA 보고서이다!
'Projects' 카테고리의 다른 글
멋쟁이사자처럼 12기 운영진 해커톤, 트렌디톤 후기 (0) | 2024.03.14 |
---|---|
온라인 칠판 편지 개발과 서비스 오픈까지(2) (1) | 2024.03.05 |
온라인 칠판 편지 개발과 서비스 오픈까지(1) (0) | 2024.03.03 |