내가 TDD를 시작하게 된 이유
TDD에 대해 처음 관심을 갖기 시작한 것은 내 코드에 대한 확신이 부족해지면서부터였다.
협업을 하는 동료와의 관계에서 가장 중요한 것은 신뢰이다. 이 신뢰를 만드는데 중요한 가치 중 하나는 '예측 가능함' 이다. 이 사람이라면 이렇게 생각하고 개발하겠지, 물어보면 어떤 식으로 대답하겠지 하는 사소한 것들이 일정 시간에 걸쳐 쌓이다 보면 얻을 수 있다. 그리고 그보다 더 기저에 존재하는 것이 있다. 그것은 업무에 대한 퍼포먼스이다.
이것은 개발을 잘하고 화려한 스킬을 갖춘다는 의미와는 다르다. 내가 강조하고 싶은 것은 예측 가능함의 범주에 속한 퍼포먼스이다. 이 사람에게 업무를 맡기면 어느 정도의 시간에 걸쳐 이정도의 결과물이 나오겠구나, 하는 생각이 무의식적으로 들게 만드는 사람이다. A라는 업무를 시켰더니 10이라는 퍼포먼스가 나왔는데 B업무는 5밖에 안나왔다면 예측 가능하지 않다. 이것은 동료로서 신뢰를 구축하는데 굉장히 중요한 가치이다. 하지만 이런 퍼포먼스를 내는 것은 생각보다 쉬운 일이 아니다. 나도 이런 개발자가 되고 싶지만 아직은 아니다. 어떤 기능이 완료되었다고 보고했는데 굉장히 기본적인 부분에서 버그가 터졌던 적도 있었다. 그때마다 자책했으나 눈에 띄는 변화는 없었다.
그러던 찰나 TDD에 대한 설명을 접했다. Test Driven Development의 준말로 테스트 주도적으로 개발한다는 의미이다. 어떤 작동하는 코드를 개발하기 전에 테스트를 먼저 작성한다. 코드가 없기 때문에 당연히 테스트는 실패한다. 그러면 실패하는 테스트가 성공할 만큼만 개발 코드를 작성한다. 테스트가 성공하면 코드에 대한 리팩토링을 진행한다. 리팩토링 후에는 다시 테스트를 돌리며 결과를 확인한다. '테스트 실패 - 코드 작성 - 테스트 성공 - 리팩토링' 의 순환 과정을 통해 개발을 진행하는 것이다. 이 테스트는 가장 기본적인 단위부터 시작하여 점진적으로 진행해야 한다. 예를 들어 A를 Input 했을 때 B를 Output 하는 기능을 개발한다면 A를 넣는데 null이 나오는가? 부터 시작하여 기능에 필요한 요구사항을 가장 간단한 부분부터 수행하는 것이다.
그러나 이것을 실무에 적용할 수 있을까? 에 대한 의문은 지워지지 않았다. 여러 커뮤니티를 봐도 TDD가 실질적으로 개발 속도를 올려주는 것에 도움이 안된다는 글도 많이 봤었고 테스트 코드 자체를 작성하지 않는 개발자도 많았다. SI 업체에서 근무하던 시절을 떠올려보면 기능 개발하는 시간도 부족했는데 테스트 코드까지 작성하는게 과연 가능할지 알 수 없었다. 하지만 해보지도 않고 포기하기엔 내 개발 퍼포먼스에 대한 자괴감이 꽤나 컸다.
TDD를 하며 느낀 장, 단점
그래서 인프런의 한 강의를 수강했다. 이규원 선생님의 'Spring Boot TDD - 입문부터 실전까지 정확하게' 라는 강의였다.(혹시나 궁금한 분들을 위해 강의 링크는 댓글에 적겠습니다.) 이 강의는 어떤 개념을 체득하고 개발적인 사고를 확장하는 느낌보다는(이러한 강의를 혹시 찾으신다면 김영한 선생님 스프링 강의를 추천합니다.) 빠르게 실무에 적용할 수 있도록 치트스러운 무기를 쥐어주는 느낌이었다. 이전에 TDD에 대한 강의를 들었을땐 개념은 파악할 수 있었으나 그래서 스프링 부트, 자바, MVC 아키텍처, 프로젝트에 어떻게 적용할지 감은 잡히지 않았는데 이 강의를 통해 동일한 환경에서 어떻게 진행하는지를 엿볼 수 있었다. 그리고 이런 방식으로 개발을 진행했을 때 얻을 수 있는 이점과 가치는 어떤 것인지도 깨달았다.
먼저 이 방식은 개발 문서의 작성을 요구한다. 이 문서는 각각의 기능에 대한 클라이언트의 요구사항을 구체화하여 작성한다.
A라는 API가 있다고 가정했을 때,
1. A는 올바른 파라미터를 설정 시 200 request를 반환한다.
부터 시작하여,
00. A는 파라미터 B로 조회할 경우 값이 존재하지 않을 시 404 request를 반환한다.
따위의 것들이다.
이렇게 문서를 작성하다보니 느꼈던 것은 강의를 들을 때 보다 실무에 직접 적용했을 때 더 체감할 수 있는 변화였는데, 내가 그동안 이해하고 개발하고 있다라는 생각이 착각이었을 수도 있겠구나를 깨달았다. 실제로 글을 쓰고 점진적으로 개발을 진행하다보니 내가 이해하지 못하는 부분이 있었다는 사실을 문득 깨달았을 때가 많았다. 이 부분을 명확하게 이해했을 때 요구사항 문서도 제대로 쓸 수 있었다.
또 디버깅에 쏟는 시간이 현저히 줄었다. 이전에는 A라는 API를 만들고 프로젝트를 실행하여 테스트 후 안되는 부분을 디버깅하는 과정을 거쳤는데 예상치 못한 오류로 인해 디버깅하는 시간이 길어질 때가 간혹 있었다. 오타로 인해 1시간을 헤맨 에피소드도 있었는데 이러한 부분들이 TDD를 진행하며 상당 부분 해소됐다. 테스트 코드를 통해 각 요구사항을 검증하며 개발하기 때문에 개발이 완료되었을 때 디버깅해야 할 부분들이 거의 없었다.
불필요한 코드가 많이 사라졌다. 과거의 코드를 보면 불필요해 보이는 코드가 더러 보인다. 하지만 이것을 무턱대고 수정했다가 또 어떤 예상치 못한 오류가 발생할지 모르니 함부로 지울 수가 없었다. 결과적으로 유지보수 비용이 점차 올라가는 결과를 낳게 된다. 하지만 TDD는 가장 간단한 요구사항 부터 시작하여 요구사항이 동작하는 최소한의 코드를 작성하는 것이 기본 원칙이다. 그러다 보니 불필요한 코드가 개입될 요소가 현저히 줄어든다. 또 개입됐다 하더라도 쉽게 제거하는 것이 가능하다. 테스트 코드가 있기 때문이다. 빼보고 돌린 다음에 테스트가 돌아가면 안심이 된다.
무엇보다 마음에 와닿았던 것은 TDD가 클라이언트 중심의 개발 방식이라는 것이었다. 개발은 도구에 불과하다. 어떤 화려한 스킬이나 최신 트렌드의 언어라 할지라도 결국 요구사항을 구현하지 못한다면 그것은 비즈니스적 관점에서 무가치하다. 요구사항을 구현한 기능이 어떠한 버그도 없이 잘 동작한다면 그 순간이 도구가 가장 빛을 발하는 순간이다. TDD는 이러한 관점에 잘 맞는 패러다임이다. 고객의 요구사항을 단순화하여 문서로 작성한다. 그리고 그것을 통과하는 만큼만 코드를 작성한다. 작성된 코드는 기능을 수정하거나 새로운 기능을 추가할 때도 테스트를 기반으로 더 안전하게 동작한다.
그러나 어떤 방식이던 간에 명암은 존재한다. TDD를 적용하며 느꼈던 단점은 새로운 기능 개발 시 시간은 어쩔 수 없이 늘어난다. 요구사항 문서를 작성하고 그에 따른 테스트 코드를 작성하는 시간이 존재하기 때문이다. 그러나 이것은 단순하게 생각했을 때 느껴지는 시간의 단위보다는 짧다. '뭐? 요구사항 문서도 작성하고 테스트 코드까지 일일히 작성한다고? 대충 원래 개발했던 시간보다 X시간은 더 많이 잡아먹겠군.' 이라는 생각이 들었을 때, 그 X시간 보다는 짧다는 의미이다. 요구사항을 구체화하고 테스트 코드를 작성했을 때 코드는 간결해지고 읽기가 쉬워진다. 이 장점이 예상치 못한 오류에 대한 디버깅 시간을 줄여주기 때문에 그 X시간에서 이 Y시간을 차감해주는 것이 맞다. 무엇보다 유지보수의 비용도 낮아지기 때문에 서비스가 지속될수록 Y시간은 증가될 것이고 어렵지 않게 X시간을 능가할 수 있을 것이다. 쓰다 보니까 이건 단점이 아닌가?
실무의 환경에 TDD 환경을 구축하는 것에 대한 비용이 또 크다. 사실 이게 제일 컸다. 이것은 TDD를 처음 접하기 때문에 숙련도 이슈가 주된 이유이긴 하다. 그럼에도 불구하고 실무에는 다양한 환경과 조건이 있고 거기에 TDD를 녹여내는 비용은 무시할 수 없다. 나의 경우에는 프로젝트에 사용되는 DB가 여러개이다. 그러다 보니 엔티티 매니저나 트랜잭션 매니저도 여러개다. 테스트 환경 구축 시 이러한 환경과 동일한 조건에서 데이터를 가져올 수 있어야 했다. 그래서 '개발 서버 DB -> H2 DB -> 도커 컨테이너 DB' 순서로 시행착오를 거쳤다. 또 테스트 환경에서 각각의 테스트가 제대로 격리되지 않으면 결과값이 오염되기 때문에 이 부분도 신경을 써줘야만 했다. 이러한 시행착오들은 뒤에서 좀 더 자세히 다룬다. 어쨌든 시간이 꽤나 많이 소요됐다는 것이다.
TDD가 모든 테스트를 커버하는 것은 아니다. 외부 API 연동이라던가(카카오 알림톡, 푸시 메시지 등), 파일 시스템 등이 연관된 부분들은 요구사항 단위로 처리할 수 있는 부분이 아니다. 별도의 유닛 테스트 등을 통해 테스트 커버리지를 만족해야 한다. 추가로 자주 사용하는 유틸 클래스의 경우도 테스트 코드가 있으면 더욱 좋겠다. 나의 경우에는 성공, 실패 조건을 만족하는 클래스 별 테스트 코드를 작성하였다. 그리고 전체 테스트 실행 시에는 스킵되도록 설정했다.(외부 API 연동이 테스트마다 연동되면 곤란하니까.)
@Test
// 운영 서버 프로시저로 실행되며 실제 푸시가 전송되므로 별도로만 실행함
@Tag("disabled")
@Disabled("운영 서버 프로시저로 실행되며 실제 푸시가 전송되므로 별도로만 실행함")
void 필수값_입력_시_메시지가_전송된다() {
// arrange
KakaoMessageDto kakaoMessageDto = getValidKakaoMessageDto();
// act
kakaoMessageService.sendKakaoMessage(
kakaoMessageDto
);
// assert
// 예외가 발생하지 않으면 테스트가 성곱한 것으로 간주
}
@ParameterizedTest
@ValueSource(strings = {
"recipientNum",
"message",
"templateCode",
"subject",
"manager"
})
// 운영 서버 프로시저로 실행되며 실제 푸시가 전송되므로 별도로만 실행함
@Tag("disabled")
@Disabled("운영 서버 프로시저로 실행되며 실제 푸시가 전송되므로 별도로만 실행함")
void 필수_파라미터_미입력_시_400_bad_request_반환(
String param
) {
// arrange
KakaoMessageDto kakaoMessageDto = getValidKakaoMessageDto();
switch (param) {
case "recipientNum":
kakaoMessageDto.setRecipientNum(null);
break;
case "message":
kakaoMessageDto.setMessage(null);
break;
case "templateCode":
kakaoMessageDto.setTemplateCode(null);
break;
case "subject":
kakaoMessageDto.setSubject(null);
break;
case "manager":
kakaoMessageDto.setManager(null);
break;
default:
break;
}
// act & assert
Assertions.assertThrows(ClientException.class, () -> {
kakaoMessageService.sendKakaoMessage(
kakaoMessageDto
);
});
}
그리고 TDD 과정에서 API 테스트 시 알림톡이 발송되는 것을 막기 위해 모킹 클래스를 별도로 설정했다.
/*
* 테스트 환경에서 알림톡 전송을 막기위해 모킹하는 클래스
*/
@Service
@Profile("test")
public class MockKakaoMessageServiceImpl implements KakaoMessageService {
@Override
public Boolean sendKakaoMessage(KakaoMessageDto kakaoMessageDto) {
return true;
}
}
이 과정을 통해 테스트 환경과 분리하였다.
TDD를 통해 기능 개발을 완료한 뒤에도 별도로 API를 실제 작동하여 테스트 하는 과정이 필요하다. 그 과정에서도 오류를 발견할 수 있기 때문이다. 이런 과정이 추가됨에 따라 발생하는 비용또한 역시 존재한다.
내가 TDD를 적용한 방법
먼저 테스트 클래스에 적용할 커스텀 어노테이션이 있다.
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test")
@TestInstance(PER_CLASS)
@Import({TestConfig.class, TestSecurityConfig.class})
public @interface SafetyCustomTest {
}
특별한 점이 있다면 TestInstance에 PER_CLASS를 적용한 것인데 클래스 별로 테스트 용 데이터를 초기화하는 기능을 넣기 위해서이다.
@BeforeAll
void init() {
testDataInit.init();
}
다음 코드를 각 클래스에 코드를 추가했으며 testDataInit은 두개의 구현체를 갖는다. 한개는 패스워드가 없는 사용자를 생성하는 기능이고 다른 한개는 패스워드가 있는 사용자를 생성하는 기능이다. 각 기능 앞에는 DELETE 구문을 통해 항상 모든 데이터를 지워주는 초기화 과정을 거친다. 이렇게 구현한 기능은 로그인이 가능한 사용자, 불가능한 사용자의 케이스를 구분하여 테스트 코드를 작성하기 위함이다.
커스텀 어노테이션을 정의해놓으면 아래와 같이 적용 가능한 테스트 클래스에 해당 어노테이션을 사용함으로써 간단하게 정의할 수 있다.
@SafetyCustomTest
public class PATCH_specs {
내가 작성한 TDD는 실제 DB의 데이터를 사용한다. 이 환경을 구성하기 위해 나름 험난한 과정을 거쳤다. 처음에는 실제 개발 서버의 DB를 사용하여 테스트 코드를 작성했는데 다른 데이터와 값이 뒤섞이다보니 일정한 결과를 도출해내지 못했다. 그래서 h2 db와 jpa auto ddl 기능을 통해 별도의 인메모리 기반 환경을 구축하고자 했다. 삽질은 다음과 같다.
@Configuration
@Profile("test")
public class TestJpaConfig {
@Bean
public JpaVendorAdapter testJpaVendorAdapter() {
HibernateJpaVendorAdapter adapter = new HibernateJpaVendorAdapter();
adapter.setGenerateDdl(false); // true 일 경우 ddl 생성(지금 false인 이유는 사용 안하니깐)
adapter.setShowSql(true);
return adapter;
}
}
테스트 용 config class를 통해 테스트 환경일 때만 ddl을 사용했다.
@Primary
@Bean(name = "defaultDataSource")
@Profile("test")
DataSource testDataSource() {
return DataSourceBuilder.create()
// H2 In-Memory DB 설정
.url("jdbc:h2:mem:testdb;MODE=MSSQLServer;DB_CLOSE_DELAY=-1")
.username("sa")
.password("")
.driverClassName("org.h2.Driver")
.build();
}
그리고 다음 설정을 통해 h2 db를 사용했다. Primary 어노테이션이 붙어있는 것은 Mssql 뿐만 아니라 Mysql db도 사용하기 때문에 빈을 하나 더 만들었는데 그 중 기본 구현체를 설정해준 것이다. 다음과 같이 설정하고 JPA의 auto DDL을 통해 h2 DB에 성공적으로 개발 서버 DB와 거의 동일한 환경을 세팅할 수 있었다. 하지만 예상치 못한 변수가 있었는데 MSSQL 자체에서 사용하는 디비 2개를 이를 h2 db에서는 각각 별도로 세팅해야 한다는 점이었다. Mssql 환경에서는 JPA에서 다음과 같이 선언하여 같은 접속 정보의 다른 DB 카탈로그에 접근이 가능했다.
@Entity
@Table(name = "plant_code", catalog = "RRP01_pdata", schema = "dbo")
@Getter
@Setter
public class Plant {
그런데 이 설정이 h2 db에서는 먹히지 않았다. 내가 찾아본 바로는 별도로 또 디비를 분리해서 구성해야 하는 것 같았다. 문제는 프로젝트가 진행되며 다른 디비를 조회할수도 있고 프로시저나 링크드 서버를 사용할 수도 있다는 것이었다. 이것들을 h2 db 세상에서 별도로 구현할 자신이 없었다. 그래서 테스트 전용 mssql db를 도커 컨테이너로 구성했다. 맥을 사용하고 있었기 때문에 다른 선택지가 없었다.(맥에서 Mssql은 설치 불가능) 도커에 대한 아주 기본 지식만 있어도 구성하는 것은 어렵지 않았다. mssql 이미지를 다운받고 도커 볼륨을 통해 백업한 데이터베이스를 정보를 마운트 해주면 된다.

다음은 이미지를 실행하여 컨테이너로 올렸을 때의 모습이다. MSSQL 이미지는 amd64 아키텍처 환경에서 작동하는데 맥은 arm64 아키텍처를 사용하고 있으므로 저런 메시지가 떠 있다. 최신버전 이미지를 다운받았을 때는 그것 때문에 작동이 안돼서 구버전으로 대체했다. 속도가 좀 느리다는 단점이 있다고 하는데 체감되진 않았다.

다음 경로를 보면 마운트가 됐다고 나온다. 내 로컬 컴퓨터에 있는 특정 경로와 도커 컨테이너 내부의 특정 경로를 동기화하는 작업이라고 이해하면 된다. 내가 해당 경로에 아무 파일이나 추가하면 도커 컨테이너 내부의 설정해 놓은 경로에도 그 파일이 올라간다. 이를 통해 백업한 파일을 도커 컨테이너에 전달할 수 있었다. 할때는 docker run 명령어에 'volume=/host/pc/의/url:/docker/container/의/url' 방식으로 작성한다.
결과적으로 프로젝트 내에 DB 환경은 운영, 개발, 테스트 서버 총 3개로 관리하게 되었다.
환경을 구성한 뒤에는 본격적인 테스트 코드를 작성했다. AAA(Arrange-Act-Assert) 패턴을 사용하여 테스트 전 기본 환경을 세팅하고, 테스트 하고자 하는 API를 TestRestTemplate 을 사용해 호출했으며, 검증하고자 하는 값을 검증했다. 예시는 다음과 같다.
@Test
void 안전일지_매니저_서명_tbmId가_없을_경우_request_FAIL_반환(
@Autowired
TestFixture fixture
) {
// arrange
HttpHeaders headers = fixture.getInitMultipartHeaders();
MultiValueMap<String, Object> body = getManagerMultipartBody(null, getMockMemberSignatureFile());
// act
ResponseEntity<Response> response = restTemplate.exchange(
"/api/tbm/manager-sign",
HttpMethod.PATCH,
new HttpEntity<>(body, headers),
Response.class
);
// assert
assertThat(response.getBody().getResultCode()).isEqualTo(ResponseCode.FAIL);
}
해당 API는 파일 첨부가 들어가 있으므로 contentType을 multipart form data로 설정해줘야 한다. 그리고 엑세스 토큰이 필요하므로 사전에 로그인 API를 호출한 뒤 토큰을 획득해야 한다. 이 사전 과정은 TestFixture라는 클래스에 별도로 작성했다. 많은 재사용이 일어나는 부분이기 때문이다. 코드는 다음과 같다.
public HttpHeaders getInitMultipartHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
headers.set("deviceId", "testDeviceId");
headers.set("Authorization", "Bearer " + getAccessToken("id", "pw", "WEB"));
return headers;
}
그 다음에는 파일 첨부에 필요한 MultipartFile 객체와 Dto 객체를 세팅해줘야 한다. 그런데 테스트 하고자 하는 것은 파일 업로드 기능이 아니기 때문에 MultipartFile은 모킹한다. 모킹이란 실제 객체를 호출하는 것이 아닌 항상 동일한 결과값을 리턴시키는 허수아비 같은 객체이다. MultipartFile은 테스트 용 MockMultipartFile 구현체를 제공하기 때문에 이것을 사용한다. 이 과정을 구현한 메소드가 'getMockMemberSignatureFile()' 이다. 코드는 다음과 같다.
private MultipartFile getMockMemberSignatureFile() {
MultipartFile memberSignatureFile = new MockMultipartFile(
"memberSignatureFile",
"test-sign.png",
"image/png",
"dummy-signature".getBytes()
);
return memberSignatureFile;
}
이 메서드를 사용하여 해당 API를 테스트하는 모든 MultipartFile 객체는 이 객체를 반환하게 한다.
Mocking한 파일 객체와 Dto 객체는 MultiValueMap 을 통해 가공한다. 이는 http에서 보내는 평범한 json 요청이 아니라 파일이 통합된 데이터를 보낼 때 그걸 자바가 이해할 수 있게 Map 형태로 가공한 객체이다. API 통신 시 개발자 모드를 통해 request body를 살펴보면 어떻게 값을 보내는지 확인이 가능할 것이다.
데이터 가공을 완성하면 api를 호출하고 결과값을 검증한다. 일반 restTemplate을 사용하지 않고 testRestTemplate을 사용한 가장 큰 이유는 예외 처리 부분에 있다. 4xx 나 5xx 관련 예외가 발생할 경우에도 ResponseEntity 에다 값을 넣기 때문에 별도로 try catch 구문을 작성할 필요가 없다. 그 외에 빈을 직접 주입하지 않아도 되고 인증 관리의 편의성이 있다고도 한다.
마지막으로 테스트 환경에서 트랜잭션의 사용이다. 테스트 환경에서는 트랜잭션을 사용하지 않는다. JPA를 사용할 경우 엔티티 매니저는 트랜잭션의 생명주기에 따라 데이터를 관리한다. 예를 들어 repository.save() 를 호출해도 DB I/O가 바로 일어나지 않고 트랜잭션이 종료될 때 저장된다. 때문에 arrange 단계에서 데이터를 여러개 넣는 상황에서 제대로 테스트가 되지 않는다. 그래서 트랜잭션은 사용하지 않고 필요한 데이터는 즉각 저장되도록 한다. 이 경우 지연 로딩은 사용할 수 없어 데이터를 조회, 삽입 시 테이블 단위로 순차적으로 진행한다. 지연 로딩 기능을 사용할 수 없는 이유는 앞서 말했듯 jpa 기능 호출 시 즉각적으로 db i/o가 일어나기 때문이다.
{
// arrange
tbmMemberRisksRepository.deleteAll();
tbmMembersRepository.deleteAll();
tbmRiskAssessmentRepository.deleteAll();
tbmMasterRepository.deleteAll();
나는 이런식으로 초기화가 필요할때 delete 구문을 순차적으로 사용했다.
느낀점과 회고
TDD는 다양한 이점을 제공하는 개발 방법이다. 하지만 모든 기술이 그렇듯 맹목적인 장점만 있는 것은 아니다. TDD를 진행하며 개발했지만 여전히 버그는 있었고 앞서 말한 예측가능한 개발자의 길은 멀었다는 것을 느꼈다. TDD를 가장 크게 잘못 사용한 부분은 등록이나 수정하는 API에서 결과값을 검증하는 부분(assert) 이었다. api의 리턴값으로 vo 객체를 받아서 각각의 값을 검증하는 방식을 사용했는데 이는 개발을 잘못하면 얼마든지 오염될 수 있는 값이다. 예시는 아래와 같다.
// act
ResponseEntity<Response> response = ...
// assert
ObjectMapper objectMapper = new ObjectMapper();
List<Vo> actual = objectMapper.convertValue(
response.getBody().getResultData(),
new TypeReference<List<Vo>>() {
});
...
실제로 값이 저장되지도 않았는데 vo에 값이 있어서 저장되었다고 테스트를 통과하는 경우가 있었다. 그래서 이 경우에는 직접 respository 계층의 find..() 구문을 사용하여 조회한 뒤 조회한 값을 검증하도록 방식을 바꿨다. 이 방식은 리파지토리 계층에서 테스트 클래스에만 의존하는 메서드가 생성될 수 있는 단점이 존재하나 직접적인 비즈니스 로직에는 의존하지 않으므로 충분히 괜찮다고 생각한다.
또 하나는 TDD와는 다소 관련이 없는 점이긴 하지만 프론트 개발자와의 소통 문제였다. 현재 나는 스웨거를 사용중이고 별도의 리드미 파일 문서를 제공한다. 그런데 스웨거와 리드미 파일 문서 모두 프론트 개발자가 직접 전달해줘야 하는 파라미터 값을 명시하지 않는다. 예를 들어,
String name, Integer age, String memberId, Boolean isDel 이라는 값이 DTO에 있을 경우 name과 memberId가 필수값이라는 사실은 문서를 통해 확인이 가능하나, isDel 이라는 값은 프론트에서 전달해줘야 하는 값이 아니고 백엔드 내에서 사용하는 데이터인데 프론트 개발자에게 노출되는 상황이다. 그래서 프론트에서 받는 DTO를 따로 분리해야 하나 라는 생각을 했는데 사실 백엔드 개발자 1명(나), 프론트 개발자 1명 개발하는 상황에서 이건 과투자가 아닌가, 라는 생각이 들었다. 그래서 리드미 문서 curl 구문에 필요한 값들을 명시하는 방식으로 변경하기로 계획했다.(추후에 확인해보니 스웨거에서 프론트 개발자가 신경쓰지 않아도 되는 파라미터는 숨김처리해주는게 가능하다는 것을 알았다.)
- subject: 안전일지 매니저 서명
- url: /api/tbm/manager-sign
- curl -X 'PATCH' \
'http://localhost:8084/api/tbm/manager-sign?tbmId=xxx' \
-H 'accept: application/json' \
-H 'Authorization: Bearer {accessToken}' \
-H 'Content-Type: multipart/form-data' \
-F 'tbmManagerSignDto={"managerDepartment":"부서","managerPosition":"소속","managerName":"이름"}' \
-F 'managerSignatureFile=@image.png;type=image/png'
- method: PATCH
- [x] 안전일지 매니저 서명 올바른 값 등록 시 200 request 반환
- [x] 안전일지 매니저 서명 tbmId가 없을 경우 request_FAIL 반환
- [x] 안전일지 매니저 서명 매니저 서명 파일이 없을 경우 request_FAIL 반환
- [x] 안전일지 매니저 서명 소속,직책,매니저명 로그인 유저의 정보로 등록
- [x] 안전일지 매니저 서명 매니저 서명 경로가 등록
- [x] 안전일지 매니저 서명 업로드[별도 테스트]
- [x] 안전일지 매니저 서명 매니저 사인 시각이 현재 시각으로 등록
- [x] 안전일지 매니저 서명 해당 안전일지의 상태값이 COMPLETED 로 변경
다음과 같이 명시하고 소통을 많이 하는 방향으로 해결 가능하지 않을까 싶다. 무엇보다 소수의 개발자와 바로 옆자리에서 붙어 개발할 때 최대 장점은 소통하기가 편하다는 점이니까.
처음으로 실무에 TDD를 적용했고 예상처럼 엄청 삐긋댔다. 지금도 내가 개발한 방향이 맞긴 한건지, 혹시 잘못된 방향인데 알아차리지 못하고 계속 나아가는 것은 아닌지 의문이 든다. 하지만 확실한 것은 TDD를 공부하며 배운 가치, 다양한 장점을 실무에 적용하며 피부로 체감하는 느낌이 들었다는 것이다. 현재 개발환경에서 TDD를 적용하는데는 결과적으로 아무런 문제가 없었다. 그리고 앞으로도 다른 변화가 있지 않는 한, TDD로 개발을 이어갈 것이다.
'취미 > 코딩일기' 카테고리의 다른 글
| 한걸음 물러서서 (2) | 2025.08.20 |
|---|---|
| 어떤 개발자가 되고 싶으신가요? (1) | 2025.01.25 |
| [일상]23년 3월 13일 코딩일기..는 아니고 그냥 기록(애드센스 승인됐다!) (16) | 2023.03.13 |
| [코딩일기]23년 3월 9일 - 오타 정도는 유도리 있게 해석하면 안되나?(feat.NoSuchBeanDefinitionException) (6) | 2023.03.09 |
댓글