
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 false
1의 로직이 아니면서총 가격이 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() <= 0) {
log.info("올바르지 않은 총 가격입니다.");
return false;
}
if (!order.hasCustomerInfo()) {
log.info("사용자 정보가 없습니다.");
return false;
}
return true;
}
배울점
리팩토링을 통해 어느 개선점이 생겼는지 설명할 수 있어야한다.
저도 동일한 조건문 순서를 변경했지만 설명까지 하지는 못했습니다.
이 부분이 강사님이 말씀하신 능동적으로 리팩토링하기가 아닌가 싶습니다.
코드를 먼저 이해할 때 주석이나 별도로 메모에 작성한다.
그리고 코드 이해한것을 간단하게 작성하고, 코드 순서를 변경하여 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)
강의 링크
댓글을 작성해보세요.