TIL

[240103] 심화 프로젝트 마무리 :: 컨벤션

진진리 2024. 1. 3. 10:41
728x90
지금까지 해왔던 프로젝트들 중에서 가장 알차면서도 힘들었던 심화 프로젝트를 마무리하면서
프로젝트 동안 배운 것들을 조금씩 정리해보려고 한다.
구현 기능, ERD, API 명세, 기술 스택 등은 깃허브 레포지토리의 readme에서 확인할 수 있다.

 

깃헙 주소: https://github.com/yejin-kingdom/value-together

 

GitHub - yejin-kingdom/value-together: 가치 있는일을 같이 진행하자! 일정 공유 어플리케이션!

가치 있는일을 같이 진행하자! 일정 공유 어플리케이션! . Contribute to yejin-kingdom/value-together development by creating an account on GitHub.

github.com


컨벤션

이번 조에서는 특히나 전체적인 코드의 통일성을 위해서 컨벤션을 설정하고 지키기 위한 노력을 많이 하였다.

이번에 새로 시도해본 것들을 정리해보려고 한다.

 

1. Mapper 사용

Service 단에서는 dto를 반환하기 위해 entity를 response dto로 변환하는 과정을 거치게 된다.

지금까지는 생성자를 이용해 객체 간 변환 작업을 수행했다면 이번에는 Mapper를 사용하였다.

Mapper를 사용하니까 코드를 작성하기 편리해지면서도 entity단의 코드가 간결해져서 더 보기 좋았다.

  • build.gradle 의존성 주입
implementation 'org.mapstruct:mapstruct:1.5.3.Final'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.3.Final'
  • Mapper 코드 예시 - TeamServiceMapper
@Mapper
public interface TeamServiceMapper {

    TeamServiceMapper INSTANCE = Mappers.getMapper(TeamServiceMapper.class);

    TeamCreateRes toTeamCreateRes(Team team);

    @Mapping(source = "user.username", target = "username")
    TeamMemberGetRes toTeamMemberGetRes(TeamRole teamRole);

    @Mapping(source = "teamRoleList", target = "memberGetResList")
    TeamGetRes toTeamGetRes(Team team);
}

 

2. Spotless 사용

협업을 하다보면 코드 간의 충돌이 발생하게 되는데 어떤 때는 공백과 같은 코드 스타일에 의해 충돌이 발생하게 된다.

spotless를 사용하게 되면 빌드 때마다 자동으로 spotless를 적용하도록 설정할 수 있고, 스타일이 맞지 않을 경우 빌드에 실패하도록 미리 막아줄 수 있다.

따라서 이번 조에서는 spotless를 통해 최대한 충돌을 방지하면서 협업할 수 있었다.

  • build.gradle에 Spotless 적용
plugins {
    id 'com.diffplug.spotless' version '6.11.0'
}

spotless {
    java {
        // 사용하지 않는 import 제거
        removeUnusedImports()
        // 구글 자바 포맷 적용
        googleJavaFormat()
        indentWithTabs(2)
        indentWithSpaces(4)
        // 공백 제거
        trimTrailingWhitespace()
        // 끝부분 New Line 처리
        endWithNewline()
    }
}

// 의존성을 설정해줌 test 끝나고 spotlessjava 수행되도록
tasks.named("spotlessJava").configure { dependsOn("compileTestJava") }

 

 

3. 기타 코드 컨벤션

  • entity
    • setter, 메소드 사용 지양
    • private builder 사용
    • 외부에서 필드값을 수정할 수 없도록 막기 위해
  • api 1개마다 req, res dto 1개씩 사용
    • 유지보수성을 위해
  • Custom ResultCode, ResponseEntity 사용
@Getter
@RequiredArgsConstructor
public enum ResultCode {

    // 성공 0번대
    SUCCESS(HttpStatus.OK, 0, "정상 처리 되었습니다"),

    // 글로벌 1000번대
    SYSTEM_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 1000, "알 수 없는 에러가 발생했습니다."),
    ACCESS_DENY(HttpStatus.FORBIDDEN, 1001, "접근 권한이 없습니다."),
    NOT_FOUND_FILE(HttpStatus.NOT_FOUND, 1002, "해당 파일을 찾을 수 없습니다."),
    NULL_FILE_TYPE(HttpStatus.BAD_REQUEST, 1003, "해당 파일의 확장자를 찾을 수 없습니다."),

    // 유저 2000번대
    NOT_FOUND_USER(HttpStatus.NOT_FOUND, 2000, "존재하지 않는 유저입니다."),
    INVALID_TOKEN(HttpStatus.BAD_REQUEST, 2001, "잘못된 토큰"),
    EXPIRED_TOKEN(HttpStatus.BAD_REQUEST, 2002, "만료된 토큰"),
    EMAIL_SEND_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, 2003, "이메일 전송에 실패하였습니다."),
    NOT_FOUND_EMAIL(HttpStatus.NOT_FOUND, 2004, "해당 이메일을 찾을 수 없습니다."),
    INVALID_USERNAME_PATTERN(HttpStatus.BAD_REQUEST, 2005, "username 형식에 맞지 않습니다."),
    // . . . 생략

    private final HttpStatus status;
    private final Integer code;
    private final String message;
}
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class RestResponse<T> implements Serializable {

    private HttpStatus status;
    private Integer code;
    private String message;
    private T data;

    public static <T> RestResponse<T> success(T data) {
        return RestResponse.<T>builder()
                .status(ResultCode.SUCCESS.getStatus())
                .code(ResultCode.SUCCESS.getCode())
                .message(ResultCode.SUCCESS.getMessage())
                .data(data)
                .build();
    }

    public static <T> RestResponse<T> error(ResultCode resultCode) {
        return RestResponse.<T>builder()
                .status(resultCode.getStatus())
                .code(resultCode.getCode())
                .message(resultCode.getMessage())
                .build();
    }
}

 

  • 네이밍 규칙
    • dto: 도메인 + 기능 + Req/Res
    • Entity: 도메인명 그대로
    • 테이블명: tb_도메인명
    • id: 도메인명 + id
  • validator file로 검증 로직을 분리: Optional도 제외
    • 다른 도메인의 검증 로직이 필요한 경우가 많으므로 분리하여 global하게 사용
  • Service 인터페이스와 impl로 분리
    • 유지보수성을 위해
  • Repository 기능을 제한: @RepositoryDefinition 사용
    • 기본적으로 모든 기능을 제공하는 JpaRepository의 메소드를 제한하기 위해
  • 패키지 구조: domain / global / infra로 분리

  • 테스트 코드 작성
    • 테스트 변수를 모아 상속받아 사용
    • 테스트 코드에 대한 컨벤션을 설정하는 것도 좋겠다는 생각을 하였음
public interface TeamRoleTest extends TeamTest, UserTest {
    Long TEST_TEAM_ROLE_ID = 1L;
    Role TEST_TEAM_ROLE_ROLE = Role.LEADER;

    TeamRole TEST_TEAM_ROLE =
            TeamRole.builder()
                    .teamRoleId(TEST_TEAM_ROLE_ID)
                    .user(TEST_USER)
                    .team(TEST_TEAM)
                    .role(TEST_TEAM_ROLE_ROLE)
                    .build();
}

깃허브 issue, PR 관련

  • 템플릿 사용
  • 커스텀한 라벨 사용
  • PR마다 관련 이슈를 생성

issue 템플릿, 라벨을 사용하여 issue 작성 - 커밋 메세지에 반영

 

PR 템플릿, 라벨을 사용하여 PR 작성

  • 다음과 같이 issue와 PR을 작성하니 작성한 코드가 어떤 기능과 관련이 있는지 한눈에 보기 좋았고 나중에 기록을 찾아보기에도 용이했다.