게시글
블로그
전체 82025. 03. 30.
0
Readable-code 인프런 워밍업 4주차 후기
📌 2025-03-30 Readable-code 학습 회고목차1. 강의 수강일주일간 강의 요약 정리일주일간 강의 회고다음 주에 학습목표2. 미션미션을 해결하는 과정어떤 관점에서 접근했는지문제를 해결하는 과정왜 그런 식으로 해결했는지미션 해결에 대한 간단한 회고강의 수강강의 요약하나의 레이어를 테스트하고 싶은 이유모의 객체가 무엇인지 확인하고 이를 활용하여 테스트를 쉽게 만들어보자하나의 레이어를 테스트하고 싶은 이유상위 레이어로 갈 수 록 준비할게 많아집니다. 그리고 의존성도 그만큼 많이 늘어나게 되는데 최상위 레이어를 테스트하기 위해서 불필요한 의존성을 모두 가져올 필요가 없습니다. 최상위 레이어는 웹 컴포넌트 기능으로 사용자의 요청을 받아 적절한 서비스 계층으로 위임하고 결과에 대한 메세지만 사용자에게 전달해주면 됩니다. 최상위 계층을 테스트할 때에는 이미 하위 계층과 단위 테스트를 모두 단단하게 완성하면서 올라왔기에 정상적으로 처리된다는 것이 보장됩니다.최상위 계층은 정상 응답 포멧과 오류 응답 포멧 그외 예외 케이스에 대해서 우리가 예상한 양식대로 사용자에게 전달되는지 테스트를 하고 싶습니다.최상위 레이어를 제외하고 서비스 계층 아래는 상황 분류에 따라 컨트롤러 계층에서 어떻게 반환하는지만 확인하면 됩니다.모든 리포지토리부터 서비스와 그외 컴포넌트를 모두 등록할 필요없이 컨트롤러 계층과 웹 관련된 컴포넌트가 정상 동작하는지만 확인하면 되기에 관련된 빈만 등록하게 한 경량 컨테이너를 제공합니다.@webMvcTest입니다. 파라미터로 등록할 컨트롤러의 이름을 넣을 수 있습니다. 모의 객체를 사용해보자모의 객체는 개발자가 설정한 요청과 응답만 반환하는 오브젝트로 테스트에서 불필요한 리소스 낭비나 경험하기 어려운 상황에 대한 테스트를 가능하게 합니다.@Mock,@Spy,@InjectMocks 와 같이 스프링 컨테이너 없이 모의 객체를 테스트 프레임워크 레벨에서 만들어주는 것과@MockBean, @SpyBean과 같이 스프링 컨테이너에 원본 오브젝트가 초기화전에 모의객체 프록시가 등록되는 방식을 사용할 수있습니다.모의 객체의 역할 종류는 5가지로 나뉩니다.DummyFakeStubSpyMock첫 번째 더미는 아무것도 하지 않은 모의 객체입니다. 개발자는 아무 구현하지 않고 의존성 컴파일 예외만 방지기두 번째 페이크는 실제 오브젝트와 유사한 기능을 하나 가벼운 수준으로 테스트를 하기 편하게 합니다.세 번째 스툽은 사용자가 설정한 값 그대로 반환하는 녹음기 오브젝트입니다.네 번째 스파이는 실제 객체와 동일하나 특정 기능만 개발자가 지정하여 문제가 어느지점인지 확인할 수 있습니다.다섯 번째는 목입니다. 목은 행위에 다한 기대를 명세합니다.Stub과 Mock 차이Stub은 어떤 기능을 요청했을 때 이런 기능을 요청하니 내부 값이 변경되어 내부 상태를 확인할 때 사용합니다.Mock은 행위에 대한 결과 값을 검증하는 방식입니다.강의 회고이번 주는 Readable-code와 Practical-Testing의 마무리를 짓는 한 주가 되었습니다.예전에 이미 봐둔 Practical-Testing 강의를 다시 보니 Readable-code에서 말씀하신 내용이 되새겨졌습니다.테스트 코드는 '문서'라고 말씀하신 내용에 덧붙여 코드 자체가 '문서'다 라고 마무리가 되었어요 테스트 코드에 필요한 여러가지 어노테이션에 대한 설명을 알려주셨지만핵심 내용은 어노테이션을 잘 활용하자가 아닌 테스트 코드의 목적을 한 눈에 들어오기 위해@DisplayName과 @xxxxTest 어노테이션을 활용하기Test Fixture 정리하기한 문단에 한 주제!( 추상과 구체 )그리고 테스트도 실제와 동일한지 확인하기 위해 스프링 컨테이너를 초기화하기 위해 설정 정보를 전달해야합니다.main 스프링 컨테이너와 다르게 @MockBean이나 @SpyBean등 특정 어노테이션으로 설정 정보가 달라질 수 있습니다.그래서 테스트 환경마다 다른 스프링 컨테이너를 매번 초기화해야하므로 같은 구성 정보를 사용하는 테스트를 모아 테스트 비용도 줄일 수 있었습니다. 이 모든 것은 테스트 개발을 잘하자가 아니라 자동화 테스트를 코드의 안정성을 높이고테스트 코드 자체도 다른 개발자가 이해하기 쉽고 목적을 알기 쉽게하기 위해 신경써야한다는 것을 느꼈습니다. 다른 개발자가 코드를 읽고, 유지보수를 하는 비용을 줄이기 위해 코드 가독성이 중요하며테스트 코드 마저도 코드의 가독성이 중요하다는 것을 강조하셨습니다. 지금까지 테스트 코드를 작성하면서 가독성에 대한 생각을 계속 했었지만Readable-code를 통해서 단계별로 코드를 진행하다보니 코드의 구체적인 가독성 높이는 방법을 알게 되었습니다. 미션미션 해결방법레이어별로 어떤 특징이 있는지 자기만의 언어로 표현하기어떻게 테스트를 하면 좋을지 자기만의 언어로 표현하기@Mock, @MockBean, @Spy, @SpyBean, @InjectMocks 의 차이를 한번 정리해 봅시다.아래 3개의 테스트가 있습니다. 내용을 살펴보고, 각 항목을 @BeforeEach, given절, when절에 배치한다면 어떻게 배치하고 싶으신가요? (@BeforeEach에 올라간 내용은 공통 항목으로 합칠 수 있습니다. ex. 1-1과 2-1을 하나로 합쳐서 @BeforeEach에 배치)미션 해결 방법은 강사님께서 알려주신 것이 나만의 언어로 표현할 수 있는 만큼 이해가 되었는지 확인하려면 진행했습니다.먼저 레이어별로 어떤 역할을 해야할까 고민을 했습니다.상위 계층으로 갈 수록 추상화 레벨이 높아야하고하위 계층으로 갈 수록 추상화 레벨이 낮아집니다.핵심은 비즈니스 로직이 다른 기술들에 의해 변동이 생기면 안되도록 해야한다고 생각했습니다.그래서 컨트롤러 계층에서 서비스 계층에게 dto를 전달할 때나 리포지토리에서 예외가 발생하여 서비스 계층으로 전달 될때도 마찬가지입니다.특정 기술에 종속된 예외가 발생되지 않도록 스프링 부트가 예외마저 추상화해서 전달하게 됩니다.그래서 생각을 했을 때 서비스 계층에서는 실제 사용자에게 전달되는 메세지 내용과 어떤 컨트롤러가 요청할 수 있도록 자신이 사용하는 dto를 노출하는 것이 중요하다고 생각했습니다. 그리고 3번은 직접 코드로 작성해보면서 느낌점을 나열해보려고 했습니다.4번은 readable-code에서 말씀해주신 내용을 생각해보면서 그리고 테스트 코드 강의를 생각해보면서 다른 개발자가 이 테스트 코드를 보고 한 번에 이해할 수 있는 test fixture가 될 수 있을지 고민했습니다.미션 회고@Test public void testTransactionManagerActiveWithMocking() { Product product = Product.builder().productNumber("001").name("아메리카노").build(); Order order = Order.builder().orderStatus(OrderStatus.INIT).build(); // saveOrder 모킹 + 트랜잭션 매니저 상태 확인 doAnswer(invocation -> { boolean isTxActive = TransactionSynchronizationManager.isActualTransactionActive(); System.out.println("트랜잭션" + isTxActive); throw new IOException(""); // 여기서 DB 작업 없이 단순 확인만 }).when(productService).saveOrder(any(Order.class)); // 실행 productService.saveProductAndOrder(order, product); // 검증 verify(productService).saveOrder(order); }처음에 저는 테스트 코드를 작성하기 전에 @SpyBean과 원본 객체의 @Transactional이 만나면 @SpyBean이 나중에 적용되는 것으로 알고 있었습니다.spy(transaction(object)) 구조로 모의 객체빈이 제일 외부에 있어서 지정한 반환값을 설정하면 그 메서드만 호출을 안하니까 트랜잭션이 적용 안되는줄 알고 있었습니다. 반전테스트를 해보면서 transactional과 mockito의 기술들은 결국 스프링 생명주기를 따라가게 될텐데 어떻게 우선순위가 정해지는 걸까? 생각을 하면서 실제 테스트 코드에 트랜잭션 매니저에서 트랜잭션이 실행중인지 확인해보려고 로그를 찍어봤으나 동작하지 않길래 제가 기존에 알고 있는 지식이 맞았구나 생각했습니다. 그런데 그러면 spy 메서드로 지정한 메서드를 트랜잭션을 적용하려면 어떻게 해야하는건가?코드레벨로 작성해야하는건가? 이런 생각에 spy으로 프록시를 만드는 beanpostprocessor를 확인해보니 트랜잭션aop보다 먼저 실행되는 생명주기 메서드를 사용했습니다. 덕분에 테스트 코드를 작성해보고 역할에 대해서 코드로 실행하다보니 잘못된 정보를 확인할 수 있었습니다. 출처 강의 링크Readable Code: 읽기 좋은 코드를 작성하는 사고법Practical Testing: 실용적인 테스트 가이드
백엔드
・
Readable-code
2025. 03. 22.
0
Readable-code 학습 회고 3주차
Readable-code 학습 회고목차1. 강의 수강일주일간 강의 요약 정리일주일간 강의 회고다음 주에 학습목표2. 미션미션을 해결하는 과정어떤 관점에서 접근했는지문제를 해결하는 과정왜 그런 식으로 해결했는지미션 해결에 대한 간단한 회고강의 수강강의 요약TDD레드 - 그린 - 리팩토링이라는 방식으로 매번 순회하여 코드를 작성하는 방법입니다.TDD 장점클라이언트가 필요한 정보만 전달한다고 생각하며 테스트를 작성할 수 있습니다.빠르게 이미 테스트 코드가 작성되어있어 상호 작용으로 메서드와 테스트 코드가 서로 윈윈하게 됩니다.DisplayName1. @DisplayName("음료 1개 추가 테스트") 2. @DisplayName("음료 1개를 추가하면 주문 목록에 담긴다.")아무것도 모르는 상태에서 테스트 코드에 추가된 정보를 보고 확인할 수 있어야합니다.Persistence layer 역할데이터 접근의 역할이 있습니다.비즈니스 가공 로직이 포함되어서는 안됩니다. 데이터를 수정, 삭제, 추가하는 것에 집중합니다.DAO, SQLMapper, QueryDSL을 사용하면 비즈니스 로직 작성이 침투할 수 있습니다.데이터 접근 계층은 데이터에 접근하는 역할만 가져야합니다. 테스트도 마찬가지로 CRUD에 집중하는 레이어 계층이 되어야합니다.Business layer 역할비즈니스 로직을 구현하는 역할입니다.데이터 접근으로 읽어온 데이터를 가공(비즈니스 프로세스)을 하는 계층트랜잭션을 보장해야한다.비즈니스 계층은 기능을 구현하는 로직이 들어옵니다.가장 중요한 역할로 트랜잭션을 보장하는 것입니다.예외가 발생하면 롤백을 할 수 있도록 작업 단위의 원자성을 가지고 있습니다.테스트는 언제나 테스트를 작성하는 목적이 있어야합니다.강의 회고이미 테스트 코드 강의는 모두 본 상태였습니다.그래서 복습한다고 생각하면서 보는 중에 readable-code에서 말씀하신 부분이 나왔습니다.테스트 코드는 내가 작성한 코드가 예상한 결과대로 동작하는지 확인하기 위해서 작성됩니다.그리고 다른 개발자가 내 테스트 코드를 보고 어떤 목적으로 테스트 코드를 작성했는지 가독성이 높여야 한다고 생각이 들었습니다.테스트 코드에서 DisplayName은 객체지향 프로그래밍의 메서도 명과 동일하다고 생각이 됩니다.그것을 보는 개발자는 테스트를 실행하여 테스트 결과 목록을 보면서 추상화된 이름을 보고 어떤 목적으로 테스트를 했는지 확인할 수 있게 작성해야한다고 생각이 변했습니다.기존에 테스트 코드 강의만 봤을 때에는 도메인? 실패/성공이 키워드로 들어가면 안된다 라고 말씀해주신게 이해가 되지 않았습니다.테스트 코드가 결국 누군가에게 보여지는 문서로 활용될 수 있으려면 DisplayName만 보더라도 테스트의 목적을 알 수 있어야하고given when then으로 문맥을 개행으로 나누며, 어떤 것을 테스트 하는지 사고의 흐름에 맞춰 추상적인 흐름이 보이도록 작성해야된다고 생각이 들었습니다.예를 들어 단순히 테스트 준비를 Order.create(모든 파라미터)로 테스트를 준비하면 다른 개발자가 보기에 여기서 중요하게 봐야하는 필드가 무엇인지 한 눈에 들오지 않습니다.이 부분을 우리가 확인하려는 필드만 파라미터로 보여준다면 this.createOreder('모델명','이름','가격')만 보더라도 테스트에서 어떤 필드가 사용되는 구나라고 한눈에 보이게 됩니다.미션Day 11테스트 작성하게된 목적을 먼저 나열해보겠습니다.InputHandler테스트 코드 작성 목적 : 사용자의 요청에 따라 어떤 결과가 나와야하는지 올바르게 확인되어야 뒤에 있는 로직이 신뢰하고 실행될 수 있기 때문입니다.@Test @DisplayName("사용자는 콘솔에 1을 입력하면 시간 이용권이 선택된다.") void selectByStringOne() { // given final String userInputAction = "1"; InputHandler inputHandler = createInputHandlerFor(userInputAction); // when StudyCafePassType userActionCafePassType = inputHandler.getPassTypeSelectingUserAction(); // then Assertions.assertThat(userActionCafePassType).isEqualTo(StudyCafePassType.HOURLY); }StudyCafePassOrder테스트 코드 작성 목적 : 사용자가 선택한 결과에 따라 올바르게 결과가 출력되는지 확인되어야합니다. 실제 비즈니스 로직에서 사용자의 요청과 비용이 다를 경우에는 상상하기 싫습니다.@Test @DisplayName("할인이 없고, 고정 이용권과 사물함 이용권을 선택하면 두 이용권의 가격을 합산한 가격이 나온다.") void calculatePrice() { // given final StudyCafePassType userActionPassType = StudyCafePassType.FIXED; final double discountRate = 0.0; StudyCafeSeatPass studyCafeSeatPass = createSeatPassFrom(userActionPassType, 200, discountRate); StudyCafeLockerPass lockerPass = createLockerPassFrom(userActionPassType, 100); StudyCafePassOrder passOrder = StudyCafePassOrder.of(studyCafeSeatPass, lockerPass); // when int price = passOrder.getTotalPrice(); // then Assertions.assertThat(price).isEqualTo(300); } StudyCafeSeatPass테스트 코드 작성 목적 : 이벤트에 따라 할인된 결과가 예상한 결과와 동일해야하는 것을 매번 확인해야합니다. 실제 돈 거래로 예상한 금액과 다를 경우 머리가 복잡해지는 것을 경험할 수 있습니다.@Test @DisplayName("할인율이 1이면 할인되는 금액은 이용권 금액과 같다") void calculatePrice() { // given final StudyCafePassType userActionPassType = StudyCafePassType.FIXED; final double discountRate = 1; int passPrice = 100; StudyCafeSeatPass studyCafeSeatPass = StudyCafeSeatPass.of(userActionPassType, 30, passPrice, discountRate); // when int discountPrice = studyCafeSeatPass.getDiscountPrice(); // then Assertions.assertThat(discountPrice).isEqualTo(passPrice); }미션 회고강사님이 OT때 말씀하신 내용이 기억납니다.AI에게 도움을 받을 수 있지만 판단력과 지혜와 같이 사람이 결정해야하는 부분에서 능력을 기르는 것이 중요하다.테스트 코드도 마찬가지라고 생각이듭니다단순한 테스트 코드 작성은 AI가 손 쉽게 작성하게 해줍니다.다만 이 테스트 코드를 목적에 맞게 AI가 작성했는지, 테스트 코드를 작성하는 목적이 올바른지 확인해야하는 것은 사람의 몫이라고 생각이 들었습니다.그리고 추가로 중간평가에 알려주신 다른 사람들과 코드를 비교해보라고 말씀해주신게 정말 도움이 많이 되었습니다.20줄 남짓한 코드 리팩토링이 사람마다 다르고 어떤 목적으로 리팩토링을 했는지 확인할 수 있는 부분이 많았습니다.그러면서 제가 놓치고 있던 부분도 확인할 수 있었습니다.총 3개의 코드를 비교해보았습니다.매직 스트링예외 상황에 대한 try-catch검증을 메소드 체이닝으로 처리이렇게 다양한 방식으로 검증을 하는 것으로 제 20줄의 코드가 처음과 많이 달라져있었고그 사이에 다른 개발자분들이 작성한 코드를 보면서 다른 인사이트도 많이 얻었습니다.출처강의 링크Readable Code: 읽기 좋은 코드를 작성하는 사고법Practical Testing: 실용적인 테스트 가이드
2025. 03. 15.
0
Readable-code 메타인지를 하다.
📚 Readable-code 학습 회고 2주차목차1. 강의 수강일주일간 강의 요약 정리일주일간 강의 회고다음 주에 학습목표 2. 미션미션을 해결하는 과정어떤 관점에서 접근했는지문제를 해결하는 과정왜 그런 식으로 해결했는지미션 해결에 대한 간단한 회고 강의 수강강의 요약추상은 하나의 문단을 한 문장으로 정리하기 위해 이 단락의 주제를 나타내는 것.우리는 추상화 레벨을 높여 가독성을 높이고 중요한 내용을 표현합니다.구체적인 구현 코드를 내부로 숨기기 때문에 사용자에게 불필요한 정보를 최소한으로 노출하게 해야합니다.잘못된 추상화는 오히려 사용자의 가독성을 떨어뜨리고, 내부 기능을 확인하는 과정이 발생합니다.객체지향은 내부 상태나 기능을 숨기고 중요한 추상 인터페이스만 노출하기 때문입니다.사용자 관점에서 내부 로직을 알지 못해도 메서드 시그니처를 통해 충분히 알 수 있도록 해야합니다.그리고 객체지향 코드가 항상 정답은 아닙니다.객체지향 프로그래밍은 요구사항으로 변경되는 환경에 기존 코드를 변경하지 않고 확장할 수 있다는 장점이 있습니다.소프트웨어에 요구사항이 없는 경우는 거의 없지만, 항상 객체지향이 정답이 아니라는 것을 말해주고 계십니다.객체의 책임과 응집도를 묶기 위해서 사용자의 요구사항이 변경 될 경우 변경되는 지점을 하나로 묶는 것도 방법입니다.화면에 출력하고, 사용자에 입력을 코드로 변환하는 과정은 input, output 으로 다른 역할이라고 할 수 있습니다.사용자의 입력을 받기 위해 콘솔에 출력하는 문자열과 사용자의 입력을 파싱하는 input 클래스 모두 같이 수정됩니다.이 두 객체 각각은 역할이 다르지만 하나의 책임(사용자의 스터디카페 이용권 선택받기)으로 묶으면 응집도도 올라가게 됩니다.관점의 차이가 신기했습니다.구체적인 동작 방식을 추상화를 하니 인터페이스도 동작 방식이 메서드 명에 녹아있게 되었습니다. 이용권 파일 읽기 기능을 추상화하여 이용권 읽기가 되니 다른 이용권이 추가되면 인터페이스에 매서드 추가가 되고 처음 만들어진 이용권 파일 읽기 클래스 메서드에 추가됩니다. 스터디 카페 이용권 읽기 클래스이기 때문입니다. 계속 이용권에 대한 변경이나 추가 사항으로 확장되면 클래스는 점점 메서드가 추가되고 인터페이스 메서드도 추가됩니다. 구현 방식을 추상화하니 이렇게 인터페이스도 구현체에 결합이 생긴 기분이 듭니다.구현체를 통해 추상화하는 방법에 구현 방식이 포함되지 않도록 하는 것이 방법일 수 있습니다. 이용권 읽기 인터페이스가 아니라 좌석 정보 제공 인터페이스, 사물함 정보 제공 인터페이스를 추가하니 내부에서 어떤 구현 방식을 사용하더라도 똑같이 결과를 받아올 수 있게 되었습니다.능동적으로 읽기코드를 읽을 때 단순히 눈으로 읽는 것이 아니라 문맥에 따라 개행이나 주석, 메서드 추출로 메서드를 분리하여 도메인 정보를 정확하게 얻는 것이 중요하다고 합니다.이 부분은 저도 정말 많이 공감하고 있습니다.메타인지제 머리 속에 아하 모먼트가 동작하게 된 키워드 입니다.메타인지 자기가 아는 영역인지, 존재 유무만 아는지, 아예 어떤지 감도 안오는 경우로 나눌 수 있습니다.아는 영역과 설명할 수 있는 영역은 100%를 만족하기 어려우므로 적절한 비율을 맞추면 좋다고 합니다.강의 회고제가 그동안 리팩토링을 하면서 고민하던 내용을 깔끔하게 정리하게 된 주간입니다.객체지향 프로그래밍에서 어디까지 객체로 묶어야하는 지 고민이 많았습니다.예를 들어, inputHandler, outputHandler 두 클래스를 하나로 뭉쳐야할지 고민했습니다.하나의 관심사를 가지고 있다는 건 생각을 했습니다.어차피 inputHandler를 수정하면 outputHandler도 수정해야한다.수정사항이 생기는 부분을 같이 묶고 내부 상태를 같이 사용하면 응집도도 높아지고 테스트도 하기 편해질거같다.생각만 했습니다.inputHandler은 이용권 정보 파일(scv)을 읽는 책임이 있기 때문에 다른 클래스의 필드로 넣는게 맞는지 확신이 서지 않았습니다.이제는 관심사에 대해서 어느정도 동일한 변경사항으로 수정해야한다면 같은 오브젝트에서 관리하는게 맞다는것을 알게되었습니다.관점의 차이에서도 머리가 띵! 했습니다.구체적인 방식을 추상화하다보니 인터페이스도 역할이 아니라 동작 방식에 대한 추상화가 되어있었습니다.그러면 인터페이스를 의존하는 다른 클래스는 인터페이스만 보고도 어떤 파일을 읽어오는지 알게됩니다.DIP에 따르면 상위 모듈은 하위 모듈을 의존하면 안되고, 상위 모듈 과 하위 모듈은 모두 인터페이스를 의존해야합니다.구체적인 방식이 인터페이스 표현되는 것을 표현하기 위한 거라면 괜찮지만그게 아니라 추상적으로 표현하려고 한거 였다면 수정해야합니다.함께 자라기 - 탑 다운 방식에 대한 회고저는 개발자로 일을 하면서 제가 해야하는 프로젝트는 요구사항이 명확한게 좋았습니다.회사에서 필요한 기능을 만드는데 어떤 기능이 필요한지 정확하게 모른다는 게 저로서는 이해가 안되었습니다.제가 받은 프로젝트를 개발하는 시간보다 오히려 기획자와 커뮤니케이션 하는 과정이 더 길때도 많아지다보니기획자가 중요하게 생각하는 프로젝트가 아닌건가 ? 라는 생각도 종종 들기도 했습니다.최근 신규 프로젝트가 기획자가 아닌 영업팀에서 필요하다는 기능을 요구했고단 글자수 200 글자 내외로 간단하게 요구를 했습니다.기획에는 어떻게 사용하는지, 무슨 목적인지, 언제 사용하는지, 누가 사용하는지 내용이 없고 단순 기능만 만들어달라는 기획이였습니다.처음에는 이런 생각이였습니다.그냥.. 만들어달라는 기능만 만들자그런데 누가 쓰고, 어떻게 쓰는지는 알아야할 거같아서 회의에 참석한 팀장님들에게 돌아다니면서 물어봤습니다.개발 팀장님, 기획 팀장님, 임원분에게 물어보면서 어떻게 기획이 되었고, 누가 사용하게 되는지 물어보고 나서프로세스를 구체적으로 만들었다가 다시 추상적으로 표현하면서 보이지 않았던 신규 프로젝트의 구조가 보이기 시작했습니다.프로젝트를 다시 들여다보니 아무 생각없이 개발하던 것보다 프로세스가 단순해졌고, 기존 프로젝트에도 적용한다면 유지보수와 사용자들도 편하도록 전체 구조가 바뀌게 되었습니다.강사님이 말씀하신게 이런게 아닌가 싶습니다.탑다운 방식으로 주어진 선택지 내에서 해결하는 방법은 빠르게 개발을 시작할 수 있으나추상과 구체를 오가면서 깨닫는게 있다는게 이런게 아닌가 싶었습니다.드나드는 것이 생각보다 저에게 많은 인사이트를 주었습니다.다음 학습 목표테스트 코드를 사용한다는 의미가 무엇인지 알고 싶습니다.테스트 코드가 불필요한 짐이 되지 않기 위해서는 어떻게 관리해야하는지 공부하고 싶습니다.추가로 기술을 배우고 기존과 달라진게 있다면 바로 정리하는 습관을 길들이겠습니다.미션미션을 해결하는 과정어떤 관점에서 접근 했는지저는 미션 코드의 큰 그림을 스프링 MVC 패턴과 톰캣 & 스프링 컨테이너 이라고 생각하며 접근을 시도했습니다.톰캣은 StudyCafeApplication 이며 사용자의 요청과 응답을 줄 수 있는 환경을 제공한다.스프링 컨테이너는 StudyCafePassMachine으로 보고 사용자의 요청과 응답을 서비스 계층에 변환하여 전달하는 역할이다.내부 구현 코드는 Service 계층이기에 외부 입력과 출력에 영향을 받지 않도록 해야한다.문제를 해결하는 과정은 무엇이었는지패스입력을 변환 하기 전까지는 컨트롤러 계층이라고 생각했습니다. 요청 파라미터를 변환하여 서비스 계층이 이해할 수 있는 언어로 변환하여 전달한다.서비스 계층은 외부가 어떠한 방식으로 요청을 받든지 상관없이 자신이 기능을 제공하기 위해 필요한 파라미터를 추상화하여 표현한다이 두 관점을 가지고 객체 관심사로 역할을 나누려고 했습니다.문제가 입력과 출력이 이미 역할이 부여된 것이라고 생각을 하다보니 객체를 합치기 어려웠고 리팩토링 하는 과정에 추상화로 묶기 어려웠습니다.그래서 아쉽게도 목적만 들어내기 위한 메서드 추출이 되었고 코드는 복잡해지기만 했습니다.왜 그런 식으로 해결했는지 내용스프링은 객체지향 프로그래밍을 하기 위한 다양한 기술을 제공하는 프레임 워크입니다.사용자가 원하는 이용권 조회 및 이용권 금액 계산, 할인 정책은 핵심 비즈니스 로직인 service 계층으로 바라본다.사용자의 입력과 출력은 HTTP 프로토콜에서 문자열을 필요한 정보와 서비스 계층에서 필요한 양식으로 변환하는 컨트롤러 구조파일에서 읽던, 메모리에서 읽던, DB에서 읽던 결과는 서비스 계층이 읽을 수 있는 데이터 구조로 반환하는 리포지토리 구조이렇게 바라보면서 코드를 작성하면 스프링 MVC 구조로 객체지향적이면서 유지보수하기 좋은 코드로 작성될 수 있다고 생각했습니다.미션 해결에 대한 간단한 회고미션해결과 중간정검을 통해 배운게 있습니다.직접 해보자.익숙해질때까지 하자.지금까지 학습 방식은 강의를 보고 끄덕끄덕, 그리고 인프런에서 보여주는 100% 게이지를 보며 만족했습니다.실제 예제 코드를 리팩토링하려니 뇌 메모리에 그동안 배운 정보가 로드되었습니다.코드로 출력하려고 하니 2시간째 코드만 바라보고 있었습니다.뇌 속에 정보는 많은데 익숙하지 않으니 키보드로 출력이 되지 않았습니다.강사님이 말씀하신 메타인지 중 들어본적이 있는 영역까지만 도달한거 같습니다.미션 해결을 하면서 강의만 듣고 이해한 것은 제가 학습한게 아니라는 것을 느꼈습니다.중간점검 피드백미션 Day4 코드를 자신의 것만 보는게 아니라 다른 사람의 것도 보는 것이 중요하다고 하셔서 비교해보니많이 배울것이 있었습니다. 짧은 20줄도 안되는 코드를 비교해보며 개선점을 찾아가기 위함입니다.미션 Day-4요구사항✔ 사용자가 생성한 '주문'이 유효한지를 검증하는 메서드. ✔ Order는 주문 객체이고, 필요하다면 Order에 추가적인 메서드를 만들어도 된다. (Order 내부의 구현을 구체적으로 할 필요는 없다.) ✔ 필요하다면 메서드를 추출할 수 있다.public boolean validateOrder(Order order) { if (order.getItems().size() == 0) { log.info("주문 항목이 없습니다."); return false; } else { if (order.getTotalPrice() > 0) { if (!order.hasCustomerInfo()) { log.info("사용자 정보가 없습니다."); return false; } else { return true; } } else if (!(order.getTotalPrice() > 0)) { log.info("올바르지 않은 총 가격입니다."); return false; } } return true; }나public boolean validateOrder(Order order) { if (isEmpty(order)) { outputHandler.showMessage("주문정보가 없습니다."); return false; } if(order.doesNotHaveItems()) { outputHandler.showMessage("주문 항목이 없습니다."); return false; } if (order.doseNotHaveValidTotalPrice()) { outputHandler.showMessage("올바르지 않은 총 가격입니다."); return false; } if (order.doesNotHaveCustomerInfo()) { outputHandler.showMessag("사용자 정보가 없습니다."); return false; } return true; } private boolean isEmpty(Object object) { return object == null; }전xx 개발자님public boolean validateOrder(Order order) { try{ if (order.hasNoOrderItems()) { throw new OrderException("주문 항목이 없습니다."); } if (order.hasInValidTotalPrice()) { throw new OrderException("올바르지 않은 총 가격입니다."); } if (order.hasNoCustomerInfo()) { throw new OrderException("사용자 정보가 없습니다."); } } catch (OrderException e){ log.info(e.getMessage()); return false; } catch (Exception e){ e.printStackTrace(); return false; } return true; }배울점전xx 개발자님이 인프런에서 작성하신 코드를 보면서 배운점을 정리했습니다.코드를 머리속에 있는 것을 먼저 정리하셨습니다.코드 이해를 하고 코드리팩토링에 대한 명확한 근거를 남겨주셨습니다.코드 이해 부분객체의 Item이 존재하지 않는다면, return false1의 로직이 아니면서 총 가격이 0보다 크지 않다면, return false1의 로직이 아니면서 총 가격이 0보다 크면서 2-1. 사용자 정보가 없다면, return false 2-2. 2-1의 로직이 아니라면, return true그 외, return true리팩토링 부분if (order.getTotalPrice() > 0) // 2번 else if (!(order.getTotalPrice() > 0)) // 3번2번 조건과 3번조건은 논리적 부정으로 if-else 구조와 동일합니다.유효하지 않은 조건을 먼저 적용해도 상관없는 순서입니다.인지적 사고와 불필요한 작업공간을 남기지 않도록 순서를 변경합니다public boolean validateOrder(Order order) { if (order.getItems().size() == 0) { log.info("주문 항목이 없습니다."); return false; } if (order.getTotalPrice() 배울점리팩토링을 통해 어느 개선점이 생겼는지 설명할 수 있어야한다.저도 동일한 조건문 순서를 변경했지만 설명까지 하지는 못했습니다.이 부분이 강사님이 말씀하신 능동적으로 리팩토링하기가 아닌가 싶습니다.코드를 먼저 이해할 때 주석이나 별도로 메모에 작성한다.그리고 코드 이해한것을 간단하게 작성하고, 코드 순서를 변경하여 early return을 사용했습니다.if (order.getTotalPrice() > 0) // 2번 else if (!(order.getTotalPrice() > 0)) // 3번2번과 3번은 논리적 부정으로 if-else구조로 변경할 수 있다.if(!(order.getTotalPrice() > 0)) else if-else 구조로 변경이 되면 early return으로 코드를 단순화하기 편하다.이렇게 정리해서 작성하니 명확하게 리팩토링한 이유가 생기게 되었습니다.리팩토링한 이유를 작성하면서 연습한다.공백으로 의미 단위를 나눠보자.공백 단위로 의미를 나누어 사고의 흐름을 원활하게 한다.메서드 추출 이유불필요한 부정 연산자 제거하여 두가지 조건을 생각하지 않도록 한다.추상화 레벨을 동등하게 하여 사고의 흐름을 원활하게 한다.이렇게 리팩토링하는 이유를 작성하면서 연습을 하는 것이 강사님이 알려주신 내용을 더 오래 기억할 수 있는거 같습니다.해피케이스와 예외케이스사전에 처리되지 않은 예외를 잡아서 개발자가 확인할 수 있도록 한다.public boolean validateOrder(Order order) { try{ if (order.hasNoOrderItems()) { throw new OrderException("주문 항목이 없습니다."); } if (order.hasInValidTotalPrice()) { throw new OrderException("올바르지 않은 총 가격입니다."); } if (order.hasNoCustomerInfo()) { throw new OrderException("사용자 정보가 없습니다."); } } catch (OrderException e){ log.info(e.getMessage()); return false; } catch (Exception e){ e.printStackTrace(); return false; } return true; }제가 코드를 작성하면서 생각하지 못한 부분이였습니다.그러면서 예외로 처리한다면 아래 항목도 고려해봐야겠다는 생각이 들었습니다.정상 처리로 변경하지 말아야하는 경우라면지금 validateOrder 메서드는 order 객체에서 발생되는 예외를 모두 잡고 정상 흐름으로 돌립니다.정상 흐름으로 넘어가도 되는 예외정상 흐름으로 넘어가면 안되는 예외오브젝트 메서드가 예외를 던지지 않는다면 try-catch가 필요할까이렇게 세분화해서 예외 처리를 하는 건 어떨까 생각이 들었습니다.개발자가 생각한 범주 내에 예외는 정상흐름으로 변경한다.개발자가 생각하지 못한 범주는 정상흐름으로 동작하지 않도록 하여 호출자에게 현재 상태를 알린다.이렇게 예상하지 못한 예외를 외부로 던져 호출자가 예외에 대한 처리를 할 수 있도록 유연하게 설계할 수 있다. 단 체크 예외는 제외하고개발자가 예측하지 못한 예외를 검증 메서드 내에서 처리하는 것은 검증 메서드의 책임이 커지는거같다.수정 코드public boolean validateOrder(Order order) { try { if (isOrderEmpty(order)) { return false; } if (order.hasNoOrderItems()) { return false; } if (order.hasInValidTotalPrice()) { return false; } if (order.hasNoCustomerInfo()) { return false; } return true; } catch (IllegalArgumentException e) { log.info(e.getMessage()); return false; } catch (Exception e) { log.error("알 수 없는 오류가 발생했습니다. {} {} {}", e.getMessage(), e.getCause(), order); throw new RunTimeException(e); } } private boolean isOrderEmpty(Order order) { return order == null; }이렇게 리팩토링을 하니 추가로 고려할만한 사항이 생겼습니다.예외 발생 가능성을 점검해보자order 엔티티가 발생시키는 예외가 진짜 있는지 확인한다. 만약 해당 메서드가 단순히 boolean만 반환한다면 try-catch를 제거해도 됩니다.예외 오브젝트를 추상화해보자Exception은 어떤 예외인지 알 수 없는 최상위 추상 예외이므로 검증하다가 발생했다는 걸 표현하는게 좋을거같습니다.최종 수정public boolean validateOrder(Order order) { try { if (order == null) { return false; } if (order.hasNoOrderItems()) { return false; } if (order.hasInValidTotalPrice()) { return false; } if (order.hasNoCustomerInfo()) { return false; } return true; } catch (IllegalArgumentException e) { log.warn("잘못된 입력값: {}", e.getMessage()); return false; } catch (Exception e) { log.error("알 수 없는 오류 발생 - 주문 ID: {}, 원인: {}", order != null ? order.getId() : "없음", e.getMessage(), e); throw new OrderValidationException(e); } }코드 출처인프런 워밍업 클럽 3기 백엔드(클린코드, 테스트코드) Day4 미션출처인프런 워밍업 클럽인프런 워밍업 클럽 스터디 3기 - 백엔드 클린코드, 테스트 코드(Java, Spring Boot)강의 링크Readable Code: 읽기 좋은 코드를 작성하는 사고법Practical Testing: 실용적인 테스트 가이드
2025. 03. 08.
0
Readable-code 추상과 구체 그리고 가독성에 대한 회고
📌 2025-03-08 Readable-code 학습 회고목차1. 강의 수강일주일간 강의 요약 정리일주일간 강의 회고다음 주에 학습목표2. 미션미션을 해결하는 과정어떤 관점에서 접근했는지문제를 해결하는 과정왜 그런 식으로 해결했는지미션 해결에 대한 간단한 회고강의 수강강의 요약개발자는 컴퓨터 프로그램을 작성하여 현실 세계에 발생된 문제를 해결합니다. 문제를 파악하고 문제를 해결하기 위한 방안을 생각합니다. 문제 해결 방안을 컴퓨터가 이해할 수 있는 언어로 작성합니다.새로운 요구사항이 들어오면 프로그램 내부 구현 코드를 읽습니다. 개발자는 신규 기능 개발보다 유지보수를 할 때가 더 많습니다.제가 생각하는 코드 작성하는 것은 비문학 소설을 작성하는 것과 동일하다고 생각합니다. 글 내용에는 어떤 내용이 중점인지 설명이 잘되어 있어야 이 책이 주는 정보를 빠르게 파악하기 쉽습니다."세탁기"세탁기를 처음 사용할 때, 가전 제품을 처음 사용할 때 사용 설명서를 읽지 않고 사용할 수 있습니다.직관적으로 어떤 기능을 제공하는지 아이콘과 옆에 라벨이 붙어있기 때문입니다.코드도 마찬가지 라고 생각이 듭니다.세탁기 레버를 돌리는 기능(Method), 세탁기 세탁 옵션에 보이는 숫자나 그림들(fields)에 대한 설명(변수명)만 봐도 알수 있게끔 하는 것이 가독성이 좋아지며 가독성에서 절약한 시간으로 추가 기능을 개발할 수도 있습니다.변수명메서드명메서드 매개변수명과 매개변수 타입매개변수 반환값추상추상은 구체에서 덜 중요한 것은 덜어내어 중요한 정보만 남기는 것을 의미합니다."세탁기"를 돌릴때 전원/시작을 누르면 알아서 기본 설정된 값으로 빨래를 합니다.이것이 추상이라고 생각합니다만약 저 버튼이 추상이 되어 있지 않다면 전원을 킨다 그리고 물을 넣는다 그리고 세탁을 한다 그리고 탈수를 한다...이렇게 모든 내부 과정을 작성하게 되는 것은 사용자로 하여금 불필요한 정보로 가독성을 떨어지게 합니다class 세탁기 { void start(세탁세제 세제,섬유유연제 유연제,보조세탁세제 보조세제){ 1. 전기를 세탁기에 흐르도록 한다. 2. 세탁기를 회전하여 세탁시간을 파악한다. 3. 세제통에 물을 넣는다. 4. 물이 다 들어가면 세탁을 한다 : } }이 과정은 하나로 추상하면 "세탁" 또는 "시작" 이라고 할 수 있을 거같습니다.추상을 하는 이유는 사용자가 빠른 이해를 도와주고 불필요한 정보를 뇌에서 가져오지 않도록 하여 가독성을 높여줍니다.객체지향 프로그래밍과 원칙SOLID는 객체지향 원칙(principle)입니다.현실 세계에서 원칙이 없다면 비효율적이고 질서가 유지되지 않을거라고 생각합니다.횡된보도 신호등에 초록불이 들어오면 사람이 건넌다.우측보행이런 원칙들이 없다면 사회는 비효율적이고 질서가 없을거라 생각합니다.원칙이 생긴 이유를 한 문장으로 정리하면사람들은 현실 세계에서 질서를 유지하고 효율적인 의사결정을 위해, 필요한 시점에 원칙을 만들고 이를 지키도록 시스템을 운영한다.이 문장을 SOLID 원칙으로 변경해보았습니다.개발자는 객체지향 세계에서 질서 유지하고 유지보수와 확장을 위해, 필요한 시점에 SOLID 원칙을 만들고 이를 지키도록 한다.SRPOCPLIPISPDIP이런 원칙들을 지켜가다보면 객체지향 프로그래밍이 효율적이고 질서가 있게 작성할 수 있기 때문에 학습하는 것이 필요하다고 생각합니다.사고의 흐름사고의 흐름이 무슨 의미인지 몰라서 물어봤습니다.사고의 흐름이란어떤 개념이나 주제에 대해 생각이 자연스럽게 연결되고 발전하는 과정을 말한다.예를 들어, 글을 읽거나 쓸 때 논리적으로 앞뒤가 맞고, 하나의 아이디어가 다음 아이디어로 매끄럽게 이어지는 경우 사고의 흐름이 좋다고 할 수 있다.그러면 개발자가 작성하는 코드가 사고의 흐름이 좋다는 것은 이렇게 정리 할 수 있을거 같습니다.논리적으로 전개가 자연스럽다.함수나 메서드의 이름만 봐도 어떤 동작을 하는지 예측이 가능하다.불필요하게 많은 if-else문이 없이 간결한 구조를 갖는다.데이터를 어디서 받아서 가공하고 전달되는지 보인다.저는 집중의 뇌과학이라는 책을 봤습니다.책에서 작업공간이라는 단어가 나옵니다."사람은 작업 공간이라는 임시 저장소가 있다. 평균적으로 3~5개를 가진다. 작업 공간에 새로운 정보가 들어올 수 없다면 기존 정보를 지우게 된다."코드를 읽을 때 논리적인 전개 사이에 복잡한 구조로 불필요한 정보를 작업 공간에 저장하게 하거나 함수의 이름이나 메서드의 이름이 어떤 동작인지 알 수 가 없어 내부 로직을 확인하는 등 이런 과정을 거치게 되면 작업 공간의 초과로 코드를 읽으면서 얻은 정보를 사라지게 합니다.올바른 추상추상화 레벨early return메서드 매개변수 타입과 변수명을 활용하여 사고의 흐름을 좋게 만들 수 있습니다.강의 회고개발자로 시작하자마자 듣는 문장이 있습니다a, b,c로 의미없는 변수명을 사용하거나 list1,list2 처럼 숫자가 뒤에 붙는 것은 가독성이 떨어지게 한다.실제 회사에서 코드를 하다보면 전자는 없다고 하더라도 후자는 종종 보게 됩니다.후자를 작성하게 되는 이유는 변수명을 고민하지말고 일단 개발을 마무리하고 리팩토링하자는 생각이였지만 새로 들어온 프로젝트로 변경되지 못한 채 남아있는 경우가 많았습니다.클린 코드가 좋고, 가독성이 좋은 코드가 중요하다는 말은 알고 있지만 제 머리속을 이해시켜주지 못했습니다.이해가 되지 않은채 가독성이 좋은 코드를 작성하려고 하니 오히려 리팩토링이후 더 알 수 없는 변수명과 메서드 명으로 혼란을 주기만 했었습니다.혼란스러운 코드를 보고 되돌리고 싶어도 어떤게 가독성이 좋은 코드인지 모르니 다시 리팩토링할 자신이 없었습니다.강의를 보고 난 후사용자에 대한 신뢰가 사라졌습니다.추상화가 무엇인지 알게 되었습니다사고의 흐름에 대해 고민하게 되었습니다.리팩토링 방식의 노하우를 알게 되었습니다.사용자라는 개념이 무엇인지MVC 패턴이나 DDD패턴이나 모두 사용하는 측(클라이언트)와 제공하는 측(서버)가 있습니다.여기서 강사님이 해주신 말이 기억납니다.사용자를 신뢰하지 않고 확인해야한다.사용자가 신뢰되는 정보를 준다는 것 자체가 사용자가 누구인지 알고 있는 것이기에 SOLID원칙에서 DIP에 위반된다고 생각이 됩니다.강의 이후 제가 재정의를 했습니다.사용자를 신뢰하지 않고 현재 문맥에 맞는 상황을 사용자에게 전달한다.추상에 대한 생각추상화에 대해 재정의하는 시간이 되었습니다.추상은 중요한 것을 보여주고 덜 중요한 것을 덜어내는 것이다.void 회원_정보이용료_납부(){ 1. 회원이 있는지 확인한다. 2. 조회된 회원이 이용기간을 확인한다. 2-1) 이용기간 중이면 아래 로직을 실행한다. 2-2) 이용기간 만료이면 아래 로직을 실행한다. 3. 이용기간을 변경한다. 4. 회원의 정보이용료 납부를 기록한다. 5. 사용자에게 변경된 정보를 반환한다. }이런 코드가 있다면 결국 필요한 내용은 정보이용료를 납부한다는 것입니다.여기서 하나의 메서드내에 여러가지 관심사기 있다면 분리를 하거나 추상화 레벨을 높이는 방법을 사용하는 것을 알게되었습니다.사고의 흐름이 무엇인지코드를 작성할 때 가독성이 좋다는 게 저에게는 어려운 문제였습니다.코드 가독성이 나쁘다, 좋다. 메서드명이 명확하다. 그런데 코드 가독성이 나쁘다고 생각하는 이유가 뭘까?이런 생각이 들었습니다.회사에 돌아가서 제 코드를 확인해보니 추상화 레벨이 맞지 않았습니다.void 회원등록(회원생성요청 회원생성정보){ 회원정보 회원정보 = 회원서비스.회원조회(회원생성정보); if(회원정보.type = 1){..} else if(회원정보.type =2){..} else if(회원정보.type =3){..} else if(회원정보.type =4){..} else {..} 회원서비스.회원추가서비스확인(회원생성정보); }이런 구조로 되어있다보니 제가 메서드명을 추상하여 중요한 정보만 가지고 와도 중간에 추상화 레벨이 다르다보니까 회원 타입에 대한 정보를 읽어와야합니다.기존 앞뒤 논리에서는 회원 정보에 대한 상태나 필드에 대한 가공이나 사용이 없다가 생기니 회원 정보 클래스에 대한 정보를 단기 기억에 찾아서 넣어야합니다.이렇게 뇌가 정보를 저장하는 방식과 사고의 흐름을 같이 연결하다보니 가독성이 좋은 코드가 무엇인지 , 추상화 레벨이 어떤 역할을 하는지 조금이라도 알게 되었습니다.리팩토링 노하우기존에는 리팩토링을 할때 브런치를 별도로 만들어서 했습니다.그러다보니 변경지점이 많은 경우 확인하기 어려웠고 기존 코드 로직을 확인하기 위한 과정도 필요했습니다.이 과정을 거치지 않고 별도 필드나 메서드를 리팩토링할 때 새로운 복사본을 가지고 리팩토링을 하니 더 효율적으로 리팩토링을 할 수 있었습니다.미션Day 2추상과 구체를 나만의 언어로 재정의하기추상을 하는 이유는 사용자가 구체를 알지 못해도 어떤 기능인지 알 수 있다는 것을 보고 가전제품을 떠올렸습니다.가전 제품을 구매하면 메뉴얼도 같이 보내줍니다.저는 가전 제품을 사용하기 위해 메뉴얼을 본 적은 극히 드문 일이였습니다.이 미션을 보고 이게 추상과 구체라고 생각하게 되었습니다.사용자는 내부 동작을 정확하게 알지 못하더라도 추상된 버튼을 누르면 알아서 동작하는 것.그것이 추상과 구체라고 생각하고 미션을 해결하였습니다.Day 4코드 리팩토링 미션과 SOLID 재정의 해보는게 미션이였습니다.코드 리팩토링은 제일 고려한 내용은 다음과 같습니다부정어를 메서드로 풀 수 있으면 메서드로 해결한다.추상화 레벨을 똑같이 한다.관심사를 분리한다.복잡한 정보를 주지 않는다.사용자를 신뢰하지 않는다.오브젝트에게 물어본다.나만의 언어로 표현하기객체지향 프로그래밍 세계에서 객체지향 원칙은 질서와 효율적인 코드를 작성을 돕는다는 것을 기반으로 문제를 해결하려고 했습니다.그러면 역시 가전 제품이다.기업이 돈을 벌기 위해서 어떠한 방식으로도 사용자의 니즈를 파악하여 물건을 판매해야합니다.거기서 사용자의 요구를 물건(객체)로 비유하기 적절한 것이 저는 생활 가전이라고 생각했습니다.생활 가전은 효율적이고 빠른 교체를 기반으로 물건을 만들어낸다고 생각합니다.그러면 SOLID의 철학과 일치하기에 생활 가전을 기반으로 비유하기로 했습니다.미션 회고추상과 구체를 하면서 사용자가 내부 로직을 몰라도 된다.위 문장을 조금이나마 이해하게 된 한 주가 되었습니다.기존기존에는 추상은 말 그대로 메서드를 공통화 하기 위한 메서드명일 뿐이라고 생각했습니다.중요한 정보만 위로 띄우고 중요하지 않은 것은 지우는 것이 추상화라는 것을 잊고 있었습니다.그러다보니 메서드 명이나 변수명도 추상이 아니라 단어만 줄이는 거였습니다.void findMember(){ 그냥 멤버가 아니라 정보이용료 미납을 조회 }제일 놀란 것은 String타입의 사용이였습니다.void findMemberByCond(String memberType){...}저는 유연한 코드를 작성한다고 오히려 String 타입을 주로 사용하고 있었습니다..(충격!!!!)강의를 보고나서객체지향 프로그래밍도 현실 세계에서 발생되는 문제를 컴퓨터에 사상하여 해결하기 위한 방식일 뿐이라는 걸 알았습니다.현실 세계에서 추상은 어떤 것이 있으며, 구체에는 어떤 것이 있는지 생각해보면서 추상화를 잘하면 사용자가 별다른 정보가 없어도 사용할 수 있다는 것을 알게 되었습니다.메서드 파라미터에 "String"이 이렇게 사용자의 혼란을 주는지 몰랐습니다.그리고 객체를 전달하는 것이 사용자는 일단 던져주면 알아서 내부에서 사용하는 게 사용자를 편하게 만들게 합니다.생각해보면 다른 개발자가 변수명에 추상화된 정보를 기입해도 String 타입이 있다면 어떤걸 전달해달라는 건지 확인하기 위해 서비스 계층부터 데이터 접속계층까지 들어가는 경우가 많았습니다.사용자에게는 광범위한 범위를 제공하는 것이 혼란을 준다는 것을 알게되었습니다. 출처인프런 워밍업 클럽인프런 워밍업 클럽 스터디 3기 - 백엔드 클린코드, 테스트 코드(Java, Spring Boot)강의 링크Readable Code: 읽기 좋은 코드를 작성하는 사고법Practical Testing: 실용적인 테스트 가이드
웹 개발
・
readable
2024. 03. 16.
7
인프런 워밍업 클럽 백엔드 0기 수료식 후기
인프런 워밍업 클럽 0기 후기 인프런 강의는 회사 업무를 빠르게 적응하려고 듣는 강의가 있고,회사 로직에 추가로 적용시키고 싶은 기술이거나 혹은 미래를 위해서 듣는 강의도 있습니다. 전자의 학습 방법은 예제 코드로 학습을 한 후에 회사에 작성된 다른 선배 개발자 분들의 실무 코드를 보면서 학습할 수 있습니다. 후자 방식으로 듣는 강의는 대부분 예제 코드로 학습을 하고 본인이 작성한 코드만 보게 됩니다.그러다보니 전자의 방식보다 아쉬운 부분이 생깁니다.실무 수준까지는 아니더라도 파일럿 프로젝트 수준의 코드나 다른 개발자 분들이 작성한 코드를 보기 어렵습니다.저는 다른 개발자 분들이 작성한 코드를 보는 것도 하나의 좋은 학습 방법이라 생각하는데 그걸 하지 못합니다. 인프런 워밍업 클럽 스터디는 저에겐 후자의 방식이지만 아쉬운 부분을 채워줬습니다.다른 러너분들이 제출한 과제를 보고 비교하며 학습할 수 있었고,과제의 응용 버전인 미니 프로젝트를 진행하면서 단순히 코드비교 뿐만 아니라 데이터 구조를 어떻게 설계를 했는지 볼 수 있었습니다.혼자서 듣는 강의였다면 이런 좋은 경험을 하지 못했을거라 생각합니다. 결론은 너무 좋은 경험이였고, 다음 1기도 시작하신다면 꼭 신청하려고 합니다. 감사인사존경하는 최태현 지식 공유자님 감사합니다.정규 라이브는 제공된 일정보다 더 길었고, 일정엔 없던 긴급 라이브까지 진행하시느라 목이 쉬시는 열정적인 강사님 모습에 제가 오히려 힘을 받았습니다.특히 과제 리팩토링과 코드 리뷰 라이브 덕분에 제가 헷갈렸던 테스트 개념과 작성하지 못했던 구현 코드까지 수정 했습니다.그외에 Live Q&A와 수료식 Q&A에 답변해주셨던 내용은 잊으면 안될거같아서 따로 정리까지 했습니다.3주간 열정적으로 스터디 진행해주셔서 감사합니다. 이 스터디를 기획하신 담당자분들과 준비해주신 담당자분들에게 모두 감사합니다.담당자분들께서 많은 러너분들 관리해주시고, 음식 준비해주시고, 나누어 주시고,야근까지 하시느라 힘드셨으텐데 친절하게 맞이해주셔서 감사합니다.덕분에 스터디를 진행하면서 처음 자바로 크레이지 아케이드를 만들면서 행복했던 그 때 감정을 다시 느꼈습니다. 그리고 수료식 마지막 시간이였던 네트워킹 시간에 대화를 리드해주신 조성륜 개발자분에게도 감사인사를 전하고 싶습니다.요즘 블로그 글을 쓸때 업데이트를 어떻게 해야할지 고민이 있었는데 위키처럼 작성하신다는 걸 알려주셔서 고민이 해결되었습니다 감사합니다.
인프런
・
인프런워밍업클럽
・
스터디0기
2024. 03. 09.
0
인프런 워밍업 0기 3주차 배포
인프런 워밍업 0기 3주차 배포 강의 수강 목차 1. JPA와 연관관계2. 배포를 위한 준비3. AWS와 EC2 배포4. 스프링 부트 설정과 버전 이해하기5. 기타 요약 JPA와 연관관계 JPA는 객체와 테이블을 매핑해주는 ORM으로 테이블끼리 가진 관계를 애노테이션으로 매핑할 수 있습니다. 다대일, 일대일, 일대다와 사용하지 않은 다대다까지 지원하고 있습니다. 연관관계라는건 테이블의 FK키를 어디서 관리하는 지에 따라 옵션 값이 달라집니다. FK 키를 가지고 있는 객체를 연관관계 주인이라고 합니다. 배포를 위한 준비 우리가 만든 애플리케이션을 다른 사람들이 사용할 수 있도록 하려면 1. 다른 사람을 내 컴퓨터를 사용하도록 한다.2. 공용 컴퓨터를 만들고 다른 사람들이 접속할 수 있도록 한다. 1번은 많은 사람들이 사용할 수 없고 24시간 동안 동작할 수 없습니다. 2번처럼 다른 사람들이 접속할 수 있도록 개발된 컴퓨터에 우리가 만든 애플리케이션을 설치하고 다른 사용자가 사용할 수 있도록 설정하는 방식을 사용합니다. 리눅스 운영체제는 여러 사용자가 동시에 접속해서 사용할 수 있도록 발전했기 때문에 해당 운영체제를 컴퓨터에 설치합니다. 현재 우리가 사용하는 MySQL, Spring, Java를 리눅스 컴퓨터에 설치해야합니다. 그러면 컴퓨터를 구매하고, 방화벽을 설정하고, ssh 인증에 대한 고민도 해야합니다. 이 과정을 클라우드 컴퓨팅 기술로 물리적인 컴퓨터를 구매하지 않고 서버 컴퓨터를 사용할 수 있습니다. 서버 컴퓨터를 구매한 뒤, 리눅스를 설치하고, MySQL을 설치하고, Java를 설치하여 내부에서 스프링 부트가 동작할 수 있도록합니다. AWS와 EC2 배포 저는 AWS가 정책이 변할 때마다 혹시 모르는 돈을 지불할 수 있는 경험을 했기 때문에 GCP(구글 클라우드 플랫폼)을 사용했습니다. GCP에서 VM Instance를 구매하고, 포트를 열고 , ssh키를 GUI로 등록하여 접속하는 방식을 선택했습니다. SpringBoot 설정,버전 업 이해하기 스프링 부트 설정은 build.gradle 파일에 작성된 스크립트를 통해서 필요한 설정을 작성할 수 있습니다. 작성되는 언어는 JVM기반의 스크립트 언어인 groovy와 Kotlin을 사용할 수 있는데 현재는 Kotlin도 공식 언어로 채택되었다는 걸 본적이 있습니다. 스크립트에 사용할 플러그인, 의존성, 그리고 작성되는 순서에 따라 실행될 로직을 작성할 수 있습니다. 스프링 부트 버전이 2.x --> 3.x로 올라가면서 JDK 17이 최소 사양으로 변경되었고, 자세한건 공식 스프링 부트 릴리스 항목에서 확인할 수 있습니다. 버전업을 할 경우 build.gradle에서 플러그인 버전을 변경하고, 컴파일 버전도 변경합니다. 그리고 인텔리제이를 사용할 경우 인텔리제이의 컴파일 버전,모듈버전 등 SDK의 버전도 변경해야합니다. 이때 마이그레이션을 할 때 미리 변경된 내용을 찾아주는 라이브러리를 사용할 수 있습니다.runtimeOnly "org.springframework.boot:spring-boot-properties-migrator" 를 추가하여 확인할 수 있습니다.회고 이번 3주차는 스프링 부트에 서비스를 제공하기 위한 준비 단계에 대해 이해를 할 수 있던 한주 였습니다. 동료 직원들과 배포를 위해서 jenkins와 docker등을 스터디하면서 배포를 하는 다양한 기술을 학습을 했지만, 원초적인 배포를 왜 해야하는지를 궁금해하지 않고, 무작정 배포를 해야하는데 어떤 기술이 있고, 요즘 기술은 이걸 사용한다니까 스터디를 해서 배워보자라는 생각이 강했습니다. 강사님께서 말씀하신 내용중에 기억나는 건, 우리 컴퓨터를 다른 사람들에게 보여줄수 없고 24시간 동안 제공할 수 없으니 다른 컴퓨터에 우리가 만든걸 설치해서 제공하는 과정을 배포라고 한다. 이 말이 기억납니다. 추상적으로 배포라는게 뭔지는 알고 있지만, 구체적으로 배포에 대해서 정리를 하니 명확하게 어떤 의미인지 와닿았습니다. 이번 주에 배운 배포에 대해서 생각을 하고 배포를 하면서 지난번에 스터디를 통해 학습한 jenkkins와 Anskble,`Docker`등을 실제로 사용해보면서 킅라우드 서버에 배포하는걸 천천히 해보려고 합니다. 미션 3주차는 미션이 없었기 때문에 미션에 대한 해결 과정을 작성해보려고 합니다. 2단계 미션인 출근 퇴근을 저장하는 방식을 처음에는 직원의 ID가 출근 API를 통해서 들어올 경우데이터를 삽입하고, 퇴근 API를 통해서 들어올 경우 퇴근 데이터 데이터를 삽입하는 방식으로 코드를 작성했습니다. 테이블 데이터 구조는 자동 id,`직원 고유번호`,`찍은시간`,`현재 상태(출근,퇴근)` 필드를 가지고 있습니다. 이렇게 작성한 이유는 출근 퇴근만 기록하면 된다는 생각이였습니다. 로직은 간단하게 출근할 경우 1. 해당 아이디로 등록된 직원이 있는지 조회 - 없는 경우 예외 발생2. 등록된 직원 정보로 출퇴근 기록지 테이블에 INSERT(직원아이디,LocalDateTime.now(),'ATTEND') 퇴근할 경우 1. 해당 아이디로 출퇴근 기록지에 직원아이디 조회 - 없는 경우 예외 발생2. 있는 경우 출퇴근 기록지에 테이블에 INSERT(직원아이디,LocalDateTime.now(),'LEAVE') 실행 이렇게 작성하다보니 문제가 발생했습니다. 1. 출퇴근 기록을 통해서 일한 시간을 가져올 때2. 휴일에 일한 경우, 추가 근무한 경우에는 어떻게 처리할 것인지3. 출근을 동시에 2번 찍었을 경우4. 퇴근을 안찍고 다음날에 출근을 찍은 경우 등 이렇게 테이블을 구조화하니 데이터를 읽어올 때, 저장할 때, 동시성이 발생할 때, 기타 요구사항이 발생할 때 모든 걸 대처할 수 없는 데이터 구조가 되었습니다.해결 과정 한달간 모든 직원의 근무일 마다 근무 시간을 조회해야한다고 한다면 { "직원 id": { "detail": [ { "date": "2024-01-01", "workingMinutes": 480 }, { "date": "2024-01-02", "workingMinutes": 490 } ], "sum": 10560 } } 이런 구조로 응답값을 줘야한다고 할때 2가지 고민이 생겼습니다. 1. workingMinutes를 필드로 저장하는게 아니라 출근 시간,퇴근 시간을 조회후 애플리케이션에서 계산을 한다.2. workingMinutes를 퇴근할 때 출근과 퇴근시간을 계산해서 필드를 추가해서 저장한다. 저는 2번을 선택했습니다. 그 이유는 첫번째는 매번 변경되는 값인 경우 (예: 장바구니 총 금액) 에는 매번 달라지기 때문에 필드에 굳이 저장할 필요가 없지만, 출퇴근 기록은 직원이 임의로 수정할 수 없도록 설계가 되어야하고, 수정을 원할 경우 출퇴근 담당자에게 별도로 요청을 해야 수정이 가능해야하기 때문에 workingMinutes필드는 한번 데이터가 입력되면 매번 수정되는 값이 아닙니다. 두번째는 만약 이 애플리케이션이 몇십몇이 아니라 몇백명 몇천명의 몇년치 데이터를 가져와야하는 경우, 설치된 서버가 사양이 낮아서 소화를 못하는 경우도 있을 거라 생각했습니다.그래서 미리 테이블에 저장해놓는다면 이런 연산 과정이 생략이 될 수 있기 때문에 필드로 추가하는게 맞다고 생각합니다. 출근,퇴근을 별도의 ROW로 관리하지 않고 하나의 ROW로 관리하기로 변경했습니다. 1. 출근은 오늘하고, 퇴근을 새벽에 할 경우 조회하려면 조건이 까다로워집니다. 2. 출퇴근 기록지에서 하루에 대한 정보를 읽어온다면 퇴근(새벽),출근(아침),퇴근(저녁)으로 조회가 될테고 연산이 복잡해집니다. 그래서 데이터 구조를 다음과 같이 변경했습니다. 출퇴근 기록지: id,`직원 id`,`오늘 날짜`,`출근 시간`,`퇴근 시간`,`등록일`,`수정일`,`등록한 위치`,`등록한 사람`,`수정한 사람` 회고 처음에 시간이 없다보니, 빠르게 구현을 해야한다는 생각에 추가 요구사항이나 엣지 케이스에 대한 염두를 하지 않고 데이터 구조를 설계했습니다. 테스트 코드를 작성하다가 이렇게 구조를 작성하면 테스트 하기도 어렵고, 수정하기도 어렵다는 걸 느꼈습니다. 결국은 요구사항은 데이터를 저장하는게 목적이 아니라 급여를 주기 위해서 데이터를 저장하는 게 목적인 기능이였습니다. 정규화도 좋고, 객체지향적인 것도 좋지만, 결국은 유지보수와 기능을 제공하기 위해 어떻게 데이터를 구조화하여 저장할지 고민을 하게 하는 문제였던거 같고 덕분에 데이터 구조를 처음에 잘못짜면 답이 없다는걸 느꼈습니다. 오히려 처음 작성하는게 더 빠르더라구요 국비교육을 받을때 강사님이 했던 말도 기억납니다. 테이블 설계는 정말 오래 고민하고 설계해야한다는 말씀이 기억납니다. 몸소 깨달은 한주가 되었습니다..
2024. 03. 03.
0
인프런 워밍업 0기 2주차 JPA와 트랜잭션
인프런 워밍업 2주차 정리 및 회고 강의 요약자바 진영에서 객체지향적으로 코드를 작성할 수 있도록 도와주는프레임워크인 스프링 부트와 ORM인 JPA를 활용할 수 있습니다.JPA란객체와 관계형 DB의 테이블을 매핑하여 데이터를 영구적으로 저장할 수 있도록 정해진 Java 진영의 규칙을 JPA라고 할 수 있습니다.JPA는 말 그대로 API이기 때문에 실제로 구현한 라이브러리중 하나인 Hibernate를 사용합니다. JPA나 Mybatis나 모두 JDBC를 사용합니다. JPA 사용하는 이유SQLMapper를 사용하면 다음과 같은 단점이 있습니다.1. 문자열로 작성하기 때문에 오류가 런타임 시점에 발견된다.2. 특정 데이터베이스 SQL 문법을 사용하기 때문에 종속적이 된다.3. CRUD를 작성하기 위한 반복작업이 많아진다.4. 데이터베이스의 테이블과 객체의 패러다임이 다르다. 스프링 컨테이너스프링 부트는 자바의 객체지향적인 코드를 작성하기 위해 사용하는 IoC 컨테이너입니다. IoC는 제어의 역전 원칙이라고 합니다.클래스의 단일책임의 원칙을 유지하기 위해서 클래스 내부 코드로 직접 의존성 객체를 생성하는게 아니라 인터페이스만 의존하도록 코드를 작성하고 외부에서 필요한 의존성을 주입하도록 하는 것을 말합니다. 스프링 컨테이너는 빈 오브젝트로 등록된 인스턴스를 관리하여 불필요한 인스턴스 생성을 줄이고,빈 오브젝트 생성시 필요한 의존성을 대신 주입해주는 역할을 합니다. 트랜잭션와 영속성 컨텍스트트랜잭션은 더 이상 나눌 수 없는 최소한의 작업 단위로스프링 부트에서 @Transactional을 추가하여 사용할 수 있습니다. 영속성 컨텍스트는 트랜잭션이 동작하여 하나의 작업이 완료되는 기준(`COMMIT`)으로 동작합니다. 영속성 컨텍스트는 테이블과 매핑된 객체(`Entity`)를 관리/보관하는 역할로1. 변경 감지2. 쓰기 지연3. 1차 캐싱기능을 제공합니다.2주차에 접어들면서2주차에 목표는 왜 사용하는지에 대해서 초점을 맞추고 학습을 했습니다.이미 이전에 JPA에 대해서 학습했기 때문에 다시 되짚어보는 시간이 되었습니다. 1주차 주말에 쉬다보니 2주차를 시작할 때 집중하지 못할까봐 걱정을 했는데2주차 시작인 월요일에 디스코드를 확인하러 들어가보니 모각공 탭에 다들 퇴근하고 밤 늦게까지 학습하시는걸 보고 다시 의지를 불태우게 되었습니다. 이미 배운 내용이라서 집중이 잘 되지 않아 다시 반복해서 들었던 기억이 있고, 아는 내용이라 생각하고 막상 코드로 작성해보려니 코드 작성이 어려웠습니다. 간단하게 스트림으로 풀수 있는 문제를 QueryDSL로 풀어보겠다고 하루를 날려보낸게 아직도 후회가 됩니다.JPA를 학습해서 간단하게 해결할 수 있다고 생각했는데 SQLMapper와 다른 방향으로 문제를 해결해야하는게 생각보다 많아서 역시 코드를 직접 작성하지 않는 이상 내꺼가 아니라는걸 다시 느꼈습니다. 다음주는 연관관계와 배포가 주제인데 예전에 Jenkins랑 Ansible을 스터디로 배운걸 적용해서직접 코드로 작성해보려고 합니다.미션 회고Service 계층이 Repository 계층을 의존할 때 직접 클래스를 의존하지 않고인터페이스를 의존하면 두 계층의 결합도가 낮아지기 때문에 인터페이스를 구현한 다양한 구현체를 변경할 수 있다는걸 배웠습니다. Repositoryinterface의 API로 INPUT 값과 반환값만 알면 서비스 로직은 거기에 따른 코드만 작성하면 되니까 서비스 로직은 수정하지 않고 변경이 가능했습니다. 그래서 JPARepository를 만들어 기존 코드에 연결을 해야하는 미션이 있었는데스프링 부트는 JpaRepository 인터페이스를 상속한 인터페이스는 빈 오브젝트로 등록을 합니다. 결국 JpaRepository를 상속한 인터페이스를 의존하는건 JPA 구현 클래스를 직접 의존하는 것과다를게 없다고 생각합니다. 물론 JPA를 사용하면 방언으로 다양한 데이터베이스를 JPQL과 메서드로 공용으로 사용할 수 있다고 했지만, 만약 QueryDSL이나 Jdbc을 사용하는 상황이 오고, 그리고 QueryDSL로 작성한 코드가QueryDSL의 업데이트가 없다는 문제로 다른 Jpql 빌더를 사용해야 된다고 한다면다른 계층에도 영향이 가기 때문에 리포지토리 계층은 인터페이스를 추가로 만들었습니다. public interface FruitRepositoryInterface { void save(String name, LocalDate warehousingDate, long price); void updateSellStatus(long id) ; List getSalesInfo(String fruitName); long getAmountBy(String name); List getFruitLte(Long price); List getFruitGte(Long price); } 이 인터페이스를 구현한 클래스를 만들고JpaRepository를 상속한 인터페이스를 스프링 컨테이너를 통해 주입받는 방식으로 코드를 작성했습니다.public interface FruitJpaRepository extends JpaRepository { List findAllByName(String name); long countByName(String name); List findAllByPriceGreaterThanEqual(Long price); List findAllByPriceLessThanEqual(Long price); } 리포지토리 구현체@Primary @Repository public class FruitRepositoryImp implements FruitRepositoryInterface { private final FruitJpaRepository repositoryJpa; public FruitRepositoryJpaImp(FruitJpaRepository repositoryJpa) { this.repositoryJpa = repositoryJpa; } @Override public void save(String name, LocalDate warehousingDate, long price) { Fruit fruit = new Fruit(name, price,null,"HAVING"); repositoryJpa.save(fruit); } @Override public void updateSellStatus(long id) { Fruit fruit = repositoryJpa.findById(id).orElseThrow(() -> new IllegalArgumentException("데이터 없음")); fruit.selling(); repositoryJpa.save(fruit); } @Override public List getSalesInfo(String fruitName) { return repositoryJpa.findAllByName(fruitName); } @Override public long getAmountBy(String name) { return repositoryJpa.countByName(name); } @Override public List getFruitLte(Long price) { return repositoryJpa.findAllByPriceLessThanEqual(price); } @Override public List getFruitGte(Long price) { return repositoryJpa.findAllByPriceGreaterThanEqual(price); } } 이렇게 JpaRepository도 빈으로 주입받고, 관련된 ORM 기술도 해당 FruitRepositoryImp에서 빈으로 주입받아 사용한다면 QueryDSL에서 다른 빌더나 다른 ORM 기술을 사용하더라도서비스 계층의 코드는 수정없이 작성이 가능합니다. 추가로 좋은점은 테스트 코드를 작성하는데 있습니다. JpaRepository를 상속한 인터페이스를 그대로 서비스 계층이 사용하면Mock없이 테스트 코드를 작성하여 서비스 계층만 단위 테스트로 작성할 때 JpaRepository의 모든 인터페이스를 구현해야했고 필요한 메서드만 구현한다고 해도 가독성이 너무 떨어지게 됩니다. 그래서 리포지토리는 인터페이스를 추가로 만들어 로컬,MySQL-Jdbc,MySql-JPA를 자유롭게 변경할 수 있게됩니다. 미션 회고테스트 코드를 작성하면서 테스트하기 편한 코드로 구조를 변경하다보니조금 복잡할 수 있어도 인터페이스를 추가하면 다른 계층도 테스트하기 쉬워지는 구조가 된다는걸 느꼈습니다.
2024. 02. 24.
0
서버와 스프링 부트 시작하기 1주차
강의 출처 : https://inf.run/Hywa 최태현 강사님의 자바와 스프링 부트로 생애 최초 서버 만들기, 누구나 쉽게 개발부터 배포까지! [서버 개발 올인원 패키지] 1주차(1강부터 18강) 워밍업을 정리해보았습니다. 1주차 학습 학습 목표스프링 부트 프로젝트를 설정하고 시작하는 방법서버란 무엇인지, 네트워크와 HTTP, API는 무엇인지, JSON은 무엇인지 등 서버 개발에 필요한 다양한 개념을 이해한다.스프링 부트를 이용해 간단한 GET/POST API를 만든다.디스크와 메모리의 차이를 이해하고 Database의 필요성을 이해한다.MySQL Database를 SQL과 함께 조작할 수 있다.스프링 서버를 이용해 Database에 접근하고 데이터를 조회,저장,수정,삭제할 수 있다.API의 예외 상황을 알아보고 예외를 처리할 수 있다.좋은 코드가 왜 중요한지 이해하기 스프링 부트 프로젝트를 설정하고 시작하는 방법기존 프로젝트 파일을 IDE를 통해서 build.gradle을 선택하여 시작합니다.스프링 https://start.spring.io/로 들어가서 스프링 환경 설정과 의존성을 추가하여 생성한뒤 1번 방식으로 실행한다. 서버란 무엇인지 서버 개발에 필요한 다양한 개념을 이해한다.서버는 serve + er 를 말하며 제공하다 단어와 er이 합쳐져 " 제공하는 것"을 의미합니다. 제공한다는 의미는 목적어가 붙게 되면, 기능을 제공하는 것을 서버라고 합니다. 것이라는 건 물건을 의미하고, 웹 서비스를 제공하기 위한 서버에서는 컴퓨터를 말합니다.서버가 기능을 제공하려면 누군가 원하는 기능을 요청을 해야 기능을 제공할 수 있습니다.요청하는 역할을 우리는 클라이언트라고 하며, 클라이언트 - 서버 관계는 요청-응답 관계로 이루어지게됩니다. 서버와 클라이언트가 서로 요청과 응답을 하기 위해서 통신 매개체가 필요한데 우리는 인터넷이라는 네트워크를 사용합니다. 인터넷을 통해서 요구사항을 서버에 전송을 하려면, 서버에 위치를 알아야합니다.인터넷 네트워크 환경에서 서버(컴퓨터)는 인터넷 회선마다 고유번호를 갖게되며 그걸 주소로 사용합니다.우리는 그걸 IP라고 합니다. 인터넷이 연결된 서버는 IP를 통해서 접근할 수 있으며, 기능을 제공하는 역할은 컴퓨터가 실행중인프로세스에게 요청을 해야하며, 프로세스는 고유의 번호인 포트를 받아 사용합니다.그래서 클라이언트가 특정 위치에 있는 컴퓨터가 실행하고 있는 특정 기능을 제공하는 프로세스에 접근하려면IP와 PORT 번호를 알고 있어야 접근하고 요청할 수 있습니다. HTTP인터넷 네트워크 환경에서 클라이언트와 서버가 안전하고 빠르게 요청과 응답을 하기 위해서 규칙이 필요한데그중 한가지 방법을 HTTP라고 합니다.요청 HTTP와 응답 HTTP는 차이가 있습니다.요청 HTTP는 HTTP Method,Path,query 또는 body 를 통해서 데이터를 전송하고응답 HTTP는 HTTP Status,body를 통해서 요청에 대한 응답을 전송합니다. APIAPI는 클라이언트와 서버가 원하는 기능을 제공하고 응답하기 위해 규칙을 정의합니다.서버가 정해진 기능을 제공하기 위해 인터페이스를 만들고,클라이언트는 해당 인터페이스 규격에 맞게 데이터를 전송하도록 하는것을 말합니다.이러한 규칙은 데이터를 교환하고 상호작용 하는데 필요한 명세서로 서버와 클라이언트 간의통신을 표준화하여 서로 다른 시스템 간의 통합을 용의하게 합니다. 스프링 부트를 이용해 간단한 GET/POST API를 만든다.GET @GetMapping("/add") // HTTP Method public int addTwoNumbers(@ModelAttribute CalculatorAddRequest request) { return request.getNumber1()+ request.getNumber2(); }POST @PostMapping("/multiply") // HTTP Method public int multiplyNumbers(@RequestBody CalculatorMultiplyRequest request) { return request.getNum1() * request.getNum2(); }요청 HTTP를 만들 때 HTTP Method가 꼭 필요하다고 했습니다.HTTP Method는 클라이언트가 원하는 동작을 서버에게 전송하기 위해 필요합니다.GET = 조회, POST = 등록 처럼 서버에게 원하는 기능을 전달하기 위해 필요합니다. 디스크와 메모리의 차이를 이해하고 Database의 필요성을 이해한다.클라이언트가 데이터 저장을 요청하여 저장했습니다.private final List memory = new ArrayList();해당 방식으로 저장할 경우, 서버가 재시작되는 상황이 되면 기존에 있는 데이터는 사라집니다. 서버를 재시작하면 기존 데이터가 초기화가 되는 이유는 컴퓨터 구조때문입니다.컴퓨터는 CPU, RAM, SSD/HDD 하드웨어 구조로 되어있습니다.CPU는 연산을 하고, RAM은 CPU가 연산을 할때 필요한 데이터를 임시 보관하는 역할을 하며,SSD,HDD는 연산된 결과를 반 영구 저장하기 위해 필요합니다. 현재 방식은 데이터를 RAM에 저장하기 때문에 서버를 재시작하면 저장된 데이터는 사라집니다. 그래서 데이터를 반영구 저장하는 SSD나 HDD에 데이터를 저장해야하는데운영체제가 제공하는 기능을 프로그래밍 언어를 통해서 저장할 수 있지만,사용자가 원하는 구조로 데이터를 저장하고, 수정하기 위해서 데이터베이스 라는걸 사용합니다. MySQL Database를 SQL과 함께 조작할 수 있다.SQL에 대한 내용은 생략하겠습니다.MySQL에는 database라는 구조가 있습니다. 자바로 치면 최상위 패키지로 데이터를 저장할때 구분할 수 있는 폴더라고 생각하면 됩니다. 스프링 서버를 이용해 Database에 접근하고 데이터를 조회,저장,수정,삭제할 수 있다.자바 진영에서 데이터베이스에 접근하는 기술은 JDBC를 사용합니다.JDBC를 구체화한 클래스인 JDBCTemplate을 사용하여 데이터베이스를 사용할 수 있습니다. @PostMapping("/user") public void saveUser(@RequestBody UserCreateRequest request) { // users.add(new User(request.getName(), request.getAge())); String sql = "INSERT INTO user (name,age) VALUES (?,?)"; jdbcTemplate.update(sql, request.getName(), request.getAge()); }API의 예외 상황을 알아보고 예외를 처리할 수 있다.클라이언트가 요청한 기능과 필요한 데이터를 서버에서 검증을 하고 올바르지 않은 데이터가 들어온다면클라이언트에게 오류를 전달해야합니다. @PostMapping("/user") public void saveUser(@RequestBody UserCreateRequest request) { if (request.getAge() 좋은 코드가 왜 중요한지 이해하기백엔드 개발자는 클라이언트가 원하는 기능을 제공하기 위해 프로그래밍 언어를 통해서요구사항을 코드로 작성하는 역할을 합니다. 기술적인 요구사항이건, 비즈니스 요구사항 이건 결국 어떤 요구사항을 수행하기 위한 기능을 동작하기 위해서 코딩을 하게됩니다. 코드를 작성하는 일을 하지만, 코드를 작성하는 시간보다 코드를 읽는 시간이 더 많습니다.그리고 혼자서 일하는 개발자보다 팀으로 개발을 하게됩니다. 요구사항을 기능으로 동작하기 위해 작성된 코드를 다른 사람이 봐도 어떤 요구사항을 코드로 작성했는지 알 수 있어야합니다. 기존에 작성된 전체 코드를 확인해보면 @PostMapping("/user") public void saveUser(@RequestBody UserCreateRequest request) { // users.add(new User(request.getName(), request.getAge())); if (request.getAge() 지금은 간단한 코드이지만, 수천줄이 된다면 해당 컨트롤러 기능인 saveUser를 수정하기 위해서는첫번째 줄부터 마지막 줄까지 읽어야합니다. 새로운 사람이 들어오거나 시간이 지나서 작성된 코드를 이해하기 위해서 소요되는 시간이 점점 늘어납니다.함수의 어느 부분을 수정할 경우, 전체 코드중 어디까지 영향을 미칠지 모릅니다.여러명이서 하나의 컨트롤러를 수정하기 위한 리소스가 많이 필요합니다.너무 큰 하나의 기능이 되어 테스트하기 어렵습니다.1~4번까지 맞물려 유지보수성이 떨어집니다.따라서 최대한 클린코드로 작성해야 생산성이 높아지게 됩니다. 1주차 간단 회고코드를 작성할때 우선 순위를 정하게 되었습니다.회사에서 저에게 주는 프로젝트는 프론트부터 백엔드까지 제가 만들기 때문에 시간에 쫒겨서 돌아기만 하는 코드 덩어리가 되었습니다.1주차 강의를 보고 직전에 작성된 프로젝트를 보니 돌아가기는 하는데 해당 함수가 무슨 기능을 하는지 알 수가 없어서처음부터 끝까지 코드를 읽어야했습니다.강사님이 말씀하신 코드는 요구사항을 표현하는 언어다 , 그리고 다른 사람이 코드를 봤을 때 요구사항을 한 눈에 파악할 수 있어야 한다는 걸 보고 반성을 많이 했습니다. 클린코드가 변수명, 하나의 기능, 하나의 책임은 알고 있어서 최대한 지키려고 했지만 이걸 하는 이유를 이해하지 못해서무작정 변수명 길게하고, 하나의 기능으로 나누고 , 하나의 책임으로 나누기만 하다보니 오히려 코드가 복잡해지고유지보수하기가 어려웠습니다. 앞으로 학습을 할 때도 새로나온 라이브러리니까, 빠르다고 하니까 , 좋다고 하니까 사용해야지라는 생각보다이걸 왜 사용하는지에 대한 이해를 먼저 하도록 하겠습니다. 2주차 학습 방법에 대한 목표2주차는 스프링 컨테이너와 트랜잭션 , JPA에 대한 강의인데이미 알고 있는 내용이지만, 왜 사용하는지에 대해 초점을 맞춰서 학습하도록 하겠습니다. 미션1일차:https://kamser0415.tistory.com/7어노테이션을 사용하는 이유 (효과) 는 무엇일까?나만의 어노테이션은 어떻게 만들 수 있을까? 어노테이션을 사용하는 이유를 찾기위해서 먼저 믿을 수 있는 공식 문서와 토비님의 강의를 참고했습니다.블로그에도 글이 많지만 공식 문서에서 작성된 내용은 만든 사람의 목적을 확인할 수 있다고 생각했고,토비님의 강의를 참고한 이유는 실무에서 오래 사용하신 개발자 분의 생각을 확인 하는 방법이라 생각했습니다.2일차: https://kamser0415.tistory.com/8간단한 GET 요청과 응답, POST 요청에 대한 문제입니다.주어진 두 수를 컨트롤어에서 받아서 덧셈,뺄셈,곱샙에 대한 결과를 응답의 결과로 내려주는게 문제입니다.연산만 책임지는 클래스를 만들어보자.연산의 결과를 반환하는 용도의 클래스를 만들자.연산만 책임지는 클래스를 만든 이유는 DTO에 연산 로직을 넣을까 생각을 했지만,단순한 값이 있다 없다, 특정 조건에 만족하는지 확인하는 것과 다른 연산이라는 로직이 들어가면다른 컨트롤러에서 사용하게 된다면 해당 로직이 필요하지 않을 수 있고, 거기서도 연산이 필요한데 연산 방식이 조금 차이가 있다면 그 DTO에 또 추가가 되면 DTO의 책임이 점점 커질수 있다고 생각했습니다.그래서 별도의 연산을 하는 클래스를 만드는게 좋다고 생각하여 만들게 되었습니다. 연산 클래스는 두 수를 인수로 받아 생성하고, 함수를 호출하면 받은 인수로 연산 결과를 반환합니다.여기서 실수한게 만약 요구사항이 변경되어 덧셈한 결과를 가지고 곱셈을 해달라고 한다면새로운 연산 클래스를 만들어야합니다. 그래서 다시 한다면 함수마다 연산할 인수를 전달받아 결과를 반환하는 순수 함수로 만들겠습니다.3일차:https://kamser0415.tistory.com/9람다가 등장한 이유람다와 익명 클래스 관계람다 간단한 사용 방법람다가 등장한 이유를 작성하기 위해서 익명 클래스가 등장한 이유익명 클래스의 단점1,2번을 먼저 찾아봤습니다. 익명 클래스를 보완하기 위해서 람다가 등장했다는건 알고 있었습니다.처음에는 익명 클래스와 람다의 관계는 제대로 찾아보기 전에는 익명 클래스 내에 람다가 포함된줄 알았습니다.자바는 1급 객체가 클래스이기 때문에 함수가 별도로 사용할 수 없기 때문에 결국 람다는 추상 메서드가 한개인 함수형 인터페이스를 익명클래스 방식에서 람다 표현식으로 만든거라 생각을 했습니다. 아직도 동일한 생각이지만, 함수형 인터페이스와 순수 함수에 대해서 한번 더 생각하게 되는 계기가 되었습니다. 4일차:https://kamser0415.tistory.com/10API를 통해서 과일 정보를 받아 저장한다.API를 통해서 판매된 과일 정보를 받고 결과를 반환한다.API를 통해서 받은 과일 이름으로 판매 완료금액과 판매되지 않은 금액의 합계를 반환한다.강사님께서 말씀하신 요청 HTTP를 서버에서 받기 위해 API 명세서를 확인하고 HTTP Method,Path,Body or Query에 맞는 컨트롤러 메서드를 만들고 매핑했습니다. API에서 숫자를 표현하는 방법은 int와 long이 있는데 long을 사용한 이유를 찾아봤습니다.제 생각에는 21억까지 표현할 수 있는 int 타입도 충분히 모든 과일의 금액을 표현할 수 있다고 생각을 했습니다. 검색을 해보니 가장 와닿은 말은 호환성이더라구요현재 데이터를 저장할 때 지금 스프링 부트를 사용하고 있지만, 다른 프레임워크나 프로그래밍언 언어에서도 사용하게 된다면 long이 더 적합하다는 것을 알게되었습니다. 회사마다 다르겠지만, 확장될 가능성이나 다른 프레임워크로 넘어가는 환경이 아니라면 int를 사용하는것도 나쁘지 않다고 생각됩니다. 5일차:https://kamser0415.tistory.com/11 클린 코드에 대해서 고민을 했습니다.전체 로직을 보면 주사위 크기는 6 이라고 지정되어있고, 밑에 추가 질문은 변경될 수 있다고 했습니다. 변수명 변경하기먼저 변수의 이름을 변경해서 해당 변수에는 어떤 데이터가 들어가는지 알 수 있도록 변경했습니다.각 기능을 함수 단위로 나누기주사위 보드판을 만드는 행위주사위를 던지는 행위주사위의 무작위 숫자가 나오는 행위나온 주사위 숫자를 기록하는 행위각 기능을 나누어보니 이걸 따로 따로 하는 것보다 주사위에 대한 놀이를 하나의 책임으로 생각하고 클래스로 만들면코드를 보기에 더 간편할거같다는 생각을 했습니다 주사위 머신 클래스 만들기그리고 주사위 머신에서 모든 일을 하는게 아니라 주사위 머신에 주사위 크기와 던지는 횟수를 입력하면내부에서 알아서 주사위를 만들고 주사위 기록을 저장하는 보드판을 만드는 책임을 가지게 합니다. 주사위를 던지는건 주사위 스스로 하도록 지정하여 숫자가 나오는건 주사위 클래스에게 책임을 위임했습니다. 코드로 작성할 때는 잘 작성했다고 생각을 했습니다. 이걸 글로 작성하다보니 책임을 어디까지 줄 것인지에 대해 한 번더 생각을 해보게 되었습니다.주사위 머신은 주사위를 만들고, 주사위 보드판을 만든다.주사위 머신은 주사위를 돌릴수 있는 기능이 있다.주사위를 돌리는건 주사위 객체가 한다.주사위 보드판은 주사위가 돌려진 숫자를 기록하는 기능이 있다.이렇게 코드를 작성하는게 더 나았을 거라는 생각이 들었습니다. 미션 회고지금까지 회사 일을 하면서 기능에 대해 집중하다보니 간단한 기능이라도 어디서 작업을 할지 고민을 하지 않고관습처럼 작성하는 습관이 있었습니다. 4일차때부터 강사님이 말씀해준 내용을 회사 프로젝트에 적용해보니 제가 생각하기에 코드를 좀 더 요구사항이 코드로 한 눈에 보이게 작성하려고 노력하였고,5일차때는 단순한 코드지만 리팩토링을 하면서 코드 작성하는게 재밌다는걸 느꼈습니다.
인프런워밍업
・
최태현강사
・
발자국