infoqoch.github.io
게시글
블로그
전체 72025. 03. 28.
0
[워밍업 클럽 3기 BE code] 4주차
고민 되던 부분이 많이 풀렸습니다.좋은 코드와 좋은 테스트 코드가 무엇인지 어떻게 코드를 짜야 할지 고민이 많았습니다. 이에 대한 분명한 대답을 강의를 통해 찾았다고 생각합니다. 그런 차원에서 이번 워밍업 클럽과 박우빈 개발자님의 강의 무척 좋았습니다.Readable Code - 코드의 추상화 수준을 맞춰보자.추상화가 무엇인지 잘 이해할 수 있게 되었습니다. 그리고 그 추상화의 수준이 코드 전체에서 적절한 수준으로 유지되어야 함을 알게되었습니다.예를 들면, 아래와 같은 코드를 작성하곤 했는데 order라는 추상화된 메서드와 getter를 통한 구체적인 비교 방식은 일종의 추상화 수준이 맞지 않은 읽기 좋지 않은 코드가 됩니다. OrderResult orderResult = orderService.order(req); if(something.getStatus() = SUCCESS){ // ... } 이런 경우 아래와 같이 order의 수준에 맞는 형태의 추상화를 통해 처리하는 것이 좋아 보입니다.OrderResult orderResult = orderService.order(req); if(orderResult.isSuccess()){} // isSuccess로 적절하게 추상화하거나 notifyMessage(orderResult); // 더 추상화하여 주문 이후 비즈니스 로직을 order 수준으로 맞춘다. 지금까제 제가 어떻게 코드를 작성했나 생각해보면, 공통 로직이나 복잡한 로직을 적절한 형태로 추출하는 것에 목표를 가졌지 추상화에 대해서 크게 고민하진 않았습니다. 이런 부분을 배울 수 있어서 좋았습니다. Practical Testing - 테스트는 문서다저는 테스트를 다소 복잡하게 작성했습니다. 예를 들면 테스트를 위한 공통 객체를 만들거나 공통 기능을 수행하는 순수 테스트 용 유틸 메서드를 만들거나, 테스트 그 자체를 추출한 테스트 메서드를 만들어 테스트를 한 적도 있습니다. 이것이 문제임을 알곤 있었지만 왜 문제인지는 잘 인지하지 못했습니다. 테스트의 목적은 테스트 대상이 어떻게 동작하고 어떤 결과를 기대하는지 설명하기 위한 문서입니다. 이를 달성하기 위해서는 테스트 대상은 반드시 테스트 블록 안에서 정의되고 동작해야 합니다. @BeforeEach나 별도의 메서드로 생성하고 처리하는 것은 테스트에 부수적으로 필요한 것에 한정되어야 합니다. 돌이켜보면 공통 추출 가능한 중복된 것은 모두 @BeforeEach에 두거나 별도의 메서드를 둬서 처리해왔습니다. 그럼 테스트를 통과할지언정 테스트 코드가 문서로서 해당 로직의 동작 원리를 보여주진 못합니다. 이런 차원으로 고려한다면 테스트 코드를 더 잘 짤 수 있을 거란 생각을 합니다. 혹시 강의를 볼까 말까 고민이라면?좋은 코드와 좋은 테스트 코드가 무엇인지 고민하시는 분이라면 꼭 보는 것 추천합니다~!
2025. 03. 27.
0
[워밍업 클럽 3기 BE code] 미션 day 18
1. @Mock, @MockBean, @Spy, @SpyBean, @InjectMocks 의 차이를 한번 정리해 봅시다.- @Mock과 @Spy: 단위 테스트에서 특정 클래스를 대역으로 만듦니다. - @InjectMocks: 단위 테스트에서 @Mock 혹은 @Spy로 선언한 객체를 자동으로 주입합니다.- @MockBean, @SpyBean: 통합 테스트에서 해당 빈을 대신합니다. - @Mock과 @MockBean은 정의하지 않은 메서드에 아무 일도 하지 않지만, @Spy과 @SpyBean은 실제 객체를 감싸며 정의하지 않은 메서드는 원래 객체와 동일한 로직을 수행합니다. 2. 아래 3개의 테스트가 있습니다. 내용을 살펴보고, 각 항목을 @BeforeEach, given절, when절에 배치한다면 어떻게 배치하고 싶으신가요? (@BeforeEach에 올라간 내용은 공통 항목으로 합칠 수 있습니다. ex. 1-1과 2-1을 하나로 합쳐서 @BeforeEach에 배치)@BeforeEach void setUp() { 댓글 전체 삭제 게시물 전체 삭제 사용자 전체 삭제 } @DisplayName("사용자가 댓글을 작성할 수 있다.") @Test void writeComment() { // given 1-1 ~ 1-2. '사용자_생성'으로 엔티티 생성 및 리포지토리에 저장 1-3 ~ 1-4. '게시글_생성'으로 엔티티 생성 및 리포지토리에 저장 1-5. 댓글 생성에 필요한 내용 준비 // when 1-6. 댓글 생성 // then 검증 } @DisplayName("사용자가 댓글을 수정할 수 있다.") @Test void updateComment() { // given 2-1 ~ 2-2. '사용자_생성'으로 엔티티 생성 및 리포지토리에 저장 2-3 ~ 2-4. '게시글_생성'으로 엔티티 생성 및 리포지토리에 저장 2-5 ~ 2.6. 댓글 생성 및 리포지토리에 저장 2-7-1. 댓글 수정 준비 + '2-5 ~ 2-6'에서 생성한 PK 활용하기 // when 2-7-2. 댓글 수정 // then 검증 } @DisplayName("자신이 작성한 댓글이 아니면 수정할 수 없다.") @Test void cannotUpdateCommentWhenUserIsNotWriter() { // given 3-1 ~ 3-2. '사용자_생성'으로 사용자 1의 엔티티 생성 및 리포지토리에 저장 3-3 ~ 3-4. '사용자_생성'으로 사용자 2의 엔티티 생성 및 리포지토리에 저장 3-5 ~ 3-6. '게시글_생성'으로 사용자 1의 게시글의 엔티티 생성 및 리포지토리에 저장 3-7 ~ 3.8. 사용자 1의 게시글에 대한 댓글 생성 및 리포지토리에 저장 // when 3-9. 사용자2가 사용자1의 댓글 수정 시도 // then 검증 } private 사용자 사용자_생성(유니크한_사용자명){} private 게시글 게시글_생성(사용자명, 게시글제목, 게시글내용){}
2025. 03. 25.
0
[워밍업 클럽 3기 BE code] 미션 day 16
Q."Layered Architecture 구조의 레이어별 테스트 작성법을 알아보았습니다.레이어별로 1) 어떤 특징이 있고, 2) 어떻게 테스트를 하면 좋을지, 자기만의 언어로 다시 한번 정리해 볼까요?" A.Layered Architecture- 레이어 별 관심사를 분리하여 코드의 결합도를 낮추고 유지보수성을 높히는 방식. Persistence layer- 특징 - DB와 통신하는 역할을 수행한다. - 비즈니스 로직에 대한 코드가 아닌 단순 데이터의 입출입을 담당한다.- 테스트 - JpaRepository 중 비즈니스 로직에서 사용하는 메서드는 테스트를 수행하여 향후 변경에 대응한다. - H2 등 테스트를 위한 DB를 활용하여 @SpringDataJpa를 사용한 통합 테스트를 수행한다. Business layer- 특징 - 비즈니스 로직을 수행한다. - 비즈니스 로직에 따른 세세한 파라미터 검증을 수행한다. - 조회용 로직은 CQRS로 코드를 작성하고 @Transactional(readonly=true)로 처리하자. - 필요에 따라 락을 사용해야할 수도 있다.- 테스트 - @DataJpaTest은 @Transactional이 포함됨을 고려하여 테스트를 수행해야 한다. 기본적으로는 @SpringBootTest을 사용하여 테스트 한다. Presentation layer- 특징 - 클라이언트가 소통하는 레이어. - 최소한의 파라미터 검증을 수행한다. - 테스트 - @WebMvcTest을 활용하여 클라이언트의 요청에 대한 대역을 사용할 수 있다. - 대역 없이 통합 테스트를 수행하기에는 범위가 넓어, 서비스 레이어에 대하여 대역을 고려하여 처리할 수 있다. 이 경우 @MockBean 등을 활용한다.
2025. 03. 23.
0
[워밍업 클럽 3기 BE code] 3주차
테스트는 문서!테스트 코드는 문서로서 사용되어야 한다. 그러므로,테스트 코드를 보고 테스트의 내용과 목표를 이해할 수 있어야 한다.테스트 코드가 외부의 클래스나 파일 등을 참조해서는 안된다.테스트 코드의 일부 내용을 @BeforeEach나 @AfterEach로 코드를 분리하거나, 객체 생성을 위한 메서드를 분리할 수 있다. 하지만 부수적인 부분에 한정해야 한다. 지금까지 테스트 코드의 중복을 제거하고 추상화 하기 위해 노력했다. 이런 방식보다는 하나의 문서로서 테스트 코드 만으로 충분하게 이해할 수 있는 코드를 만들어봐야겠다! 테스트는 하나하나JpaRepository이나 로직이 작은 객체에 대해서도 테스트 코드를 다 작성하자.향후 해당 로직이 어떻게 변경될지 모른다. 일단 작성하고 미래의 변경에 대응하자.어디서 읽은 글이 있었는데, 프레임워크를 테스트해서는 안된다는 말을 들었다. 그러므로 JpaRepository.findBySomething과 같이 스프링이 제공하는 메서드 네이밍 기반의 코드는 지금까지 테스트하지 않았다. 하지만 언제 @Query로 변경될지 모르는 것도 사실이며, 엔티티의 필드가 변경되면 이에 대응하는 코드가 필요할 수도 있어 보인다. 다음부터는 간단하게라도 사용하는 모든 메서드에 대한 테스트를 작성해보자. classist vs mockist테스트 코드를 가능하면 운영과 유사하도록 테스트를 작성하는 classist와 테스트에 필요한 객체를 가능한 대역으로 만들어 테스트 자원을 최소화하고 단위 테스트를 수행하는 것을 선호하는 mockist가 있다고 한다.https://cl8d.tistory.com/43 글의 ✔ 각각의 장단점을 체크해보기 을 보면 실제로 어떻게 다르게 접근하는지 이해하기 쉽게 작성되어 있다.개인적으로는 스프링을 사용하면 전자가 맞다는 생각을 한다. 왜냐하면 H2 등 스프링 차원에서 훌륭하고 가벼운 대역을 제공하기 때문이다. 굳이 given(someRepository.getSomething()).willReturn(new Something())의 형태로 작성하기보다 그냥 H2에서 given에서 실제로 저장하고, then에서 getSomething()으로 가져오면 된다. 이로 인한 코드 분량도 매우 줄어들고 직관적으로 된다고 생각한다.다만 외부 API 등 대역이 불가피하게 필요한 경우 mockist처럼 작성해야 한다고 생각한다. 그렇지 않으면 테스트를 위한 별도의 대역 클래스를 작성해야 하며, 이는 앞서 작성한 테스트는 문서! 의 원칙에 어긋난다. 기타 배우거나 느낀 점assertThat().extracting()형태로 테스트 코드를 작성하는 것을 처음 알게 되었다. 실무에서 사용해보니 무척 편해서 자주 애용할 것 같다.@DataJpaTest은 @Transactional이 포함되어있고 이로 인하여 비즈니스 로직에서의 누락된 @Transactional이 있는 것처럼 될 수 있음을 알게 되었다. 기본적으로는 @SpringBootTest로 하자.레이어 간 파라미터에 대한 명칭을 어떻게 할지 고민이 많았는데 강의에서 좋은 예제가 있었다.controller : ProductInsertRequestservice: ProductInsertServiceRequestrepository: Product다음부터 ServiceRequest란 형태로 나도 써야겠다.
2025. 03. 16.
0
[워밍업 클럽 3기 BE code] 2주차
DAY 7 미션과 리팩토링https://github.com/infoqoch/readable-code/tree/main/src/main/java/cleancode/studycafe/tobe7일차 미션은 리팩토링하기 였습니다.리팩토링 과정에서 제가 했던 습관을 개선할 수 있어서 좋았습니다! 이미 이것만으로 제가 이번 강의에서 목표한 것을 달성한 것 같아 만족스럽습니다! 구현체가 하나밖에 없는 인터페이스를 분리하는 것이 낭비라고 생각했습니다. 하지만 도메인 기능과 이를 구현하는 클래스를 분리하는 것이 더 가치가 있음을 알게 되었습니다. 좀 더 적극적으로 인터페이스를 사용하려 합니다. 상태 변경은 객체 내부의 메서드로 처리하였으나, 검증은 getter를 사용해 밖에서 처리해왔습니다. 한 객체에 너무 많은 기능이 있으면 읽기 불편해서 나름의 역할 분리라 생각했는데 잘못 생각했음을 알게 되었습니다. getter로 제공하기보다 의미 있는 표현을 제공하는 것이 더 적합하다는 것을 알게 되었습니다. 일급컬렉션은 사용해본 적이 거의 없고 보통 List나 Map, Set을 밖에 노출해서 사용해왔습니다. 일급컬렉션의 자체적인 메서드를 사용하는 것이 사용에 더 좋다는 것을 알게 되었습니다. 자주 사용할 것 같습니다.
2025. 03. 09.
0
[워밍업 클럽 3기 BE code] 1주차
워밍업 클럽 3기 BE를 시작한 이유최근 '내가 코드를 잘 작성하고 있는가'란 의문이 들었습니다. 최범균 개발자님의 도메인주도설계시작하기 책을 기반으로 정리하면서 어느 정도는 정리할 수 있었습니다. 추가적인 자료나 실질적인 예제가 있을까 찾다가 readable code와 practical test 강의에 흥미가 생겼고, 우연찮게 워밍업 클럽이 있어서 신청하였습니다. 미션2. 추상과 구체추상: 드립 커피를 만든다. 구체: - 물을 끓인다.- 원두를 간다.- 컵 위에 드리퍼를 올리고, 드리퍼 위에 필터를 둔다.- 필터 위에 원두 가루를 담고 평탄화 한다.- 다 끓은 물을 드립 포트에 담고, 원두 가루가 파이지 않도록 조심스럽게 드립 포트의 물을 드리퍼에 가득 채운다.- 물이 다 빠지면 다시 물을 채운다. 총 세 번 물을 채운다. 미션3-1 리팩토링@Slf4j public class Order { private List items; private Customer customer; public boolean validateOrder() { if (isEmptyItem()) { log.info("주문 항목이 없습니다."); return false; } if (isInvalidTotalPrice()) { log.info("올바르지 않은 총 가격입니다."); return false; } if (isCustomerInfoMissing()) { log.info("사용자 정보가 없습니다."); return false; } return true; } private boolean isCustomerInfoMissing() { return customer == null; } private boolean isInvalidTotalPrice() { return getTotalPrice() > 0; } private boolean isEmptyItem() { return items == null || items.isEmpty(); } private int getTotalPrice() { return items.stream() .map(Item::getPrice) .reduce(0, Integer::sum); } } class Item { private int price; public int getPrice() { return price; } } class Customer {} 미션 3-2 SOLIDSRP(Single Responsibility Principle, 단일 책임 원칙): 하나의 클래스는 하나의 책임만을 가져야 한다. 해당 책임 이외의 이유로 인하여 변경되어서는 안된다.OCP(Open Closed Priciple, 개방 폐쇄 원칙): 확장은 열려있고, 수정에는 닫혀있다. 기능 변경과 확장에 대해서는 유연하도록 구현해야 한다. LSP(Listov Substitution Priciple, 리스코프 치환 원칙): 자식 타입은 부모 타입으로 교체 가능하다. 자식은 상속한 부모의 기능이 의도하는 방향대로 동작해야 한다.ISP(Interface Segregation Principle, 인터페이스 분리 원칙): 클라이언트가 자신이 이용하지 않는 인터페이스에 의존하지 않도록 적정한 수준의 메서드를 가진다.DIP(Dependency Inversion Principle, 의존 역전 원칙): 인터페이스에 의존하며 유연하게 변경할 수 있다. 이번 주 소감생각보다 바쁘고 강의 양도 많아 여유 있게 진행하지는 못하고 있다. 그래도 한 달 간 열심히 해서 잘 마무리해야지^^
2025. 02. 28.
0
hello!
world!