블로그
전체 9#카테고리
- 백엔드
2025. 06. 24.
0
[워밍업 클럽 4기 - 백엔드] 4주차 발자국
개인일정이 있어 늦었지만 작성해봅니다! 과제 피드백 반영일반과제https://inf.run/yndvbDay 18 미션 중 DRY가 무조건 안 좋은 것은 아니며 댓글에 대한 테스트이기 때문에 맥락이해를 떨어뜨릴 수 있는게시글, 사용자 관련 로직은 BeforeEach로 분리해도 이해에 지장을 주지않는다는 것을 이해하고 적용. 지뢰찾기https://github.com/jsween5723/readable-code/pull/1일반 생성자를 꼭 private으로 반영 해야하는 것은 아니다. Board(int rowCount, int columnCount, int mineCount) { this.mineCount = mineCount; this.cells = new Cell[rowCount][columnCount]; this.rowCount = rowCount; this.columnCount = columnCount; initNormalCell(); initMineCell(); }테스트 코드 작성시 맥락을 이해하기 어렵다면 단락을 나눈다 @Test @DisplayName("깃발이 달렸거나 이미 열린 셀은 열리지 않는다.") void openCellWithCoordinatesn() { //given BoardConfig config = MineSweeperGameLevel.BEGINNER.boardConfig; Board board = spy(Board.withConfig(config)); Coordinate flaggedCoordinate = new Coordinate(1, 1); Cell flagCell = spy(Cell.normalCell()); flagCell.toggleFlag(); when(board.get(flaggedCoordinate)).thenReturn(flagCell); Coordinate openedCoordinate = new Coordinate(1, 2); Cell openedCell = spy(Cell.normalCell()); openedCell.open(); when(board.get(openedCoordinate)).thenReturn(openedCell); //when board.open(flaggedCoordinate); board.open(openedCoordinate); //then verify(flagCell, never()).open(); verify(openedCell, times(1)).open(); }너무 많지 않다면 Nested는 오히려 테스트 코드의 가독성을 떨어뜨린다.if 블록과 비슷하다. 추가적인 리팩토링public 메소드를 상위로 올리고 주요도 순으로 재배치함. public void open(Coordinate coordinate) {} public void toggleFlag(Coordinate coordinate) {} public boolean isCleared() {} public boolean isMineOpened() {}Config을 필드로 들고있지 않고 값만 필드에 대입하도록 변경public class Board { public static final int[][] DELTAS = new int[][]{{-1, -1}, {-1, 0}, {-1, +1}, {0, -1}, {0, +1}, {1, -1}, {1, 0}, {1, +1}}; private final Cell[][] cells; private final int rowCount; private final int columnCount; private final int mineCount; private final BoardStringGenerator stringGenerator = new BoardStringGenerator(); } String 생성관련 로직 BoardStringGenerater에 역할 분리 및 추상화 레벨 균일화 class BoardStringGenerator { public String generate() { StringBuilder sb = new StringBuilder(); appendColumnDefinition(sb); for (int row = 0; row BoardStringGenerator 테스트 작성 @Test @DisplayName("toString 테스트") void generateTest() { //given int rowCount = 4; int columnCount = 4; int mineCount = 3; Board board = spy(new Board(rowCount, columnCount, mineCount)); for (int row = 0; row generateFlagCell(); case 1 -> generateOpenedCell(row); case 2 -> Cell.normalCell(); case 3 -> generateOpenedMineCell(); default -> throw new IllegalStateException("Unexpected value: " + col); }); } } //when String boardString = board.new BoardStringGenerator().generate(); //then CLOSED("□"), FLAGGED("⚑"), NORMAL_OPENED("■"), MINE_OPENED("☼"); assertThatCharSequence(boardString).isEqualToIgnoringWhitespace(""" A B C D 1 ⚑ ☼ □ ☼ 2 ⚑ 1 □ ☼ 3 ⚑ ■ □ ☼ 4 ⚑ ■ □ ☼ """); } //private 생략복잡한 초기화 로직 추상화레벨 균일화class A { @Test @DisplayName("지뢰가 아니고 주변에 지뢰가 없다면, 깃발을 제외하고 함께 연다.") void openNormalCell() { //given Coordinate targetCoordinate = new Coordinate(1, 1); Cell[] cells = createCellsForAround(); Coordinate[] coordinates = createCoordinatesForAround(targetCoordinate); for (int i = 0; i new Coordinate(targetCoordinate.row() + delta[0], targetCoordinate.column() + delta[1])) .toArray(Coordinate[]::new); } private Cell[] createCellsForAround() { return IntStream.range(0, 8).mapToObj((i) -> spy(Cell.normalCell())) .toArray(Cell[]::new); } }강의에서 언급된 ParameterizedTest 적용 static Stream serveIncorrectCoordinates() { int rowCount = 4; int columnCount = 4; return Stream.of( Arguments.of(rowCount + 1, columnCount), Arguments.of(rowCount, columnCount + 1), Arguments.of(-1, columnCount), Arguments.of(rowCount, -1) ); } @DisplayName("좌표가 범위밖이면 예외를 던진다.") @ParameterizedTest() @MethodSource("serveIncorrectCoordinates") void configCoordinateBalidate2(int row, int column) { //given Coordinate targetCoordinate = new Coordinate(row, column); //when //then assertThatThrownBy(() -> board.validateCoordinates(targetCoordinate)) .isInstanceOf(IllegalArgumentException.class); } 가독성을 해치는 경우 ParameterizedTest를 활용해 해결했다. 이외 일반과제 피드백도 지뢰찾기에 반영 private Board board; private int rowCount; private int columnCount; private int mineCount; @BeforeEach void setUp() { rowCount = 4; columnCount = 4; mineCount = 5; board = new Board(4, 4, 5); }
2025. 06. 19.
0
[인프런 워밍업클럽 백엔드 4기] Day18미션
@Mock, @MockBean, @Spy, @SpyBean, @InjectMocks 의 차이를 한번 정리해 봅시다.Mock해당 클래스 혹은 인터페이스의 목 인스턴스를 생성한다. stub을 통해 사용되는 케이스에 대해 반환값을 지정해주어야한다.MockBean해당 인터페이스 혹은 클래스의 목 인스턴스를 생성하고 스프링 빈 중 해당 클래스, 인터페이스의 빈이 필요할 경우 주입한다.Spy해당 클래스의 구현체를 기반으로 스파이 인스턴스를 생성한다.기본적으로 위임을 통해 기존로직들을 수행하고 추가적으로 stub이 가능하다.SpyBean해당 인터페이스 혹은 클래스의 스파이 인스턴스를 생성하고 스프링 빈 중 해당 클래스, 인터페이스의 빈이 필요할 경우 주입한다.InjectMocks해당 클래스의 생성자에 필요한 멤버들을 mock으로 주입한다. 2.@BeforeEach void setUp(){ 0-1. 사용자1 생성에 필요한 내용 준비 0-2. 사용자1 생성 0-3. 사용자2 생성에 필요한 내용 준비 0-4. 사용자2 생성 0-5. 사용자1의 게시물 생성에 필요한 내용 준비 0-6. 사용자1의 게시물 생성 } @DisplayName("사용자가 댓글을 작성할 수 있다.") @Test void writeComment() { // given 1 - 5. 댓글 생성에 필요한 내용 준비 // when 1 - 6. 댓글 생성 // then 검증 } @DisplayName("사용자가 댓글을 수정할 수 있다.") @Test void updateComment() { // given 2 - 1. 사용자 생성에 필요한 내용 준비 2 - 2. 사용자 생성 2 - 3. 게시물 생성에 필요한 내용 준비 2 - 4. 게시물 생성 2 - 5. 댓글 생성에 필요한 내용 준비 2 - 6. 댓글 생성 // when 2 - 7. 댓글 수정 // then 검증 } @DisplayName("자신이 작성한 댓글이 아니면 수정할 수 없다.") @Test void cannotUpdateCommentWhenUserIsNotWriter() { // given 3 - 7. 사용자1의 댓글 생성에 필요한 내용 준비 3 - 8. 사용자1의 댓글 생성 // when 3 - 9. 사용자2가 사용자1의 댓글 수정 시도 // then 검증 } 각 테스트의 공통요소들을 BeforeEach에 배정해도 좋지만 DRY한 코드를 작성함으로써각 테스트를 볼 때 한눈에 플로우를 파악할 수 없게 된다. 따라서 컴포넌트등의 초기화를 할 때 사용하고,엔티티,VO 클래스의 경우 Fixture를 통해 생성하고 필요시 메소드로 동작을 명시한다.----------------------------우빈님의 줌 밋업 이후 변경한 사항으로, 맥락에 필요 없는 내용은 BeforeEach로 분리합니다.
2025. 06. 18.
0
[워밍업 클럽 4기 백엔드] 수료증
2025. 06. 17.
0
[워밍업 클럽 4기 백엔드] Day 16 미션
프레젠테이션 레이어특징외부로 값을 변환해 반환한다. 클라이언트의 변화에 민감하다. 예외를 처리해 유효한 값으로 반환하는 역할을 한다.테스트 방법MockMvc 등 외부로 요청을 할 수 있는 라이브러리를 사용한다!given when then을 사용할 수 있는 RestAssured도 개인적으로 선호한다.반환된 값은 문자열인데, json path 쿼리를 통해 검증한다.SpringbootTest를 활용한다.비즈니스 레이어특징도메인 로직이 할당되는 레이어다. 각 엔티티들의 상호작용으로 값을 반환한다.테스트 방법SpringbootTest를 사용한다테스트할 클래스를 주입받는다.레포지터리를 주입받아 given절에서 데이터를 insert한다.when절에서 해당 클래스의 테스트하고싶은 메소드를 호출한다.then절에서 결과값을 Assertj등을 활용해 검증한다. 퍼시스턴스 레이어특징데이터를 저장소에 저장하고 쿼리하는 역할을 담당한다. RDB기반으로 동작하는 JPA뿐만 아니라 다른 저장소와도 연계될 수 있다. 테스트 방법DataJpaTest는 JPA 관련 빈만 초기화하므로 SpringbootTest를 사용한다.각 쿼리 메소드들이 정상적으로 원하는 결과를 반환하는지 확인하는 목적으로 작성한다.given절에서 검증에 필요한 데이터를 insert하고 when절에서 해당 쿼리를 발생시키는 메소드를 호출한다.반환값을 then절에서 테스트 프레임워크를 통해 검증한다
2025. 06. 15.
1
[워밍업 클럽 4기 - 백엔드] 3주차 발자국
강의 수강 소감Practical Testing: 실용적인 테스트 가이드각 레이어별로 통합테스트방법을 알 수 있다.프레젠테이션 레이어은 MockMvc로 응답, 결과값을 검증한다.비즈니스 레이어의 경우 given 절에 데이터 삽입, when 절에 검증할 비즈니스 레이어의 함수를 사용하고 then 절에서 결과값을 검증한다.영속 레이어의 경우 DataJpaTest 혹은 프레젠테이션레이어처럼 SpringbootTest를 사용한다.다른 영속 인프라와의 통합테스트가 있을 경우를 대비해 SpringbootTest를 사용하는 것을 권장빈 구성이 다르므로 SpringbootTest와 별도로 한번더 띄워지기 때문에 주의과제 회고과정지뢰찾기의 각 클래스의 역할을 재정의했다.TDD를 적용해보기 위해 강의에서 정의해준 역할이 아니라 의문이 들었던 내용에 대해 재분배했다.지뢰가 주변 지뢰를 알고 있는 것이 맞는가 -> Board에 해당 역할 할당.8칸 고정 수에 대한 연산이므로 O(1)이기 때문에 이를 아끼기 위해 선배정하는 것은 과한 최적화라고 생각했음.인수분해하듯이 지뢰 셀과 일반 셀의 같은 구현을 가진 역할을 생각했다. 열고난 후에는 출력시 값을 보여주는 것열지않은 셀에 깃발을 꽂고 회수할 수 있는 것추상화된 역할은 있으나 다르게 구현해야하는 것은 리스코프 치환원칙을 준수해 구현했다.클리어 조건열렸을 때 주변 셀도 열어야하는지 여부지뢰인지 여부toString 메소드가 문자열로 전환하는 역할을 가졌다고 생각해 display와 같은 별도 함수를 사용하지 않았다. 반성강의에서 언급된 리팩토링 테크닉을 최대한 활용하지 않았다.인터페이스에 의존하게 하는 것 합성과 구현관계가 아닌 추상 클래스를 활용해 부모클래스를 알아야하도록 한 것toString으로 인해 도메인 클래스가 단일책임을 지키지 못하는 것처럼 보였다.가로열 수를 알고 있는 Config이 가로열 수를 캡슐화하면 좋겠다는 생각으로 Config에 해당 문자열을 구하는 기능을 배정했으나 클래스의 단일 책임 원칙을 어기도록하는 과도한 캡슐화라고 생각된다.toString으로 getter를 캡슐화한 것도 마찬가지 3주차 회고요구사항을 먼저 정하고 테스트를 작성한 후 작업하다보니, 리팩토링이 아니라 새로 구현하는 느낌을 받았다. 도중에 역할을 몇번 재분배하여 테스트 코드도 많이 변경되었지만 내가 직접 산출한 요구사항을 지키는 코드를 작성하다보니 재미있었다.
2025. 06. 08.
1
[워밍업 클럽 4기 - 백엔드] 2주차 발자국
강의 완강 소감Readable Code: 읽기 좋은 코드를 작성하는 사고법이미 동작하는 코드를 리팩토링 해보면서 다양한 도메인 지식을 습득할 수 있다도메인 지식을 통해 객체의 응집성을 강화하고 확장에 열린 코드를 작성할 수 있다.추상화 단계를 균일하게하여 읽는 사람으로 하여금 자연스럽게 읽히게 한다.SOLID를 준수하여 코드를 작성하되, 오버엔지니어링을 하지 않을 수 있다.Practical Testing: 실용적인 테스트 가이드테스트 코드는 요구사항 문서로 볼 수 있다.명확한 테스트 목적을 기입하여 협업하는 팀원들이 테스트 코드를 통해 빠르게 이해할 수 있다.요구사항을 만족시키는 기능을 구현하면서 정말 필요한 요소들만 클래스에 할당할 수 있다.테스트 코드를 작성하기 위해 시간 등 외부 요인 의존성이 높은 것은 외부에서 주입할 수 있게 분리할 수 있다. 과제 회고과정카페 리팩토링을 진행하면서 남이 작성한 코드를 읽는 법을 익힐 수 있었다.도메인 지식을 얻기 위해 이 코드가 어떤 요구사항인지를 분석하고 해당 요구사항의 목적을 파악한다.만약 목적을 달성하기 위한 더 나은 방법이 있다면 기존 요구사항을 변경한다.락커와 좌석을 합성 관계로 재정의반성패스타입 enum에 할당해도 되는 요구사항까지 추상클래스 및 분화로 오버 엔지니어링 했다.공개 코드리뷰 신청을 하지 못했다. 2주차 회고2주차 내용은 직접 고민해보고 적용할 수 있는 내용이 많아 좋았습니다. 조금 게으른 탓에 기한이 얼마 남지 않았을 때 몰아서 한 것이 아쉽습니다. 3주차 부터는 열심히 임해보겠습니다.
2025. 06. 01.
1
인프런 워밍업 클럽 스터디 4기 - Clean Code & Test <1주차 발자국>
지인의 추천으로 박우빈님의 워밍업 클럽을 신청했습니다. 예제로 주어진 건 지뢰찾기 콘솔 프로그램이었고 리팩토링을 진행하는 강의였습니다.좋은 코드에 대한 내용을 잘 녹이기 위해 설계가 잘 된 강의였습니다. 칭찬하고 싶은 점은 일정이 많았는데 진도가 뒤쳐지지않게 잘 따라간 것아쉬웠던 점은 1주차에 단기목표로 잡았던 지뢰찾기 리팩토링 실습을 전부하지 못한 것 다음주에는 화요일까지 지뢰찾기 및 카페 리팩토링 실습을 마치고 테스트 강의를 예습하는 것이 목표입니다.
2025. 05. 28.
0
워밍업 클럽 4기 - 백엔드 Day 4 미션 및 회고
1.class Order { private OrderItems items = new OrderItems(); private Customer customer; public void validate() { items.validate(); if(hasNotCustomer) throw new IllegalStateException("사용자 정보가 없습니다.") } private boolean hasNotCustomer() { return !hasCustomer() //기존 코드의 메소드 } } class OrderItems { private List items; public void validate() { if(items.isEmpty()) throw new IllegalStateException("주문 항목이 없습니다.") if(getTotalPrice() 생성자에서 수행할 것 같습니다. 원본 메소드의 boolean 반환형을 유지하기 위해 메소드만 이관했습니다.로그보다는 예외를 던진후 예외처리단에서 message 로그를 찍는 것이 바람직해보여 예외를 던지도록 변경했습니다.2. SOLID에 대하여 자기만의 언어로 정리해 봅시다.단일 책임 원칙은 한 클래스는 두가지 이상의 역할을 수행하지 않는다.개방폐쇄원칙은 수정엔 닫혀있고 확장엔 열려있어야 한다는 것이다. 구체클래스가 추가되더라도 부모, 추상클래스가 변경되는 일이 없어야한다.리스코프 치환 원칙은 부모 클래스를 사용하는 로직에서 값만 구체 클래스로 변경해도 로직이 정상수행돼야하는 원칙이다.인터페이스 분리 원칙은 인터페이스에 과도한 책임이 부여될 경우 각각을 분리해 역할을 명확히 하는 원칙이다.의존성 역전 원칙은 객체의 필드의 자료형은 추상타입으로 하여 구체클래스를 자유롭게 주입할 수 있게 하는 원칙이다.
2025. 05. 27.
0
워밍업 클럽 4기 - 백엔드 Day 2 미션 및 회고
1. 강의에서 안내하는 것처럼, 프로젝트를 개인 계정으로 fork하고 강의를 수강해 주세요.https://github.com/jsween5723/readable-code https://github.com/wbluke/readable-code/pull/272. "추상과 구체" 강의를 듣고, 생각나는 추상과 구체의 예시가 있다면 한번 3~5문장 정도로 적어봅시다.추상) 이슈를 처리한다.이슈목록에서 이슈를 확인한다.문제를 식별하고 테스트 케이스를 확인한다.테스트 코드를 작성한다.해당하는 로직의 코드를 수정한다.테스트 코드가 만족하면 커밋과 PR을 작성한다.코드 리뷰를 받는다.main브랜치에 병합한다. 콘솔 프로그램을 다루는 건 오랜만이다보니 재미있었습니다.
백엔드