Skip to content

[BE] 코드 컨벤션

linirini edited this page Aug 2, 2024 · 4 revisions

1. Backend Code Convention

자바 소스 파일의 구조

  • 라이센스 또는 저작권 정보 (있을 경우)
  • package 명세
  • import 명세
    • 와일드 카드(java.enum.*)로는 가져오지 않는다.
    • static import, non-static import는 따로 모아서 블록을 만든다. 블록은 static, non-static 순서다. 블록 사이에는 1줄의 개행을 넣는다.
    • 각 블록 내에서의 import문은 같은 패키지 네임을 갖고 있는 것끼리 모은다.

클래스

  • 하나의 자바 파일 내의 최상위 클래스는 하나여야 한다.
  • 클래스의 첫 줄과 마지막 줄에는 개행을 넣지 않는다.
  • 클래스는 반드시 하나의 역할을 해야한다.
  • 클래스(레코드)의 필드는 애너테이션과 필드 사이에 개행한다.
public record Spot(
        @Column(nullable = false)
        String address,
        @Column(nullable = false, columnDefinition = "DECIMAL(16, 14)")
        BigDecimal latitude,
        @Column(nullable = false, columnDefinition = "DECIMAL(17, 14)")
        BigDecimal longitude
) {
}

메소드

  • 메소드는 다음과 같이 배치한다.

    1. 생성자

    2. static 메서드

    3. 메서드

    4. Override된 메서드

    5. getter & setter

  • 사용되는 메소드는 사용하는 메소드와 최대한 가까운 위치에 있어야한다.

    • 단, A메소드를 사용하는 메소드가 여러 개일경우, A메소드는 마지막으로 사용한 메소드 아래에 위치시킨다.
  • 메소드의 파라미터는 3개를 넘기지 않도록 노력한다.

  • 메서드의 파라미터가 길어질 경우 파라미터마다 개행한다.

    • 파라미터에 붙는 애너테이션 사이에는 개행하지 않는다.
        @GetMapping
        public ResponseEntity<TravelResponses> readAllTravels(
                @MemberId Long memberId,
                @RequestParam(value = "year", required = false) Integer year
        ) {
            return ResponseEntity.ok(travelService.readAllTravels(memberId, year));
        }

중괄호

  • 괄호는 생략하지 않는다.
  • { 의 경우
    • 여는 중괄호 전에는 space-bar를 한다.
    • 여는 중괄호 전에는 개행하지 않는다.
    • 여는 중괄호 뒤에서는 개행한다.} 의 경우
    • 닫는 괄호 앞에서 개행한다.
    • 닫는 괄호 뒤의 개행은, 중괄호가 끝나거나 생성자, 메소드, 클래스가 끝날 때 개행한다.

비어있는 블록

  • 빈 블록도 개행한다.

    public void foo(){
    }

들여쓰기

  • 들여쓰기는 4공백으로 지정한다.

조건문

  • 스위치문은 사용하지 않는다.
  • else if, else문은 사용하지 않는다.

주석

  • 주석은 사용하지 않는다.

제어자

  • 클래스와 멤버의 제어자는 다음과 같은 순서로 배치한다. (순서가 우선순위는 아니다)
    • public, protected, private, abstract, default, static, final
  • 클래스와 멤버는 항상 최소한으로 노출시킨다.
    • privatepackage-privateprotectedpublic
  • 상속하지 말아야 할 클래스는 final로 제약을 건다.
  • 멤버는 가능하다면 항상 final로 만든다.
  • static은 남용하지 않는다.

콤마

  • 모든 콤마 뒤에는 space-bar 를 사용한다.

변수

  • 꼭 필요한 경우에만 Wrapper 타입을 사용하고, 최대한 원시 타입을 쓴다.

네이밍

클래스 이름

  • 클래스 이름은 UpperCamelCase로 작성한다.
  • 클래스 이름은 명사나 명사구로 작성한다.
    • 인터페이스 이름도 명사나 명사구로 작성한다. 하지만, 형용사나 형용사구로 작성해도 된다. ex)Comparable
  • 클래스, 인터페이스 이름은 클래스, 인터페이스의 역할을 표현해야 한다.
  • 구현체 이름은 XXXImpl 은 쓰지 않는다.

메서드 이름

  • 메서드 이름은 lowerCamelCase로 작성한다.
  • 메서드 이름은 동사로 작성한다.
    • 명사가 더 어울릴 수도 있다. ex) Math.pow()

필드 변수 이름

  • 필드 변수는 lowerCamelCase를 사용한다.
  • 필드 변수는 명사나 명사구로 표현한다.

상수 이름

  • 상수 static final 이름은 CONSTANT_CASE로 작성한다. (enum의 필드도 상수다.)

catch절 이름

  • 예외를 잡는 이름은 e로 한다.
try {
	/* some code... */
} catch(IllegalStateException e) {
	/* some code... */
}

테스트 메서드 이름

  • when에서 검증하고자 하는 메서드 이름을 그대로 사용한다. (단, 실패하는 테스트는 앞에 fail을 붙임)

기타

  • 이 외의 이름은 모두 lowerCamelCase를 사용하며 명사나 명사구로 작성한다.

함수

  • 함수(또는 메서드)의 구현부가 10라인(한 줄은 ; 기준)을 넘어가지 않도록 구현한다. 단, 테스트 코드는 예외로 한다.
  • 함수(또는 메서드)가 한 가지 일만 잘 하도록 구현한다.

Swagger

  1. 인터페이스 분리

    Controller의 인터페이스를 분리하여 API 명세 작업을 한다.

    ex) TravelControllerTravelControllerDocs

  2. @Schema

    • 클래스와 메서드 모두 붙여준다.
    • 클래스에는 속성 값으로 description만 설정해준다.
    • 메서드에는 속성 값으로 example만 설정해준다.
    • 해당 애노테이션은 가장 상위에 붙여준다.
  3. 예외

    • 전역적인 예외는 GlobalExceptionHandler에서 명시해준다.
    • 도메인별 특정 예외는 controllerDocs에서 명시해준다.
  4. Parameter

    Path variable이나 Query String, ArgumentResolver에서 처리하는 인자에 대해 명시해준다.

    만약, 클라이언트 쪽에서 직접 전달하는 값이 아닌 경우(ex: ArgumentResolver나 Interceptor에서 처리하는 값) Parameter(hidden=true)를 설정해야한다.

  5. 전역적인 default Media Type은 yml에서 설정했다.

  6. @Tag

    ControllerDocs(클래스 레벨)에 @Tag를 붙여서 namedescription 속성을 설정한다.

예시 코드

@Schema(description = "여행 상세를 생성/수정하기 위한 요청 형식입니다.")
public record TravelRequest(
        @Schema(example = "http://example.com/london.png")
        String travelThumbnail,
        @Schema(example = "런던 여행")
        @NotNull(message = "여행 제목을 입력해주세요.")
        @Size(max = 30, message = "제목의 최대 허용 글자수는 공백 포함 30자입니다.")
        String travelTitle,
        @Schema(example = "런던 시내 탐방")
        @Size(max = 500, message = "내용의 최대 허용 글자수는 공백 포함 500자입니다.")
        String description,
        @Schema(example = "2024-07-27")
        @NotNull(message = "여행 시작 날짜를 입력해주세요.")
        @DateTimeFormat(pattern = "yyyy-MM-dd")
        LocalDate startAt,
        @Schema(example = "2024-07-29")
        @NotNull(message = "여행 끝 날짜를 입력해주세요.")
        @DateTimeFormat(pattern = "yyyy-MM-dd")
        LocalDate endAt) {
    public Travel toTravel() {
        return Travel.builder()
                .thumbnailUrl(travelThumbnail)
                .title(travelTitle)
                .description(description)
                .startAt(startAt)
                .endAt(endAt)
                .build();
    }
}
@Tag(name = "Travel", description = "Travel API")
public interface TravelControllerDocs {
		...
    @Operation(summary = "여행 상세 수정", description = "여행 상세 정보(제목, 내용, 기간)를 수정합니다.")
    @ApiResponses(value = {
            @ApiResponse(description = "여행 상세 수정 성공", responseCode = "200"),
            @ApiResponse(description = """
                    <발생 가능한 케이스>
                                        
                    (1) 필수 값(여행 제목, 기간)이 누락되었을 때
                                        
                    (2) 날짜 형식(yyyy-MM-dd)이 잘못되었을 때
                                        
                    (3) 제목이 공백 포함 30자를 초과했을 때
                                        
                    (4) 내용이 공백 포함 500자를 초과했을 때
                                        
                    (5) 기간 설정이 잘못되었을 때
                                        
                    (6) 변경하려는 여행 기간이 이미 존재하는 방문 기록을 포함하지 않을 때
                                        
                    (7) 수정하려는 여행이 존재하지 않을 때
                                        
                    (8) Path Variable 형식이 잘못되었을 때
                    """,
                    responseCode = "400")
    })
    ResponseEntity<Void> updateTravel(
            @Parameter(description = "여행 상세 ID") Long travelId,
            TravelRequest travelRequest,
            @Parameter(hidden = true) Long memberId);
    ...
}

테스트

  • 테스트 작성은 given, when, then 규약을 따른다. (AAA 패턴: Arrange-Act-Assert)

    • given : 테스트에서 사용할 자원 명시
    • when : 테스트의 동작 수행
    • then : 테스트 결과 검증
    @DisplayName()
    @Test
    void test(){
    	// given
    	
    	// when
    	/* 실행부가 2줄 이상이라면, 테스트 분리를 고려하자. */
    	
    	// then
    	
    }
  • Annotation은 @DisplayName, @Test 순서로 배치한다.

  • DisplayName은 문장 형식으로 작성한다.

  • static import를 사용한다.

    • ex) import static org.assertj.core.api.Assertions.*assertThat*; assertThat(..)..;
  • 검증부가 2줄 이상이라면 assertAll을 사용한다.

예외 메시지

  • 사용자가 다음 단계로 무엇을 해야 할지 안내하자.
    • 지금 사용자가 어떤 상황에 처했는지(상황 설명)
    • 그것이 왜 발생했는지(이유)
    • 해결하려면 어떻게 해야하는지(해결책)
  • 이중 부정 금지
  • 사용자를 위한 적절한 단어 선택
  • 긍정적인 어조 사용

Null

  • 의도된 경우가 아니라면, null을 반환하고 체크하는 코드를 작성하지 않는다.
  • null이 예상되는경우 Optional을 고려한다.
💡 [[구글 자바 컨벤션](https://google.github.io/styleguide/javaguide.html)](https://google.github.io/styleguide/javaguide.html) 과 [[번역본](https://github.com/JunHoPark93/google-java-styleguide)](https://github.com/JunHoPark93/google-java-styleguide)을 참고해서 작성됨

2. Code Review Rules

규칙

  • 존댓말 사용
  • 추상적인 리뷰 금지
  • 피드백은 근거나 예시를 함께 들어서 (ex. 코드 예제, 블로그 링크 첨부)
  • 피드백에 대한 답변
    • 이모지 남기기
    • 수정 요청 피드백
      • 👍 : 확인
      • 🚀 : 피드백 반영 완료
      • 👀 : 다른 리뷰어 피드백에 대한 동의
      • 모든 코드 관련 소통은 댓글로 하기
  • 리뷰 작성 기한
    • 18시 이전이면 다음날 18시까지, 18시 이후면 이틀 뒤 10시 이전까지 리뷰를 남겨준다.

    • 코드 리뷰가 늦어지는 경우, 언제까지 리뷰가 가능한지 슬랙 알림 댓글에 미리 알려준다.

    • 급한 PR의 경우, 슬랙 PR방에 리뷰어들 태그해서 알림을 보낸다. (간단한 사유와 기한 명시)

      @폭포 @리니
      로그인 구현 , 바로 다음 기능 구현해야 해서 7 27 오후 7시까지 리뷰 부탁드립니다. ^^