Java 코딩 표준
Spring Boot 및 Quarkus 서비스에서 읽기 쉽고 유지보수 가능한 Java (17+) 코드를 위한 표준입니다.
사용 시점
- Spring Boot 또는 Quarkus 프로젝트에서 Java 코드를 작성하거나 리뷰할 때
- 네이밍, 불변성, 예외 처리 관습을 적용해야 할 때
- 레코드(records), 봉인 클래스(sealed classes), 패턴 매칭(pattern matching) (Java 17+)을 사용할 때
- Optional, 스트림(streams), 제네릭(generics) 사용을 리뷰할 때
- 패키지 및 프로젝트 레이아웃을 구성할 때
- [QUARKUS]: CDI 범위, Panache 엔티티, 리액티브 파이프라인 작업 시
작동 방식
프레임워크 감지
표준을 적용하기 전에 빌드 파일에서 프레임워크를 확인합니다.
- 빌드 파일에
quarkus포함 → [QUARKUS] 규칙 적용 - 빌드 파일에
spring-boot포함 → [SPRING] 규칙 적용 - 둘 다 감지되지 않음 → 공유 규칙만 적용
핵심 원칙
- 명확성을 영리함보다 선호합니다.
- 기본적으로 불변(immutable)으로 하며, 공유 가변 상태를 최소화합니다.
- 의미 있는 예외로 빠르게 실패(fail fast)합니다.
- 일관된 네이밍과 패키지 구조를 유지합니다.
- [QUARKUS]: 런타임 처리보다 빌드 타임을 선호하며, 가능한 경우 런타임 리플렉션을 피합니다.
예제
다음 섹션에서는 네이밍, 불변성, 의존성 주입, 리액티브 코드, 예외, 프로젝트 레이아웃, 로깅, 설정, 테스트에 대한 Spring Boot, Quarkus 및 공유 Java 예제를 보여줍니다.
네이밍
// PASS: 클래스/레코드: PascalCase
public class MarketService {}
public record Money(BigDecimal amount, Currency currency) {}
// PASS: 메서드/필드: camelCase
private final MarketRepository marketRepository;
public Market findBySlug(String slug) {}
// PASS: 상수: UPPER_SNAKE_CASE
private static final int MAX_PAGE_SIZE = 100;
// PASS: [QUARKUS] JAX-RS 리소스는 *Controller가 아닌 *Resource로 네이밍
public class MarketResource {}
// PASS: [SPRING] REST 컨트롤러는 *Resource가 아닌 *Controller로 네이밍
public class MarketController {}
불변성
// PASS: 레코드와 final 필드 선호
public record MarketDto(Long id, String name, MarketStatus status) {}
public class Market {
private final Long id;
private final String name;
// getter만, setter 없음
}
// PASS: [QUARKUS] Panache 액티브 레코드 엔티티는 public 필드 사용 (Quarkus 관습)
@Entity
public class Market extends PanacheEntity {
public String name;
public MarketStatus status;
// Panache가 빌드 타임에 접근자를 생성합니다. public 필드는 여기서 관용적입니다.
}
// PASS: [QUARKUS] Panache MongoDB 엔티티
@MongoEntity(collection = "markets")
public class Market extends PanacheMongoEntity {
public String name;
public MarketStatus status;
}
Optional 사용법
// PASS: find* 메서드에서 Optional 반환
// [SPRING]
Optional<Market> market = marketRepository.findBySlug(slug);
// [QUARKUS] Panache
Optional<Market> market = Market.find("slug", slug).firstResultOptional();
// PASS: get() 대신 map/flatMap 사용
return market
.map(MarketResponse::from)
.orElseThrow(() -> new EntityNotFoundException("Market not found"));
스트림 모범 사례
// PASS: 변환에 스트림 사용, 파이프라인은 짧게 유지
List<String> names = markets.stream()
.map(Market::name)
.filter(Objects::nonNull)
.toList();
// FAIL: 복잡한 중첩 스트림은 피하고 명확성을 위해 루프 선호
의존성 주입
// PASS: [SPRING] 생성자 주입 (필드 @Autowired보다 선호)
@Service
public class MarketService {
private final MarketRepository marketRepository;
public MarketService(MarketRepository marketRepository) {
this.marketRepository = marketRepository;
}
}
// PASS: [QUARKUS] 생성자 주입
@ApplicationScoped
public class MarketService {
private final MarketRepository marketRepository;
@Inject
public MarketService(MarketRepository marketRepository) {
this.marketRepository = marketRepository;
}
}
// PASS: [QUARKUS] 패키지-전용(private) 필드 주입 (Quarkus에서 허용 — 프록시 문제 방지)
@ApplicationScoped
public class MarketService {
@Inject
MarketRepository marketRepository;
}
// FAIL: [SPRING] @Autowired를 사용한 필드 주입
@Autowired
private MarketRepository marketRepository; // 생성자 주입 사용
// FAIL: [QUARKUS] 인터셉션이나 지연 초기화가 필요할 때 @Singleton 사용
@Singleton // 프록시 불가능 — 대신 @ApplicationScoped 사용
public class MarketService {}
리액티브 패턴 [QUARKUS]
// PASS: 리액티브 엔드포인트에서 Uni/Multi 반환
@GET
@Path("/{slug}")
public Uni<Market> findBySlug(@PathParam("slug") String slug) {
return Market.find("slug", slug)
.<Market>firstResult()
.onItem().ifNull().failWith(() -> new MarketNotFoundException(slug));
}
// PASS: 논블로킹 파이프라인 구성
public Uni<OrderConfirmation> placeOrder(OrderRequest req) {
return validateOrder(req)
.chain(valid -> persistOrder(valid))
.chain(order -> notifyFulfillment(order));
}
// FAIL: Uni/Multi 파이프라인 내부의 블로킹 호출
public Uni<Market> find(String slug) {
Market m = Market.find("slug", slug).firstResult(); // BLOCKING — 이벤트 루프 중단
return Uni.createFrom().item(m);
}
// FAIL: 공유 Uni에 두 번 이상 구독
Uni<Market> shared = fetchMarket(slug);
shared.subscribe().with(m -> log(m));
shared.subscribe().with(m -> cache(m)); // 이중 구독 — Uni.memoize() 사용
예외
- 도메인 오류에는 확인되지 않은 예외(unchecked exception)를 사용하고, 기술적 예외는 컨텍스트와 함께 래핑합니다.
- 도메인별 예외 생성 (예:
MarketNotFoundException) - 중앙에서 다시 던지거나 로깅하지 않는 한 광범위한
catch (Exception ex)를 피합니다.
throw new MarketNotFoundException(slug);
중앙 예외 처리
// [SPRING]
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MarketNotFoundException.class)
public ResponseEntity<ErrorResponse> handle(MarketNotFoundException ex) {
return ResponseEntity.status(404).body(ErrorResponse.from(ex));
}
}
// [QUARKUS] 옵션 A: ExceptionMapper
@Provider
public class MarketNotFoundMapper implements ExceptionMapper<MarketNotFoundException> {
@Override
public Response toResponse(MarketNotFoundException ex) {
return Response.status(404).entity(ErrorResponse.from(ex)).build();
}
}
// [QUARKUS] 옵션 B: @ServerExceptionMapper (RESTEasy Reactive)
@ServerExceptionMapper
public RestResponse<ErrorResponse> handle(MarketNotFoundException ex) {
return RestResponse.status(Status.NOT_FOUND, ErrorResponse.from(ex));
}
제네릭 및 타입 안전성
- 원시 타입(raw types)을 피하고 제네릭 매개변수를 선언합니다.
- 재사용 가능한 유틸리티에는 제한된 와일드카드(bounded generics)를 선호합니다.
public <T extends Identifiable> Map<Long, T> indexById(Collection<T> items) { ... }
프로젝트 구조
[SPRING] Maven/Gradle
src/main/java/com/example/app/
config/
controller/
service/
repository/
domain/
dto/
util/
src/main/resources/
application.yml
src/test/java/... (main과 동일하게 미러링)
[QUARKUS] Maven/Gradle
src/main/java/com/example/app/
config/ # @ConfigMapping, @ConfigProperty 빈, Producer
resource/ # JAX-RS 리소스 ("controller" 아님)
service/
repository/ # PanacheRepository 구현 (액티브 레코드 사용하지 않는 경우)
domain/ # JPA/Panache 엔티티, MongoDB 엔티티
dto/
util/
mapper/ # MapStruct 매퍼 (사용하는 경우)
src/main/resources/
application.properties # Quarkus 관습 (quarkus-config-yaml로 YAML 지원)
import.sql # 개발/테스트용 Hibernate 자동 임포트
src/test/java/... (main과 동일하게 미러링)
포맷팅 및 스타일
- 2 또는 4 공백을 일관되게 사용 (프로젝트 표준)
- 파일당 하나의 public 최상위 타입
- 메서드는 짧고 집중적으로 유지; 헬퍼 추출
- 멤버 순서: 상수, 필드, 생성자, public 메서드, protected, private
피해야 할 코드 냄새
- 긴 파라미터 목록 → DTO/빌더 사용
- 깊은 중첩 → 조기 반환(early returns)
- 매직 넘버 → 명명된 상수
- 정적 가변 상태 → 의존성 주입 선호
- 침묵하는 catch 블록 → 로깅하고 조치하거나 다시 던지기
- [QUARKUS]:
@ApplicationScoped가 의도된 곳에@Singleton사용 — 프록싱 및 인터셉션 중단 - [QUARKUS]:
quarkus-resteasy-reactive와quarkus-resteasy(클래식) 혼합 — 하나의 스택 선택 - [QUARKUS]: 같은 바운디드 컨텍스트 내 Panache 액티브 레코드 + 리포지토리 패턴 혼합 — 하나 선택
로깅
// [SPRING] SLF4J
private static final Logger log = LoggerFactory.getLogger(MarketService.class);
log.info("fetch_market slug={}", slug);
log.error("failed_fetch_market slug={}", slug, ex);
// [QUARKUS] JBoss Logging (기본값, 빌드 타임에 제로 비용)
private static final Logger log = Logger.getLogger(MarketService.class);
log.infof("fetch_market slug=%s", slug);
log.errorf(ex, "failed_fetch_market slug=%s", slug);
// [QUARKUS] 대안: @Inject로 간소화된 로깅
@Inject
Logger log; // CDI-주입, 선언 클래스로 범위 지정
Null 처리
- 불가피한 경우에만
@Nullable수용; 그 외에는@NonNull사용 - 입력값에 Bean Validation (
@NotNull,@NotBlank) 적용 - [QUARKUS]:
@BeanParam,@RestForm, 요청 본문 파라미터에@Valid적용
설정
// [SPRING] @ConfigurationProperties
@ConfigurationProperties(prefix = "market")
public record MarketProperties(int maxPageSize, Duration cacheTtl) {}
// [QUARKUS] @ConfigMapping (타입 안전, 빌드 타임 검증)
@ConfigMapping(prefix = "market")
public interface MarketConfig {
int maxPageSize();
Duration cacheTtl();
}
// [QUARKUS] @ConfigProperty로 간단한 값 설정
@ConfigProperty(name = "market.max-page-size", defaultValue = "100")
int maxPageSize;
테스트 기대 사항
공통
- JUnit 5 + AssertJ로 유창한 어설션 사용
- 목킹에는 Mockito 사용; 가능한 부분 목(partial mock)은 피하기
- 결정적 테스트 선호; 숨은 sleep 없음
[SPRING]
- 컨트롤러 슬라이스 테스트:
@WebMvcTest, 리포지토리 슬라이스 테스트:@DataJpaTest @SpringBootTest는 전체 통합 테스트에만 사용@MockBean으로 Spring 컨텍스트의 빈 교체
[QUARKUS]
- 단위 테스트는
@QuarkusTest없이 일반 JUnit 5 + Mockito 사용 @QuarkusTest는 CDI 통합 테스트에만 사용@InjectMock으로 통합 테스트에서 CDI 빈 교체- Dev Services for database/Kafka/Redis — Dev Services로 충분할 때 수동 Testcontainers 설정 피함
@QuarkusTestResource로 사용자 정의 외부 서비스 라이프사이클 관리
// [SPRING] 컨트롤러 테스트
@WebMvcTest(MarketController.class)
class MarketControllerTest {
@Autowired MockMvc mockMvc;
@MockBean MarketService marketService;
}
// [QUARKUS] 통합 테스트
@QuarkusTest
class MarketResourceTest {
@InjectMock
MarketService marketService;
@Test
void should_return_404_when_market_not_found() {
given().when().get("/markets/unknown").then().statusCode(404);
}
}
// [QUARKUS] 단위 테스트 (CDI 없음, @QuarkusTest 없음)
@ExtendWith(MockitoExtension.class)
class MarketServiceTest {
@Mock MarketRepository marketRepository;
@InjectMocks MarketService marketService;
}
기억하세요: 코드는 의도적이고, 타입이 명시되며, 관찰 가능하게 유지하세요. 입증되지 않은 미세 최적화보다 유지보수성을 최적화하세요.