<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>통합 테스트 on 얼쑤 · Team Edo</title><link>https://team-edo.github.io/tags/%ED%86%B5%ED%95%A9-%ED%85%8C%EC%8A%A4%ED%8A%B8/</link><description>Recent content in 통합 테스트 on 얼쑤 · Team Edo</description><generator>Hugo</generator><language>ko-kr</language><lastBuildDate>Mon, 09 Nov 2026 00:00:00 +0900</lastBuildDate><atom:link href="https://team-edo.github.io/tags/%ED%86%B5%ED%95%A9-%ED%85%8C%EC%8A%A4%ED%8A%B8/index.xml" rel="self" type="application/rss+xml"/><item><title>폐기용 MySQL을 실행 단위로 분리한 이유</title><link>https://team-edo.github.io/posts/2026/11/isolate-disposable-mysql-tests/</link><pubDate>Mon, 09 Nov 2026 00:00:00 +0900</pubDate><guid>https://team-edo.github.io/posts/2026/11/isolate-disposable-mysql-tests/</guid><description>&lt;p>MySQL 통합 테스트는 실제 스키마를 만들고 데이터를 씁니다. 같은 데이터베이스와 계정을 다음 실행에 재사용하면 앞선 실행이 남긴 데이터가 새 결과에 영향을 줄 수 있습니다. 얼쑤에서는 실행마다 테스트 DB와 전용 계정을 만들고, 다른 DB에서 쓰는 접속 정보와 분리했습니다. 서버의 기동 조건이나 연결 한도를 시험할 때는 MySQL 인스턴스도 따로 띄웠습니다.&lt;/p>
&lt;h2 id="어디까지-분리할까">어디까지 분리할까&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>시험에서 바꾸는 것&lt;/th>
 &lt;th>실행마다 분리할 대상&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>테이블, 테스트 데이터, 스키마 변경&lt;/td>
 &lt;td>데이터베이스와 해당 DB에만 접근하는 전용 계정&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>기동 옵션, 최대 연결 수&lt;/td>
 &lt;td>MySQL 인스턴스, 데이터 폴더, 접속 주소&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Gradle의 통합 테스트에는 &lt;code>TEST_DB_URL&lt;/code>, &lt;code>TEST_DB_USERNAME&lt;/code>, &lt;code>TEST_DB_PASSWORD&lt;/code>를 그 실행에만 전달했습니다. 새 계정은 시험에 필요한 권한을 받고, 데이터 쓰기 권한은 해당 DB에 한정했습니다. 실패한 실행도 정리할 수 있게 종료 절차를 테스트의 성공 여부와 분리했습니다.&lt;/p></description></item><item><title>Spring 테스트 53개 실패와 MySQL 연결 한도</title><link>https://team-edo.github.io/posts/2026/11/spring-test-mysql-connections/</link><pubDate>Sun, 08 Nov 2026 00:00:00 +0900</pubDate><guid>https://team-edo.github.io/posts/2026/11/spring-test-mysql-connections/</guid><description>&lt;p>얼쑤 백엔드의 &lt;code>databaseTest&lt;/code>는 Spring 애플리케이션과 실제 MySQL을 연결해 실행하는 통합 테스트입니다. 앞선 실행에서 161개가 모두 통과했는데, 다른 변경을 병합한 뒤에는 174개 중 53개가 실패했습니다. 서로 다른 기능을 확인하는 테스트에서 같은 메시지가 45번 나왔습니다. &lt;code>Too many connections&lt;/code>였습니다.&lt;/p>
&lt;h2 id="실패한-테스트보다-먼저-연결을-확인했다">테스트보다 먼저 연결 확인&lt;/h2>
&lt;p>Spring 테스트가 애플리케이션 컨텍스트를 올리는 도중 DB에 연결하지 못하면 테스트 코드까지 실행되지 못합니다. 같은 설정의 컨텍스트를 여는 데 이미 실패한 뒤에는 Spring이 다음 로딩 시도를 건너뛰면서 &lt;code>ApplicationContext failure threshold exceeded&lt;/code>를 남길 수 있습니다. 이 메시지만 보면 원인이 비슷한 실패가 여러 개 생긴 것처럼 보입니다. 앞선 로딩 실패의 예외까지 내려가면 MySQL이 새 연결을 거부했다는 단서가 나옵니다. &lt;a href="https://docs.spring.io/spring-framework/reference/testing/testcontext-framework/ctx-management/failure-threshold.html">Spring의 컨텍스트 로딩 실패 정책&lt;/a> · &lt;a href="https://dev.mysql.com/doc/refman/8.4/en/too-many-connections.html">MySQL의 연결 초과 오류&lt;/a>&lt;/p></description></item><item><title>MySQL 행 잠금 대기로 요청 경합 확인</title><link>https://team-edo.github.io/posts/2026/11/test-observed-mysql-row-lock/</link><pubDate>Mon, 02 Nov 2026 00:00:00 +0900</pubDate><guid>https://team-edo.github.io/posts/2026/11/test-observed-mysql-row-lock/</guid><description>&lt;p>연습 기록을 저장하는 요청은 세션을 읽고 값을 갱신한 뒤 응답에 넣을 행을 다시 읽습니다. 그 사이 같은 기록을 삭제하면 마지막 조회가 대상을 잃을 수 있습니다. 저장이 끝날 때까지 삭제가 기다리도록 저장 트랜잭션에서 세션 행을 &lt;code>SELECT … FOR UPDATE&lt;/code>로 잠갔습니다.&lt;/p>
&lt;p>두 작업을 서로 다른 스레드에서 출발시키는 것만으로는 이 잠금을 검사하기 어렵습니다. 스케줄링에 따라 삭제 요청이 저장보다 늦게 시작하면 두 작업이 실제로 겹치지 않습니다. 일정 시간 멈춘 뒤 삭제가 아직 끝나지 않았다고 단언해도, 삭제 스레드가 DB에 도달했는지 알 수 없습니다.&lt;/p></description></item><item><title>녹음 재생 API가 내부 주소를 반환한 이유</title><link>https://team-edo.github.io/posts/2026/11/playback-url-store-boundary/</link><pubDate>Mon, 02 Nov 2026 00:00:00 +0900</pubDate><guid>https://team-edo.github.io/posts/2026/11/playback-url-store-boundary/</guid><description>&lt;p>얼쑤의 녹음 다시 듣기 API에 기존 저장소의 읽기 주소 발급 기능을 연결했습니다. 로컬 저장소에서는 분석 컨테이너가 쓰는 내부 주소가 HTTP 200 응답에 들어왔습니다. 테스트는 그 주소의 호스트를 테스트 프로세스가 접근할 수 있는 주소로 바꿔 읽었기 때문에 성공했습니다. 휴대폰은 응답에 들어 있는 주소 그대로 접속하므로 같은 결과를 기대할 수 없었습니다.&lt;/p>
&lt;h2 id="주소를-받을-쪽을-기준으로-연결을-나눴다">앱이 접근할 주소만 발급&lt;/h2>
&lt;p>DB의 &lt;code>recording_url&lt;/code>에는 녹음 원본을 찾는 객체 키를 저장합니다. 앱에 넘길 임시 주소는 그 키로 다시 듣기를 요청할 때 따로 만듭니다. 그래서 같은 세션의 객체 키를 읽어도 연결한 저장소에 따라 앱이 사용할 주소를 발급할 수 있는지가 달랐습니다.&lt;/p></description></item><item><title>회원 한 행이 막은 테스트 준비</title><link>https://team-edo.github.io/posts/2026/10/preserve-seed-parent-fixtures/</link><pubDate>Thu, 29 Oct 2026 00:00:00 +0900</pubDate><guid>https://team-edo.github.io/posts/2026/10/preserve-seed-parent-fixtures/</guid><description>&lt;p>기본 악기와 악보를 DB 시드로 넣고 나니 매퍼 테스트가 시드와 같은 ID에 다른 값을 넣어야 했습니다. 처음에는 고정 데이터를 넣기 직전에 시드 행을 지웠습니다. 깨끗한 DB에서는 동작했습니다. 가입 테스트가 먼저 회원을 커밋해 둔 상태에서는 테스트 본문을 실행하기도 전에 준비 단계가 실패했습니다.&lt;/p>
&lt;h2 id="회원이-참조하는-악기는-지울-수-없었다">기본 악기를 가리키는 외래 키&lt;/h2>
&lt;p>실제 HTTP로 가입하는 테스트는 회원 행을 커밋합니다. 그 행은 기본 악기인 단소를 참조합니다. 정상 실행에서는 회원 행을 정리하지만, 테스트가 중간에 멈추면 참조가 남을 수 있습니다. 그 상태에서 매퍼 테스트가 악기를 &lt;code>DELETE&lt;/code>하면 MySQL은 외래 키 오류 &lt;strong>1451&lt;/strong>로 막습니다.&lt;/p></description></item><item><title>Flyway를 끈 운영 프로필에서 API 켜기</title><link>https://team-edo.github.io/posts/2026/10/spring-profile-ops-terms-route/</link><pubDate>Wed, 28 Oct 2026 00:00:00 +0900</pubDate><guid>https://team-edo.github.io/posts/2026/10/spring-profile-ops-terms-route/</guid><description>&lt;p>얼쑤의 백엔드는 운영에서 &lt;code>ops&lt;/code> 프로필로 실행했습니다. 이 프로필에서는 앱이 이미 만들어진 DB에 연결하고, 스키마 변경은 별도 작업에서 적용합니다. 업무 API를 켜기 전의 구성을 확인해 보니 약관을 비롯한 업무 컨트롤러와 서비스가 &lt;code>db&lt;/code> 프로필에만 등록돼 있었습니다. 프로세스가 기동해도 &lt;code>ops&lt;/code>로 뜬 컨텍스트에는 약관 요청을 받을 컨트롤러가 없었습니다.&lt;/p>
&lt;h2 id="업무-빈과-스키마-변경의-조건을-분리했다">업무 빈과 마이그레이션 분리&lt;/h2>
&lt;p>처음에는 두 프로필을 함께 켜면 업무 빈도 만들 수 있다고 생각했습니다. 하지만 당시 &lt;code>db&lt;/code>는 Flyway를 켰고 &lt;code>ops&lt;/code>는 껐습니다. 두 프로필에 같은 설정 키가 있으면 뒤에 지정한 프로필의 값이 적용됩니다. 순서를 바꾸는 것만으로 운영 앱이 기동할 때 마이그레이션을 실행할 수 있는 구성이었습니다.&lt;/p></description></item><item><title>Git 병합 뒤 DB 테스트를 한 이유</title><link>https://team-edo.github.io/posts/2026/10/verify-git-merge-beyond-conflicts/</link><pubDate>Wed, 28 Oct 2026 00:00:00 +0900</pubDate><guid>https://team-edo.github.io/posts/2026/10/verify-git-merge-beyond-conflicts/</guid><description>&lt;p>진행 중인 작업을 바꾸지 않고 기준 브랜치와 충돌할지 미리 보고 싶을 때 &lt;code>git merge-tree --write-tree HEAD origin/develop&lt;/code>를 사용했습니다. 작업 트리와 인덱스를 그대로 두고 두 커밋을 병합한 결과를 확인할 수 있습니다. 한 브랜치에서는 기준 브랜치에 새 커밋이 23개 들어왔을 때 검사에 충돌이 없었습니다. 이후 다른 변경이 먼저 병합되자 다시 실행한 검사에서 문서 충돌이 나타났고, 기준 브랜치를 실제로 합쳐 그 파일을 정리했습니다. 미리 본 결과는 그때 비교한 두 커밋에 대한 것이었습니다.&lt;/p></description></item><item><title>HTTP 406에도 공통 오류 본문 적용하기</title><link>https://team-edo.github.io/posts/2026/10/keep-common-error-body-for-http-406/</link><pubDate>Wed, 28 Oct 2026 00:00:00 +0900</pubDate><guid>https://team-edo.github.io/posts/2026/10/keep-common-error-body-for-http-406/</guid><description>&lt;p>앱은 요청에 실패하면 HTTP 상태와 응답 본문의 &lt;code>result.errorCode&lt;/code>를 읽습니다. 컨트롤러가 거절한 요청에 공통 형식을 쓰더라도, Spring이 컨트롤러 호출 전에 예외를 처리하면 다른 형태의 본문이 내려갈 수 있습니다. 지원하지 않는 응답 형식을 요청해 406이 나오는 경우가 그 예였습니다.&lt;/p>
&lt;h2 id="상태와-헤더를-살리고-본문을-공통-형식으로-맞췄다">상태와 헤더를 유지한 공통 본문&lt;/h2>
&lt;p>필수 쿼리 파라미터가 없거나 값의 형식이 틀리면 Spring MVC가 400을 만듭니다. 공통 예외 처리기는 이때 앱이 사용하는 &lt;code>INVALID_REQUEST&lt;/code> 본문을 돌려줍니다. 405는 허용 메서드를 알려 주는 &lt;code>Allow&lt;/code> 헤더도 보존해야 합니다. 본문을 맞추면서 Spring이 이미 정한 상태와 헤더를 함께 전달한 이유입니다.&lt;/p></description></item><item><title>HTTP 테스트에서만 수신 시각 고정하기</title><link>https://team-edo.github.io/posts/2026/10/scope-fixed-clock-to-http-test/</link><pubDate>Wed, 28 Oct 2026 00:00:00 +0900</pubDate><guid>https://team-edo.github.io/posts/2026/10/scope-fixed-clock-to-http-test/</guid><description>&lt;p>악보 화면을 본 시간을 서버에 보낼 때, 서버는 앱이 보낸 시간과 실제로 요청을 받은 시각을 대조합니다. 화면을 본 시간이 수신 시각에서 시작 시각을 뺀 값보다 지나치게 길면 저장하지 않습니다. 이런 API를 고정된 날짜로 테스트하면, 테스트를 실행하는 날짜에 따라 같은 요청이 허용되거나 거절될 수 있습니다.&lt;/p>
&lt;h2 id="실제-http-테스트에만-수신-시계를-고정했다">테스트 설정에 고정 &lt;code>Clock&lt;/code> 주입&lt;/h2>
&lt;p>체류 기록 테스트는 애플리케이션을 띄우고 실제 HTTP로 요청합니다. 서비스만 직접 호출하는 테스트처럼 생성자에 시계를 건네는 것으로는 서버의 수신 시각을 고정할 수 없습니다. 그래서 이 테스트가 가져오는 &lt;code>@TestConfiguration&lt;/code>에 고정된 &lt;code>Clock&lt;/code>을 만들고 &lt;code>@Primary&lt;/code>를 붙였습니다. 이 설정을 가져온 테스트에서는 같은 타입을 주입할 때 고정 시계를 우선합니다.&lt;/p></description></item><item><title>JDBC로 바꾼 값을 MyBatis가 다시 읽지 못한 이유</title><link>https://team-edo.github.io/posts/2026/10/mybatis-session-cache-in-db-tests/</link><pubDate>Wed, 28 Oct 2026 00:00:00 +0900</pubDate><guid>https://team-edo.github.io/posts/2026/10/mybatis-session-cache-in-db-tests/</guid><description>&lt;p>연습 세션에 분석 시도 번호와 시작 시각을 추가할 때 두 가지를 확인하려고 했습니다. 기존 행을 조회하면 새 필드에 기본값이 들어가야 하고, 값을 채운 행을 조회하면 매퍼가 새 필드를 올바르게 읽어야 했습니다. 처음에는 테스트 행 하나로 두 가지를 검사했습니다. MyBatis로 먼저 읽고 &lt;code>JdbcTemplate&lt;/code>로 값을 바꾼 뒤 같은 매퍼에서 다시 읽었습니다. 두 번째 조회에는 변경 전 시도 번호가 남아 있었습니다.&lt;/p>
&lt;h2 id="같은-sql을-다시-불러도-db까지-가지-않을-수-있다">세션 캐시가 재조회를 가로막은 이유&lt;/h2>
&lt;p>테스트는 Spring 트랜잭션 안에서 실행됐고, MyBatis는 같은 &lt;code>SqlSession&lt;/code>을 사용했습니다. MyBatis의 기본 로컬 캐시는 세션 안에서 같은 쿼리를 같은 인자로 요청하면 이전 결과를 재사용할 수 있습니다. 매퍼의 갱신은 이 캐시를 비우지만, 여기서 값을 바꾼 것은 &lt;code>JdbcTemplate&lt;/code>였습니다. 같은 트랜잭션에서 SQL을 실행했어도 MyBatis가 자신의 캐시에 있는 조회 결과를 무효화할 계기는 없었습니다.&lt;/p></description></item><item><title>SQL 조건을 빼도 테스트가 통과한 이유</title><link>https://team-edo.github.io/posts/2026/10/sql-test-fixtures-overlapping-filters/</link><pubDate>Wed, 28 Oct 2026 00:00:00 +0900</pubDate><guid>https://team-edo.github.io/posts/2026/10/sql-test-fixtures-overlapping-filters/</guid><description>&lt;p>얼쑤의 성취도 화면은 곡을 처음부터 끝까지 연주해 채점을 마친 기록을 비교합니다. 조회 SQL은 세션에 평가 행이 있는지, 세션 상태가 &lt;code>DONE&lt;/code>인지, 연주 구간이 악보의 첫 음부터 마지막 음까지인지 확인합니다. 실제 MySQL에 데이터를 넣고 HTTP로 조회하는 테스트도 통과했습니다. 화면에서 왜 같은 조건의 연주를 고르는지는 점수 비교 글에 정리했습니다. &lt;a href="https://team-edo.github.io/posts/2026/10/compare-like-for-like-practice-scores/">같은 조건의 연주만 비교한 이유&lt;/a>&lt;/p>
&lt;p>독립 리뷰에서 SQL 조건을 일부러 빼 보니 예상과 달랐습니다. 완료 상태를 확인하는 조건이나 시작 음을 확인하는 조건을 빼도 테스트는 계속 통과했습니다.&lt;/p></description></item><item><title>동시 업로드에서 접수된 녹음만 남기기</title><link>https://team-edo.github.io/posts/2026/10/concurrent-upload-keep-winning-object/</link><pubDate>Wed, 28 Oct 2026 00:00:00 +0900</pubDate><guid>https://team-edo.github.io/posts/2026/10/concurrent-upload-keep-winning-object/</guid><description>&lt;p>얼쑤에서 녹음을 올리면 서버는 파일을 객체 저장소에 쓴 뒤, 세션에 원본의 키를 기록하고 상태를 &lt;code>PENDING&lt;/code>으로 바꿉니다. 같은 세션에 업로드 요청이 거의 동시에 오면 두 요청 모두 처음에는 상태를 &lt;code>RECORDING&lt;/code>으로 읽을 수 있습니다. DB에서 한 요청만 접수하더라도 저장소에는 두 요청 모두 파일을 쓰는 시점이 생깁니다.&lt;/p>
&lt;p>세션마다 고정된 객체 키를 쓴다고 가정해 봤습니다. 요청 A가 먼저 DB 갱신을 마쳐도, 요청 B가 같은 키에 늦게 쓰면 DB가 가리키는 파일은 B의 바이트가 됩니다. B가 갱신에 실패하고 자신이 쓴 파일을 지우는 경우에도 같은 키라 A의 원본이 사라집니다. DB가 접수 순서를 정하는 것과 별도로, 어느 요청이 어느 파일을 소유하는지 저장소에서도 구분해야 했습니다.&lt;/p></description></item></channel></rss>