zooxop
수강평 작성수
1
평균평점
5.0
블로그
전체 6#카테고리
- 백엔드
#태그
- 백엔드
- 워밍업클럽
- 테스트코드
- 클린코드
- 미션
- 테스트
- 리팩터링
- 발자국
- 회고

2024. 10. 25.
0
인프런 워밍업 스터디 클럽 2기 백엔드(클린코드&테스트코드) 발자국 - 4주차
인프런 워밍업 클럽 2기, 백엔드(클린코드&테스트코드) 과정에 참여하고 있습니다.마지막 주차인 이번 4주차에는 Layered Architecture 의 각 레이어별 역할과 실무에 가까운 테스트 방법들을 배워보는 시간을 가졌습니다.강의 링크: Readable Code: 읽기 좋은 코드를 작성하는 사고법 [학습 요약] Layered ArchitecturePersistence LayerData Access의 역할을 한다.~~Repository비즈니스 가공 로직이 포함되어서는 안된다.Data에 대한 CRUD에만 집중한 레이어Business Layer비즈니스 로직을 구현하는 역할Persistence Layer와의 상호작용(Data를 읽고 쓰는 행위)을 통해 비즈니스 로직을 전개시킨다.트랜잭션을 보장해야 한다.작업단위에 대한 원자성Presentation Layer외부 세계의 요청을 가장 먼저 받는 계층파라미터에 대한 최소한의 검증을 수행한다.MockMvcMock: 가짜, 대역의 의미를 갖고 있다.Mock(가짜) 객체를 사용해 스프링 MVC 동작을 재현할 수 있는 프레임워크 Test Double테스트를 수행하기 위한 대역 역할을 하는 객체들을 칭하는 표현Dummy아무것도 하지 않는 깡통 객체Fake단순한 형태로 동일한 기능은 수행하나, 프로덕션에서 쓰기에는 부족한 객체 (ex. FakeRepository)Stub테스트에서 요청한 것에 대해 미리 준비한 결과를 제공하는 객체.그 외에는 응답하지 않는다.SpyStub 이면서 호출된 내용을 기록하여 보여줄 수 있는 객체.일부는 실제 객체처럼 동작시키고 일부만 Stubbing 할 수 있다.Mock행위에 대한 기대를 명세하고, 그에 따라 동작하도록 만들어진 객체.Stub과 Mock의 차이Stub은 상태 검증(State Verififation) 을 위해 사용Mock은 행위 검증(Behavior Vefirification) 을 위해 사용 순수 Mockito로 검증해보기@ExtendWith(MockitoExtension.class) class MailServiceTest { @Spy private MailSendClient mailSendClient; @Mock private MailSendHistoryRepository mailSendHistoryRepository; @InjectMocks private MailService mailService; @DisplayName("메일 전송 테스트") @Test void sendMail() { // given // 1) doReturn(true) .when(mailSendClient) .sendEmail(anyString(), anyString(), anyString(), anyString()); // 2) doNothing() .when(mailSendClient) .a(); // when boolean result = mailService.sendMail("", "", "", ""); // then assertThat(result).isTrue(); verify(mailSendHistoryRepository, times(1)).save(any(MailSendHistory.class)); // 3) } } @Spy 객체는 Mockito 의 메서드가 아니라 Stubber 메서드를 이용해서 Stubbing 을 수행해야 한다.1) MailSendClient 의 .sendEmail() 메서드만 Stubbing 했고, 나머지는 실제 구현 코드를 사용한다.2) 이렇게 하면 .a() 도 Stubbing 했으므로, 동작을 안하게 됨.3) verify() 를 이용해서 Mock 객체의 여러가지 행위를 검증할 수 있다. BDDMockitoMockito 를 한번 더 감싸서, 동일한 기능을 제공하되 BDD 스타일로 작성된 메서드를 사용할 수 있도록 도와주는 라이브러리이다.// Before // given Mockito.when(mailSendClient.sendEmail(anyString(), anyString(), anyString(), anyString())) .thenReturn(true); // After // given BDDMockito.given(mailSendClient.sendEmail(anyString(), anyString(), anyString(), anyString())) .willReturn(true);Mockito를 사용하게 되면, given 절에 when() 이라는 이름의 메서드를 사용해야 된다. 이는 부자연스럽게 느껴질 수 있으므로, BDDMockito 를 사용하면 동일하게 when() 동작을 수행하지만 이름은 given() 인 메서드를 사용할 수 있다. 더 나은 테스트를 작성하기 위한 구체적 조언 한 문단에 한 주제테스트 코드는 문서의 역할을 한다. 그러니, 글쓰기를 한다는 마음 가짐으로 테스트를 작성하자. 완벽하게 제어하기현재시간, 랜덤값 등을 외부에서 주입받도록 DI 구조로 리팩토링 하여, given 데이터를 완벽하게 제어할 수 있도록 한다.이메일 발송과 같은 외부 세계와 소통하는 기능은 Mocking 을 통해 제어할 수 있도록 한다. 테스트 환경의 독립성을 보장하자한 테스트 메서드에서 두가지 이상의 기능을 테스트하지 말자논리적인 사고가 한번 더 필요해지므로, 가독성에 안좋은 영향을 준다.when/then 절의 assert 구문을 호출하기 전에 given 에서 예외가 발생할 수도 있다.given절에 사용할 데이터를 만들때는 가급적이면 순수한 생성자 또는 Builder 를 통해 생성하는 것이 좋다.생성 과정에 검증이 포함되어 있는 Factory 메서드 패턴을 사용하면, 생성 과정에서 예외가 발생할 수도 있음. 테스트 간 독립성을 보장하자테스트간에는 순서라는 개념이 없어야 한다.각각 독립적으로, 언제 어떤 순서로 어떻게 수행되든 항상 같은 결과를 내야만 한다. 한 눈에 들어오는 Test Fixture 구성하기Fixture: 고정물, 고정되어 있는 물체테스트를 위해 원하는 상태로 고정시킨 일련의 객체Fixture를 생성하는 코드를 메서드로 분리시킨 다면,이 테스트에서 필요한 파라메터만 넘길 수 있도록 메서드 내부에서 기본값을 하드코딩으로 설정해주는 방법이 가독성 향상에 좋다.@BeforeEach, @BeforeAll중복 코드를 줄이기 위해 사용하는 기능이지만, 각 테스트간 결합이 생기게 만든다는 맹점이 존재함.각 테스트 입장에서 봤을 때, 아래 두가지 항목을 만족하면 BeforeEach 절에 사용해도 괜찮다.아예 몰라도 테스트 내용을 이해하는 데에 문제가 없는가?수정해도 모든 테스트에 영향을 주지 않는가?data.sql테스트 클래스 코드가 아닌, 쿼리로 데이터를 미리 셋업하도록 처리하는 방식은 데이터를 셋업하는 코드의 파편화로 인해, 유지보수 포인트가 증가하고 추후 중복코드를 만들어낼 수도 있는 등 프로젝트의 복잡도만 높이는 행위이므로 지양하는 것이 좋다. Test Fixture 클렌징.deleteAll() vs .deleteAllInBatch()`.deleteAll()` : 지울 테이블을 먼저 select 하고, 데이터 건수만큼 delete 쿼리의 where 절에 key값을 포함한 쿼리가 요청됨.`.deleteAllInBatch()` : 냅다 delete All 쿼리를 요청해버림.그럼 .deleteAll()은 왜 사용??.deleteAll() 은 모든 연관관계를 먼저 찾은 다음 삭제를 해주기 때문에, 외래키 같은 제약 조건을 고려해서 삭제해준다.어지간하면 .deleteAllInBatch()를 사용하자. [후기] 이로써 총 4주간의 워밍업 클럽 일정이 마무리 되었습니다. 지금까지는 인프런 강의를 구매만 해놓고 수강을 미뤄놓기만 해왔는데, 우연한 기회에 알게된 워밍업 클럽 덕분에 밀도있게 학습을 할 수 있는 경험을 겪어본 것이 가장 좋았던 점이었습니다.실무에서도 테스트를 적극적으로 작성하지 않는 상황이 정말 많아, 테스트를 어떻게 하면 더 잘 작성할 수 있는지에 대한 의문점이 항상 있었습니다. 그런 저에게 실용적인 테스트를 작성할 수 있게 실무에 가까운 예제를 알려준 이번 강의가 정말 많은 도움이 되었습니다.좋은 강의를 준비해주신 박우빈 강사님께 감사드리고, 워밍업 클럽이라는 시스템을 마련하고 운영해주신 인프런 운영진분께도 감사드립니다.
백엔드
・
백엔드
・
워밍업클럽
・
테스트코드
・
클린코드

2024. 10. 23.
0
인프런 워밍업 스터디 클럽 2기 백엔드(클린코드&테스트코드) Day 18 미션
인프런 워밍업 클럽 2기, 백엔드(클린코드&테스트코드) 과정에 참여하고 있습니다.이번 글은 Day 18 미션 제출을 위해 작성하였습니다.[미션 내용]@Mock, @MockBean, @Spy, @SpyBean, @InjectMocks 의 차이를 한번 정리해 봅시다.미션에서 제공된 예제를 살펴보고, 각 항목을 @BeforeEach, given절, when절에 배치해보기 강의 링크: Readable Code: 읽기 좋은 코드를 작성하는 사고법 1. Test Double Annotations 특징@Mock실제 객체 대신 가짜 객체를 생성한다.기본적으로 모든 메서드가 아무런 동작도 하지 않음.when() 을 통해 특정 메서드의 동작과 결과값을 임의로 지정할 수 있음.Spring context 의존 X.@Mock private UserService userService; // 사용 예시 when(userService.findById(1L)).thenReturn(new User());@MockBean기본적으로 @Mock 과 유사하지만, Spring context에 의존한다.Spring context에 Mock 객체를 Bean으로 등록함.같은 타입의 기존 Bean을 찾아서 Mock으로 교체하는 방식통합 테스트에서 특정 Bean만 Mock으로 대체하고 싶을 때 유용함.@MockBean private UserService userService; @Spy실제 객체를 생성하고 일부 메서드만 Mock으로 지정할 수 있음.명시적으로 지정하지 않은 메서드는 실제 동작을 수행한다.일부 메서드만 수정하고 싶을 때 유용함.@Spy private UserService userService; // 특정 메서드만 Mock 처리 doReturn(new User()).when(userService).findById(1L);@SpyBean기본적으로 @Spy 와 유사하지만, Spring context에 의존한다.Spring context의 실제 Bean을 Spy로 감싸서 사용함.실제 Bean 의 일부 동작만 수정하고 싶을 때 유용함. @SpyBean private UserService userService;@InjectMocksSpring context 의존 없이(통합 테스트 없이) 의존성 주입이 필요한 클래스(ex. Service class)를 테스트할 때 유용하다.@Mock 이나 @Spy로 생성된 객체를 자동으로 주입받도록 해준다.@Mock private UserRepository userRepository; @InjectMocks private UserService userService; // userRepository가 자동 주입됨 주요 차이점통합 테스트 필요 여부 차이필요: @MockBean, @SpyBean불필요: @Mock, @Spy, @InjectMocks동작 방식완전한 가짜 객체: @Mock, @MockBean실제 객체를 부분적으로 Mocking: @Spy, @SpyBean @BeforeEach, given절, when절에 배치해보기@BeforeEach void setUp() { (1-1., 2-1., 3-1) 사용자1 생성에 필요한 내용 준비 (1-2., 2-2., 3-2) 사용자1 생성 (1-3., 2-3., 3-5) 사용자1의 게시물 생성에 필요한 내용 준비 (1-4., 2-4., 3-6) 사용자1의 게시물 생성 } @DisplayName("사용자가 댓글을 작성할 수 있다.") @Test void writeComment() { // given 1-5. 댓글 생성에 필요한 내용 준비 // when 1-6. 댓글 생성 // then 검증 } @DisplayName("사용자가 댓글을 수정할 수 있다.") @Test void updateComment() { // given 2-5. 댓글 생성에 필요한 내용 준비 2-6. 댓글 생성 // when 2-7. 댓글 수정 // then 검증 } @DisplayName("자신이 작성한 댓글이 아니면 수정할 수 없다.") @Test void cannotUpdateCommentWhenUserIsNotWriter() { // given 3-3. 사용자2 생성에 필요한 내용 준비 3-4. 사용자2 생성 3-7. 사용자1의 댓글 생성에 필요한 내용 준비 3-8. 사용자1의 댓글 생성 // when 3-9. 사용자2가 사용자1의 댓글 수정 시도 // then 검증 }
백엔드
・
워밍업클럽
・
백엔드
・
클린코드
・
테스트코드
・
미션

2024. 10. 22.
0
인프런 워밍업 스터디 클럽 2기 백엔드(클린코드&테스트코드) Day 15 미션
인프런 워밍업 클럽 2기, 백엔드(클린코드&테스트코드) 과정에 참여하고 있습니다.이번 글은 Day 15 미션 제출을 위해 작성하였습니다.[미션 내용]Layered Architecture 에서 각 레이어별 어떤 특징이 있고, 어떻게 테스트를 하면 좋을지에 대해 나만의 언어로 표현해보자강의 링크: Readable Code: 읽기 좋은 코드를 작성하는 사고법 Layered ArchitectureLayered Architecture는 관심사와 책임의 분리를 위해 만들어진 계층 구조 아키텍쳐이다. 관심사를 기준으로 책임을 나누면, 각 계층의 응집도를 높이고 결합도를 낮춘, 유지보수에 용이한(확장이 쉬운) 코드를 작성할 수 있게 된다는 이점을 얻을 수 있다.Spring 진영에서 일반적으로 얘기하는 Layered Architecture는 보통 아래와 같이 3-tier(3 계층)로 구성되어 있는 아키텍쳐이다. Persistence Layer (영속성, 데이터베이스 레이어)Data Access의 역할을 한다. ~~Repository 비즈니스 가공 로직이 포함되어서는 안된다.Data에 대한 CRUD에만 집중한 레이어4-tier 구조의 Database Layer 가 3-tier 구조에서는 이 레이어에 포함된다.Business Layer (비즈니스 레이어)비즈니스 로직을 구현하는 역할Persistence Layer와의 상호작용(Data를 읽고 쓰는 행위)을 통해 비즈니스 로직을 전개시킨다.트랜잭션을 보장해야 한다.작업 단위에 대한 원자성 Presentation Layer (사용자 요청&응답 레이어)외부 세계의 요청을 가장 먼저 받는 계층 파라미터에 대한 최소한의 검증을 수행한다. 각 Layer 별 테스트 방법 공통적으로 테스트 코드를 작성할때는 BDD(Behavior-Driven Development) 스타일로 작성하는 것을 권장한다. 이는 테스트 코드의 가독성을 높여준다는 장점이 있다. BDD는 given/when/then 구조로 작성하는 것을 기본으로 한다.테스트 시나리오는 가급적 작은 코드 단위를 독립적으로 검증하는 것을 목표로 하는것이 좋다. 검증 속도가 빠르고, 외부에 의존을 하지 않기 때문에 안정적으로 테스트를 수행할 수 있게 한다. 그리고 해피 케이스와 예외 케이스를 도출 해낼수 있어야 한다. 이를 위해선 테스트를 작성할 때 항상 "암묵적이거나, 아직 드러나지 않은 요구사항이 있는가?" 라는 질문을 끊임없이 스스로에게 던지는 습관을 기르는 것이 도움이 된다.또, given 코드를 작성할 때는 가급적 경계값이 되는 데이터를 사용하여 긍정 케이스와 부정 케이스를 각각 검증하도록 시나리오를 작성하는 것이 좋다. 여기서 말하는 경계값이란, 성공과 실패 조건의 경계에 걸쳐 있는 값을 의미한다. 예를 들어, 주어진 값이 10 이상일 때 부터 성공하는 케이스에 대해 실패 테스트에는 9를, 성공 테스트에는 10을 사용하는 것을 말한다. Persistence LayerJPA Repository에 대한 단위 테스트를 수행하기 위해서 @DataJpaTest 애노테이션을 사용한다. @DataJpaTest 는 JPA와 관련된 설정만 로드하기 때문에 최소 비용으로 JPA 테스트를 수행할 수 있게 해주고, @Transactional 을 기본적으로 내장하고 있으므로 매 테스트 코드가 종료되면 자동으로 DB가 롤백되도록 해준다.다만, 의존을 주입받기 위해서는 @AutoWired를 사용해야 한다는 것을 주의해야 한다.@ActiveProfiles("test") @DataJpaTest class UserRepositoryTest { @Autowired private UserRepository userRepository; @DisplayName("User 이름으로 User 를 찾을 수 있다.") @Test public void findUserByName() { // given User givenUser = new User("alex", "alex@example.com"); userRepository.save(givenUser); // when User foundUser = userRepository.findByName(givenUser.getName()); // then assertThat(foundUser.getName()).isEqualTo(givenUser.getName()); } }위 예시는 아주 단순한 JPA Repository의 쿼리 메서드를 테스트하는 예제이다. 예시를 위해 최대한 간단하게 작성한 것이므로, 위와 같은 Hibernate 라는 거대한, 검증된 라이브러리가 제공해주는 쿼리 메서드에 대한 테스트는 굳이 매번 작성할 필요는 없다고 생각한다. 그러니 스스로가 직접 작성한 쿼리 메서드에 대한 테스트를 꼼꼼하게 하는것에 집중하는 것을 권장한다.참고로, @ActiveProfiles("test") 는 테스트 코드 실행 영역에서는 실제 DB가 아닌 테스트용 DB로 연결되도록 하기 위해 추가해준 애노테이션임을 알아두자. Business LayerService 코드에 대한 테스트를 수행하기 위해서는 JPA Repository에 대한 의존성이 추가로 필요하게 된다. 이를 위해 @SpringBootTest 애노테이션을 사용해서 자동으로 의존성 주입을 받고 코드를 간단하게 작성할 수 있지만, @SpringBootTest는 간편한 만큼 많은 비용이 들어간다는 점을 항상 염두에 두어야 한다.다음은 @SpringBootTest 를 사용한 Business Layer 테스트 코드 예제이다.@ActiveProfiles("test") @SpringBootTest class UserServiceTest { @Autowired private UserService userService; @Autowired private UserRepository userRepository; @AfterEach void tearDown() { userRepository.deleteAllInBatch(); } @DisplayName("유효 상태인 유저를 모두 조회할 수 있다.") @Test void findAllUserByValid() { // given User user1 = new User("testUser", "test@example.com", true); // 이름, 이메일, 유효상태 User user2 = new User("testUser", "test@example.com", false); userRepository.saveAll(List.of(user1, user2)); // when List foundUsers = userService.findAllUserByValid(); // then assertThat(foundUsers).hasSize(1) .extracting("name", "email", "isValid") .contains( tuple("testUser", "test@example.com", true) ); } }위 예제에서 @Transactional 을 사용하지 않고 @AfterEach 를 통해 매 테스트마다 Repository의 데이터 클렌징을 수행하도록 코드를 작성하였는데, 이는 휴먼 에러를 줄여주기 위한 코딩 습관과 연관이 있다.만약, 실수로 실제 Service 코드에서 데이터를 저장, 수정, 삭제하는 메서드(또는 클래스 레벨)에 @Transactional 을 달아주지 않았다면 트랜잭션 관리의 부재로 인해 개발자가 의도하지 않은 에러가 발생할 수 있는 가능성이 생긴다. 만약 이 때 테스트 코드에 @Transactional 을 달아서 테스트를 수행했다면 테스트 시점에는 그 에러를 발견할 가능성이 희박해진다. 따라서, 개발자가 Service 레이어 코드에 @Transacional 애노테이션을 빼먹는 실수를 테스트 코드에서 잡아낼 수 있는 가능성을 열어주기 위해, 테스트 코드에서 @Transactional 대신 tearDown 클렌징 기법을 사용한 경우인 것이다. 그리고 @SpringBootTest 를 사용하지 않고 테스트를 작성하기 위해서는 Repository 코드를 @Mock 으로 작성하면 된다. @InjectMocks 애노테이션이 붙어있는 UserService 객체가 생성될 때, UserRepository 에는 @Mock 이 붙어있으므로 대안(가짜) 객체가 생성되어 주입되게 된다. 따라서 테스트 메서드 내부에서 해당 쿼리 메서드의 행동을 함께 정의해주는 방법으로 테스트를 작성할 수 있다.@ExtendWith(MockitoExtension.class) class UserServiceTest { @Mock private UserRepository userRepository; @InjectMocks private UserService userService; @DisplayName("유효 상태인 유저를 모두 조회할 수 있다.") @Test void findAllUserByValid() { // given User user1 = new User("testUser1", "test1@example.com", true); User user2 = new User("testUser2", "test2@example.com", false); List mockUsers = List.of(user1, user2); when(userRepository.findAll()).thenReturn(mockUsers); // when List foundUsers = userService.findAllUserByValid(); // then assertThat(foundUsers).hasSize(1) .extracting("name", "email", "isValid") .contains( tuple("testUser1", "test1@example.com", true) ); } } Presentation LayerController 코드에 대한 테스트는 @WebMvcTest 애노테이션과 MockMvc 객체를 활용해서 를 수행할 수 있다. 이를 통해 API 엔드포인트의 동작, HTTP 상태 코드, 헤더, 요청과 응답에 대한 검증을 할 수 있다.@WebMvcTest(UserController.class) public class UserControllerTest { @Autowired private MockMvc mockMvc; @MockBean private UserService userService; @Test public void testGetUser() throws Exception { User user = new User(1L, "testUser", "test@example.com"); when(userService.getUserById(1L)).thenReturn(user); mockMvc.perform(get("/api/users/1")) .andExpect(status().isOk()) .andExpect(jsonPath("$.username").value("testUser")); } }Presentation Layer는 Service 코드에 대한 의존성이 필요하게 된다. 마찬가지로, Mock 객체로 대체해서 테스트를 수행해서 Controller 로직만을 독립적으로 검증해야 한다. 이 때 Business Layer와의 차이점은, @Mock 이 아닌 @MockBean 을 사용해서 의존성을 주입받을 수 있다는 것인데, 이는 @WebMvcTest 는 Spring Boot 테스트 프레임워크의 전반적인 설정과 부트스트래핑 과정을 포함하고 있기 때문에 가능한 것이다.
백엔드
・
워밍업클럽
・
백엔드
・
미션
・
클린코드
・
테스트코드

2024. 10. 20.
0
인프런 워밍업 스터디 클럽 2기 백엔드(클린코드&테스트코드) 발자국 - 3주차
인프런 워밍업 클럽 2기, 백엔드(클린코드&테스트코드) 과정에 참여하고 있습니다.이번 3주차에는 테스트에 대한 이론적인 내용과 TDD, 실용적인 테스트 방법등을 학습했습니다.강의 링크: Readable Code: 읽기 좋은 코드를 작성하는 사고법 [학습 요약] 테스트의 필요성왜 작성해야 할까?사람이 수동으로 직접 테스트하는 데에는 한계가 있다.테스트 코드 작성을 통해 얻는 이점빠른 피드백자동화안정감테스트 코드를 작성하지 않는다면..변화가 생기는 매순간마다 발생할 수 있는 모든 Case를 고려해야 한다.변화가 생기는 매순간마다 모든 팀원이 동일한 고민을 해야한다.빠르게 변화하는 소프트웨어의 안정성을 보장할 수 없다.테스트 코드가 병목이 된다면..프로덕션 코드의 안정성을 제공하기 힘들어진다.테스트 코드 자체가 유지보수하기 어려운, 새로운 짐이 된다.잘못된 검증이 이루어질 가능성이 생긴다.올바른 테스트 코드는,자동화 테스트로 비교적 빠른 시간 안에 버그를 발견할 수 있고, 수동 테스트에 드는 비용을 크게 절약할 수 있다.소프트웨어의 빠른 변화를 지원한다.팀원들의 집단 지성을 팀 차원의 이익으로 승격시킨다.가까이 보면 느리지만, 멀리 보면 가장 빠르다.테스트는 귀찮다. 귀찮지만, 해야한다! 단위 테스트작은 코드 단위를 독립적으로 검증하는 테스트작은 코드: 클래스 or 메서드검증 속도가 빠르고, 안정적이다.외부에 의존을 하지 않기 때문에JUnit5단위 테스트를 위한 테스트 프레임워크XUnit - Kent BeckSUnit(SmallTalk), JUnit(Java), NUnit(.NET), DUnit(Delphi)AssertJ테스트 코드 작성을 원할하게 돕는 테스트 라이브러리풍부한 API, 메서드 체이닝 지원테스트 케이스 세분화하기항상 질문하기: 암묵적이거나, 아직 드러나지 않은 요구사항이 있는가?해피 케이스와 예외 테스트를 도출해낼 수 있어야 한다.경계값 테스트경계값?: 범위(이상, 이하, 초과, 미만), 구간, 날짜 등테스트하기 어려운 영역을 구분하고 분리하기테스트하기 어려운 영역을 갖고 있는 코드가 추가된다면, 전체 테스트가 망가질 가능성이 생긴다.예시)LocalDateTime.now() 를 사용하는 기능을 테스트할 때, 테스트를 실행하는 시간에 따라 성공할수도, 실패할수도 있게 됨.이 때, LocalDateTime.now() 를 사용하는 코드가 곧 테스트하기 어려운 영역이 된다.이 때, 테스트가 어려운 영역을 외부에서 주입받도록 변경하면 테스트 하기 수월해진다.외부로 분리할수록, 테스트 가능한 코드는 많아진다.테스트하기 어려운 영역 코드 예시관측할 때마다 다른 값에 의존하는 코드현재 날짜/시간, 랜덤 값, 전역 변수/함수, 사용자 입력 등외부 세계에 영향을 주는 코드표준 출력, 메시지 발송, 데이터베이스에 기록하기 등테스트하기 쉬운 영역 코드 예시순수 함수같은 입력에는 항상 같은 결과 반환외부 세상과 단절된 형태 (전역 변수를 변경하지 않는) TDD프로덕션 코드보다 테스트 코드를 먼저 작성하여 테스트가 구현 과정을 주도하도록 하는 방법론[Red - Green - Refactor] 3단계 구조로 순환하며 진행Red: 실패하는 테스트 작성의도적으로 실패하게 테스트를 작성하라는게 아니다.도메인적 지식을 기반으로, 테스트가 통과해야만 하는 조건으로 테스트 코드를 작성하라는 의미.ex) 테스트에서는 10 이라는 값이 나올것을 기대하지만, 구현이 미완성이라서 0이 리턴되는 경우에 기대값을 변경하지 않고 그대로 두는 것을 말한다.Green: 테스트 통과, 최소한의 코딩최대한 빠르게 테스트 통과만을 위한 코딩을 수행.TDD 방법론에서는 하드코딩도 허용.Refactor: 구현 코드 개선, 테스트 통과 유지구현부 코드를 변경한 다음에도 테스트가 통과 된다 -> 구현이 올바르게 되었다 라고 판단할 근거가 된다.피드백TDD가 제공하는 핵심 가치테스트 코드를 통해 내가 구현한 프로덕션 코드에 대해서 자주, 빠르게 피드백을 받을 수 있다.기능 구현 후 테스트 작성 vs. 테스트 작성 후 기능 구현선 기능 구현, 후 테스트 작성 케이스테스트 자체의 누락 가능성특정 테스트 케이스(해피 케이스)만을 검증할 가능성잘못된 구현을 다소 늦게 발견할 가능성선 테스트 작성, 후 기능 구현복잡도가 낮은, 테스트 가능한 코드로 구현할 수 있게 한다.복잡도가 낮다?: 유연하고, 유지보수하기 쉽다쉽게 발견하기 어려운 엣지(Edge) 케이스를 놓치지 않게 해준다.구현에 대한 빠른 피드백을 받을 수 있다.과감한 리팩토링이 가능해진다.TDD: 관점의 변화TDD 이전의 관점: 테스트는 구현부 검증을 위한 보조 수단TDD 이후의 관점: 테스트와 상호 작용하며 발전하는 구현부TDD는 클라이언트 관점에서 피드백을 주는 Test Driven 도구이다. 테스트는 "문서"다테스트는 "문서"다. 왜?프로덕션 기능을 설명하는 테스트 코드 문서다양한 테스트 케이스를 통해 프로덕션 코드를 이해하는 시각과 관점을 보완어느 한 사람이 과거에 경험했던 고민의 결과물을 팀 차원으로 승격시켜서, 모두의 자산으로 공유할 수 있다.우리는 항상 팀으로 일한다 DisplayName을 섬세하게명사의 나열보다 문장으로 작성하자테스트 행위에 대한 결과까지 기술하기before: 음료 1개 추가 테스트after: 음료를 1개 추가할 수 있다.도메인 용어를 한층 추상화된 내용을 담기메서드 자체의 관점보다, 도메인 정책관점으로 생각하자.테스트의 현상을 중점으로 기술하지 말 것before: 특정 시간 이전에 주문을 생성하면 실패한다.after: 영업 시작 시간 이전에는 주문을 생성할 수 없다.BDD, Behavior Driven DevelopmentTDD에서 파생된 개발 방법함수 단위의 테스트에 집중하기 보다, 시나리오에 기반한 테스트케이스(TC) 자체에 집중하여 테스트한다.개발자가 아닌 사람이 봐도 이해할 수 있을 정도의 추상화 수준(레벨)을 권장Given / When / ThenGiven: 시나리오 진행에 필요한 모든 준비 과정 (객체, 값, 상태 등)When: 시나리오 행동 진행Then: 시나리오 진행에 대한 결과 명시, 검증 Spring & JPA 기반 테스트 레이어드 아키텍처와 테스트Layered Architecture 는 관심사의 분리를 위해 수행한다!책임을 나누고, 유지보수하기 용이하게 만들자Spring & JPA 기반 애플리케이션에서 일반적으로 사용하는 레이어 구조User [Presentation Layer] [Business Layer] [Persistence Layer] DB통합 테스트여러 모듈이 협력하는 기능을 통합적으로 검증하는 테스트일반적으로 작은 범위의 단위 테스트만으로는 기능 전체의 신뢰성을 보장할 수 없다.풍부한 단위 테스트 & 큰 기능 단위를 검증하는 테스트Spring / JPA 훑어보기스프링은 Library? Framework?라이브러리는 내 코드가 주체가 된다.프레임워크는 말 그대로 이미 프레임이 짜여있고, 그 프레임에 짜맞추는 방식으로 코드를 작성하게 됨.Spring IoC(Inversion of Control)DI(Dependency Injection)AOP(Aspect Oriented Programming)ORM (Object-Relational Mapping)객체 지향 패러다임과 관계형 DB 패러다임의 불일치이전에는 개발자가 객체의 데이터를 한땀한땀 매핑하여 DB에 저장 및 조회 (CRUD)ORM을 사용함으로써 개발자는 단순 작업을 줄이고, 비즈니스 로직에 집중할 수 있다.JPA (Java Persistence API)Java 진영의 ORM 기술 표준인터페이스이고, 여러 구현체가 있지만 보통 Hibernate를 주로 사용한다.반복적인 CRUD SQL을 생성 및 실행해주고, 여러 부가 기능들을 제공한다.편리하지만 쿼리를 직접 작성하지 않기 때문에, 어떤 식으로 쿼리가 만들어지고 실행되는지 명확하게 이해하고 있어야 한다.Spring 진영에서는 JPA를 한번 더 추상화한 Spring Data JPA 제공QueryDSL과 조합하여 많이 사용한다.타입 체크, 동적 쿼리JPA 에서 주로 사용되는 어노테이션들@Entity, @Id, @Column@ManyToOne, @OneToMany, @OneToOne, @ManyToMany@ManyToMany: 일대다 - 다대일 관계로 풀어서 사용한다. [후기]본격적으로 테스트 코드에 대한 학습을 시작한 주간이었다. 개발자로 일을 한지 꽤 오래되었지만 테스트를 작성해본 적이 많지 않아서 테스트를 작성하는것에 언제나 애를 먹었었는데, 기초적이고 이론적인 내용부터 자세히 공부하고 예제로 따라해보니 지금까지 잘 모르고 지금까지 완벽하게 이해하지 못하고 넘어갔었던 내용들을 어느정도 해소할 수 있었던 것 같아 좋았다.
백엔드
・
워밍업클럽
・
백엔드
・
테스트
・
클린코드

2024. 10. 13.
0
인프런 워밍업 스터디 클럽 2기 백엔드(클린코드&테스트코드) 발자국 - 2주차
인프런 워밍업 클럽 2기, 백엔드(클린코드&테스트코드) 과정에 참여하고 있습니다.이번 2주차에는 코드를 다듬을 때 기억하면 좋은 스킬과 팁을 배우고, 강사님께서 제공해주신 예제 프로젝트를 직접 리팩토링 해보는 시간을 가졌습니다.강의 링크: Readable Code: 읽기 좋은 코드를 작성하는 사고법 [학습내용 핵심 요약]자세한 설명이나 예제 및 코드는 강의 저작권 보호를 위해 기재하지 않았습니다. 추상주석의 양면성: 코드 품질 저하 vs. 필요한 정보 전달좋은 주석: 의사 결정 히스토리, 코드로 표현 불가능한 정보 전달주석 관리: 자주 변하는 정보 지양, 코드 변경 시 주석 업데이트변수/메서드 나열: 사용 순서, 중요도, 그룹화로 의도 전달패키지 구조: 문맥 정보 제공, 적절한 분할로 관리 용이성 확보패키지 변경: 같은 프로젝트에서 작업하는 팀과 합의 후 변경하는 것이 중요하다.(초기 설계의 중요성)객체 지향 설계로 인해, 버그 수정 용이성이 증가할 수 있다.재귀 알고리즘을 사용할때는 Stack Overflow 에러를 주의해야 한다. 이를 피하기 위해, Stack 자료 구조를 사용할 수 있다.IDE 활용: 코드 포맷팅, 품질 체크 도구(Sonarlint), .editorconfig 사용코드 스타일: 팀과 합의된 기준을 지키는 것이 중요하고, 지속적인 개선을 해나가는 것이 중요하다. 공개/비공개 메서드 배치: 객체의 기능 드러내기메서드 그룹화: 중요도, 종류별 배치로 일관성 유지스택 메모리 이해: 재귀 함수 사용 시 주의점코드 품질 관리를 위해서는 지속적인 리팩토링과 개선에 관심을 갖는 것이 중요하다. 기억하면 좋은 조언들능동적 읽기: 복잡한 코드 이해를 위한 리팩토링 접근법코드 이해 방법: 단락 구분, 추상화, 주석 활용 코드 읽기 목표: 도메인 지식 증진, 작성자 의도 파악오버 엔지니어링: 불필요한 복잡성 증가, 유지보수 어려움인터페이스 남용: 구현체가 하나일 때는 인터페이스를 남용하지 않도록 주의가 필요함이른 추상화 위험: 복잡도 증가, 의도 파악 어려움은탄환은 없다: 클린 코드도 만능 해결책이 아님실무 현실: 품질과 빠른 결과물 사이의 균형클린 코드 사고: 미래 수정 가능성 고려한 코드 작성적정 기술: 상황에 맞는 방법론 적용 중요성도구 사용 능력: 극한 사용과 적절한 제한에 대한 이해 필요적정 수준 파악을 위한 경험 축적을 위해, 클린 코드를 극단적으로 시도해보는 경험이 도움이 된다. 지속적 학습: 다양한 상황 경험을 통한 판단력 향상[후기]강의를 따라하기 전에, 혼자서 리팩토링 하며 고민해보는 시간을 가져본 다음 강사님의 강의를 따라가는 과정이 매우 효과적인 학습법으로 느껴져서 좋았던 시간이었습니다. 리팩토링 할 때 제가 놓친 부분들이 너무 많았던 것 같아 아쉽기도 했지만, 현재 내 실력을 보다 객관적인 관점으로 체감할 수 있었던 시간이라 더 좋았던 것 같습니다.이번 주차에서 배운 내용을 토대로 느낀점은 다음과 같습니다.내가 중요하게 생각했던 도메인의 추상 포인트와 다른 사람이 생각하는 포인트는 매번 다를 수 있다.추상화에 정답은 없고, 현재 시점에 적용할 수 있는 최선의 방법을 찾아내는 것이 그 시기에 할 수 있는 가장 최선의 방법이다.위의 두가지 내용은 계속 머리속에 새기고 있으면 좋을 것 같다는 생각이 들었습니다.이상으로, 2주차 학습 내용 요약과 후기를 정리한 발자국 포스팅을 마칩니다.
백엔드
・
워밍업클럽
・
백엔드
・
리팩터링
・
클린코드
・
테스트코드
・
발자국
・
회고

2024. 10. 06.
0
인프런 워밍업 스터디 클럽 2기 백엔드(클린코드&테스트코드) 발자국 - 1주차
인프런 워밍업 클럽 2기, 백엔드(클린코드&테스트코드) 과정에 참여하고 있습니다.이번 1주차에는 클린 코드를 추구하는 이유와 클린한 코드를 작성하는 방법, 객체 지향적 사고, 객체 지향으로 프로젝트를 리팩토링하는 디테일한 스킬을 예제와 함께 배웠습니다.강의 링크: Readable Code: 읽기 좋은 코드를 작성하는 사고법 [학습내용 핵심 요약]자세한 설명이나 예제 및 코드는 강의 저작권 보호를 위해 기재하지 않았습니다. 1. 추상클린 코드의 목적: 가독성 향상, 유지보수 용이성 증대, 시간과 자원 절약추상화: 중요 정보 추출, 불필요 정보 생략하는 과정컴퓨터에서 추상화: 복잡한 데이터와 로직 단순화, 이해도 향상적절한 추상화: 도메인 문맥 고려, 핵심 개념만 표현이름 짓기: 추상적 사고 기반, 도메인 문맥 내 이해 가능한 용어 사용메서드 추상화: 단일 책임 원칙, 명확한 의도 전달메서드 선언부: 적절한 메서드명, 파라미터, 반환 타입 선택추상화 레벨: 동일한 추상화 수준 유지, 일관성 있는 코드 작성매직 넘버/스트링 제거: 상수 추출로 의미 부여, 가독성 향상좋은 코드 습득: 업계 표준 용어 학습, 지속적인 개선 2. 논리, 사고의 흐름뇌 메모리 효율화: 최소 인지 노력으로 최대 정보 제공범주화와 인지적 경제성: 효율적 정보 처리 위한 핵심 전략Early return: 코드 가독성 향상, 불필요한 중첩 구조 제거사고의 depth 줄이기: 반복문, 조건문 구조 단순화, 변수 근접 선언부정어 사용 주의: 긍정 표현 선호, 메서드명에 부정 의미 포함 고려해피 케이스와 예외 처리: 예외 발생 가능성 최소화, 의도적/비의도적 예외 구분Null 처리: NullPointerException 방지, Optional 활용 고려Optional 사용 지침: 반환 타입에만 사용, 파라미터로 받지 않기, 빠른 해소Optional 해소 방법: 풍부한 API 활용 (orElseGet, orElseThrow, ifPresent 등)orElse, orElseGet, orElseThrow 차이 이해: 실행 시점과 용도 구분즉시 평가와 지연 평가 개념 숙지: Optional 메서드 사용 시 고려 3. 객체 지향 패러다임객체 지향: 추상화된 데이터와 코드의 결합, 객체 간 협력과 책임 중시관심사의 분리: 높은 응집도, 낮은 결합도 추구객체 설계: 비공개 필드/로직, 공개 메서드로 외부와 소통객체 생성 주의점: 단일 책임, 생성자에서 유효성 검증, setter 자제getter 사용 최소화: 객체에 메시지 전송 권장필드 수 최소화: 복잡도 감소, 불필요한 데이터 제거SOLID 원칙: 객체 지향 설계의 기본 원칙들SRP: 단일 책임 원칙, 변경 이유 하나로 제한OCP: 확장에는 열려있고, 수정에는 닫혀있어야 함LSP: 상속 관계에서 하위 클래스가 상위 클래스를 대체 가능해야 함ISP: 인터페이스 분리, 불필요한 의존성 제거DIP: 추상화에 의존, 의존성 역전을 통한 유연성 확보도메인 지식: 만드는 것이 아닌 발견하는 것의존성 주입(DI)과 제어의 역전(IoC) 개념 이해 4. 객체 지향 적용하기상속보다 조합 선호: 유연성 증가, 결합도 감소Value Object: 도메인 개념 추상화, 불변성/동등성/유효성 보장VO vs Entity: 식별자 유무와 동등성 판단 기준 차이일급 컬렉션: 컬렉션을 포장한 객체, 가공 로직 포함 가능Enum 활용: 상수 집합과 관련 로직 관리, 도메인 개념 명시적 표현다형성 활용: 반복적 if문 단순화, OCP 원칙 적용변하는 것과 변하지 않는 것 분리: 추상화와 구체화 구분도메인 개념 도출: 발견하는 과정, 현실 세계 흉내내기설계 접근: 근시적/거시적 관점에서 미래 예측유연한 코드 작성: 변경 가능성 고려, 재설계 용이성 확보완벽한 설계 없음: 현 시점 최선의 설계 추구일급 시민 개념: 변수 할당, 파라미터 전달, 함수 반환 가능컬렉션 반환 시 주의: 새로운 컬렉션으로 복사하여 반환Enum vs DB: 변경 빈도에 따른 선택객체 지향 설계: 현실 세계 완벽 반영이 아닌 모방 [고민했던 점]섹션 5. 객체지향 적용하기 중 CellPosition 을 분리하는 부분에 대한 강의를 들을 때, 강사님이 치고 계신 코드가 Minesweeper 파일인지 GameBoard 파일인지 헷갈릴때가 많았습니다. 강의를 멈추고 코드를 다시 읽어보다보니, 어떤 클래스에 들어있는지 한눈에 안들어오거나, 위치가 기억이 잘 안나거나 헷갈리는 메서드가 꽤 많았다고 느꼈습니다.Cell 이나, GameLevel, Handler 는 명확한 느낌을 받았습니다. Cell 안에 들어있는 메서드는 명확하게 Cell 에 대한 update 또는 get 동작을 수행하도록 설계되어 있다고 느꼈기 때문입니다.그런데 제 머릿속에는 "지뢰찾기" 라는 게임 그 자체에 "Board" 라는 개념이 항상 함께하는데, 그 "Board"를 Minesweeper 가 아닌 다른 파일로 쪼개놓았기 때문에 메서드의 위치가 헷갈린다는 느낌을 받았습니다. 제가 생각하기에는, GameBoard 라는 클래스가 일반적인 Board 기반 게임이 아닌, "지뢰찾기"에 특화된 로직을 포함하고 있기 때문에 헷갈린다는 느낌이 강하게 드는 것 같습니다. 예를 들어, initialize() 메서드 내부에는 Cell을 지뢰 칸으로 설정하는 로직과 숫자 칸으로 설정하는 로직이 녹아들어 있습니다. "지뢰찾기"만을 위한 로직이 녹아있는 것입니다. 그렇다면..?1. 추가적인 추상화?처음에는 GameBoard 내의 지뢰찾기 관련 로직을 별도의 클래스로 분리하는 것을 고려했습니다. 하지만 이는 "이른 최적화"의 함정에 빠질 수 있다는 생각이 들었습니다.2. 현실적인 접근현재로서는 이 GameBoard를 다른 보드 게임에 재사용할 계획이 없습니다. 따라서 과도한 추상화는 오히려 코드의 가독성을 해칠 수 있다고 판단했습니다. 결론지금으로서는 가장 좋은 방법은 GameBoard 라는 클래스 이름을 MinesweeperBoard 라는 이름으로 바꾸는게 좀 더 낫지않을까? 라는 결론을 내리는것이 가장 최선의 선택이 아닌가 싶습니다. 애초에 GameBoard 라는 이름이 추상적인 "Game"으로 시작하기 때문에, initialize 내부에 지뢰찾기와 관련된 로직이 녹아있는 것이 어색하게 느껴진것 아닌가? 라는 생각입니다. 그래서 클래스의 이름을 좀 더 구체적으로 변경하면, 비교적 덜 헷갈리지 않을까? 라는 생각입니다. 이상으로, 1주차 학습 내용 요약과 강의를 들으며 고민해봤던 내용을 정리한 발자국 포스팅을 마칩니다.
백엔드
・
워밍업클럽
・
백엔드
・
클린코드
・
테스트코드
・
발자국
・
회고




