JDBC 저장소와 매개변수
75분 안팎
학습 목표
JdbcTemplate과 바인딩으로 SQL·연결 자원을 관리합니다.
개념
파일에서 DB로 바꾸는 이유
앞 모듈은 파일 API를 유지하면서 books·members·loans의 관계를 만들었습니다. 이번에는 HTTP 요청으로 등록한 도서를 SQL에서 직접 확인하려고 합니다. 컨트롤러가 파일 경로를 알고 있으면 저장 방식을 바꿀 때 요청 처리 코드까지 흔들립니다. BookRepository 인터페이스를 경계로 두면 저장 구현만 교체하면서 GET의 도서 모양과 POST의 Location 계약을 유지할 수 있습니다. 저장소 교체의 성공 기준은 클래스 이름이 아니라 기존 요청이 같은 의미를 가지는지입니다.
실습 환경을 한 번 준비합니다
이 모듈의 Java 실습은 JDK 17·Spring Boot 3.1.5·H2 2.1.214를 사용합니다. starter.zip을 별도 폴더에 풀고 그 안에서 ./mvnw test를 실행합니다. wrapper와 테스트가 함께 제공되므로 시스템 Maven을 따로 설치하지 않습니다. 작성 환경은 준비된 캐시로 오프라인 검사하지만 학습자 명령은 일반 실행입니다. 운영 설정을 복사하지 않습니다. 테스트는 임시 H2 파일과 MockMvc를 사용하므로 서버를 시작하거나 종료할 필요가 없습니다.
연결과 SQL의 책임을 나눕니다
DataSource는 연결을 얻는 경계이며 JdbcTemplate은 SQL 실행과 JDBC 자원 정리를 맡습니다. 이 실습은 DriverManagerDataSource를 사용하므로 연결 풀을 구현한 예제가 아닙니다. 요청마다 열린 연결을 직접 필드에 보관하지 않습니다. JdbcTemplate의 콜백에서 ResultSet을 읽고 필요한 도서 값만 반환합니다. 콜백 밖으로 ResultSet을 넘겨 나중에 읽으면 이미 정리된 자원을 사용하게 될 수 있습니다. DB 객체 수명과 Java 도서 객체 수명을 구별합니다.
값은 SQL 구문에서 분리합니다
INSERT INTO books(id,title) VALUES (?,?)에 ID와 제목을 별도 인자로 전달합니다. 제목을 작은따옴표 사이에 연결하면 O'Reilly 같은 정상 값도 문법을 깨뜨립니다. 따옴표를 직접 치환하는 방식보다 매개변수 바인딩을 사용해 SQL 모양과 값을 구분합니다. DROP TABLE처럼 보이는 제목도 이 경로에서는 제목 문자열입니다. 모든 입력을 SQL 금칙어로 거부하는 것은 제목의 의미를 불필요하게 제한하며 안전한 SQL 작성의 대체 방법도 아닙니다.
바인딩 가능한 범위를 판단합니다
물음표는 데이터 값을 받습니다. 테이블 이름이나 ORDER BY의 컬럼 이름을 입력에서 물음표로 넘긴다고 식별자가 되지는 않습니다. 목록 정렬은 이 실습에서 ID 오름차순으로 고정합니다. 나중에 정렬 옵션을 받는다면 허용된 선택값을 이미 정해 둔 SQL 조각으로 대응시킵니다. 사용자가 준 문자열을 그대로 SQL 뒤에 붙이지 않습니다. 값 바인딩과 식별자 선택은 서로 다른 책임이라는 점을 리뷰에서 설명합니다.
행을 객체로 바꾸는 과정을 확인합니다
books의 id와 title을 읽고 활성 loans가 존재하는지 EXISTS로 계산해 borrowed를 만듭니다. 이력 테이블을 단순 JOIN하면 한 권의 여러 대여 때문에 목록에 같은 책이 반복될 수 있습니다. EXISTS는 활성 이력의 존재 여부만 계산해 도서 한 행을 한 객체로 유지합니다. 아직 대여 쓰기 API는 구현하지 않았습니다. 테스트는 SQL로 이력을 준비해 조회의 의미만 확인합니다. boolean을 books에 중복 저장해 이력과 어긋나는 상태를 만들지 않습니다.
없는 행과 장애를 구별합니다
find는 조회 결과가 비어 있으면 Optional.empty를 반환하고 서비스는 이를 404로 바꿉니다. 연결 실패나 SQL 문법 오류를 빈 결과로 바꾸면 DB 장애가 없는 도서처럼 보입니다. JdbcTemplate이 변환한 DataAccessException은 실패 경로로 올라가도록 둡니다. 잘못된 쿼리에서 BadSqlGrammarException을 보면 테이블과 컬럼 이름, 적용한 스키마부터 확인합니다. 연결 오류라면 테스트의 JDBC URL과 파일 위치를 확인하고 비밀번호를 출력하지 않습니다.
이관과 재연결을 검증합니다
앞 모듈 SqlModel.importBooks는 파일 ID·제목을 보존하며 borrowed=true를 거부합니다. 이번 미션에도 그 코드와 SQL 테스트를 남깁니다. 이미 이관한 파일을 매 기동마다 다시 넣으면 PK 충돌이 생기므로 API 설정은 스키마 존재 여부만 준비하고 파일을 자동 이관하지 않습니다. 새 도서를 등록한 뒤 다른 컨텍스트와 연결에서 조회하는 테스트로 같은 H2 파일에 반영됐음을 확인합니다. 메모리 맵의 값이 우연히 남은 것을 DB 저장 증거로 삼지 않습니다.
설정 호환과 다음 단계의 한계
기본 실행은 JDBC 저장소를 쓰고 catalog.path를 명시하면 이전 파일 저장소를 선택합니다. 이것은 과거 실습과 회귀 테스트를 함께 유지하는 전환용 선택입니다. 새 JDBC 테스트에서는 catalog.jdbc.url을 임시 경로로 지정합니다. MAX(id)+1 생성은 기존 단일 JVM 계약을 유지한 것이며 여러 인스턴스의 동시 생성과 삭제 뒤 ID 재사용을 해결하지 않습니다. 운영용 식별자 전략이나 동시성 보장을 이 실습만으로 완성했다고 설명하지 않습니다.
실습을 완료하는 순서
JdbcRepository.all의 TODO를 ID 오름차순 조회와 LinkedHashMap 수집으로 바꿉니다. SQL은 books를 기준으로 하고 borrowed 계산을 보존합니다. 먼저 ID 10이 있는 상태에서 POST가 11을 만드는지 확인하고 이어 따옴표 제목의 SQL 조회 값을 확인합니다. 누적 테스트에는 파일 경로로 재시작해 복원하는 검사도 남아 있습니다. 새 구현을 위해 그 테스트를 지우지 않습니다. 결과 요약의 Failures와 Errors가 모두 0일 때 저장소 계약을 설명하며 제출합니다.
사용한 API의 범위는 JdbcTemplate 공식 API에서 확인합니다. 실습 결과는 제공된 버전과 임시 DB에서 직접 검사한 범위입니다.
따라하기
JdbcRepository.all을 완성합니다
실습 starter의 해당 TODO를 아래 코드와 주변 문맥을 보고 완성합니다. solution과 테스트는 먼저 접어 두고 각 인자의 의미를 설명합니다.
jdbc.query(SELECT+" ORDER BY b.id", r -> {
out.put(r.getInt("id"), read(r));
});누적 테스트를 실행합니다
JDBC 목록·매개변수·새 연결 읽기 검사와 이전 파일 테스트를 함께 실행합니다. 상세 로그에서 bindsQuotesAndSql과 survivesNewConnectionAndContext의 실패 여부를 확인합니다.
./mvnw test > test.log 2>&1실행 보고서를 집계합니다
새 JDBC 구현과 기존 코드가 함께 통과한 실제 집계입니다. 저장소 교체 중 과거 검사를 삭제하면 Tests 수가 줄어들기 때문에 95개라는 수치도 확인합니다.
python3 report.py실행 결과
Tests=95 Failures=0 Errors=0 Skipped=0
확인 문제
실습
JdbcRepository.all의 TODO를 완성합니다. 앞서 저장한 ID 10 다음의 신규 ID와 도서 목록을 복구하고 SQL 모양 제목은 문자열 그대로 보존합니다. 테스트를 삭제하거나 비활성화하지 않습니다. 제공된 README와 API-SPEC의 제한을 설명하며 제출합니다.
실행 명령
./mvnw test
기대 결과
누적 테스트 95개, 실패·오류·건너뜀 0입니다.
모범 답안
모범 답안 내려받기더 읽기
면접 질문
- 도서 조회·추가 API의 메서드와 상태 코드를 설명합니다.