컨벤션 설정

2026. 5. 19. 19:20·BackEnd/프로그래머스 데브코스

개념공부까지 마친 뒤 본격적인 구현에 대한 내용을 작성하기 전 우리 팀이 설정한 컨벤션들에 대해 공유하려고 한다.

컨벤션 및 그라운드 룰을 간과하는 사람들이 있는데 팀 프로젝트를 할 때 매우매우매우 중요한 것이라고 생각한다.

특히, 취준생에게는 설정부터 지키는 것 까지 매우 중요하다.

회사마다 컨벤션이 존재할 것이고 이를 따라야 하기 때문이다.

내가 속한 회사가 때로는 비효율적인 방식으로 개발하고 있거나

내가 해왔던 방식과 다른 방식으로 흘러가고 있을 확률이 높다고 생각한다.

이런 상황에서 내 방식만을 고집할 수 없는 것 또한 현실이다.

 

우리가 회의하면서 정한 Git 컨벤션이다.

 

1. Issue

name: "Feature"
description: "새로운 기능 추가"
title: "feature: "
labels: ["feature"]
body:
  - type: input
    id: branch_keyword
    attributes:
      label: "Branch keyword"
      description: "브랜치 키워드 (예: login-api, user-create)"
      placeholder: "login-api"
    validations:
      required: true

  - type: textarea
    id: description
    attributes:
      label: "이슈 설명"
      description: "구현할 내용 / 요구사항 / 배경"
      placeholder: |
        예)
        - user crud 구현
        - 회원가입/로그인 API 추가
    validations:
      required: true
name: Create branch from issue

on:
  issues:
    types: [opened]

permissions:
  contents: write
  issues: write

jobs:
  create-branch:
    runs-on: ubuntu-latest

    steps:
      - name: Create branch
        uses: actions/github-script@v8
        with:
          script: |
            const issue = context.payload.issue;
            const repo = context.repo;
            const body = issue.body || "";
            function extractField(label) {
              const regex = new RegExp(`###\\s*${label}\\s*\\n\\s*([^\\n\\r]+)`, "i");
              const match = body.match(regex);
              return match ? match[1].trim() : "";
            }
            function normalize(value) {
              return value
                .toLowerCase()
                .replace(/[^a-z0-9/_-]/g, "-")
                .replace(/-+/g, "-")
                .replace(/\/+/g, "/")
                .replace(/_+/g, "-")
                .replace(/\/-/g, "/")
                .replace(/-\//g, "/")
                .replace(/^\/|\/$/g, "")
                .replace(/^-|-$/g, "");
            }
            // 1) Issue Form에서 값 추출
            const packageRaw = extractField("Package name");
            const keywordRaw = extractField("Branch keyword");
            if (!packageRaw) {
              core.warning("Package name not found. Skip creating branch.");
              return;
            }
            if (!keywordRaw) {
              core.warning("Branch keyword not found. Skip creating branch.");
              return;
            }
            // 2) 정규화
            const packageName = normalize(packageRaw);
            const featureName = normalize(keywordRaw);
            if (!packageName || !featureName) {
              core.warning("Normalized package name or branch keyword is empty. Skip creating branch.");
              return;
            }
            // 3) 라벨 기반 prefix 결정
            const labels = (issue.labels || []).map(l => (typeof l === "string" ? l : l.name));
            const allowedPrefixes = new Set([
              "feature", "hotfix", "fix", "refactor", "deploy", "chore", "test"
            ]);
            let prefix = "chore";
            for (const label of labels) {
              if (allowedPrefixes.has(label)) {
                prefix = label;
                break;
              }
            }
            // 4) 브랜치명 생성
            const issueNumber = issue.number;
            const branchName = `${prefix}/${packageName}/${featureName}/${issueNumber}`;
            // 5) base 브랜치
            const baseBranch = "dev";
            // 6) base SHA 가져오기
            const base = await github.rest.git.getRef({
              owner: repo.owner,
              repo: repo.repo,
              ref: `heads/${baseBranch}`,
            });
            const sha = base.data.object.sha;
            // 7) 브랜치 생성
            try {
              await github.rest.git.createRef({
                owner: repo.owner,
                repo: repo.repo,
                ref: `refs/heads/${branchName}`,
                sha,
              });
            } catch (e) {
              if (e.status === 422) {
                core.warning(`Branch already exists: ${branchName}`);
              } else {
                throw e;
              }
            }
            // 8) 안내 댓글
            await github.rest.issues.createComment({
              owner: repo.owner,
              repo: repo.repo,
              issue_number: issueNumber,
              body: [
                `✅ 브랜치 생성됨: \`${branchName}\``,
                `- base: \`${baseBranch}\``,
                `- package: \`${packageName}\``,
                `- keyword: \`${featureName}\``,
              ].join("\n"),
            });

 

우리는 github actions를 통해 사진에 보이는 Package name, Branch keyword를 작성하면 자동으로 브랜치가 생성되게 설정을 했다.

프로젝트 시작하자마자 팀원중 한 명이 뚝딱 만들어주셨다. 짱이다.

2. Branch

  1. 브랜치는 배포용 브랜치인 main 브랜치와 개발용 브랜치인 dev 브랜치로 나누어 관리한다.
  2. 모든 작업은 이슈를 먼저 생성한 뒤, dev 브랜치를 기준으로 feature branch를 생성하여 진행한다.
  3. 브랜치는 이슈 단위로 생성하며, 브랜치명은 아래 규칙에 맞춰 작성한다.
    • {prefix}/{package-name}/{branch-keyword}/{issue-number}
  4. prefix는 이슈 라벨과 동일한 값을 사용한다.
    • 예: feature, fix, refactor, chore, test, deploy, hotfix
  5. package-name은 작업 대상 모듈명을 사용하고, branch-keyword는 작업 내용을 대표할 수 있는 기능 키워드로 작성한다.
  6. 브랜치 예시는 다음과 같다.
    • feature/user/login/12
    • fix/payment/validation-error/18
    • refactor/order/service-split/23
  7. 작업이 완료되면 dev 브랜치로 Pull Request를 생성하고, merge 이후 작업 브랜치는 삭제한다.
  8. 배포가 필요한 경우 dev 브랜치에서 main 브랜치로 merge 한다.

3. Commit

  1. 커밋 메세지 규칙은 유다 시티 컨벤션을 사용한다.
feat: commit 핵심 내용

---
body -> 해결 내용(필요한 경우 이유 설명)
- 1번 기능 구현
- 2번 기능 구현
---
<!-- 필요 시 --> 
Closes: #이슈 번호
See also: #이슈 번호
Co-authored-by: agfalcon <agfalcon12@gmail.com>
  • commit prefix:
    • fix: 버그 수정
    • docs : 문서 작업
    • refactor : 어디에도 해당하지 않는 코드 수정
    • hotfix : 급한 수정
    • test : 테스팅 관련
    • chore : 어디에도 해당하지 않는 것들
    • style : 라인 포맷팅 등
    • build : 라이브러리 추가 등
    • ui/ux : ui 요소
    • feat : 기능 추가

4. Pull Request

  1. PR 템플릿에 맞춰서 작성한다.
  2. PR 제목은 브랜치명 : PR에 대한 간단한 요약으로 작성한다.
  3. 리뷰이는 리뷰어를 배려해 최대한 자세히 상세히 열심히 작성한다.
## 관련 이슈
- Close #

## 변경 요약
- 
-

## 주요 변경점
- 

## 테스트
- [ ] 로컬 실행 확인
- [ ] 테스트 통과
- [ ] (해당 시) Postman/Swagger로 API 확인

## 리뷰 포인트
- 
  1. 리뷰어와 리뷰이는 모두 상대를 존중하는 태도로 작성한다.🙏

5. Merge

  • 리뷰는 48시간 이내 해주기
  • 급한 리뷰는 따로 요청하여 처리한다.

6. Labels

  1. feat
  2. hotfix
  3. fix
  4. refactor
  5. deploy
  6. chore
  7. test
          script: |
            const labels = [
              { name: 'feature',  color: '0e8a16', description: '새로운 기능' },
              { name: 'hotfix',   color: 'b60205', description: '긴급 수정' },
              { name: 'fix',   color: '', description: '수정' },
              { name: 'refactor', color: 'fbca04', description: '리팩토링' },
              { name: 'deploy',   color: '0052cc', description: '배포' },
              { name: 'chore',    color: 'fef2c0', description: '기타 작업' },
              { name: 'test',     color: '1d76db', description: '테스트' },
            ];

7. Merge 전략

  • rebase 사용 x
    • merge commit 제목, 내용디폴트로 처리.

*rebase는 잘 사용하면 좋은 전략이 될 수 있지만 팀원 모두가 Git을 다루는 것에 있어서 익숙한 것은 아니고 

git push force를 유발할 수 있기에 사용하지 않기로 설정했다.

 

 

다음은 code 컨벤션이다.

1. 코딩 컨벤션

** 기본적으로 구글 컨벤션 적용 **

Naming

  • 패키지 이름은 소문자로
  • 클래스, 인터페이스 이름 대문자 카멜표기법 적용 ex) UserRepository
  • 테스트 클래스 이름은 항상 ‘Test’로 끝내기 ex) UserServiceTest

Method

  • 메서드 이름은 소문자 카멜표기법 적용
  • 메서드 이름은 동사/전치사로 시작
public String readBook() {...}
public String toString() {...}

<aside> 💡

메서드는 한 가지 기능만 하도록! 메서드의 이름이 길어져도 의도를 잘 나타낼 수 있다면 좋은 네이밍!

</aside>

Constant

  • 상수는 대문자 및 언더스코어로 작성
public static final String DEFAULT_SCORE = 1500;

Variable

  • 변수는 소문자 카멜표기법 적용
  • 임시 변수 외에는 한글자 이름 금지
    • 임시 변수: 반복문이나 인덱스, 람다 표현식의 파라미터와 같은 짧은 범위의 변수 ex) int i
  • boolean 타입 변수는 is/can/has 등 boolean 타입에 맞는 적절한 변수명 사용
  • 변수의 이름이 길어져도 의도를 잘 나타낼 수 있다면 좋은 네이밍임을 유념

Declarations

  • import 문에 와일드 카드 (*) 금지
    • 설정하는법설정 경로바꿔야 하는 값
      • Class count to use import with '*'
      • Names count to use static import with '*'
      보통 둘 다 999 정도로 두면 사실상 와일드카드 import가 안 생겨.
      • Class count to use import with '*' → 999
      • Names count to use static import with '*' → 999
      추가로 확인할 것
      • Use single class import
      • 이 체크되어 있으면 좋다.
      이렇게 하면대신처럼 들어가.
      적용 후 정리 방법
      • Code > Optimize Imports
      • 단축키:
        • mac: Control + Option + O
        • win/linux: Ctrl + Alt + O
      하면 한 번에 정리돼.
      팀 전체 통일하려면
      • .editorconfig
      • IntelliJ 코드스타일 xml 공유
      • Spotless 같은 포맷터 </aside>
    • 개인 설정만 하면 팀원마다 달라질 수 있어서 보통은
    • 이미 와일드카드로 들어간 파일은
    • importjava.util.List; importjava.util.Map;
    • importjava.util.*;
    • 같은 화면에서
    • 예:
    • 여기서 아래 두 개를 아주 크게 바꾸면 돼.
    • IntelliJ IDEA > Settings(또는 Preferences) > Editor > Code Style > Java > Imports
    • <aside> 💡
  • 제한자 순서는 아래대로 작성
public protected private abstract static final transient volatile synchronized native strictfp
  • 배열에서 대괄호는 타입 뒤에 선언 (변수명 뒤에 선언하지 않음)
  • Long 타입의 값에는 마지막에 ‘L’로 끝내기 ex) long orderId = 1001L;
  • static으로 선언할 때는 final도 붙여주기

Braces

  • 중괄호의 선언은 K&R 스타일을 적용
    • 줄의 마지막에서 시작 중괄호 '{' 를 열고 블럭을 마친후 새줄 삽입 후 '}' 작성
  • else, catch, finally, while은 닫는 중괄호와 같은 줄에 선언
    • while은 do-while문을 나타내는 것
  • 단, 빈 블럭이라면 새줄 없이 중괄호를 닫는 것을 허용
  • 조건문, 반복문에는 중괄호를 필수적으로 사용
  • 조건식에는 부정문 대신 긍정문을 지향

Blank lines

  • if, for, while, catch 같은 제어문은 공백을 삽입하고 소괄호를 시작

Whitespace

  • 생성자, 메서드의 선언, 호출, 애너테이션 선언 뒤에 쓰이는 소괄호는 공백을 삽입하지 않음
public removeData() {}

@NotNull(...)
private String email; 
  • 콤마와 반복문의 구분자로 쓰이는 세미콜론에는 뒤에만 공백을 삽입
  • 콜론의 앞 뒤에는 공백을 삽입
  • 이항 / 삼항 연산자의 앞 뒤에 공백 삽입
  • 단한 연산자와 연산 대상 사이에는 공백을 미삽입
  • DTO:
    • 요청 DTO: ~RequestDto
    • 응답 DTO: ~ResponseDto
  • record
  • Controller:
    • ~RestController
  • UseCase
    • ~UseCase
  • Service기본적으로 클래스 레벨에 @Transactional(readOnly = true)를 선언하고,이 규칙의 목적은 다음과 같다.
    • 조회 전용 로직임을 코드만 보고 바로 파악할 수 있다.
    • 수정 메서드를 명시적으로 드러낼 수 있다.
    • 실수로 조회 메서드에서 불필요한 변경 감지를 유발하는 것을 줄일 수 있다.
    • Service 계층의 트랜잭션 정책을 일관되게 유지할 수 있다.
    ;
    import org.springframework.transaction.annotation.Transactional;
    
    @Service
    @RequiredArgsConstructor
    @Transactional(readOnly = true)
    public class OrderService {
    
        private final OrderRepository orderRepository;
    
        public OrderResponse getOrder(Long orderId) {
            Order order = orderRepository.findById(orderId)
                    .orElseThrow(() -> new IllegalArgumentException("주문이 존재하지 않습니다."));
    
            return OrderResponse.from(order);
        }
    
        public OrderResponse getMyOrder(Long userId, Long orderId) {
            Order order = orderRepository.findByIdAndUserId(orderId, userId)
                    .orElseThrow(() -> new IllegalArgumentException("주문이 존재하지 않습니다."));
    
            return OrderResponse.from(order);
        }
        
        @Transactional
        public Long createOrder(OrderCreateRequest request) {
            Order order = Order.create(request.productId(), request.userId(), request.quantity());
            Order savedOrder = orderRepository.save(order);
            return savedOrder.getId();
        }
    }
    
    객체지향 생활 체조 원칙 기반 컨벤션
    1. 한 메서드에 세 단계 이상의 들여쓰기(indent)를 사용하지 않는다.
    2. else 예약어를 사용하지 않는다.
    3. 게터 사용 가능
    4. 세터를 지양한다.
    5. 메서드 체이닝의 경우 줄바꿈을 통해 절차를 표현한다.
    메서드 체이닝 예시일급 컬렉션 사용
    1. 세터 대신 change, update 등의 메서드 이름으로 목적을 나타낸다.
    2. 의존성 주입 시 @RequiredArgsConstructor, @NoArgsConstructor를 사용한다.
    3. @AllArgsConstructor는 사용하지 않는다.
    4. record는 DTO와 VO에서 사용한다.
    테스트 컨벤션
    1. 테스트 메서드명을 한글로 작성한다.
    2. 테스트 메서드의 @DisplayName 애노테이션을 생략한다.
      • @SuppressWarnings("NonAsciiCharacters")와 @DisplayNameGeneration(ReplaceUnderscores.class) 애노테이션을 추가하여 경고를 무시한다.
    3. 예외 케이스에 대한 테스트 메서드 네이밍:
      • ~ 예외가 발생한다.
    4. 생성 로직에 대한 테스트 메서드 네이밍:
      • ~ 생성한다.
    테스트 클래스
    1. 테스트 클래스의 빈 주입은 필드 주입을 사용한다.
    2. given, when, then 주석을 명시적으로 작성한다.
      • 나누기 곤란한 경우 &로 합쳐 작성한다.
        • // given & when
        • // when & then
        • // given & when & then
    테스트 코드 예시스웨거예시)
    @RestController
    @RequiredArgsConstructor
    @RequestMapping("/api/v1/users")
    public class UserController implements UserApi {
    
        private final UserService userService;
    
        @Override
        @GetMapping("/me")
        public ResponseEntity<UserInfoResponse> me(
           @AuthenticationPrincipal CustomUserDetails userDetails
        ) {
           return ResponseEntity.ok(userService.getMe(userDetails.getUserId()));
        }
    
    }
    
  • @Tag(name = "User", description = "유저 API") public interface UserApi { @Operation( summary = "내 정보 조회", description = """ 현재 로그인된 사용자의 정보를 조회합니다. - JWT 인증이 필요합니다. (Swagger 우측 상단 Authorize에 Access Token 입력) - 반환: userId, provider """ ) @SecurityRequirement(name = "bearerAuth") @ApiErrorResponses({ @ApiErrorResponse(value = UserErrorCode.class, constant = "NOT_FOUND_USER", summary = "회원을 찾을 수 없습니다"), @ApiErrorResponse(value = UserErrorCode.class, constant = "USER_INACTIVE", summary = "비활성화된 사용자입니다"), }) ResponseEntity<UserInfoResponse> me( @AuthenticationPrincipal CustomUserDetails userDetails ); }
  • 컨트롤러와 스웨거파일 분리하여 의존하여 처리
  • @Test void 없는_장소를_조회하면_예외가_발생한다() { // given final Place place = new Place(...); placeRepository.save(place); // when & then assertThatThrownBy(() -> placeRepository.findById(-1L)) .isInstanceOf(PlaceException.class); } @Test void 장소를_아이디로_조회한다() { // given final Place expected = new Place(...); placeRepository.save(place); // when final Place actual = placeRepository.findById(place.getId()); // then assertThat(actual).isEqualTo(expected); }
  • 네이밍 규칙
  • List<Car> highScoreCars = cars.stream() .filter(car -> car.getScore() == getMaxScore()) .collect(Collectors.toList());
  • 기본 원칙
  • 데이터를 수정하는 메서드만 별도로 @Transactional을 선언한다.
  • Service 계층에서는 조회와 변경 작업의 의도를 명확히 구분하기 위해

로그 처리

@Slf4j 사용

코드 포멧터

적용방법

https://soeun2537.tistory.com/67?utm_source=chatgpt.com

네이버

<code_scheme name="Naver-coding-convention-v1.2">
  <option name="CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND" value="99" />
  <option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="1" />
  <option name="IMPORT_LAYOUT_TABLE">
    <value>
      <emptyLine />
      <package name="" withSubpackages="true" static="true" />
      <emptyLine />
      <package name="java" withSubpackages="true" static="false" />
      <emptyLine />
      <package name="javax" withSubpackages="true" static="false" />
      <emptyLine />
      <package name="org" withSubpackages="true" static="false" />
      <emptyLine />
      <package name="net" withSubpackages="true" static="false" />
      <emptyLine />
      <package name="com" withSubpackages="true" static="false" />
      <emptyLine />
      <package name="" withSubpackages="true" static="false" />
      <emptyLine />
      <package name="com.nhncorp" withSubpackages="true" static="false" />
      <emptyLine />
      <package name="com.navercorp" withSubpackages="true" static="false" />
      <emptyLine />
      <package name="com.naver" withSubpackages="true" static="false" />
      <emptyLine />
    </value>
  </option>
  <option name="RIGHT_MARGIN" value="120" />
  <option name="ENABLE_JAVADOC_FORMATTING" value="false" />
  <option name="JD_KEEP_EMPTY_LINES" value="false" />
  <option name="FORMATTER_TAGS_ENABLED" value="true" />
  <XML>
    <option name="XML_LEGACY_SETTINGS_IMPORTED" value="true" />
  </XML>
  <codeStyleSettings language="JAVA">
    <option name="LINE_COMMENT_AT_FIRST_COLUMN" value="false" />
    <option name="LINE_COMMENT_ADD_SPACE" value="true" />
    <option name="KEEP_FIRST_COLUMN_COMMENT" value="false" />
    <option name="KEEP_CONTROL_STATEMENT_IN_ONE_LINE" value="false" />
    <option name="KEEP_BLANK_LINES_IN_DECLARATIONS" value="1" />
    <option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
    <option name="KEEP_BLANK_LINES_BEFORE_RBRACE" value="1" />
    <option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
    <option name="SPACE_AFTER_TYPE_CAST" value="false" />
    <option name="SPACE_BEFORE_ARRAY_INITIALIZER_LBRACE" value="true" />
    <option name="CALL_PARAMETERS_WRAP" value="1" />
    <option name="METHOD_PARAMETERS_WRAP" value="1" />
    <option name="EXTENDS_LIST_WRAP" value="1" />
    <option name="METHOD_CALL_CHAIN_WRAP" value="5" />
    <option name="THROWS_LIST_WRAP" value="5" />
    <option name="EXTENDS_KEYWORD_WRAP" value="1" />
    <option name="BINARY_OPERATION_WRAP" value="1" />
    <option name="BINARY_OPERATION_SIGN_ON_NEXT_LINE" value="true" />
    <option name="TERNARY_OPERATION_WRAP" value="1" />
    <option name="ARRAY_INITIALIZER_WRAP" value="1" />
    <indentOptions>
      <option name="CONTINUATION_INDENT_SIZE" value="4" />
      <option name="USE_TAB_CHARACTER" value="true" />
    </indentOptions>
  </codeStyleSettings>
</code_scheme>

환경변수 명

DB_HOST=HOST명
DB_PORT=PORT번호
DB_NAME=DB이름
DB_USERNAME=DB유저
DB_PASSWORD=DB비밀번호

 

 

프로젝트가 완료된 시점에서 돌아보면, 나를 포함한 모든 팀원이 모든 코드에서 컨벤션을 완벽하게 지키지는 못했다.

그럼에도 불구하고, 함께 기준을 정하고 이를 지키기 위해 노력했다는 점에 의미가 있다고 생각한다.
이러한 과정을 통해 팀 프로젝트 협업 방식에 익숙해지고, 더 나은 개발 문화를 만들어가는 경험을 할 수 있었다.

'BackEnd > 프로그래머스 데브코스' 카테고리의 다른 글

JWT란 무엇일까?  (0) 2026.05.19
MSA 환경에서의 DB 모델링  (0) 2026.05.15
세미 프로젝트 시작  (0) 2026.05.14
프로그래머스 데브코스 단기 심화 시작  (0) 2026.05.08
'BackEnd/프로그래머스 데브코스' 카테고리의 다른 글
  • JWT란 무엇일까?
  • MSA 환경에서의 DB 모델링
  • 세미 프로젝트 시작
  • 프로그래머스 데브코스 단기 심화 시작
JK-LEE98
JK-LEE98
백엔드 개발자
  • JK-LEE98
    JK-LEE98
    JK-LEE98
  • 전체
    오늘
    어제
    • 분류 전체보기 (60)
      • 나의 지식 공유 (4)
      • SQL (4)
      • PS (10)
      • BackEnd (32)
        • Java (3)
        • Spring (4)
        • 내일배움캠프 (20)
        • 프로그래머스 데브코스 (5)
      • 건강한 나 되기🍀 (10)
  • 인기 글

  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.6
JK-LEE98
컨벤션 설정
상단으로

티스토리툴바