Skip to content

백엔드 컨벤션

권예진 edited this page Oct 18, 2023 · 3 revisions

백엔드 코드 외 컨벤션

개발 진행 방식

  • 솔로 프로그래밍
    • 기능이 간단한 경우
  • 페어 프로그래밍
    • 기능이 너무 복잡하거나 어려운 경우
    • 해당 기능을 구현하고 싶은 크루가 다수인 경우

PR 리뷰 방식

  • 솔로 프로그래밍
    • 해당 PR을 요청하지 않은 3명의 크루가 모두 리뷰
  • 페어 프로그래밍
    • 다른 모든 크루들이 리뷰
  • 모든 리뷰어가 approve를 해줘야 merge 가능
    • merge는 다같이! (캠퍼스)

백엔드 코드 컨벤션

자바 & 스프링 버전

  • 자바 : 17
  • 스프링 : 3.0 이상

스프링 설정 파일

  • 스프링 설정 파일로는 yml을 사용한다. (ex. application.yml)
  • 깃 서브모듈로 설정 파일을 관리한다.

코드 컨벤션

  • 자바 코드 컨벤션을 지키면서 프로그래밍한다.
    • 기본적으로 Google Java Style Guide를 원칙으로 한다.
    • 단, 들여쓰기는 '2 spaces'가 아닌 '4 spaces'로 한다.
  • indent(인덴트, 들여쓰기) depth를 2를 넘지 않도록 구현한다.
  • 3항 연산자를 쓰지 않는다.
  • else 예약어를 쓰지 않는다.
  • switch/case도 쓰지 않는다.
  • 모든 기능을 TDD로 구현해 단위 테스트가 존재해야 한다.
  • 배열 대신 컬렉션을 사용한다.
  • 줄여 쓰지 않는다(축약 금지).
  • 일급 컬렉션을 쓴다.
  • 모든 엔티티를 작게 유지한다.

개행

  • 클래스 이름과 필드 사이에 공백을 둔다.

    public class Line { 
    
        private final Long id; 
        private final LineName name; 
        private final LineColor color; 
        private final Sections sections;
    }
  • 메서드의 매개변수가 많거나 길어서 120자를 넘어가면 개행한다.

    public ReadAuctionsDto readAllByCondition(  
            final Pageable pageable,  
            final ReadAuctionSearchCondition readAuctionSearchCondition
    ) {
    
        ....
        
    }

final 적용 대상

  • 메소드 파라미터
  • 클래스
  • 필드
  • 변수

생성자 및 메소드 접근 제어자 설정

  • 패키지 외부에서 생성자 및 메소드를 호출해서는 안 되는 경우, 생성자의 접근제어자는 default로 엄격하게 지킨다.

메소드 정렬 순서

  • 호출 순서에 따라 정렬한다.
  • private 메서드를 사용하는 다른 메서드가 많을 경우에도 첫 번째로 사용하는 메서드 바로 밑에 둔다.
    public a() {
      c(); 
    }
    
    private c() {
    
    }
    
    public b() {
      c();
      d(); 
    } 
    
    private d() {
    
    }

메서드 네이밍

  • 필드 관련 메서드 외 메서드 명에 get 키워드를 사용하지 않는다.
  • process, calculate, find 등의 단어로 대체한다.

Lombok

equals & hashcode 재정의

  • 롬복 사용
  • 상태를 갖는 객체(도메인 엔티티 등)에서는 일괄적으로 재정의
  • 엔티티는 id만, VO는 모든 필드 포함

toString

  • 롬복 사용
  • 상태를 갖는 객체(도메인 엔티티 등)에서는 일괄적으로 재정의
  • 연관관계 매핑 되어있는 필드는 제외

getter

  • 롬복 사용

롬복 어노테이션 정렬 순서

@Entity 
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter 
@EqualsAndHashCode(of = "id", callSuper = false)
@ToString(of = {"id", "..."})
@Table(name = "orders") 

DTO

DTO 사용 범위

  • Controller → Service
    • 별도의 DTO로 변환
      • 클라이언트에게 입력받은 데이터를 유효한 타입으로 변환하는 등의 비즈니스 로직과 연관이 없는 작업 수행
  • Service → Repository
    • 도메인 엔티티(JPA Entity)
  • Repository → Service
    • 도메인 엔티티(JPA Entity)
  • Service → Controller
    • create() 의 경우
      • Long을 반환
      • 특수한 케이스(create의 응답으로 id 이외의 데이터도 넘겨야 하는 경우)에만 별도의 dto를 반환
    • 그 외의 경우
      • 별도의 DTO로 변환
  • Controller에서 반환하는 Response
    • 서비스에서 반환하는 dto와 필드 값이 같은 경우에도 API 별로 별도의 Response를 정의

DTO 네이밍

  • Request, Response
    • (CRUD)Request (ex. CreateAuctionRequest)
    • (CRUD)Response (ex. ReadAuctionResponse)
    • C : Create
    • R : Read
    • U : Update
    • D : Delete
  • 나머지 DTO
    • (기능)Dto (ex. ReadAuctionDto)

DTO 변환 위치

  • DTO에 정적 팩토리 메소드 추가

Exception

  • 커스텀 예외 사용
  • 예외 메시지가 중복되더라도 상수로 빼지 않는다.

패키지 구조

  • 도메인형 구조

    • configuration
      • 최상단에 생성
      • 도메인별 configuration 생성해야 한다면 패키지 생성
  • 예시 구조

    - line
    	- application (service)
    	- domain 
    	- presentation (controller)
    	- infrastructure
    		- jpa
    		- redis
    		- jgrapht
    - station
    	- application (service)
    	- domain 
    	- presentation (controller)
    	- infrastructure (repository)
    

레포지토리

  • 도메인 패키지에 레포지토리 인터페이스 생성

  • infrastructure.persistence 패키지에 레포지토리 구현체 생성 및 구현체가 필요한 repository를 가지고 있는 구조

    - domain
    	- Auction.java
    	- AuctionRepository.java (인터페이스)
    - infrastructure
        - persistence
            - AuctionRepositoryImpl.java (인터페이스 구현체)
        - JpaAuctionRepository.java
        - QuerydslAuctionRepository.java
    
    public interface AuctionRepository {  
    
        Auction save(final Auction auction);  
        List<Auction> findAll();
    }
    
    public class AuctionRepositoryImpl implementation AuctionRepository { 
    
        private final JpaAuctionRepository jpaAuctionRepository; 
        private final QueryAuctionRepository queryAuctionRepository; 
    
        @Overide
        public Auction save(final Auction auction) {
            return jpaAuctionRepository.save(auction);
        }
    
        @Overide
        public List<Auction> findById(final Long id) {
            return querydslAuctionRepository.findAll(id);
        }
    }
    
    public interface JpaAuctionRepository extends JpaRepository<Auction, Long> {
    
    }
    
    public class QueryUserRepository { 
    
        private final QueryFactory queryFactory; 
    
        List<Auction> findAll() {
    	  queryFactory.selectFrom(auction)  
                          .leftJoin(~).fetchJoin()  
                          .join(~).fetchJoin()
                          .where(auction.id)
                          .orderBy(~)
                          .fetch();
        }
    }

테스트

테스트 네이밍

  • 테스트 메소드는 한글로 작성한다.
    • 한글 사용 시 인텔리제이의 경고를 억제하기 위해 @SuppressWarnings("NonAsciiCharacters") 를 추가한다.
    • 언더바(_) 대신 공백으로 바꿔주기 위해 @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) 를 추가한다.

테스트 방식

  • 도메인
    • 단위 테스트
  • Repository
    • @DataJpaTest 사용
  • Service
    • 통합테스트
    • @SpringBootTest 사용
  • 컨트롤러
    • 단위테스트
    • @WebMvcTest 사용
    • Mock으로는 BddMockito 사용

테스트 전략

  • Given - When - Then 주석으로 어떤 절인지 표현한다.
  • 테스트 비교 대상 (when절에서 반환하는 객체)의 변수명은 actual을 사용한다.

테스트 메소드

Assertions 사용

  • assertThat().isEqualTo()
  • assertThatThrownBy()
  • 테스트 검증 사항이 2개 이상이라면 SoftAssertions.assertSoftly() 사용

테스트 의존성 주입

  • @Autowired
    • 필드 주입

테스트 픽스처

  • 공통

    • 변수명 네이밍
      • 여러개의 동일 타입의 변수를 생성해야 하는 경우 숫자로 구분한다.
    • 클래스 네이밍
      • 테스트 클래스 명(-Test) + Fixture
    • 필드 네이밍
      • 한글로 작성 (픽스처임을 알리고 어떤 의미인지 명확하게 하기 위해)
    • 픽스처 클래스에서 픽스처 필드의 접근지정자는 protected, 픽스처가 아닌 필드의 접근 지정자는 private
    • 모든 필드는 final을 붙이지 않는다
    • 모킹을 한 필드의 필드명에 "mock" 키워드 사용하지 않기
    • 해당 테스트가 존재하는 패키지에 픽스처 패키지 생성해서 픽스처 클래스 두기
    - auction
        - fixture
            - AuctionServiceFixture.java
        - AuctionServiceTest.java
    
  • 통합 테스트

    • 각 테스트 별로 클래스로 분리
    • 파라미터 DTO와 응답 DTO을 모두 Fixture로 생성
    • then절에서 Fixture로 만든 응답 DTO를 결과(actual)과 isEqualTo()로 검증
  • 단위 테스트

    • 컨트롤러 슬라이스 테스트
      • 각 테스트 별로 클래스로 분리
      • 픽스처가 적은 테스트라고 할지라도 별도의 클래스에서 픽스처 관리
      • given절의 모든 객체를 픽스처로 처리
      • mocking 과정(BDDMockito.given() 등등)은 given절에 유지
      • mocking 과정 중 발생하는 예외도 given절에 유지
      • 예외 발생 시 willThrow() 등의 메서드 내부에서 new 키워드로 생성
      • 예외 객체는 별도의 필드로 분리하지 않음
      • 예외 메세지 테스트 검증 시 exists() 사용
      • 추후 예외 메세지 문서화 시에는 메세지 명시
      • 테스트 코드 중 문서화는 별도의 private 메서드로 분리하고 맨 밑으로 메서드 정렬 (ex. private void createAuction_문서화(final ResultActions resultActions))
  • 레포지토리 테스트

    • 각 테스트 별로 클래스로 분리
      • 픽스처가 적은 테스트라고 할지라도 별도의 클래스에서 픽스처 관리
    • 저장 테스트 시
      • save()의 id가 positive인지만 확인
      • assertThat(actual.getId()).isPositive())
    • 조회 테스트 시
      • 단일조회 시 Optional 반환값과 픽스쳐를 contains()로 비교
      • assertThat(actual).contains(expected);
      • 목록조회 시 컬렉션의 크기 비교 + 인덱스끼리의 isEqualTo()로 비교
      SoftAssertions.assertSoftly(softAssertions -> {
              softAssertions.assertThat(actual).hasSize(3); 
              softAssertions.assertThat(actual.getTitle()).isEqualTo(~);
      });
      
  • 도메인 테스트

    • repository 사용하지 않음
    • id가 필요한 경우 ReflectionTestUtils.setField() 사용해서 id 세팅
    • 테스트하려는 도메인 객체 외에는 모두 픽스처 생성

DB

DB 테이블, 필드 네이밍

  • 기본적으로는 도메인 엔티티와 동일한 이름을 사용하되, 테이블 명이 예약어와 중복될 경우 복수형을 사용한다.

외래키 네이밍

  • fk_엔티티이름_필드이름
  • 예시: fk_auction_seller

삭제가 가능한 경우

  • is_deleted 필드를 두고 soft delete 방식 사용

DB 제약 조건

  • DB 조건도 JPA 엔티티에도 표현을 한다.
    @Entity 
    public class Member { 
    
        @Id 
        @GeneratedValue 
        @Column(name = "member_id") 
        private Long id; 
    
        @Column(unique = true, length = 10) // 테이블 제약조건 표현 
        private String name; 
    
    }
    

문서화

  • spring rest docs 사용
  • 컨트롤러 단위테스트를 대상으로 문서화
  • andDo 부분은 별도의 메서드로 분리
    • 해당 클래스 가장 하단의 메서드를 만들어 andDo를 분리한다.
    • 메서드명 : 문서화_{문서화할 컨트롤러 메서드} (ex. create_문서화)
Clone this wiki locally