LabHub
배우기 러닝패스 코스

Spring Boot — Count How Many Queries Go Out

301 Queries for One List

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

목표

Spring 이 대신 해 주는 일을 봅니다. 검증·예외 변환·트랜잭션 경계·지연 로딩은 전부 눈에 안 보이는 곳에서 일어나고, 그래서 문제도 눈에 안 보입니다.

특히 마지막 하나 — 목록 하나 그리는 데 쿼리가 몇 개 나가는지 — 를 실제로 세어 봅니다.

환경

Java 21 · Maven · Spring Boot 3.4.1 · H2(메모리). 네트워크가 없습니다.

mkdir -p /root/work/spring
cp -r /opt/lab/samples/spring-starter/* /root/work/spring/
cd /root/work/spring
mvn -o -B test          # -o 를 빠뜨리면 원격을 보러 가서 실패합니다

의존성은 뼈대 pom.xml 에 있는 것만 쓸 수 있습니다 — web, validation, data-jpa, actuator, h2, test. 새 의존성은 받을 수 없습니다.

계약

채점기는 여러분의 클래스를 참조하지 않고 HTTP 와 쿼리 수로만 봅니다. 클래스를 어떻게 쪼개든 아래만 맞으면 됩니다.

경로 계약
GET /healthz {"status":"ok"}
POST /items {name, price} · 정상 200/201 · 위반 400
GET /items 저장된 목록(JSON 배열)
GET /items/{id} 있으면 200 · 없으면 404 + 메시지
POST /items/bulk 배열 · 하나라도 실패하면 전부 롤백
GET /shops 가게마다 물품 수 · 쿼리 2개 이하

패키지는 labhub 로 두세요.

단계

  1. 뼈대 · /healthz
  2. @Valid → 400
  3. @RestControllerAdvice → 404
  4. 생성자 주입
  5. @Transactional 롤백
  6. N+1 세고 줄이기 → 06-queries.txt
  7. @WebMvcTest 슬라이스
  8. 정리 → 08-notes.md

참고

채점기는 프로젝트를 복사본에 옮겨 검사합니다. 여러분의 src 에 채점용 파일이 남지 않습니다.

프로젝트를 세운다

/root/work/spring 에 Spring Boot 프로젝트를 만들고 GET /healthz{"status":"ok"} 를 주게 하세요. 패키지는 labhub 입니다.

뼈대가 준비돼 있습니다: mkdir -p /root/work/spring && cp -r /opt/lab/samples/spring-starter/* /root/work/spring/. 네트워크가 없으므로 항상 -o(오프라인)를 붙입니다: mvn -o -B test. 뼈대의 pom 에 없는 의존성은 받을 수 없습니다.

검증을 프레임워크에 맡긴다

POST /items 를 만들고 본문을 @Valid 로 검증하세요. 이름이 비었거나 가격이 음수면 400, 정상이면 200/201 이어야 합니다.

record ItemRequest(@NotBlank String name, @Min(1) int price). @Valid 를 안 붙이면 제약 애너테이션은 아무 일도 하지 않습니다 — 붙여 두고 안 걸리는 경우의 대부분이 이것입니다. if (name == null) 을 쓰고 있다면 프레임워크가 할 일을 뺏은 것입니다.

예외를 한곳에서 상태 코드로

GET /items/{id} 에서 없는 id 는 404 와 메시지를 주게 하세요. 처리는 @RestControllerAdvice 한곳에 모읍니다.

컨트롤러마다 try-catch 를 흩어 두면 어느 예외가 어떤 코드로 나가는지 아무도 모르게 됩니다. return ResponseEntity.ok(Map.of("error", ...)) 로 200 을 주면 안 됩니다 — 상태 코드는 계약이고 프록시·모니터링이 그걸 보고 동작합니다.

생성자 주입으로 바꾼다

@Service 계층을 두고 필드에 @Autowired 를 붙이지 마세요. 생성자 주입만 씁니다(private final).

생성자가 하나면 @Autowired 자체가 필요 없습니다. 얻는 것: final 로 둘 수 있고, 테스트에서 그냥 넘겨 갈아 끼울 수 있고, 의존성이 몇 개인지 눈에 보이고, 순환 참조가 기동 시점에 터집니다. 운영 중에 터지는 것보다 낫습니다.

묶음이 실패하면 전부 되돌린다

POST /items/bulk 로 배열을 받아 저장하세요. 하나라도 잘못되면 전부 롤백돼야 하고, 전부 정상이면 전부 저장돼야 합니다.

@Transactional 을 서비스 메서드에 붙입니다. 주의 셋 — 체크 예외는 기본적으로 롤백하지 않고(rollbackFor 필요), 같은 클래스 안에서 this.method() 로 부르면 프록시를 안 거쳐 무시되며, private 메서드에는 안 붙습니다.

쿼리를 세고 줄인다

가게와 물품을 1:N 으로 두고 GET /shops 가 각 가게의 물품 수를 함께 주게 하세요. 쿼리 수를 재서 고치기 전과 후06-queries.txt 에 적고, 최종적으로 2개 이하가 되게 하세요.

application.propertiesspring.jpa.properties.hibernate.generate_statistics=true 를 넣고, 테스트에서 Statistics.getPrepareStatementCount() 로 셉니다. 먼저 findAll() 로 짜서 가게 수만큼 쿼리가 더 나가는 것을 확인한 뒤, @Query("select distinct s from Shop s join fetch s.items") 로 1개로 줄이세요. 세는 방법을 아는 것이 고치는 방법보다 중요합니다.

웹 층만 띄우는 테스트

@WebMvcTest 로 컨트롤러만 검증하는 테스트를 하나 쓰세요. 서비스는 @MockitoBean 으로 대신 넣습니다.

@WebMvcTest 는 서비스 빈을 띄우지 않으므로 목을 넣지 않으면 컨텍스트가 뜨지 않습니다. MockMvc 로 부릅니다. 전체 컨텍스트를 매번 띄우면 테스트가 느려지고, 느려지면 아무도 안 돌립니다.

보이지 않던 것 셋

08-notes.md 에 세 줄 이상. 왜 생성자 주입인지, N+1 이 왜 생기고 무엇으로 고쳤는지, @Transactional 이 무엇을 되돌리는지(그리고 안 되돌리는 경우).

본문에 생성자, N+1, 롤백 이 들어가야 합니다. 6단계의 두 숫자를 함께 적어 두면 나중에 같은 코드를 볼 때 판단이 빨라집니다.