프로젝트 2주차 회고

송형근·2024년 10월 6일

Side Project - Astraphe

목록 보기
2/4
post-thumbnail

2주차 진행사항

  • 2차 피드백 이후 설계 수정
  • WBS 작성
  • 깃 컨벤션 작성
  • 깃 템플릿 작성
  • 멀티 모듈 구조 구성
  • 공통 작성 규칙 작성
  • docker-compose.yaml 작성
  • Common Module 작성
  • Company CRUDS, Test Code 작성
  • Product 관련 Entity 작성

설계 수정 및 WBS 작성

  • API 설계 과정에서 미처 발견 못한 부분 수정
  • Product 옵션을 String -> 하위 엔티티로 변경
  • WBS 담당자 별로 작성

깃 컨벤션 및 템플릿 작성

개발 프로세스

이슈 발행 > 브랜치 발행 > 구현 (커밋) > PR

브랜치 컨벤션

  • 기능도메인이슈번호
    • ex.) featorder#1

커밋 컨벤션

  • 양식
    • {타입}: {내용} ({이슈넘버})
    • ex) feat: 유저 create api (#3)
  • 타입
    feat새로운 기능을 추가
    fix버그 수정
    designCSS 등 사용자 UI 디자인 변경
    hotfix급하게 치명적인 버그를 고쳐야하는 경우
    style코드 포맷 변경, 세미 콜론 누락, 코드 수정이 없는 경우
    refactor프로덕션 코드 리팩토링
    comment필요한 주석 추가 및 변경
    docs문서 수정
    test테스트 코드, 리펙토링 테스트 코드 추가, Production Code(실제로 사용하는 코드) 변경 없음
    chore빌드 업무 수정, 패키지 매니저 수정, 패키지 관리자 구성 등 업데이트, Production Code 변경 없음
    rename파일 혹은 폴더명을 수정하거나 옮기는 작업만인 경우
    remove파일을 삭제하는 작업만 수행한 경우

이슈 템플릿

---
name: Feature issue template
about: Describe this issue template's purpose here.
title: "[Feat]"
labels: ''
assignees: ''

---

## TODO List

- [ ] TODO
- [ ] TODO
- [ ] TODO

## 참고


<!-- 
    제목 설정 시 참고 후 아래 내용 삭제
    | feat | 새로운 기능을 추가 |
    | --- | --- |
    | fix | 버그 수정 |
    | design | CSS 등 사용자 UI 디자인 변경 |
    | hotfix | 급하게 치명적인 버그를 고쳐야하는 경우 |
    | style | 코드 포맷 변경, 세미 콜론 누락, 코드 수정이 없는 경우 |
    | refactor | 프로덕션 코드 리팩토링 |
    | comment | 필요한 주석 추가 및 변경 |
    | docs | 문서 수정 |
    | test | 테스트 코드, 리펙토링 테스트 코드 추가, Production Code(실제로 사용하는 코드) 변경 없음 |
    | chore | 빌드 업무 수정, 패키지 매니저 수정, 패키지 관리자 구성 등 업데이트, Production Code 변경 없음 |
    | rename | 파일 혹은 폴더명을 수정하거나 옮기는 작업만인 경우 |
    | remove | 파일을 삭제하는 작업만 수행한 경우 |

-->

PR 템플릿

## 📌 Related Issue
> 이슈 번호를 기재 해주세요 (ex) #0)

- 

## 📢 Changes
> 변경 사항을 기재 해주세요

- 

## 📸 Test
> 테스트 결과를 공유 해주세요

- 

## 📢 Check List
> 체크 리스트를 확인하고 완료되면 체크 해주세요

- [ ] 테스트를 완료 하셨나요?
- [ ] Source Branch & Target Branch 확인 하셨나요??
- [ ] 코드 리뷰 요청을 하셨나요??

## 🚀 참고 자료
> 참고 자료가 있다면 첨부 해주세요
- 

멀티 모듈 구조


공통 작성 규칙

Entity 작성 규칙


@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Builder // 빌더 패턴 협의
@Getter
@Entity
@Table(name = "tb_example")
public class Example {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Setter private String name;
    // other fields and methods
}

DTO 작성 규칙

  • DTO Package 규칙
    • application.dto.{domain}
      • ex) application.dto.user.SignupRequestDto
  • DTO 네이밍 규칙
    • Request DTO : ~~(Create / Update) RequestDto
      @Getter
      public class MyDomainRequestDto {
      		private String myName;
      		// ...
      }
    • Response DTO : ~~ResponseDto
      @Builder
      @Getter
      public class MyDomainResponseDto {
      		private Long myId;
      		private String myName;
      		// ...
      }

docker-compose.yaml 작성

version: '3.9'

services:
  postgresql:
    image: postgres:latest
    container_name: postgres
    environment:
        POSTGRES_USER: postgres
        POSTGRES_PASSWORD: 1234
        POSTGRES_DB: postgres
    ports:
      - "5432:5432"
  redis:
    image: redis:latest
    container_name: redis
    command: redis-server --requirepass systempass --port 6379
    ports:
      - "6379:6379"
  zookeeper:
    image: wurstmeister/zookeeper:latest
    platform: linux/amd64
    container_name: zookeeper
    ports:
      - "2181:2181"
    environment:
      ZOOKEEPER_CLIENT_PORT: 2181
      ZOOKEEPER_TICK_TIME: 2000

  kafka:
    image: wurstmeister/kafka:latest
    platform: linux/amd64
    container_name: kafka
    ports:
      - "9092:9092"
    depends_on:
      - zookeeper
    environment:
      KAFKA_ADVERTISED_LISTENERS: INSIDE://kafka:29092,OUTSIDE://localhost:9092
      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: INSIDE:PLAINTEXT,OUTSIDE:PLAINTEXT
      KAFKA_LISTENERS: INSIDE://0.0.0.0:29092,OUTSIDE://0.0.0.0:9092
      KAFKA_INTER_BROKER_LISTENER_NAME: INSIDE
      KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock

  kafka-ui:
    image: provectuslabs/kafka-ui:latest
    platform: linux/amd64
    container_name: kafka-ui
    ports:
      - "28080:8080"
    depends_on:
      - kafka
    environment:
      KAFKA_CLUSTERS_0_NAME: local
      KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka:29092
      KAFKA_CLUSTERS_0_ZOOKEEPER: zookeeper:2181
      KAFKA_CLUSTERS_0_READONLY: "false"


  prometheus:
    image: prom/prometheus
    container_name: prometheus
    restart: always
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml

  grafana:
    image: grafana/grafana
    container_name: grafana
    restart: always
    ports:
        - "3000:3000"

  zipkin:
    image: openzipkin/zipkin:latest
    container_name: zipkin
    ports:
      - "9411:9411"

Common Module 작성

  • Custom Response, Exception, Handler

    • Custom Response

        @Getter
        @Builder
        public class ApiResponse<T> {
            private int status;
            private T data;
            private String message;
            private LocalDateTime timeStamp;
      
            public static <T> ApiResponse<?> success(T data) {
                return ApiResponse.builder()
                        .status(200)
                        .data(data)
                        .timeStamp(LocalDateTime.now())
                        .build();
            }
      
            public static ApiResponse<?> error(int status, String message) {
                return ApiResponse.builder()
                        .status(status)
                        .message(message)
                        .timeStamp(LocalDateTime.now())
                        .build();
            }
        }
    • Custom Exception

        @Getter
        public class ApiException extends RuntimeException{
      
            private final HttpStatus httpStatus;
            private final String logMessage;
      
            public ApiException(HttpStatus httpStatus, String errorMessage, String logMessage) {
                super(errorMessage);
                this.httpStatus = httpStatus;
                this.logMessage = logMessage;
            }
        }
    • Custom Exception Handler

        @Slf4j
        @RestControllerAdvice
        public class ApiExceptionHandler {
      
            @ExceptionHandler(ApiException.class)
            public ResponseEntity<ApiResponse<?>> globalExceptionHandler(ApiException e) {
                log.error(e.getLogMessage());
                return ResponseEntity
                        .status(e.getHttpStatus().value())
                        .body(ApiResponse.error(e.getHttpStatus().value(), e.getMessage()));
            }
        }
  • Cors Config, Cache Config

    • Cors config

        @Configuration
        public class CorsConfigurer implements WebMvcConfigurer {
      
            // TODO: prod 환경에서 localhost를 바꿔야 함.
            private final String[] ORIGIN_WHITE_LIST = {
                    "http://localhost:18080",
                    "http://localhost:18081",
                    "http://localhost:18082",
                    "http://localhost:18083",
                    "http://localhost:18084"
            };
      
            /**
             * Web browser에서 요청 시 swagger-ui를 사용하기 위함.
             */
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping("/**")
                        .allowedOrigins(ORIGIN_WHITE_LIST)
                        .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH")
                        .allowedHeaders("*");
                WebMvcConfigurer.super.addCorsMappings(registry);
            }
      
        }
    • Cache Config

        @Configuration
        @EnableCaching
        public class CacheConfig {
      
            @Bean
            public RedisConnectionFactory redisConnectionFactory() {
                // TODO: prod 환경에서 localhost를 바꿔야 함.
                RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration("localhost", 6379); // Redis 서버 정보
                return new LettuceConnectionFactory(configuration);
            }
      
            @Bean
            public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
                RedisCacheConfiguration configuration = RedisCacheConfiguration
                        .defaultCacheConfig()
                        .disableCachingNullValues()
                        .entryTtl(Duration.ofSeconds(1200))
                        .computePrefixWith(CacheKeyPrefix.simple())
                        .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(RedisSerializer.java()));
      
                Map<String, RedisCacheConfiguration> customConfigurations = new HashMap<>();
      
                /*
                customConfigurations.put("Reset",
                        RedisCacheConfiguration
                                .defaultCacheConfig()
                                .disableCachingNullValues()
                                .entryTtl(Duration.ofMinutes(5))
                                .computePrefixWith(CacheKeyPrefix.simple())
                                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(RedisSerializer.json())));
                */
      
                return RedisCacheManager
                        .builder(redisConnectionFactory)
                        .cacheDefaults(configuration)
                        .withInitialCacheConfigurations(customConfigurations)
                        .build();
            }
        }
  • Auditor

    @Configuration
    @EnableJpaAuditing
    public class AuditorAwareConfig {
    
        @Bean
        public AuditorAware<Long> auditorProvider() {
            return new AuditorAwareImpl();
        }
    }
    ---
    public class AuditorAwareImpl implements AuditorAware<Long> {
    
        @Override
        public Optional<Long> getCurrentAuditor() {
            HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
            String updatedBy = request.getHeader("X-USER-ID");
            if (updatedBy == null) {
                updatedBy = "-1";
            }
            return Optional.of(Long.parseLong(updatedBy));
        }
    
    }
  • BaseEntity

    @EntityListeners(AuditingEntityListener.class)
    @MappedSuperclass
    @Getter
    public class BaseEntity {
    
        @CreationTimestamp
        @Column(updatable = false)
        private LocalDateTime createdAt;
    
        @CreatedBy
        @Column
        private Long createdBy;
    
        @UpdateTimestamp
        @Column
        private LocalDateTime updatedAt;
    
        @LastModifiedBy
        @Column
        private Long updatedBy;
    
        private LocalDateTime deletedAt;
    
        @Column
        private Long deletedBy;
    
        @Column(name = "is_delete")
        private boolean isDelete = false;
    
        public void softDelete(Long deletedBy){
            this.deletedAt = LocalDateTime.now();
            this.deletedBy = deletedBy;
            this.isDelete = true;
        }
    
    }

Company CRUDS, Test Code 작성

  • Company CRUDS 작성 및 Test Code 작성 완료
  • Company CRUDS 구현 브랜치
  • Delete의 경우 Transaction이 끝나지않아 softdelete 후 getCompany 해도 값을 찾아올 수 있는 상태라서 어떻게 테스트 코드 작성해야할 지 고민중
  • Mock을 활용한 단위 테스트의 경우 아직 테스트코드 작성법 감이 안와서 SpringBootTest를 활용해 통합 테스트 코드만 작성

Product 관련 Entity 작성

  • Product, Category, Product Option Entity 작성

  • Category의 경우 data.sql을 활용해서 카테고리 주입 (임시로 3개만 작성)

    • data.sql

      INSERT INTO TB_CATEGORIES (category_code, category_name)
      SELECT 1000, '식품'
          WHERE NOT EXISTS (SELECT 1 FROM TB_CATEGORIES WHERE category_code = 1000);
      
      INSERT INTO TB_CATEGORIES (category_code, category_name)
      SELECT 2000, '전자제품'
          WHERE NOT EXISTS (SELECT 1 FROM TB_CATEGORIES WHERE category_code = 2000);
      
      INSERT INTO TB_CATEGORIES (category_code, category_name)
      SELECT 3000, '의류'
          WHERE NOT EXISTS (SELECT 1 FROM TB_CATEGORIES WHERE category_code = 3000);

P.S.

  • 개인 일정으로 인해 금요일에 Company CRUDS를 작성을 못해서 일정이 0.5일 정도 지연된 상태
profile
티스토리로 이주 (https://lucas-song94.tistory.com)

0개의 댓글