자바 코딩 표준은(는) 무엇을 하나요?
Spring Boot 및 Quarkus 서비스에서 가독성 높고 유지보수하기 쉬운 자바 17+ 코드를 위한 표준입니다.
사용 시기
- Spring Boot 또는 Quarkus 프로젝트에서 자바 코드를 작성하거나 리뷰할 때
- 네이밍, 불변성, 예외 처리 규칙을 적용할 때
- 레코드(record), 봉인 클래스(sealed class), 패턴 매칭(Java 17+)을 사용할 때
- Optional, 스트림, 제네릭 사용을 검토할 때
- 패키지 및 프로젝트 구조를 구성할 때
- [QUARKUS]: CDI 스코프, Panache 엔티티, 리액티브 파이프라인 작업 시
작동 방식
프레임워크 감지
표준을 적용하기 전에 빌드 파일에서 프레임워크를 확인합니다.
- 빌드 파일에
quarkus포함 → [QUARKUS] 규칙 적용 - 빌드 파일에
spring-boot포함 → [SPRING] 규칙 적용 - 둘 다 감지되지 않음 → 공통 규칙만 적용
핵심 원칙
- 명확함이 기발함보다 우선
- 기본적으로 불변(immutable); 공유 가변 상태 최소화
- 의미 있는 예외로 빠르게 실패(fail fast)
- 일관된 네이밍과 패키지 구조
- [QUARKUS]: 런타임보다 빌드 타임 처리를 선호; 가능한 런타임 리플렉션 회피
예제
아래 섹션은 Spring Boot, Quarkus, 공통 자바 예제를 각각 보여줍니다: 네이밍, 불변성, 의존성 주입, 리액티브 코드, 예외, 프로젝트 레이아웃, 로깅, 설정, 테스트.
네이밍
// 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 컨트롤러는 *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] 패키지-프라이빗 필드 주입 (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(); // 블로킹 — 이벤트 루프 중단
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 type)을 피하고 제네릭 매개변수를 선언
- 재사용 가능한 유틸리티에는 제한된 와일드카드를 선호
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 빈, Producers
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 # dev/test용 Hibernate 자동 임포트
src/test/java/... (main 미러링)
포맷팅과 스타일
- 2 또는 4 공백을 일관성 있게 사용 (프로젝트 표준)
- 파일당 하나의 public 최상위 타입
- 메서드를 짧고 집중적으로 유지; 헬퍼 추출
- 멤버 순서: 상수, 필드, 생성자, public 메서드, protected, private
피해야 할 코드 냄새
- 긴 매개변수 목록 → DTO/빌더 사용
- 깊은 중첩 → 조기 반환
- 매직 넘버 → 이름 있는 상수
- 정적 가변 상태 → 의존성 주입 선호
- 무음 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 주입, 선언 클래스에 스코프 지정
널 처리
- 피할 수 없는 경우에만
@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 모킹; 가능한 부분 모킹 피함
- 결정론적 테스트 선호; 숨은 sleep 금지
[SPRING]
- 컨트롤러 슬라이스 테스트:
@WebMvcTest, 리포지토리 슬라이스:@DataJpaTest @SpringBootTest는 전체 통합 테스트용으로 제한@MockBean으로 Spring 컨텍스트의 빈 교체
[QUARKUS]
- 단위 테스트: 일반 JUnit 5 + Mockito (
@QuarkusTest없음) @QuarkusTest는 CDI 통합 테스트용으로 제한@InjectMock으로 통합 테스트에서 CDI 빈 교체- Dev Services로 데이터베이스/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;
}
기억하세요: 코드는 의도적이고, 타입화되어 있으며, 관찰 가능하게 유지하세요. 필요하다고 입증되지 않은 미세 최적화보다 유지보수성을 최적화하세요.