Hustle #11. 처음 시작하는 사람도 작업할 수 있도록, 스프링 부트 개발 컨벤션 정하기
안녕하세요, 다람쥐입니다.
지난 포스팅에선 허슬의 프로젝트 구조를 잡아봤습니다.
이제 본격적인 개발을 시작했습니다.
오프라인으로 허슬 팀원들과 다 같이 모였는데요.
해야 할 작업을 분배했고, 다음 오프라인 모임 때 까지 개발을 해보자고 했습니다.
하지만 큰 난관이 있었습니다..!
😅 생각치도 못한 문제가...
그 다음 모임은 오프라인으로 서버 팀끼리 모였습니다.
역 근처 카페안의 공간을 대여했어요.
온라인으로 작업하다가 처음으로 서버 팀끼리 모여서
스프링 부트 개발이 이번이 처음인 팀원들이 있었는데요.
서버 실행하는 것조차 난관이었다고 하더라고요.
저번에 Jasypt 암호화 라이브러리를 추가해서 애플리케이션을 실행할 때 암호화 키를 전달을 해줘야 했습니다.
당시에 노션으로도 정리를 해서 공유를 했었어요.
별다른 말이 없었어서 잘 하고 있는 줄 알았는데 몇몇 사람은 어려웠다고 하더라고요.
실행도 못하다 보니 작업도 매우 더뎠습니다.
오프라인으로 만나길 잘했다 생각했고 모여서 설정을 도와주고 작업을 같이 봐줬습니다.
오프라인으로 만나서 코드 리뷰도 실시간으로 했는데요.
그 동안은 오고 가는 커뮤니케이션이 많아서 시간을 많이 썼었어요.
오프라인으로 코드 리뷰를 해서 시간을 많이 아낄 수 있었어요.
그러던 중에 온라인으로 코드 리뷰하면 왜 이렇게 많이 걸릴까? 고민이 되었습니다.
스프링 부트 개발 컨벤션 정하기
각자의 작업물을 모아보니 가장 먼저 눈에 띄는 점이 있었어요.
바로, 코드 스타일이 모두 다르다는 점이었어요.
스프링 부트 개발을 처음 하는 팀원들은, 개발할 줄 아는 팀원의 코드를 따라 쳤다고 하더라고요.
그럼에도 개발하는 게 솔직하게 쉽지 않았다고 합니다.
그래서 개발 컨벤션을 확정하고 일종의 매뉴얼처럼 만들자고 생각을 했어요.
복잡한 비즈니스 로직이 아닌 이상, CRUD 는 어느정도 작성 패턴이 비슷하다는 걸 느꼈는데요.
처음 하는 사람도 작업을 할 수 있도록 다음과 같은 점을 고려했어요.
- 변수명, 상수명, 함수명, 클래스명 등 줄여쓰지 않고 명확하게 쓰기
- 다소 길다는 점을 의식해 IDE의 자동완성 기능을 사용하는 걸 권장합니다.
- IDE 에서 자동으로 이름을 지을 때 클래스명의 카멜케이스를 알려주는데, 이를 그대로 사용할 수 있습니다.
- 예시. logs -> homeMatchResultPostScoreLogs, MatchResultPost post -> MatchResultPost matchResultPost
- @Transactional 어노테이션은 클래스에 붙이는 것보다 메서드에 바로 붙임 (메서드 코드 조각만 보고도 바로 파악하기 위함)
- 서비스 코드에서 조회 메소드는 무조건 @Transactional(readOnly = true) 어노테이션 붙이기 ( 하위 메소드는 선택 )
- 서비스 코드에서 생성 / 수정 / 삭제 메소드는 무조건 @Transactional 어노테이션 붙이기 ( 하위 메소드는 선택 )
- DTO 는 총 3가지의 종류가 있습니다.
- 엔티티를 담는 엔티티 DTO, 응답 DTO 에서 사용합니다.
- 엔티티 DTO 는 '엔티티명 + ResponseDTO' 이름을 사용합니다. ex) CompetitionResponseDTO
- 엔티티 DTO 는 Lombok 의 Builder 패턴 기능을 사용합니다.
- 엔티티 DTO 는 Lombok 의 Getter 어노테이션을 사용합니다.
- from 정적 메소드로 엔티티 정보를 엔티티 DTO 로 변환합니다.
- 엔티티 DTO 는 여러 응답 DTO 에서 이용합니다.
- 매 번 엔티티 정보를 엔티티 DTO 로 변환하는 코드가 중복되어 코드량이 길어지는 문제가 있습니다.
- 엔티티 DTO 에서 표현해야할 값이 많고 많은 연관 관계가 포함된다면 그에 따라 코드량이 길어집니다.
- 긴 코드량의 중복을 막기 위해 엔티티 DTO 의 정적 메소드로 코드를 분리했습니다.
- 아래 처럼 from 메소드를 호출하여 단일 값을 변환하거나, 배열로 변환할 때 stream().map() 메소드에 전달할 수 있습니다.
- 엔티티 -> 엔티티 DTO 변환 예시
- ClubResponseDTO.from(club)
- 엔티티 배열 -> 엔티티 DTO 배열 변환 예시
- List<MatchResultPostResponseDTO> matchResultPostResponseDTOs =
matchResultPosts.stream()
.map(MatchResultPostResponseDTO::from)
.collect(Collectors.toList());
- List<MatchResultPostResponseDTO> matchResultPostResponseDTOs =
- 엔티티 -> 엔티티 DTO 변환 예시
- 컨트롤러에서 받는 요청 DTO. 이후 서비스 단으로 보냅니다.
- 요청 DTO 는 '메소드명 + 엔티티명 + RequestDTO' 이름을 사용합니다. ex) CreateCompetitionRequestDTO
- 요청 DTO 는 Lombok 의 Getter 어노테이션을 사용합니다.
- 서비스 단에서 반환할 응답 DTO, 이후 컨트롤러에서 응답으로 보냅니다.
- 응답 DTO 는 '메소드명 + 엔티티명 + ResponseDTO' 이름을 사용합니다. ex) CreateCompetitionResponseDTO
- 응답 DTO 는 Lombok 의 Builder 패턴 기능을 사용합니다.
- 응답 DTO 는 Lombok 의 Getter 어노테이션을 사용합니다.
- 응답 DTO 는 2 가지의 종류가 있습니다.
- 페이지네이션이 없는 응답 DTO. 데이터는 단일 값이 들어가거나 배열이 들어갑니다.
- 페이지네이션이 있는 응답 DTO. 현재 페이지 수, 총 페이지 수, 현재 페이지 갯수를 포함하고, 데이터는 무조건 배열로 응답합니다.
- 공통 응답 DTO 형식이 있습니다.
- code : 응답의 종류를 알려주는 상수, 성공과 실패를 구분하고 어떤 응답인지 알려줍니다. ex) SUCCESS_GET_COMPETITIONS, SUCCESS_DELETE_COMPETITION, USER_NOT_FOUND (예외)
- message : 코드에 대한 설명을 문장으로 서술합니다. ex) "성공적으로 대회 목록을 조회했습니다."
- data : 엔티티 객체를 표현하는 엔티티 DTO 를 응답합니다. 하나일 경우 단일 값으로, 여러 개 일 경우 배열로 표현합니다.
- count : (페이지네이션 응답 시) 현재 페이지의 총 개수를 나타냅니다.
- totalPage : (페이지네이션 응답 시) 총 페이지 수를 알려줍니다.
- totalCount : (페이지네이션 응답 시) 총 개수를 알려줍니다.
- error : (RunTime Exception 발생시) error 값 아래에 code 와 message 값이 들어갑니다.
- 페이지네이션 시 필드를 통일하기 위해 공통 페이지 BasePageable<xxxxxResponseDTO> 클래스 사용
- 엔티티를 담는 엔티티 DTO, 응답 DTO 에서 사용합니다.
- JPA 의 더티 체킹 기능을 의도적으로 사용하지 않습니다. 더티 체킹으로 DB 에 반영된다고 해도 명시적으로 리포지토리의 저장 메소드를 호출합니다.
- 더티 체킹으로 값이 변경되면 커밋 시점에 영속성 컨텍스트에 있는 1, 2차 캐시의 값과 비교하여, 값이 다르면 캐시 값 업데이트 후 DB 에 반영합니다.
- 그러나 코드를 파악할 때 트랜잭션이 커밋되는 시점을 한 눈에 파악하기 힘들고, 더티 체킹이 이뤄진다는 걸 파악하기 힘듭니다.
- DB 에 명시적으로 저장한다는 걸 한 눈에 알기 위해 리포지토리의 save 메소드를 사용합니다.
- 예시.
- matchResultPost.updateHomeTeam(
awayEntryTeam,
updateMatchResultPostRequestDTO.getAwayScore(),
updateMatchResultPostRequestDTO.getIsAwayWin(),
awayMatchResultPostScoreLogs
);
matchResultPostRepository.save(matchResultPost);
- matchResultPost.updateHomeTeam(
- 복잡한 쿼리는 JPA 보다 QueryDSL 이용, 코드 재사용 가능하도록 BooleanExpression 을 활용하여 작성
- Controller 에선 Service 으로 UserId 와 RequestDTO 를 그대로 넘기고 ResponseDTO 를 받아 바로 반환해주는 로직으로 통일
- 필드 검증은 Spring Bean Validation 어노테이션과 서비스 단에서 검증
- 공통 조회 로직 (findById 등) 은 UserUtils, CompetitionUtils 등으로 static method 로 분리 ( 불필요한 공통 orElseThrow 코드 반복 방지 )
- 코드가 여러 곳에서 재사용 된다면 분리
- Service 에서 검증하는 로직은 다른 메서드 validateXXX(XXX : 보통 Service 메서드명) 으로 분리
- 해당 메서드 밑에 작성하여 한 눈에 검증 로직이 들어오도록 메서드 위치 조정
- TODO 주석을 제외하고 개발 중에 일어난 모든 불필요한 주석은 최종 커밋에 반드시 삭제 ( 타인이 그 주석을 삭제하기가 판단 내리기 어려움 )
- 필요한 주석은 충분한 부연 설명을 포함시키고 코드 가독성을 해치지 않는 선에서 작성
- 코드 개행은 가로가 너무 길거나, 세로로 읽기가 편할 때 작성
- Builder 패턴의 경우 Competition.builder() 까지만 작성하고 모든 체이닝 메서드는 개행으로 구분
- 변수가 선언하는 위치와 사용하는 위치는 최대한 가깝게 위치
기본적으로 클린 코드 기반으로 저희들만의 그라운드 룰을 정했어요.
그 동안의 온오프라인 코드 리뷰를 바탕으로 위와 같은 많은 내용들이 오고갔어요.
코드 컨벤션 이후
코드 컨벤션 문화가 정착된 후, 개발 생산성이 향상됐어요.
어떻게 컨트롤러와 서비스, 엔티티, DTO 를 작성하고, CRUD 에 대한 이해가 잡혔다고 피드백을 받았어요.
모두 동일한 코드 퀄리티를 유지한 채로 허슬 프로젝트 API 서버를 작성하게 됐습니다.
긴 글 읽어주셔서 감사합니다.