밤빵's 개발일지
[TIL]20241117 TDD 본문
TDD라는 개념을 실제로 어떻게 적용하는지 그리고 그 목적과 장단점에 대해 깊이 이해해본 적은 없었다. 이번 개발일지를 통해 TDD의 개념과 목적, 그리고 이를 적용하면서 겪었던 어려움과 배운 점들을 정리해보고자 한다. 또한, 이를 통해 앞으로 TDD를 습관화하고, 더 나은 코드를 작성하기 위해 노력하려고 한다!
▶TDD란?
TDD(Test-Driven Development)는 소프트웨어 개발 방법론 중 하나로, 이름 그대로 테스트를 먼저 작성하고, 그 테스트를 통과하기 위한 프로덕션 코드를 작성하는 방식이다. TDD의 핵심 과정은 Red-Green-Refactor로 나뉜다.
- Red 단계: 테스트를 작성하고, 처음에는 당연히 이 테스트가 실패하도록 만든다. 테스트할 프로덕션 코드가 아직 없기 때문에 테스트는 실패하게 된다.
- Green 단계: 테스트를 통과할 수 있을 만큼의 최소한의 프로덕션 코드를 작성한다. 이 단계에서는 코드의 품질보다 테스트를 통과하는 것이 우선이다.
- Refactor 단계: 테스트를 모두 통과한 후, 프로덕션 코드를 개선하는 단계이다. 여기서 코드를 더 깔끔하고 효율적으로 다듬어 최종적인 형태로 만든다.
이렇게 TDD는 작은 테스트를 먼저 작성하고, 그 테스트를 통과하기 위해 필요한 만큼의 코드를 작성하면서 점진적으로 코드를 개선해 나가는 방식이다. 이를 통해 테스트가 주도하는 개발 과정을 가지게 되며, 코드의 안정성과 유지보수성을 높일 수 있다.
▶TDD의 목적과 장점
TDD의 가장 큰 목적은 올바른 설계를 하도록 돕는 것이다. 처음에는 테스트 코드를 작성하는 것이 주된 작업처럼 보이지만, 사실 TDD의 핵심은 테스트를 통해 소프트웨어의 설계를 점진적으로 개선하고 더 좋은 구조를 만들기 위함이다. TDD를 적용하면 다음과 같은 장점이 있다.
- 작은 단위의 테스트를 통한 코드 안정성 확보: 테스트를 먼저 작성하기 때문에, 코드가 의도한 대로 동작하는지 항상 확인할 수 있다. 특히, 코드가 변경되거나 기능이 추가될 때, 기존의 테스트들이 이를 확인해 주기 때문에 사이드 이펙트(부작용)를 방지할 수 있다.
- 리팩토링이 쉬워짐: TDD는 코드 작성 후 리팩토링 과정을 중요하게 여긴다. 테스트가 코드의 올바른 동작을 보장해주기 때문에, 개발자는 마음 편히 리팩토링을 진행할 수 있다. 기능 추가나 코드 수정 시 기존 테스트를 돌려보면, 변경 사항이 다른 부분에 어떤 영향을 주는지 쉽게 파악할 수 있다.
- 기능 요구사항에 집중: TDD를 통해 테스트를 먼저 작성하면, 해당 기능의 명확한 요구사항을 정의하게 된다.테스트가 그 기능이 해야 할 일을 명확히 기술하므로, 불필요한 기능을 추가하거나 과도한 구현을 피할 수 있다.
▶TDD 적용 시의 어려움
처음 TDD를 적용하려고 하면 여러 가지 어려움이 있을 수 있다. TDD에 익숙하지 않다 보니, 도메인 코드부터 작성하는 실수를 하곤 했다. TDD는 테스트를 먼저 작성하고 이를 통과하기 위한 최소한의 코드를 작성해야 하지만, 급한 마음에 프로덕션 코드를 먼저 작성하게 되는 경우가 많았다. 이렇게 되면 테스트 코드는 이미 작성된 프로덕션 코드를 검증하기 위해 작성되는 형태가 되기 쉽고, TDD의 핵심인 설계를 개선하는 과정이 누락될 위험이 있다.
또한, 너무 큰 기능 단위로 개발을 진행하게 되면 TDD와 멀어지게 되는 경우가 많았다. 큰 기능을 먼저 구현해 놓고 나면, 어디서부터 테스트를 시작해야 할지 막막해지기 때문에 작은 단위로 기능을 나누어 TDD를 적용하는 연습이 필요하다.
▶TDD의 적용 사례와 학습한 점
▷예시 코드: TDD 적용 사례
다음은 TDD를 적용하여 사용자 서비스 코드를 작성한 예시로, 사용자 등록 기능을 테스트 주도 개발 방식으로 구현한 것이다. 먼저 테스트 코드를 작성하고, 이를 통과하기 위한 최소한의 프로덕션 코드를 작성해 나갔다.
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import org.mockito.Mockito;
public class UserServiceTest {
private UserRepository userRepository = Mockito.mock(UserRepository.class);
private PasswordEncoder passwordEncoder = Mockito.mock(PasswordEncoder.class);
private JwtUtil jwtUtil = Mockito.mock(JwtUtil.class);
private UserService userService = new UserService(userRepository, passwordEncoder, jwtUtil);
@Test
public void testRegisterUser_emailAlreadyExists() {
// given
String existingEmail = "test@example.com";
UserSignupRequestDto signupRequestDto = new UserSignupRequestDto(existingEmail, "password", "nickname", Platform.WEB);
Mockito.when(userRepository.findByEmail(existingEmail)).thenReturn(Optional.of(new User()));
// when
UserSignupResponseDto response = userService.registerUser(signupRequestDto);
// then
assertEquals("사용자가 이미 존재합니다.", response.getMessage());
}
@Test
public void testRegisterUser_success() {
// given
String email = "newuser@example.com";
String password = "password";
String nickname = "newnickname";
UserSignupRequestDto signupRequestDto = new UserSignupRequestDto(email, password, nickname, Platform.WEB);
Mockito.when(userRepository.findByEmail(email)).thenReturn(Optional.empty());
Mockito.when(userRepository.findByNickname(nickname)).thenReturn(Optional.empty());
Mockito.when(passwordEncoder.encode(password)).thenReturn("encodedPassword");
// when
UserSignupResponseDto response = userService.registerUser(signupRequestDto);
// then
assertEquals("회원 가입 성공!", response.getMessage());
}
}
→ 이 테스트 코드에서는 UserService의 registerUser 메서드를 테스트하고, 이메일이 이미 존재하는 경우와 새로운 사용자를 성공적으로 등록하는 경우를 다루고 있다. 이처럼 TDD의 첫 단계인 테스트 작성을 명시적으로 포함하여 단계별로 프로덕션 코드를 발전시키는 과정을 보여주면, TDD의 핵심 과정을 잘 이해할 수 있다.
Mockito는 객체의 행동을 모의(Mock)하기 위한 도구로, 실제 객체를 생성하는 대신 테스트 목적에 맞게 가짜 객체를 생성하여 테스트할 수 있도록 도와준다.
TDD를 적용하면서 가장 많이 느낀 점은, 작은 단위로 개발을 진행해야 한다는 것이다. 큰 기능을 한 번에 개발하려고 하면 TDD의 장점을 살리기 어렵고, 테스트 코드 작성도 점점 미루게 된다. 예를 들어, '사용자 로그인' 기능을 구현한다고 할 때, 이를 작은 단위로 나누어 먼저 '아이디와 비밀번호가 입력되었을 때 유효성 검사'와 같은 테스트를 작성하고 이를 통과시키기 위한 코드를 작성하는 방식으로 접근하면 좋다. 이런 작은 스텝의 반복을 통해 TDD를 실천할 수 있었다.
또한, TDD는 테스트 코드 작성의 중요성을 깨닫게 해 주었다. 특히 유닛 테스트를 통해 코드의 작은 단위에서부터 제대로 동작하는지를 확인함으로써, 이후 코드가 커지더라도 안정성을 유지할 수 있다. 예를 들어, 한 번 작성된 테스트 코드는 나중에 기능이 추가되거나 변경될 때 기존 기능이 의도한 대로 동작하는지 확인하는 안전망 역할을 한다. 이는 유지보수할 때 매우 큰 장점으로 다가왔다.
▶TDD의 실용성과 한계
TDD가 모든 상황에서 항상 유리한 것은 아니다. 특히, 일회성으로 사용되고 버려질 코드에서는 TDD를 적용하는 것이 효율적이지 않을 수 있다. 테스트 코드를 작성해도 실제로 이를 다시 돌려볼 일이 없기 때문이다. 하지만 지속적으로 유지보수해야 하는 코드에서는 TDD가 매우 큰 도움이 된다. 테스트 코드를 통해 어떤 기능을 하는지 명확히 파악할 수 있고, 코드 수정 시 다른 부분에 미치는 영향을 쉽게 파악할 수 있기 때문이다.
또한, TDD는 처음 적용할 때 시간이 더 걸리고 익숙해지기까지 많은 연습이 필요하다. 하지만 일단 TDD에 익숙해지고 나면, 리팩토링과 기능 추가가 용이해지고, 코드의 품질을 높일 수 있는 개발 방법론이라는 것을 배울 수 있었다. 따라서 지속적으로 유지보수가 필요한 프로젝트에서는 TDD를 통해 코드의 안정성과 확장성을 보장하는 것이 좋다고 생각한다.
한 번 작성된 테스트 코드를 통해 이후 기능 추가나 코드 수정 시 기존 코드가 정상 동작하는지를 쉽게 확인할 수 있었다.
이번 개발일지를 통해 TDD의 개념과 장점, 그리고 이를 적용하면서 느낀 어려움과 해결 방법에 대해 정리해 보았다. TDD는 단순히 테스트 코드를 작성하는 것이 아닌, 올바른 설계를 위한 개발 방법론이라는 점에서 큰 가치를 가진다. 처음에는 익숙하지 않아서 도메인 코드부터 작성하는 실수를 하거나, 큰 기능 단위로 접근하여 테스트를 놓치는 경우도 있었지만, 작은 단위로 나누어 점진적으로 테스트하고 코드를 개선하는 연습을 통해 조금씩 TDD에 익숙해지려고 한다...! 앞으로도 TDD를 습관화하여 코드의 안정성과 품질을 높이는 데 집중하고자 한다. 특히 유지보수가 필요한 프로젝트에서는 TDD를 통해 리팩토링에 대한 두려움을 줄이고, 자신 있게 기능을 추가할 수 있는 환경을 만들어 나가려 한다.
'개발Article' 카테고리의 다른 글
[TIL]20241120 FACADE 패턴 (14) | 2024.11.20 |
---|---|
[TIL]20241118 Spring Security에서 권한별 URL 접근 제어에 대한 고민 (1) | 2024.11.19 |
[TIL]20241116 @Data를 써도 될까? (0) | 2024.11.16 |
[TIL]20241115 소프트웨어에서 도메인이란? (9) | 2024.11.15 |
[TIL]20241114 인터페이스와 추상클래스의 용도 차이? (0) | 2024.11.14 |