一个列表页跑 301 条查询
한국어 원문으로 표시합니다.
목표
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 로 두세요.
단계
- 뼈대 ·
/healthz @Valid→ 400@RestControllerAdvice→ 404- 생성자 주입
@Transactional롤백- N+1 세고 줄이기 →
06-queries.txt @WebMvcTest슬라이스- 정리 →
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.properties 에 spring.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단계의 두 숫자를 함께 적어 두면 나중에 같은 코드를 볼 때 판단이 빨라집니다.