add common module
This commit is contained in:
@@ -0,0 +1,662 @@
|
||||
# 백엔드 개발 가이드
|
||||
|
||||
[요청사항]
|
||||
- <개발원칙>을 준용하여 개발
|
||||
- <개발순서>에 따라 아래 3단계로 개발
|
||||
- '0. 준비'를 수행하고 완료 후 다음 단계 진행여부를 사용자에게 확인
|
||||
- '1. common 모듈 개발'을 수행하고 완료 후 다음 단계 진행여부를 사용자에게 확인
|
||||
- '2. 각 서비스별 구현'은 사용자와 함께 각 서비스를 개발
|
||||
|
||||
[가이드]
|
||||
<개발원칙>
|
||||
- '개발주석표준'에 맞게 주석 작성
|
||||
- API설계서와 일관성 있게 설계. Controller에 API를 누락하지 말고 모두 개발
|
||||
- '외부시퀀스설계서'와 '내부시퀀스설계서'와 일치되도록 개발
|
||||
- 각 서비스별 지정된 {설계 아키텍처 패턴}을 적용하여 개발
|
||||
- Layered 아키텍처 적용 시 Service레이어에 Interface 사용
|
||||
- Clean아키텍처 적용 시 Port/Adapter라는 용어 대신 Clean 아키텍처에 맞는 용어 사용
|
||||
- 백킹서비스 연동은 '데이터베이스설치결과서', '캐시설치결과서', 'MQ설치결과서'를 기반으로 개발
|
||||
- 빌드도구는 Gradle 사용
|
||||
- 설정 Manifest(src/main/resources/application*.yml) 작성 시 '[설정 Manifest 표준]' 준용
|
||||
<개발순서>
|
||||
- 0. 준비:
|
||||
- 참고자료 분석 및 이해
|
||||
- '패키지구조표준'의 예시를 참조하여 모든 클래스와 파일이 포함된 패키지 구조도를 작성
|
||||
- plantuml 스크립트가 아니라 트리구조 텍스트로 작성
|
||||
- 결과파일: develop/dev/package-structure.md
|
||||
- settings.gralde 파일 작성
|
||||
- build.gradle 작성
|
||||
- '<Build.gradle 구성 최적화>' 가이드대로 최상위와 각 서비스별 build.gradle 작성
|
||||
- '[루트 build.gradle 표준]'대로 최상위 build.gradle 작성
|
||||
- SpringBoot 3.3.0, Java 21 사용
|
||||
- common을 제외한 각 서비스에서 공통으로 사용되는 설정과 Dependency는 루트 build.gradle에 지정
|
||||
- 서비스별 build.gradle 작성
|
||||
- 최상위 build.gradle에 정의한 설정은 각 마이크로서비스의 build.gradle에 중복하여 정의하지 않도록 함
|
||||
- 각 서비스의 실행 jar 파일명은 서비스명과 동일하게 함
|
||||
- 각 서비스별 설정 파일 작성
|
||||
- 설정 Manifest(application.yml) 작성: '[설정 Manifest 표준]' 준용
|
||||
|
||||
- 1. common 모듈 개발
|
||||
- 각 서비스에서 공통으로 사용되는 클래스를 개발
|
||||
- 외부(웹브라우저, 데이터베이스, Message Queue, 외부시스템)와의 인터페이스를 위한 클래스는 포함하지 않음
|
||||
- 개발 완료 후 컴파일 및 에러 해결: {프로젝트 루트}/gradlew common:compileJava
|
||||
|
||||
- 2. 각 서비스별 개발
|
||||
- 사용자가 제공한 서비스의 유저스토리, 외부시퀀스설계서, 내부시퀀스설계서, API설계서 파악
|
||||
- 기존 개발 결과 파악
|
||||
- API 설계서의 각 API를 순차적으로 개발
|
||||
- Controller -> Service -> Data 레이어순으로 순차적으로 개발
|
||||
- 컴파일 및 에러 해결: {프로젝트 루트}/gradlew {service-name}:compileJava
|
||||
- 컴파일까지만 하고 서버 실행은 하지 않음
|
||||
- 모든 API개발 후 아래 수행
|
||||
- 컴파일 및 에러 해결: {프로젝트 루트}/gradlew {service-name}:compileJava
|
||||
- 빌드 및 에러 해결: {프로젝트 루트}/gradlew {service-name}:build
|
||||
- SecurityConfig 클래스 작성: '<SecurityConfig 예제>' 참조
|
||||
- JWT 인증 처리 클래스 작성: '<JWT 인증처리 예제>' 참조
|
||||
- Swagger Config 클래스 작성: '<SwaggerConfig 예제>' 참조
|
||||
- 테스트 코드 작성은 하지 않음
|
||||
|
||||
|
||||
<Build.gradle 구성 최적화>
|
||||
- **중앙 버전 관리**: 루트 build.gradle의 `ext` 블록에서 모든 외부 라이브러리 버전 통일 관리
|
||||
- **Spring Boot BOM 활용**: Spring Boot/Cloud에서 관리하는 라이브러리는 버전 명시 불필요 (자동 호환성 보장)
|
||||
- **Common 모듈 설정**: `java-library` + Spring Boot 플러그인 조합, `bootJar` 비활성화로 일반 jar 생성
|
||||
- **서비스별 최적화**: 공통 의존성(API 문서화, 테스트 등)은 루트에서 일괄 적용
|
||||
- **JWT 버전 통일**: 라이브러리 버전 변경시 API 호환성 확인 필수 (`parserBuilder()` → `parser()`)
|
||||
- **dependency-management 적용**: 모든 서브프로젝트에 Spring BOM 적용으로 버전 충돌 방지
|
||||
|
||||
[참고자료]
|
||||
- 유저스토리
|
||||
- API설계서
|
||||
- 외부시퀀스설계서
|
||||
- 내부시퀀스설계서
|
||||
- 데이터베이스설치결과서
|
||||
- 캐시설치결과서
|
||||
- MQ설치결과서
|
||||
- 테스트코드표준
|
||||
- 패키지구조표준
|
||||
|
||||
---
|
||||
|
||||
[설정 Manifest 표준]
|
||||
- common모듈은 작성하지 않음
|
||||
- application.yml에 작성
|
||||
- 하드코딩하지 않고 환경변수 사용
|
||||
특히, 데이터베이스, MQ 등의 연결 정보는 반드시 환경변수로 변환해야 함: '<DB/Redis 설정 예제>' 참조
|
||||
- spring.application.name은 서비스명과 동일하게 함
|
||||
- Redis Database는 각 서비스마다 다르게 설정
|
||||
- 민감한 정보의 디폴트값은 생략하거나 간략한 값으로 지정
|
||||
- JWT Secret Key는 모든 서비스가 동일해야 함
|
||||
- '[JWT,CORS,Actuaotr,OpenAPI Documentation,Loggings 표준]'을 준수하여 설정
|
||||
|
||||
[JWT, CORS, Actuaotr,OpenAPI Documentation,Loggings 표준]
|
||||
```
|
||||
# JWT
|
||||
jwt:
|
||||
secret: ${JWT_SECRET:}
|
||||
access-token-validity: ${JWT_ACCESS_TOKEN_VALIDITY:1800}
|
||||
refresh-token-validity: ${JWT_ACCESS_TOKEN_VALIDITY:86400}
|
||||
|
||||
# CORS Configuration
|
||||
cors:
|
||||
allowed-origins: ${CORS_ALLOWED_ORIGINS:http://localhost:*}
|
||||
|
||||
# Actuator
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info,metrics,prometheus
|
||||
base-path: /actuator
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
||||
show-components: always
|
||||
health:
|
||||
livenessState:
|
||||
enabled: true
|
||||
readinessState:
|
||||
enabled: true
|
||||
|
||||
# OpenAPI Documentation
|
||||
springdoc:
|
||||
api-docs:
|
||||
path: /v3/api-docs
|
||||
swagger-ui:
|
||||
path: /swagger-ui.html
|
||||
tags-sorter: alpha
|
||||
operations-sorter: alpha
|
||||
show-actuator: false
|
||||
|
||||
# Logging
|
||||
logging:
|
||||
level:
|
||||
{서비스 패키지 경로}: ${LOG_LEVEL_APP:DEBUG}
|
||||
org.springframework.web: ${LOG_LEVEL_WEB:INFO}
|
||||
org.hibernate.SQL: ${LOG_LEVEL_SQL:DEBUG}
|
||||
org.hibernate.type: ${LOG_LEVEL_SQL_TYPE:TRACE}
|
||||
pattern:
|
||||
console: "%d{yyyy-MM-dd HH:mm:ss} - %msg%n"
|
||||
file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
|
||||
file:
|
||||
name: ${LOG_FILE_PATH:logs/{서비스명}.log}
|
||||
|
||||
```
|
||||
|
||||
[루트 build.gradle 표준]
|
||||
```
|
||||
plugins {
|
||||
id 'java'
|
||||
id 'org.springframework.boot' version '3.3.0' apply false
|
||||
id 'io.spring.dependency-management' version '1.1.6' apply false
|
||||
id 'io.freefair.lombok' version '8.10' apply false
|
||||
}
|
||||
|
||||
group = 'com.unicorn.{시스템명}'
|
||||
version = '1.0.0'
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
subprojects {
|
||||
apply plugin: 'java'
|
||||
apply plugin: 'io.freefair.lombok'
|
||||
|
||||
java {
|
||||
sourceCompatibility = JavaVersion.VERSION_21
|
||||
targetCompatibility = JavaVersion.VERSION_21
|
||||
}
|
||||
|
||||
configurations {
|
||||
compileOnly {
|
||||
extendsFrom annotationProcessor
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named('test') {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
// Common versions for all subprojects
|
||||
ext {
|
||||
jjwtVersion = '0.12.5'
|
||||
springdocVersion = '2.5.0'
|
||||
mapstructVersion = '1.5.5.Final'
|
||||
commonsLang3Version = '3.14.0'
|
||||
commonsIoVersion = '2.16.1'
|
||||
hypersistenceVersion = '3.7.3'
|
||||
openaiVersion = '0.18.2'
|
||||
feignJacksonVersion = '13.1'
|
||||
}
|
||||
}
|
||||
|
||||
// Configure all subprojects with Spring dependency management
|
||||
subprojects {
|
||||
apply plugin: 'io.spring.dependency-management'
|
||||
|
||||
dependencyManagement {
|
||||
imports {
|
||||
mavenBom "org.springframework.cloud:spring-cloud-dependencies:2023.0.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Configure only service modules (exclude common)
|
||||
configure(subprojects.findAll { it.name != 'common' }) {
|
||||
apply plugin: 'org.springframework.boot'
|
||||
|
||||
dependencies {
|
||||
// Common module dependency
|
||||
implementation project(':common')
|
||||
|
||||
// Actuator for health checks and monitoring
|
||||
implementation 'org.springframework.boot:spring-boot-starter-actuator'
|
||||
|
||||
// API Documentation (common across all services)
|
||||
implementation "org.springdoc:springdoc-openapi-starter-webmvc-ui:${springdocVersion}"
|
||||
|
||||
// Testing
|
||||
testImplementation 'org.springframework.boot:spring-boot-starter-test'
|
||||
testImplementation 'org.springframework.security:spring-security-test'
|
||||
testImplementation 'org.testcontainers:junit-jupiter'
|
||||
testImplementation 'org.mockito:mockito-junit-jupiter'
|
||||
|
||||
// Configuration Processor
|
||||
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
|
||||
}
|
||||
}
|
||||
|
||||
// Java version consistency check for all modules
|
||||
tasks.register('checkJavaVersion') {
|
||||
doLast {
|
||||
println "Java Version: ${System.getProperty('java.version')}"
|
||||
println "Java Home: ${System.getProperty('java.home')}"
|
||||
}
|
||||
}
|
||||
|
||||
// Clean task for all subprojects
|
||||
tasks.register('cleanAll') {
|
||||
dependsOn subprojects.collect { it.tasks.named('clean') }
|
||||
description = 'Clean all subprojects'
|
||||
}
|
||||
|
||||
// Build task for all subprojects
|
||||
tasks.register('buildAll') {
|
||||
dependsOn subprojects.collect { it.tasks.named('build') }
|
||||
description = 'Build all subprojects'
|
||||
}
|
||||
```
|
||||
|
||||
<DB/Redis 설정 예제>
|
||||
```
|
||||
spring:
|
||||
datasource:
|
||||
url: jdbc:${DB_KIND:postgresql}://${DB_HOST:localhost}:${DB_PORT:5432}/${DB_NAME:phonebill_auth}
|
||||
username: ${DB_USERNAME:phonebill_user}
|
||||
password: ${DB_PASSWORD:phonebill_pass}
|
||||
driver-class-name: org.postgresql.Driver
|
||||
hikari:
|
||||
maximum-pool-size: 20
|
||||
minimum-idle: 5
|
||||
connection-timeout: 30000
|
||||
idle-timeout: 600000
|
||||
max-lifetime: 1800000
|
||||
leak-detection-threshold: 60000
|
||||
# JPA 설정
|
||||
jpa:
|
||||
show-sql: ${SHOW_SQL:true}
|
||||
properties:
|
||||
hibernate:
|
||||
format_sql: true
|
||||
use_sql_comments: true
|
||||
hibernate:
|
||||
ddl-auto: ${DDL_AUTO:update}
|
||||
|
||||
# Redis 설정
|
||||
data:
|
||||
redis:
|
||||
host: ${REDIS_HOST:localhost}
|
||||
port: ${REDIS_PORT:6379}
|
||||
password: ${REDIS_PASSWORD:}
|
||||
timeout: 2000ms
|
||||
lettuce:
|
||||
pool:
|
||||
max-active: 8
|
||||
max-idle: 8
|
||||
min-idle: 0
|
||||
max-wait: -1ms
|
||||
database: ${REDIS_DATABASE:0}
|
||||
|
||||
```
|
||||
|
||||
<SecurityConfig 예제>
|
||||
```
|
||||
/**
|
||||
* Spring Security 설정
|
||||
* JWT 기반 인증 및 API 보안 설정
|
||||
*/
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@RequiredArgsConstructor
|
||||
public class SecurityConfig {
|
||||
|
||||
private final JwtTokenProvider jwtTokenProvider;
|
||||
|
||||
@Value("${cors.allowed-origins:http://localhost:3000,http://localhost:8080,http://localhost:8081,http://localhost:8082,http://localhost:8083,http://localhost:8084}")
|
||||
private String allowedOrigins;
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
return http
|
||||
.csrf(AbstractHttpConfigurer::disable)
|
||||
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
|
||||
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
// Actuator endpoints
|
||||
.requestMatchers("/actuator/**").permitAll()
|
||||
// Swagger UI endpoints - context path와 상관없이 접근 가능하도록 설정
|
||||
.requestMatchers("/swagger-ui/**", "/swagger-ui.html", "/v3/api-docs/**", "/swagger-resources/**", "/webjars/**").permitAll()
|
||||
// Health check
|
||||
.requestMatchers("/health").permitAll()
|
||||
// All other requests require authentication
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider),
|
||||
UsernamePasswordAuthenticationFilter.class)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CorsConfigurationSource corsConfigurationSource() {
|
||||
CorsConfiguration configuration = new CorsConfiguration();
|
||||
|
||||
// 환경변수에서 허용할 Origin 패턴 설정
|
||||
String[] origins = allowedOrigins.split(",");
|
||||
configuration.setAllowedOriginPatterns(Arrays.asList(origins));
|
||||
|
||||
// 허용할 HTTP 메소드
|
||||
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"));
|
||||
|
||||
// 허용할 헤더
|
||||
configuration.setAllowedHeaders(Arrays.asList(
|
||||
"Authorization", "Content-Type", "X-Requested-With", "Accept",
|
||||
"Origin", "Access-Control-Request-Method", "Access-Control-Request-Headers"
|
||||
));
|
||||
|
||||
// 자격 증명 허용
|
||||
configuration.setAllowCredentials(true);
|
||||
|
||||
// Pre-flight 요청 캐시 시간
|
||||
configuration.setMaxAge(3600L);
|
||||
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
source.registerCorsConfiguration("/**", configuration);
|
||||
return source;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<JWT 인증처리 예제>
|
||||
|
||||
1) JwtAuthenticationFilter
|
||||
```
|
||||
/**
|
||||
* JWT 인증 필터
|
||||
* HTTP 요청에서 JWT 토큰을 추출하여 인증을 수행
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
|
||||
private final JwtTokenProvider jwtTokenProvider;
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
FilterChain filterChain) throws ServletException, IOException {
|
||||
|
||||
String token = jwtTokenProvider.resolveToken(request);
|
||||
|
||||
if (StringUtils.hasText(token) && jwtTokenProvider.validateToken(token)) {
|
||||
String userId = jwtTokenProvider.getUserId(token);
|
||||
String username = null;
|
||||
String authority = null;
|
||||
|
||||
try {
|
||||
username = jwtTokenProvider.getUsername(token);
|
||||
} catch (Exception e) {
|
||||
log.debug("JWT에 username 클레임이 없음: {}", e.getMessage());
|
||||
}
|
||||
|
||||
try {
|
||||
authority = jwtTokenProvider.getAuthority(token);
|
||||
} catch (Exception e) {
|
||||
log.debug("JWT에 authority 클레임이 없음: {}", e.getMessage());
|
||||
}
|
||||
|
||||
if (StringUtils.hasText(userId)) {
|
||||
// UserPrincipal 객체 생성 (username과 authority가 없어도 동작)
|
||||
UserPrincipal userPrincipal = UserPrincipal.builder()
|
||||
.userId(userId)
|
||||
.username(username != null ? username : "unknown")
|
||||
.authority(authority != null ? authority : "USER")
|
||||
.build();
|
||||
|
||||
UsernamePasswordAuthenticationToken authentication =
|
||||
new UsernamePasswordAuthenticationToken(
|
||||
userPrincipal,
|
||||
null,
|
||||
Collections.singletonList(new SimpleGrantedAuthority(authority != null ? authority : "USER"))
|
||||
);
|
||||
|
||||
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
|
||||
log.debug("인증된 사용자: {} ({})", userPrincipal.getUsername(), userId);
|
||||
}
|
||||
}
|
||||
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldNotFilter(HttpServletRequest request) {
|
||||
String path = request.getRequestURI();
|
||||
return path.startsWith("/actuator") ||
|
||||
path.startsWith("/swagger-ui") ||
|
||||
path.startsWith("/v3/api-docs") ||
|
||||
path.equals("/health");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
1) JwtTokenProvider
|
||||
```
|
||||
/**
|
||||
* JWT 토큰 제공자
|
||||
* JWT 토큰의 생성, 검증, 파싱을 담당
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class JwtTokenProvider {
|
||||
|
||||
private final SecretKey secretKey;
|
||||
private final long tokenValidityInMilliseconds;
|
||||
|
||||
public JwtTokenProvider(@Value("${jwt.secret}") String secret,
|
||||
@Value("${jwt.access-token-validity:3600}") long tokenValidityInSeconds) {
|
||||
this.secretKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
|
||||
this.tokenValidityInMilliseconds = tokenValidityInSeconds * 1000;
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP 요청에서 JWT 토큰 추출
|
||||
*/
|
||||
public String resolveToken(HttpServletRequest request) {
|
||||
String bearerToken = request.getHeader("Authorization");
|
||||
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
|
||||
return bearerToken.substring(7);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* JWT 토큰 유효성 검증
|
||||
*/
|
||||
public boolean validateToken(String token) {
|
||||
try {
|
||||
Jwts.parser()
|
||||
.setSigningKey(secretKey)
|
||||
.build()
|
||||
.parseClaimsJws(token);
|
||||
return true;
|
||||
} catch (SecurityException | MalformedJwtException e) {
|
||||
log.debug("Invalid JWT signature: {}", e.getMessage());
|
||||
} catch (ExpiredJwtException e) {
|
||||
log.debug("Expired JWT token: {}", e.getMessage());
|
||||
} catch (UnsupportedJwtException e) {
|
||||
log.debug("Unsupported JWT token: {}", e.getMessage());
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.debug("JWT token compact of handler are invalid: {}", e.getMessage());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* JWT 토큰에서 사용자 ID 추출
|
||||
*/
|
||||
public String getUserId(String token) {
|
||||
Claims claims = Jwts.parser()
|
||||
.setSigningKey(secretKey)
|
||||
.build()
|
||||
.parseClaimsJws(token)
|
||||
.getBody();
|
||||
|
||||
return claims.getSubject();
|
||||
}
|
||||
|
||||
/**
|
||||
* JWT 토큰에서 사용자명 추출
|
||||
*/
|
||||
public String getUsername(String token) {
|
||||
Claims claims = Jwts.parser()
|
||||
.setSigningKey(secretKey)
|
||||
.build()
|
||||
.parseClaimsJws(token)
|
||||
.getBody();
|
||||
|
||||
return claims.get("username", String.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* JWT 토큰에서 권한 정보 추출
|
||||
*/
|
||||
public String getAuthority(String token) {
|
||||
Claims claims = Jwts.parser()
|
||||
.setSigningKey(secretKey)
|
||||
.build()
|
||||
.parseClaimsJws(token)
|
||||
.getBody();
|
||||
|
||||
return claims.get("authority", String.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 토큰 만료 시간 확인
|
||||
*/
|
||||
public boolean isTokenExpired(String token) {
|
||||
try {
|
||||
Claims claims = Jwts.parser()
|
||||
.setSigningKey(secretKey)
|
||||
.build()
|
||||
.parseClaimsJws(token)
|
||||
.getBody();
|
||||
|
||||
return claims.getExpiration().before(new Date());
|
||||
} catch (Exception e) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 토큰에서 만료 시간 추출
|
||||
*/
|
||||
public Date getExpirationDate(String token) {
|
||||
Claims claims = Jwts.parser()
|
||||
.setSigningKey(secretKey)
|
||||
.build()
|
||||
.parseClaimsJws(token)
|
||||
.getBody();
|
||||
|
||||
return claims.getExpiration();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
1) UserPrincipal
|
||||
```
|
||||
/**
|
||||
* 인증된 사용자 정보
|
||||
* JWT 토큰에서 추출된 사용자 정보를 담는 Principal 객체
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@RequiredArgsConstructor
|
||||
public class UserPrincipal {
|
||||
|
||||
/**
|
||||
* 사용자 고유 ID
|
||||
*/
|
||||
private final String userId;
|
||||
|
||||
/**
|
||||
* 사용자명
|
||||
*/
|
||||
private final String username;
|
||||
|
||||
/**
|
||||
* 사용자 권한
|
||||
*/
|
||||
private final String authority;
|
||||
|
||||
/**
|
||||
* 사용자 ID 반환 (별칭)
|
||||
*/
|
||||
public String getName() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 관리자 권한 여부 확인
|
||||
*/
|
||||
public boolean isAdmin() {
|
||||
return "ADMIN".equals(authority);
|
||||
}
|
||||
|
||||
/**
|
||||
* 일반 사용자 권한 여부 확인
|
||||
*/
|
||||
public boolean isUser() {
|
||||
return "USER".equals(authority) || authority == null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<SwaggerConfig 예제>
|
||||
```
|
||||
/**
|
||||
* Swagger/OpenAPI 설정
|
||||
* AI Service API 문서화를 위한 설정
|
||||
*/
|
||||
@Configuration
|
||||
public class SwaggerConfig {
|
||||
|
||||
@Bean
|
||||
public OpenAPI openAPI() {
|
||||
return new OpenAPI()
|
||||
.info(apiInfo())
|
||||
.addServersItem(new Server()
|
||||
.url("http://localhost:8084")
|
||||
.description("Local Development"))
|
||||
.addServersItem(new Server()
|
||||
.url("{protocol}://{host}:{port}")
|
||||
.description("Custom Server")
|
||||
.variables(new io.swagger.v3.oas.models.servers.ServerVariables()
|
||||
.addServerVariable("protocol", new io.swagger.v3.oas.models.servers.ServerVariable()
|
||||
._default("http")
|
||||
.description("Protocol (http or https)")
|
||||
.addEnumItem("http")
|
||||
.addEnumItem("https"))
|
||||
.addServerVariable("host", new io.swagger.v3.oas.models.servers.ServerVariable()
|
||||
._default("localhost")
|
||||
.description("Server host"))
|
||||
.addServerVariable("port", new io.swagger.v3.oas.models.servers.ServerVariable()
|
||||
._default("8084")
|
||||
.description("Server port"))))
|
||||
.addSecurityItem(new SecurityRequirement().addList("Bearer Authentication"))
|
||||
.components(new Components()
|
||||
.addSecuritySchemes("Bearer Authentication", createAPIKeyScheme()));
|
||||
}
|
||||
|
||||
private Info apiInfo() {
|
||||
return new Info()
|
||||
.title("AI Service API")
|
||||
.description("AI 기반 시간별 상세 일정 생성 및 장소 추천 정보 API")
|
||||
.version("1.0.0")
|
||||
.contact(new Contact()
|
||||
.name("TripGen Development Team")
|
||||
.email("dev@tripgen.com"));
|
||||
}
|
||||
|
||||
private SecurityScheme createAPIKeyScheme() {
|
||||
return new SecurityScheme()
|
||||
.type(SecurityScheme.Type.HTTP)
|
||||
.bearerFormat("JWT")
|
||||
.scheme("bearer");
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,518 @@
|
||||
# 개발주석표준 가이드
|
||||
|
||||
## 📋 개요
|
||||
|
||||
이 문서는 CMS 프로젝트의 JavaDoc 주석 작성 표준을 정의합니다. 일관된 주석 스타일을 통해 코드의 가독성과 유지보수성을 향상시키는 것을 목표로 합니다.
|
||||
|
||||
## 🎯 주석 작성 원칙
|
||||
|
||||
### 1. **기본 원칙**
|
||||
- **명확성**: 코드의 의도와 동작을 명확하게 설명
|
||||
- **일관성**: 프로젝트 전체에서 동일한 스타일 적용
|
||||
- **완전성**: 모든 public 메서드와 클래스에 주석 작성
|
||||
- **최신성**: 코드 변경 시 주석도 함께 업데이트
|
||||
|
||||
### 2. **주석 대상**
|
||||
- **필수**: public 클래스, 인터페이스, 메서드
|
||||
- **권장**: protected 메서드, 중요한 필드
|
||||
- **선택**: private 메서드 (복잡한 로직인 경우)
|
||||
|
||||
## 📝 JavaDoc 기본 문법
|
||||
|
||||
### 1. **기본 구조**
|
||||
```java
|
||||
/**
|
||||
* 클래스나 메서드의 간단한 설명 (첫 번째 문장)
|
||||
*
|
||||
* <p>상세한 설명이 필요한 경우 여기에 작성합니다.</p>
|
||||
*
|
||||
* @param paramName 파라미터 설명
|
||||
* @return 반환값 설명
|
||||
* @throws ExceptionType 예외 상황 설명
|
||||
* @since 1.0
|
||||
* @author 작성자명
|
||||
* @see 관련클래스#메서드
|
||||
*/
|
||||
```
|
||||
|
||||
### 2. **주요 JavaDoc 태그**
|
||||
|
||||
| 태그 | 설명 | 사용 위치 |
|
||||
|------|------|-----------|
|
||||
| `@param` | 메서드 파라미터 설명 | 메서드 |
|
||||
| `@return` | 반환값 설명 | 메서드 |
|
||||
| `@throws` | 예외 상황 설명 | 메서드 |
|
||||
| `@since` | 도입 버전 | 클래스, 메서드 |
|
||||
| `@author` | 작성자 | 클래스 |
|
||||
| `@version` | 버전 정보 | 클래스 |
|
||||
| `@see` | 관련 항목 참조 | 모든 곳 |
|
||||
| `@apiNote` | API 사용 시 주의사항 | 메서드 |
|
||||
| `@implNote` | 구현 관련 참고사항 | 메서드 |
|
||||
|
||||
## 🎨 HTML 태그 활용 가이드
|
||||
|
||||
### 1. **HTML 태그 사용 이유**
|
||||
|
||||
JavaDoc은 소스코드 주석을 파싱하여 **HTML 형태의 API 문서**를 자동 생성합니다. HTML 태그를 사용하면:
|
||||
|
||||
- **가독성 향상**: 구조화된 문서로 이해하기 쉬움
|
||||
- **자동 문서화**: JavaDoc 도구가 예쁜 HTML 문서 생성
|
||||
- **IDE 지원**: 개발 도구에서 리치 텍스트로 표시
|
||||
- **표준 준수**: Oracle JavaDoc 스타일 가이드 준수
|
||||
|
||||
### 2. **자주 사용되는 HTML 태그**
|
||||
|
||||
#### **텍스트 서식**
|
||||
```java
|
||||
/**
|
||||
* <p>단락을 구분할 때 사용합니다.</p>
|
||||
* <b>중요한 내용</b>을 강조할 때 사용합니다.
|
||||
* <i>이탤릭체</i>로 표시할 때 사용합니다.
|
||||
* <code>method()</code>와 같은 코드를 표시할 때 사용합니다.
|
||||
*/
|
||||
```
|
||||
|
||||
#### **목록 작성**
|
||||
```java
|
||||
/**
|
||||
* <p><b>주요 기능:</b></p>
|
||||
* <ul>
|
||||
* <li>첫 번째 기능</li>
|
||||
* <li>두 번째 기능</li>
|
||||
* <li>세 번째 기능</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p><b>처리 과정:</b></p>
|
||||
* <ol>
|
||||
* <li>첫 번째 단계</li>
|
||||
* <li>두 번째 단계</li>
|
||||
* <li>세 번째 단계</li>
|
||||
* </ol>
|
||||
*/
|
||||
```
|
||||
|
||||
#### **코드 블록**
|
||||
```java
|
||||
/**
|
||||
* <p>사용 예시:</p>
|
||||
* <pre>
|
||||
* AuthController controller = new AuthController();
|
||||
* LoginRequest request = new LoginRequest("user", "password");
|
||||
* ResponseEntity<LoginResponse> response = controller.login(request);
|
||||
* </pre>
|
||||
*/
|
||||
```
|
||||
|
||||
#### **테이블**
|
||||
```java
|
||||
/**
|
||||
* <p><b>HTTP 상태 코드:</b></p>
|
||||
* <table>
|
||||
* <tr><th>상태 코드</th><th>설명</th></tr>
|
||||
* <tr><td>200</td><td>성공</td></tr>
|
||||
* <tr><td>400</td><td>잘못된 요청</td></tr>
|
||||
* <tr><td>401</td><td>인증 실패</td></tr>
|
||||
* </table>
|
||||
*/
|
||||
```
|
||||
|
||||
### 3. **HTML 태그 사용 규칙**
|
||||
|
||||
- **<와 >**: 제네릭 타입 표현 시 `<T>` 사용
|
||||
- **줄바꿈**: `<br>` 태그 사용 (가급적 `<p>` 태그 권장)
|
||||
- **링크**: `{@link ClassName#methodName}` 사용
|
||||
- **인라인 코드**: `{@code variableName}` 또는 `<code>` 사용
|
||||
|
||||
## 📋 클래스 주석 표준
|
||||
|
||||
### 1. **클래스 주석 템플릿**
|
||||
```java
|
||||
/**
|
||||
* 클래스의 간단한 설명
|
||||
*
|
||||
* <p>클래스의 상세한 설명과 목적을 여기에 작성합니다.</p>
|
||||
*
|
||||
* <p><b>주요 기능:</b></p>
|
||||
* <ul>
|
||||
* <li>기능 1</li>
|
||||
* <li>기능 2</li>
|
||||
* <li>기능 3</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p><b>사용 예시:</b></p>
|
||||
* <pre>
|
||||
* ClassName instance = new ClassName();
|
||||
* instance.someMethod();
|
||||
* </pre>
|
||||
*
|
||||
* <p><b>주의사항:</b></p>
|
||||
* <ul>
|
||||
* <li>주의사항 1</li>
|
||||
* <li>주의사항 2</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author 작성자명
|
||||
* @version 1.0
|
||||
* @since 2024-01-01
|
||||
*
|
||||
* @see 관련클래스1
|
||||
* @see 관련클래스2
|
||||
*/
|
||||
public class ClassName {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### 2. **Controller 클래스 주석 예시**
|
||||
```java
|
||||
/**
|
||||
* 사용자 관리 API 컨트롤러
|
||||
*
|
||||
* <p>사용자 등록, 조회, 수정, 삭제 기능을 제공하는 REST API 컨트롤러입니다.</p>
|
||||
*
|
||||
* <p><b>주요 기능:</b></p>
|
||||
* <ul>
|
||||
* <li>사용자 등록 및 인증</li>
|
||||
* <li>사용자 정보 조회 및 수정</li>
|
||||
* <li>사용자 권한 관리</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p><b>API 엔드포인트:</b></p>
|
||||
* <ul>
|
||||
* <li>POST /api/users - 사용자 등록</li>
|
||||
* <li>GET /api/users/{id} - 사용자 조회</li>
|
||||
* <li>PUT /api/users/{id} - 사용자 수정</li>
|
||||
* <li>DELETE /api/users/{id} - 사용자 삭제</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p><b>보안 고려사항:</b></p>
|
||||
* <ul>
|
||||
* <li>모든 엔드포인트는 인증이 필요합니다</li>
|
||||
* <li>개인정보 처리 시 데이터 마스킹 적용</li>
|
||||
* <li>입력값 검증 및 XSS 방지</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author cms-team
|
||||
* @version 1.0
|
||||
* @since 2024-01-01
|
||||
*
|
||||
* @see UserService
|
||||
* @see UserRepository
|
||||
* @see UserDTO
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/users")
|
||||
public class UserController {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## 📋 메서드 주석 표준
|
||||
|
||||
### 1. **메서드 주석 템플릿**
|
||||
```java
|
||||
/**
|
||||
* 메서드의 간단한 설명
|
||||
*
|
||||
* <p>메서드의 상세한 설명과 동작을 여기에 작성합니다.</p>
|
||||
*
|
||||
* <p><b>처리 과정:</b></p>
|
||||
* <ol>
|
||||
* <li>첫 번째 단계</li>
|
||||
* <li>두 번째 단계</li>
|
||||
* <li>세 번째 단계</li>
|
||||
* </ol>
|
||||
*
|
||||
* <p><b>주의사항:</b></p>
|
||||
* <ul>
|
||||
* <li>주의사항 1</li>
|
||||
* <li>주의사항 2</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param param1 첫 번째 파라미터 설명
|
||||
* - 추가 설명이 필요한 경우
|
||||
* @param param2 두 번째 파라미터 설명
|
||||
*
|
||||
* @return 반환값 설명
|
||||
* - 성공 시: 설명
|
||||
* - 실패 시: 설명
|
||||
*
|
||||
* @throws ExceptionType1 예외 상황 1 설명
|
||||
* @throws ExceptionType2 예외 상황 2 설명
|
||||
*
|
||||
* @apiNote API 사용 시 주의사항
|
||||
*
|
||||
* @see 관련메서드1
|
||||
* @see 관련메서드2
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public ReturnType methodName(Type param1, Type param2) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### 2. **API 메서드 주석 예시**
|
||||
```java
|
||||
/**
|
||||
* 사용자 로그인 처리
|
||||
*
|
||||
* <p>사용자 ID와 비밀번호를 검증하여 JWT 토큰을 생성합니다.</p>
|
||||
*
|
||||
* <p><b>처리 과정:</b></p>
|
||||
* <ol>
|
||||
* <li>입력값 검증 (@Valid 어노테이션)</li>
|
||||
* <li>사용자 인증 정보 확인</li>
|
||||
* <li>JWT 토큰 생성</li>
|
||||
* <li>사용자 세션 시작</li>
|
||||
* <li>로그인 메트릭 업데이트</li>
|
||||
* </ol>
|
||||
*
|
||||
* <p><b>보안 고려사항:</b></p>
|
||||
* <ul>
|
||||
* <li>비밀번호는 BCrypt로 암호화된 값과 비교</li>
|
||||
* <li>로그인 실패 시 상세 정보 노출 방지</li>
|
||||
* <li>로그인 시도 로그 기록</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param request 로그인 요청 정보
|
||||
* - username: 사용자 ID (3-50자, 필수)
|
||||
* - password: 비밀번호 (6-100자, 필수)
|
||||
*
|
||||
* @return ResponseEntity<LoginResponse> 로그인 응답 정보
|
||||
* - 성공 시: 200 OK + JWT 토큰, 사용자 역할, 만료 시간
|
||||
* - 실패 시: 401 Unauthorized + 에러 메시지
|
||||
*
|
||||
* @throws InvalidCredentialsException 인증 정보가 올바르지 않은 경우
|
||||
* @throws RuntimeException 로그인 처리 중 시스템 오류 발생 시
|
||||
*
|
||||
* @apiNote 보안상 이유로 로그인 실패 시 구체적인 실패 사유를 반환하지 않습니다.
|
||||
*
|
||||
* @see AuthService#login(LoginRequest)
|
||||
* @see UserSessionService#startSession(String, String, java.time.Instant)
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
@PostMapping("/login")
|
||||
public ResponseEntity<LoginResponse> login(@Valid @RequestBody LoginRequest request) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## 📋 필드 주석 표준
|
||||
|
||||
### 1. **필드 주석 템플릿**
|
||||
```java
|
||||
/**
|
||||
* 필드의 간단한 설명
|
||||
*
|
||||
* <p>필드의 상세한 설명과 용도를 여기에 작성합니다.</p>
|
||||
*
|
||||
* <p><b>주의사항:</b></p>
|
||||
* <ul>
|
||||
* <li>주의사항 1</li>
|
||||
* <li>주의사항 2</li>
|
||||
* </ul>
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
private final ServiceType serviceName;
|
||||
```
|
||||
|
||||
### 2. **의존성 주입 필드 예시**
|
||||
```java
|
||||
/**
|
||||
* 인증 서비스
|
||||
*
|
||||
* <p>사용자 로그인/로그아웃 처리 및 JWT 토큰 관리를 담당합니다.</p>
|
||||
*
|
||||
* <p><b>주요 기능:</b></p>
|
||||
* <ul>
|
||||
* <li>사용자 인증 정보 검증</li>
|
||||
* <li>JWT 토큰 생성 및 검증</li>
|
||||
* <li>로그인/로그아웃 처리</li>
|
||||
* </ul>
|
||||
*
|
||||
* @see AuthService
|
||||
* @since 1.0
|
||||
*/
|
||||
private final AuthService authService;
|
||||
```
|
||||
|
||||
## 📋 예외 클래스 주석 표준
|
||||
|
||||
```java
|
||||
/**
|
||||
* 사용자 인증 실패 예외
|
||||
*
|
||||
* <p>로그인 시 사용자 ID 또는 비밀번호가 올바르지 않을 때 발생하는 예외입니다.</p>
|
||||
*
|
||||
* <p><b>발생 상황:</b></p>
|
||||
* <ul>
|
||||
* <li>존재하지 않는 사용자 ID</li>
|
||||
* <li>잘못된 비밀번호</li>
|
||||
* <li>계정 잠금 상태</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p><b>처리 방법:</b></p>
|
||||
* <ul>
|
||||
* <li>사용자에게 일반적인 오류 메시지 표시</li>
|
||||
* <li>보안 로그에 상세 정보 기록</li>
|
||||
* <li>브루트 포스 공격 방지 로직 실행</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author cms-team
|
||||
* @version 1.0
|
||||
* @since 2024-01-01
|
||||
*
|
||||
* @see AuthService
|
||||
* @see SecurityException
|
||||
*/
|
||||
public class InvalidCredentialsException extends RuntimeException {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## 📋 인터페이스 주석 표준
|
||||
|
||||
```java
|
||||
/**
|
||||
* 사용자 인증 서비스 인터페이스
|
||||
*
|
||||
* <p>사용자 로그인, 로그아웃, 토큰 관리 등 인증 관련 기능을 정의합니다.</p>
|
||||
*
|
||||
* <p><b>구현 클래스:</b></p>
|
||||
* <ul>
|
||||
* <li>{@link AuthServiceImpl} - 기본 구현체</li>
|
||||
* <li>{@link LdapAuthService} - LDAP 연동 구현체</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p><b>주요 기능:</b></p>
|
||||
* <ul>
|
||||
* <li>사용자 인증 및 토큰 생성</li>
|
||||
* <li>로그아웃 및 토큰 무효화</li>
|
||||
* <li>토큰 유효성 검증</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author cms-team
|
||||
* @version 1.0
|
||||
* @since 2024-01-01
|
||||
*
|
||||
* @see AuthServiceImpl
|
||||
* @see TokenProvider
|
||||
*/
|
||||
public interface AuthService {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## 📋 Enum 주석 표준
|
||||
|
||||
```java
|
||||
/**
|
||||
* 사용자 역할 열거형
|
||||
*
|
||||
* <p>시스템 사용자의 권한 수준을 정의합니다.</p>
|
||||
*
|
||||
* <p><b>권한 계층:</b></p>
|
||||
* <ol>
|
||||
* <li>{@link #ADMIN} - 최고 관리자 권한</li>
|
||||
* <li>{@link #MANAGER} - 관리자 권한</li>
|
||||
* <li>{@link #USER} - 일반 사용자 권한</li>
|
||||
* </ol>
|
||||
*
|
||||
* @author cms-team
|
||||
* @version 1.0
|
||||
* @since 2024-01-01
|
||||
*/
|
||||
public enum Role {
|
||||
|
||||
/**
|
||||
* 시스템 관리자
|
||||
*
|
||||
* <p>모든 시스템 기능에 대한 접근 권한을 가집니다.</p>
|
||||
*
|
||||
* <p><b>주요 권한:</b></p>
|
||||
* <ul>
|
||||
* <li>사용자 관리</li>
|
||||
* <li>시스템 설정</li>
|
||||
* <li>모든 데이터 접근</li>
|
||||
* </ul>
|
||||
*/
|
||||
ADMIN,
|
||||
|
||||
/**
|
||||
* 관리자
|
||||
*
|
||||
* <p>제한된 관리 기능에 대한 접근 권한을 가집니다.</p>
|
||||
*/
|
||||
MANAGER,
|
||||
|
||||
/**
|
||||
* 일반 사용자
|
||||
*
|
||||
* <p>기본적인 시스템 기능에 대한 접근 권한을 가집니다.</p>
|
||||
*/
|
||||
USER
|
||||
}
|
||||
```
|
||||
|
||||
## 📋 주석 작성 체크리스트
|
||||
|
||||
### ✅ **클래스 주석 체크리스트**
|
||||
- [ ] 클래스의 목적과 역할 명시
|
||||
- [ ] 주요 기능 목록 작성
|
||||
- [ ] 사용 예시 코드 포함
|
||||
- [ ] 주의사항 및 제약사항 명시
|
||||
- [ ] @author, @version, @since 태그 작성
|
||||
- [ ] 관련 클래스 @see 태그 추가
|
||||
|
||||
### ✅ **메서드 주석 체크리스트**
|
||||
- [ ] 메서드의 목적과 동작 설명
|
||||
- [ ] 처리 과정 단계별 설명
|
||||
- [ ] 모든 @param 태그 작성
|
||||
- [ ] @return 태그 작성 (void 메서드 제외)
|
||||
- [ ] 가능한 예외 @throws 태그 작성
|
||||
- [ ] 보안 관련 주의사항 명시
|
||||
- [ ] 관련 메서드 @see 태그 추가
|
||||
|
||||
### ✅ **HTML 태그 체크리스트**
|
||||
- [ ] 목록은 `<ul>`, `<ol>`, `<li>` 태그 사용
|
||||
- [ ] 강조는 `<b>` 태그 사용
|
||||
- [ ] 단락 구분은 `<p>` 태그 사용
|
||||
- [ ] 코드는 `<code>` 또는 `<pre>` 태그 사용
|
||||
- [ ] 제네릭 타입은 `<`, `>` 사용
|
||||
|
||||
## 📋 도구 및 설정
|
||||
|
||||
### 1. **JavaDoc 생성**
|
||||
```bash
|
||||
# Gradle 프로젝트
|
||||
./gradlew javadoc
|
||||
|
||||
# Maven 프로젝트
|
||||
mvn javadoc:javadoc
|
||||
|
||||
# 직접 실행
|
||||
javadoc -d docs -cp classpath src/**/*.java
|
||||
```
|
||||
|
||||
### 2. **IDE 설정**
|
||||
- **IntelliJ IDEA**: Settings > Editor > Code Style > Java > JavaDoc
|
||||
- **Eclipse**: Window > Preferences > Java > Code Style > Code Templates
|
||||
- **VS Code**: Java Extension Pack + JavaDoc 플러그인
|
||||
|
||||
### 3. **정적 분석 도구**
|
||||
- **Checkstyle**: JavaDoc 누락 검사
|
||||
- **SpotBugs**: 주석 품질 검사
|
||||
- **SonarQube**: 문서화 품질 메트릭
|
||||
|
||||
## 📋 참고 자료
|
||||
|
||||
- [Oracle JavaDoc 가이드](https://docs.oracle.com/javase/8/docs/technotes/tools/windows/javadoc.html)
|
||||
- [Google Java Style Guide](https://google.github.io/styleguide/javaguide.html)
|
||||
- [Spring Framework 주석 스타일](https://github.com/spring-projects/spring-framework/wiki/Code-Style)
|
||||
|
||||
---
|
||||
|
||||
> **💡 팁**: 이 가이드를 팀 내에서 공유하고, 코드 리뷰 시 주석 품질도 함께 검토하세요!
|
||||
@@ -0,0 +1,173 @@
|
||||
패키지 구조 표준
|
||||
|
||||
레이어드 아키텍처 패키지 구조
|
||||
|
||||
├── {SERVICE}
|
||||
│ ├── domain
|
||||
│ ├── service
|
||||
│ ├── controller
|
||||
│ ├── dto
|
||||
│ ├── repository
|
||||
│ │ ├── jpa
|
||||
│ │ └── entity
|
||||
│ ├── config
|
||||
└── common
|
||||
├── dto
|
||||
├── util
|
||||
├── response
|
||||
└── exception
|
||||
|
||||
Package명:
|
||||
- com.{ORG}.{ROOT}.{SERVICE}
|
||||
예) com.unicorn.lifesub.mysub, com.unicorn.lifesub.common
|
||||
|
||||
변수:
|
||||
- ORG: 회사 또는 조직명
|
||||
- ROOT: Root Project 명
|
||||
- SERVICE: 서비스명으로 Root Project의 서브 프로젝트임
|
||||
|
||||
|
||||
예시
|
||||
|
||||
com.unicorn.lifesub.member
|
||||
├── MemberApplication.java
|
||||
├── controller
|
||||
│ └── MemberController.java
|
||||
├── dto
|
||||
│ ├── LoginRequest.java
|
||||
│ ├── LogoutRequest.java
|
||||
│ └── LogoutResponse.java
|
||||
├── service
|
||||
│ ├── MemberService.java
|
||||
│ └── MemberServiceImpl.java
|
||||
├── domain
|
||||
│ └── Member.java
|
||||
├── repository
|
||||
│ ├── entity
|
||||
│ │ └── MemberEntity.java
|
||||
│ └── jpa
|
||||
│ └── MemberRepository.java
|
||||
└── config
|
||||
├── SecurityConfig.java
|
||||
├── DataLoader.java
|
||||
├── SwaggerConfig.java
|
||||
└── jwt
|
||||
├── JwtAuthenticationFilter.java
|
||||
├── JwtTokenProvider.java
|
||||
└── CustomUserDetailsService.java
|
||||
|
||||
|
||||
클린 아키텍처 패키지 구조
|
||||
|
||||
├── biz
|
||||
│ ├── usecase
|
||||
│ │ ├── in
|
||||
│ │ ├── out
|
||||
│ ├── service
|
||||
│ └── domain
|
||||
│ └── dto
|
||||
├── infra
|
||||
│ ├── controller
|
||||
│ ├── dto
|
||||
│ ├── gateway
|
||||
│ │ ├── repository
|
||||
│ │ └── entity
|
||||
│ └── config
|
||||
|
||||
|
||||
Package명:
|
||||
- com.{ORG}.{ROOT}.{SERVICE}.biz
|
||||
- com.{ORG}.{ROOT}.{SERVICE}.infra
|
||||
예) com.unicorn.lifesub.mysub.biz, com.unicorn.lifesub.common
|
||||
|
||||
변수:
|
||||
- ORG: 회사 또는 조직명
|
||||
- ROOT: Root Project 명
|
||||
- SERVICE: 서비스명으로 Root Project의 서브 프로젝트임
|
||||
|
||||
예시
|
||||
|
||||
|
||||
com.unicorn.lifesub.mysub
|
||||
├── biz
|
||||
│ ├── dto
|
||||
│ │ ├── CategoryResponse.java
|
||||
│ │ ├── ServiceListResponse.java
|
||||
│ │ ├── MySubResponse.java
|
||||
│ │ ├── SubDetailResponse.java
|
||||
│ │ └── TotalFeeResponse.java
|
||||
│ ├── service
|
||||
│ │ ├── FeeLevel.java
|
||||
│ │ └── MySubscriptionService.java
|
||||
│ ├── usecase
|
||||
│ │ ├── in
|
||||
│ │ │ ├── CancelSubscriptionUseCase.java
|
||||
│ │ │ ├── CategoryUseCase.java
|
||||
│ │ │ ├── MySubscriptionsUseCase.java
|
||||
│ │ │ ├── SubscribeUseCase.java
|
||||
│ │ │ ├── SubscriptionDetailUseCase.java
|
||||
│ │ │ └── TotalFeeUseCase.java
|
||||
│ │ └── out
|
||||
│ │ ├── MySubscriptionReader.java
|
||||
│ │ ├── MySubscriptionWriter.java
|
||||
│ │ └── SubscriptionReader.java
|
||||
│ └── domain
|
||||
│ ├── Category.java
|
||||
│ ├── MySubscription.java
|
||||
│ └── Subscription.java
|
||||
└── infra
|
||||
├── MySubApplication.java
|
||||
├── controller
|
||||
│ ├── CategoryController.java
|
||||
│ ├── MySubController.java
|
||||
│ └── ServiceController.java
|
||||
├── config
|
||||
│ ├── DataLoader.java
|
||||
│ ├── SecurityConfig.java
|
||||
│ ├── SwaggerConfig.java
|
||||
│ └── jwt
|
||||
│ ├── JwtAuthenticationFilter.java
|
||||
│ └── JwtTokenProvider.java
|
||||
└── gateway
|
||||
├── entity
|
||||
│ ├── CategoryEntity.java
|
||||
│ ├── MySubscriptionEntity.java
|
||||
│ └── SubscriptionEntity.java
|
||||
├── repository
|
||||
│ ├── CategoryJpaRepository.java
|
||||
│ ├── MySubscriptionJpaRepository.java
|
||||
│ └── SubscriptionJpaRepository.java
|
||||
├── MySubscriptionGateway.java
|
||||
└── SubscriptionGateway.java
|
||||
|
||||
|
||||
---
|
||||
|
||||
common 모듈 패키지 구조
|
||||
|
||||
├── common
|
||||
├── dto
|
||||
├── entity
|
||||
├── config
|
||||
├── util
|
||||
└── exception
|
||||
|
||||
|
||||
com.unicorn.lifesub.common
|
||||
├── dto
|
||||
│ ├── ApiResponse.java
|
||||
│ ├── JwtTokenDTO.java
|
||||
│ ├── JwtTokenRefreshDTO.java
|
||||
│ └── JwtTokenVerifyDTO.java
|
||||
├── config
|
||||
│ └── JpaConfig.java
|
||||
├── entity
|
||||
│ └── BaseTimeEntity.java
|
||||
├── aop
|
||||
│ └── LoggingAspect.java
|
||||
└── exception
|
||||
├── ErrorCode.java
|
||||
├── InfraException.java
|
||||
└── BusinessException.java
|
||||
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
1.TDD 기본 이해
|
||||
|
||||
1) TDD 목적
|
||||
코드 품질 향상으로 유지보수 비용 절감
|
||||
- 설계 품질 향상: 테스트를 먼저 작성하면서 코드 구조와 인터페이스를 먼저 고민
|
||||
- 회귀 버그 방지: 테스트 자동화로 코드 변경 시 기존 기능의 오작동을 빠르게 감지
|
||||
- 리팩토링 검증: 코드 개선 후 테스트 코드로 검증할 수 있어 리팩토링에 대한 자신감 확보
|
||||
- 살아있는 문서: 테스트 코드에 샘플 데이터를 이용한 예시가 있으므로 실제 코드의 동작 방식을 문서화
|
||||
|
||||
---
|
||||
|
||||
2) 테스트 유형
|
||||
- 단위 테스트(Unit Test): 외부 기술요소(DB, 웹서버 등)와의 인터페이스 없이 단위 클래스의 퍼블릭 메소드 테스트
|
||||
- 통합 테스트(Integration Test): 일부 아키텍처 영역에서 외부 기술 요소와 인터페이스까지 테스트
|
||||
- E2E 테스트(E2E Test): 모든 아키텍처 영역에서 외부 기술 요소와 인터페이스를 테스트
|
||||
|
||||
* 아키텍처 영역: 클래스를 아키텍처적으로 나눈 레이어를 의미함(예: controller, service, domain, repository)
|
||||
|
||||
---
|
||||
|
||||
3) 테스트 피라미드
|
||||
|
||||
- 단위 테스트 70%, 통합 테스트 20%, E2E 테스트 10%의 비율로 권장
|
||||
- Mike Cohn이 "Succeeding with Agile"에서 처음 제시한 개념
|
||||
- 단위 테스트에서 E2E 테스트로 가면서 속도는 느려지고 비용은 높아짐
|
||||
|
||||
---
|
||||
|
||||
4) Red-Green-Refactor 사이클
|
||||
|
||||
Red-Green-Refactor는 TDD(Test-Driven Development)를 수행하는 핵심 사이클임
|
||||
- Red (실패하는 테스트 작성)
|
||||
- 새로운 기능에 대한 테스트 코드를 먼저 작성
|
||||
- 아직 구현이 없으므로 테스트는 실패
|
||||
- 이 단계에서 기능의 인터페이스를 설계
|
||||
- Green (테스트 통과하는 코드 작성)
|
||||
- 테스트를 통과하는 최소한의 코드 작성
|
||||
- 품질보다는 동작에 초점
|
||||
- Refactor (리팩토링)
|
||||
- 중복 제거, 가독성 개선
|
||||
- 테스트는 계속 통과하도록 유지
|
||||
- 코드 품질 개선
|
||||
|
||||
---
|
||||
2. 테스트 전략
|
||||
|
||||
1) 테스트 수행 원칙: FIRST 원칙
|
||||
- Fast: 테스트는 빠르게 실행되어야 함
|
||||
- Isolated: 각 테스트는 독립적이어야 함
|
||||
- Repeatable: 어떤 환경에서도 동일한 결과가 나와야 함
|
||||
- Self-validating: 테스트는 성공/실패가 명확해야 함
|
||||
- Timely: 테스트는 실제 코드 작성 전/직후에 작성되어야 함
|
||||
|
||||
---
|
||||
|
||||
2) 공통 전략: 테스트 코드 작성 관련
|
||||
- 한 테스트는 한 가지만 테스트
|
||||
- Given-When-Then 패턴 사용
|
||||
- Given(준비): 테스트에 필요한 상태와 데이터를 설정
|
||||
- When(실행): 테스트하려는 동작을 수행
|
||||
- Then(검증): 기대하는 결과가 나왔는지 확인
|
||||
- 깨끗한 테스트 코드 작성
|
||||
- 테스트 의도를 명확히 하는 네이밍
|
||||
- 테스트 케이스는 시나리오 중심으로 구성
|
||||
- 공통 설정은 별도 메서드로 분리
|
||||
- 매직넘버 대신 상수 사용
|
||||
- 테스트 데이터는 최소한으로 사용
|
||||
- 경계값 테스트가 중요
|
||||
- null 값
|
||||
- 빈 컬렉션
|
||||
- 최대/최소값
|
||||
- 0이나 1과 같은 특수값
|
||||
- 잘못된 포맷의 입력값
|
||||
|
||||
---
|
||||
|
||||
2) 공통 전략: 테스트 코드 관리 관련
|
||||
- 비용 효율적인 테스트 전략
|
||||
- 자주 변경되는 비즈니스 로직에 대한 테스트 강화
|
||||
- 실제 운영 환경과 유사한 통합 테스트 구성
|
||||
- 테스트 실행 시간과 리소스 사용량 모니터링
|
||||
- 지속적인 테스트 개선
|
||||
- 테스트 커버리지보다 테스트 품질 중시
|
||||
- 깨진 테스트는 즉시 수정하는 문화 정착
|
||||
- 테스트 코드도 실제 코드만큼 중요하게 관리
|
||||
- 팀 협업을 위한 가이드라인 수립
|
||||
- 테스트 네이밍 컨벤션 수립
|
||||
- 테스트 데이터 관리 전략 합의
|
||||
- 테스트 실패 시 대응 프로세스 수립
|
||||
|
||||
---
|
||||
|
||||
3) 단위 테스트 전략
|
||||
- 테스트 범위 명확화
|
||||
- 클래스의 각 public 메소드가 수행하는 단일 책임을 검증
|
||||
- private 메서드는 public 메서드를 통해 간접적으로 테스트
|
||||
- 외부 의존성 처리
|
||||
- DB, 파일, 네트워크 등 외부 시스템은 가짜 객체로 대체(Mocking)
|
||||
- 테스트 더블(스턴트맨을 Stunt Double이라고 함. 대역으로 이해)은 꼭 필요한 동작만 구현
|
||||
- Mock: 메소드 호출 여부와 파라미터 검증
|
||||
- Stub: 반환값의 일치 여부 검증
|
||||
- Spy: Mocking하지 않고 실제 메소드를 감싸서 호출횟수, 호출순서등 추가 정보 검증
|
||||
- 격리성 확보
|
||||
- 테스트 간 상호 영향 없도록 설계: 동일 공유 자원/객체를 사용하지 않게 함
|
||||
- 테스트 실행 순서와 무관하게 동작
|
||||
- 가독성과 유지보수성
|
||||
- 테스트 대상 클래스당 하나의 테스트 클래스
|
||||
- 테스트 메서드는 한 가지 시나리오만 검증
|
||||
|
||||
---
|
||||
|
||||
4) 단위 테스트 시 Mocking 전략
|
||||
- 외부 시스템(DB, 외부 API 등)은 반드시 Mocking
|
||||
- 같은 레이어의 의존성 있는 클래스는 실제 객체 사용
|
||||
- 예외적으로 의존 객체가 매우 복잡하거나 무거운 경우 Mocking 고려
|
||||
|
||||
* 참고: 모의 객체 테스트 균형점 찾기
|
||||
출처: When to mocking by Uncle Bob(https://blog.cleancoder.com/uncle-bob/2014/05/10/WhenToMock.html)
|
||||
- 모의 객체를 이용 안 하면: 테스트가 오래 걸리고 결과를 신뢰하기 어려우며 인프라에 너무 많은 영향을 받음
|
||||
- 모의 객체를 지나치게 사용하면: 복잡하고 수정에 영향을 너무 많이 받으며 모의 인터페이스가 폭발적으로 증가
|
||||
- 균형점 찾기
|
||||
- 아키텍처적으로 중요한 경계에서만 모의 테스트를 수행하고, 그 경계 안에서는 하지 않는다.
|
||||
(Mock across architecturally significant boundaries, but not within those boundaries.)
|
||||
- 여기서 경계란 Controller, Service, Repository, Domain등의 레이어를 의미함
|
||||
|
||||
---
|
||||
5) 통합 테스트 전략
|
||||
- 웹 서버 인터페이스
|
||||
- @WebMvcTest, @WebFluxTest 활용
|
||||
- Controller 계층의 요청/응답 검증
|
||||
- Service 계층은 Mocking 처리
|
||||
|
||||
- Database 인터페이스
|
||||
- @DataJpaTest 활용
|
||||
- TestContainer로 실제 DB 엔진 실행
|
||||
|
||||
- 외부 서비스 인터페이스
|
||||
- WireMock 등을 활용한 Mocking
|
||||
- 실제 API 스펙 기반 테스트
|
||||
|
||||
- 테스트 환경 구성
|
||||
- 테스트용 별도 설정 파일 구성
|
||||
- 테스트 데이터는 테스트 시작 시 초기화
|
||||
- @Transactional을 활용한 테스트 격리
|
||||
- 테스트 간 독립성 보장
|
||||
|
||||
---
|
||||
6) E2E 테스트 전략
|
||||
- 원칙
|
||||
- 단위 테스트나 컴포넌트 테스트에서 놓칠 수 있는 시나리오를 찾아내는 것이 목표임
|
||||
- 조건별 로직이나 분기 상황(edge cases)이 아닌 상위 수준의 일반적인 시나리오만 테스트
|
||||
- 만약 어떤 시스템 테스트 시나리오가 실패 했는데 단위 테스트나 통합 테스트가 없다면 만들어야 함
|
||||
|
||||
- 운영과 동일한 테스트 환경 구성: 웹서버/WAS, DB, 캐시, MQ, 외부시스템
|
||||
- 테스트 데이터 관리
|
||||
- 테스트용 마스터 데이터 구성
|
||||
- 시나리오별 테스트 데이터 세트 준비
|
||||
- 데이터 초기화 및 정리 자동화
|
||||
- 테스트 자동화 전략
|
||||
- UI 테스트: Selenium, Cucumber, Playwright 등 도구 활용
|
||||
- API 테스트: Rest-Assured, Postman 등 도구 활용
|
||||
|
||||
---
|
||||
|
||||
7) 테스트 코드 네이밍 컨벤션
|
||||
|
||||
- 패키지 네이밍
|
||||
```
|
||||
[Syntax]
|
||||
{프로덕션패키지}.test.{테스트유형}
|
||||
|
||||
[Example]
|
||||
- 단위테스트: com.company.order.test.unit
|
||||
- 통합테스트: com.company.order.test.integration
|
||||
- E2E테스트: com.company.order.test.e2e
|
||||
```
|
||||
|
||||
- 클래스 네이밍
|
||||
```
|
||||
[Syntax]
|
||||
{대상클래스}{테스트유형}Test
|
||||
|
||||
[Example]
|
||||
- 단위테스트: OrderServiceUnitTest
|
||||
- 통합테스트: OrderServiceIntegrationTest
|
||||
- E2E테스트: OrderServiceE2ETest
|
||||
```
|
||||
|
||||
- 메소드 네이밍
|
||||
```
|
||||
[Syntax]
|
||||
given{초기상태}_when{행위}_then{결과}
|
||||
|
||||
[Example]
|
||||
givenEmptyCart_whenAddItem_thenSuccess()
|
||||
givenInvalidToken_whenAuthenticate_thenThrowException()
|
||||
givenExistingUser_whenUpdateProfile_thenProfileUpdated()
|
||||
```
|
||||
|
||||
- 테스트 데이터 네이밍
|
||||
```
|
||||
[Syntax]
|
||||
상수: {상태}_{대상}
|
||||
변수: {상태}{대상}
|
||||
|
||||
[Example]
|
||||
// 상수
|
||||
VALID_USER_ID = 1L
|
||||
EMPTY_ORDER_LIST = Collections.emptyList()
|
||||
|
||||
// 변수
|
||||
normalUser = new User(...)
|
||||
emptyCart = new Cart()
|
||||
```
|
||||
Reference in New Issue
Block a user