

이슈 발행 > 브랜치 발행 > 구현 (커밋) > PR
| feat | 새로운 기능을 추가 |
|---|---|
| fix | 버그 수정 |
| design | CSS 등 사용자 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 | 파일을 삭제하는 작업만 수행한 경우 |
-->
## 📌 Related Issue
> 이슈 번호를 기재 해주세요 (ex) #0)
-
## 📢 Changes
> 변경 사항을 기재 해주세요
-
## 📸 Test
> 테스트 결과를 공유 해주세요
-
## 📢 Check List
> 체크 리스트를 확인하고 완료되면 체크 해주세요
- [ ] 테스트를 완료 하셨나요?
- [ ] Source Branch & Target Branch 확인 하셨나요??
- [ ] 코드 리뷰 요청을 하셨나요??
## 🚀 참고 자료
> 참고 자료가 있다면 첨부 해주세요
-

@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
}
@Getter
public class MyDomainRequestDto {
private String myName;
// ...
}@Builder
@Getter
public class MyDomainResponseDto {
private Long myId;
private String myName;
// ...
}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"
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;
}
}
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);