조회로 저장 결과 검증
75분 안팎
학습 목표
NULL·중복·조인 행 수를 기대 결과와 비교합니다.
개념
응답에서 SQL 증거로 이동합니다
동아리 가입 요청의 성공 응답은 저장 결과의 일부만 보여 줍니다. 앞 모듈에서 회원 증가와 닉네임 변경을 비교했다면 이번에는 사용자와 프로필의 관계를 조회합니다. QA가 하는 일은 DB에서 보기 좋은 숫자를 고르는 것이 아니라 요구사항의 실패 조건을 독립적으로 관찰하는 것입니다. 사용자 한 명마다 프로필 한 개가 있어야 한다는 계약을 먼저 적고, 누락·중복·값 미설정을 서로 다른 위반으로 정의합니다.
이 모듈의 브라우저 실습은 합성 입력으로 실행합니다. SQL 입력은 테이블 생성과 데이터 삽입 문장이고 정답은 조회문입니다. JavaScript 입력은 표준 입력의 JSON 한 개이며 출력은 줄 단위 문자열입니다. 로컬 실습은 starter 압축을 푼 폴더에서 명령을 실행하고 테스트 파일을 유지한 채 TODO를 완성합니다. Java 17과 Node, Python 3을 사용하며 Maven 의존성은 최초 온라인 실행으로 준비하고 검증 환경에서는 캐시로 실행합니다. 운영 데이터와 개인 계정은 사용하지 않습니다.
조회 기준과 스키마를 고정합니다
조회 연습의 users는 id 기본키와 nullable email, profiles는 user_id와 nullable nickname을 가집니다. 일부러 프로필 외래키와 유일 제약을 생략하여 잘못된 상태를 조회할 수 있게 했습니다. 이 스키마가 실제 서비스의 저장 제약이라는 뜻은 아닙니다. 미션의 기존 members 테이블은 email 기본키이므로 동일 email 행을 두 번 저장할 수 없습니다. 실습에서 검출하는 손상 상태와 실제 앱이 허용하는 상태를 구분합니다.
정답 SQL은 누락 프로필 사용자 ID와 중복 이메일 값을 각각 출력합니다. 첫 조회는 사용자 id 순서, 둘째 조회는 email 순서로 정렬합니다. 결과를 섞어 한 줄로 만들지 않습니다. 중복 이메일은 NULL을 제외한 완전히 같은 문자열끼리 센다는 계약입니다. 대소문자 통일이나 앞뒤 공백 제거가 요구되면 그 규칙을 별도로 합의하고 다른 쿼리와 테스트를 추가해야 합니다.
누락과 빈 값을 구분합니다
LEFT JOIN은 사용자를 기준으로 프로필이 없어도 사용자 행을 남깁니다. 짝이 없는 오른쪽 열은 NULL이 됩니다. WHERE p.user_id IS NULL로 연결 자체가 없는 사용자를 찾습니다. nickname IS NULL을 쓰면 프로필은 있지만 닉네임을 아직 설정하지 않은 사용자까지 누락으로 분류합니다. 어떤 NULL이 무엇을 의미하는지 키 열과 업무 값 열을 나누어 읽습니다.
INNER JOIN으로 누락을 찾으려 하면 짝이 없는 사용자는 결과에 들어오지 않습니다. 빈 결과를 보고 모두 정상이라고 판단하는 것이 흔한 실수입니다. 조회를 검증할 때 정상 사용자, 프로필이 없는 사용자, 프로필은 있지만 nickname이 NULL인 사용자 세 가지를 함께 넣습니다. 조건이 누락만 선택하고 나머지를 제외하는지 확인하면 쿼리의 의도를 실제 데이터로 증명할 수 있습니다.
행 수의 의미를 읽습니다
COUNT(*)는 조인 뒤의 행을 셉니다. 사용자 한 명에 프로필 두 개가 잘못 연결되면 조인 결과는 두 행이 됩니다. 사용자 수를 기대하면서 조인 행 수를 비교하면 과대 집계됩니다. COUNT(DISTINCT u.id)는 사용자 식별자의 개수를 세지만 잘못된 다중 프로필 자체를 숨길 수도 있습니다. 집계값을 줄이는 것과 데이터 결함을 발견하는 것은 다른 목표입니다.
사용자별 프로필 수를 검증할 때는 LEFT JOIN 뒤 GROUP BY u.id와 COUNT(p.user_id)를 사용합니다. COUNT(*)를 쓰면 프로필이 없는 사용자도 NULL 보충 행 때문에 1로 보입니다. COUNT(p.user_id)는 NULL을 제외하여 그 사용자는 0이 됩니다. 요구하는 1과 다른 그룹을 선택하면 누락과 다중 프로필을 모두 찾습니다. 이 추가 검사는 따라하기에서 실행하고 브라우저 제출은 지정한 두 조회만 포함합니다.
중복을 찾는 조건을 설계합니다
중복 이메일은 GROUP BY email로 묶은 뒤 HAVING COUNT(*)가 1보다 큰 그룹을 고릅니다. WHERE는 개별 행을 고르고 HAVING은 그룹 집계 뒤 조건을 적용합니다. WHERE COUNT(*)를 쓰면 집계를 잘못 사용했다는 오류가 나옵니다. 오류를 숫자 계산 실패로 해석하지 말고 행 단계와 그룹 단계가 바뀌었는지 확인합니다. SQL의 처리 단계에 맞게 조건을 옮깁니다.
NULL 이메일은 이메일이 아직 없는 상태이며 이번 중복 정책에서는 제외합니다. NULL끼리 묶인 그룹이 여러 행이어도 중복 이메일로 출력하지 않습니다. email = NULL은 참을 만드는 비교가 아니므로 IS NULL 또는 IS NOT NULL을 씁니다. 비어 있는 문자열은 NULL과 다르며 이번 계약에서는 값으로 집계됩니다. 빈 문자열을 허용하지 않는 요구는 입력 검증이나 별도 결함 조회에 연결합니다.
증거를 남기는 방식입니다
쿼리와 기대 결과는 응답 본문에서 복사하지 않습니다. 준비한 합성 사용자 ID를 기준으로 기대 집합을 먼저 적고 실행 결과와 비교합니다. 조회가 실패하면 테이블과 열 이름, 조건, 조인 키를 차례로 읽습니다. no such table 메시지는 입력 DDL 누락이나 잘못된 이름을 뜻할 수 있고 ambiguous column name은 여러 테이블의 같은 열 이름에 별칭을 붙여야 한다는 신호입니다.
정렬 없이 출력한 결과는 실행 계획에 따라 순서가 달라질 수 있으므로 ORDER BY를 정답 계약에 넣습니다. 정렬해도 내용이 틀리면 통과시키지 않습니다. 같은 사용자의 중복 프로필을 DISTINCT로 지운 뒤 정상이라고 보고하지도 않습니다. 관찰 결과를 바꾸는 보정과 실제 결함을 찾는 조회를 구별하고 원본 상태를 유지한 채 문제 ID를 기록합니다.
프로젝트에는 실행 전후 SQL 문장, 바인딩한 합성 식별자, 예상 행 목록과 실제 행 목록을 남깁니다. secret 열은 이 레슨의 조회에 넣지 않습니다. 누락과 중복 조회가 통과한 것은 지정한 데이터 관계의 증거이며 비밀번호 저장이나 동시 가입의 안전성까지 확인한 결과는 아닙니다. 조인 종류 전반의 설명은 더 읽기의 서재 장에서 이어 갑니다.
따라하기
프로필 누락을 조회합니다
독립적으로 실행하여 결과를 비교합니다.
SELECT u.id FROM users u LEFT JOIN profiles p ON p.user_id=u.id WHERE p.user_id IS NULL ORDER BY u.id;실행 결과
3
중복 이메일을 조회합니다
독립적으로 실행하여 결과를 비교합니다.
SELECT email FROM users WHERE email IS NOT NULL GROUP BY email HAVING COUNT(*)>1 ORDER BY email;실행 결과
a
조인 뒤 행 수를 해석합니다
독립적으로 실행하여 결과를 비교합니다.
SELECT u.id,COUNT(*),COUNT(p.user_id) FROM users u LEFT JOIN profiles p ON u.id=p.user_id GROUP BY u.id ORDER BY u.id;실행 결과
1 2 2 2 1 0
확인 문제
실습
users(id,email), profiles(user_id,nickname)에서 프로필 연결이 없는 id를 오름차순으로 출력하고, 이어 NULL 제외 완전히 같은 중복 email을 오름차순으로 출력합니다. 제목과 건수는 출력하지 않습니다. 두 SELECT를 제출합니다.
모범 답안
SELECT u.id FROM users u LEFT JOIN profiles p ON p.user_id=u.id WHERE p.user_id IS NULL ORDER BY u.id; SELECT email FROM users WHERE email IS NOT NULL GROUP BY email HAVING COUNT(*)>1 ORDER BY email;
더 읽기
면접 질문
- API 응답 코드가 성공이어도 테스트가 실패할 수 있는 상황을 설명해 주시면 됩니다.