-
(1) 클린코드와 리팩토링CS 지식/▷ Software Engineering 2021. 8. 4. 18:33
1. 클린코드란?
1-1) 정의
클린코드는 가독성이 높은 코드, 잘 읽히는 코드
1-2) 구현 방법
- 네이밍이 잘 되어야 함
- 오류가 없어야 함
- 중복이 없어야 함
- 의존성을 최대한 줄여야 함
- 클래스 혹은 메소드가 한가지 일만 처리해야 함
1-3) 예시
public int AAA(int a, int b){ return a+b; }public int BBB(int a, int b){ return a-b; }
(a) 문제점
(1) 함수 네이밍 : 무슨 역할을 하는 함수인 지 알 수 있는 이름을 사용
(2) 함수와 함수 사이의 간격 : 간격을 나누지 않으면 시작과 끝을 구분하는 것이 매우 힘들고, 결과적으로 잘 읽히지 않음
(b) 개선후
public int sum(int a, int b){ return a+b; } public int sub(int a, int b){ return a-b; }
2. 클린코드 구현
2-1) 변수 이름 작성
(a) 의미 있게 구분
구분 짓지 못하는 변수명은 의미가 없다. 예를 들어 ProductInfo와 ProductData의 개념은 분리 되는가?
또 변수 뒤에 타입을 작성하는 것도 의미가 없다면 좋지 않다. NameString과 Name이 다른 점이 무엇인가?
> 모든 naming( 변수 / 함수)는 길어도 좋으니, 의미가 명확하고 애매모호한 단어를 넣지 않을 것
> 추가 참조 : https://wipis.tistory.com/30
[프로그래밍] 프로젝트, 변수, 클래스 네이밍규칙
이 네이밍 규칙은 닷넷 스파이더 팀에서 만든 [C# Coding Standards and Best Programming Practices]를 바탕으로 만들었습니다. 1. 네이밍 관례와 표준 이 문서 전반에 걸쳐 파스칼 표기법과 카멜 표기법이.
wipis.tistory.com
https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=vicfaith&logNo=221166340935
좋은 변수, 함수 이름 만들기
프로그램을 짤 때 좋은 변수, 함수 이름은 좋은 코딩 스타일과 더불어 기본 중의 기본이다. 각 회사마다 명...
blog.naver.com
(b) 한 개념에 유일성을 줄 한 단어만 사용
Controller, Manager, driver와 같이 의미가 비슷한 단어들은 혼란을 유발한다. 어휘를 일관성있게 작성하라.
(c) 의미 있는 맥락을 추가
예를 들어 사용자의 주소를 나타내는 변수로 state라는 이름을 지었다면, state만 봐서는 이게 주소인지, 상태인지 의미를 유추하기 힘들다. 이 때, state와 함께 선언되는 다른 변수들의 이름이 주소와 연관된 단어라면 state의 옳은 뜻을 쉽게 유추할 수 있다.
2-2) 함수 작성하기
(a) 작게 만들기
if(isTestPage(pageData){ ... } private Boolean isTestPage(Page pageData){ return pageData.size > 0; }
함수 구현을 크게 만들어서 좋을 것은 하나도 없다. 어떤 것도 최대한 작고 간결하게 만들려고 하자.
가정문 하나도 저렇게 쓰이니까 읽기도 좋고 테스트 하기도 쉬워진다. 그렇다고 만들기 어려운 것도 아님.
[작게 만들기 예제 - refactoring ▼] (얼마나 작아야 할까?)
더보기※ 예시 코드
간단 리펙터링 의미
- 객체지향적인 코드로 유지보수를 가능하게 하는 코드로 만들기
- 가독성을 높이도록 클린코드화 하기
1) 온라인 투표 코드, 이미 선택한 추천을 변경하기 위해 버튼을 누르면
vote_change(old_vote, new_bote) 함수를 호출var vote_changed = function (old_vote, new_vote) { var score = get_score(); if (new_vote !== old_vote) { if (new_vote == 'Up') { score += (old_vote === 'Down' ? 2 : 1); } else if (new_vote == 'Down') { score -= (old_vote === 'Up' ? 2 : 1); } else if (new_vote == '') { score += (old_vote === 'Up' ? -1 : 1); } } set_score(score); };
> 총점 변경 함수는 1가지 일을 하고 있는게 아니다
1) old_vote와 new_vote의 상태에 따른 score 계산
2) 총점 계산
> 특이하게도 get_score 함수로 원본을 가져오는건 잘 만듦
2) 별도 함수로 분리하여 가독성을 향상
> 단 몇줄만 가지는 함수이라도 분리하는게 후일 유지보수에 더 좋은 방향
> 가져다 쓰는 곳에서 "가져온 함수의 내부"에 신경쓰지 않아도 되기 때문에
개발자가 처음 코드를 접했을 때, 함수의 "역할"에 더 집중할 수 있고 가독성이 좋음var vote_value = function (vote) { if(vote === 'Up') { return +1; } if(vote === 'Down') { return -1; } return 0; }; var vote_changed = function (old_vote, new_vote) { var score = get_score(); score -= vote_value(old_vote); // 이전 값 제거 score += vote_value(new_vote); // 새로운 값 더함 set_score(score); };
(b) 객체의 상태를 변경한 후 출력하는 함수를 만들지 말것
객체의 변경은 해당 객체 내부에서만 하는 것이다. 다른 객체에게 변경이나 생성을 맡기려 하지마라.
public class MovieRepository { private static List<Movie> movies = new ArrayList<>(); static { Movie movie1 = new Movie(1, "생일", 8_000); movie1.addPlaySchedule(new PlaySchedule(createDateTime("2019-04-16 12:00"), 6)); movie1.addPlaySchedule(new PlaySchedule(createDateTime("2019-04-16 14:40"), 6)); ...이하 생략 movies.add(movie1); }
영화 예매 목록을 담고 있는 객체이다.
Movie의 스케쥴을 추가하고 있는데, MovieRespository에서 추가하는 것이 아니라, Movie 내의 메소드에서 추가하고 있다.
public class Movie { private final int id; private final String name; private final int price; private List<PlaySchedule> playSchedules = new ArrayList<>(); void addPlaySchedule(PlaySchedule playSchedule) { playSchedules.add(playSchedule); } }
엄연히 스케쥴을 관리해야 하는 것은 변수를 가지고 있는 Movie의 역할이다.
(c) 함수는 하나의 일만 해야함
(1) 부수 효과를 일으키지말 것
(2) 명령과 조회를 분리 → 조회 / 변경은 다른 곳에서
이 모든게 결국 함수가 하나의 일만 한다면 해결 될 일이다.
상태를 변경만 하면 변경만 하고 출력 할 것이라면 출력만 해라. 두 가지 모두는 오해를 일으키기도, 테스트 하기도 어려움
(d) 반복하지 말것
함수로 반복을 최대한 줄일 것
(e) 파라미터는 없거나 한 두개가 좋음
클래스를 생성하여 파라미터를 최대한 줄이려고 노력하자
(f) boolian을 돌려주는 함수/변수는 긍정문으로
if isExisting(): ... if isFound: ...
2-3) 주석 작성하기
(a) 주석은 작성 하지 않는 것이 가장 좋음
최대한 코드만 보고 이해할 수 있도록 코드를 구현
(b) Todo 주석을 작성하자
일을 마치고 오늘 어디 까지 했고, 내일 무엇을 하면 좋을지 기록
2-4) 객체 만들기
(a) 자료를 추상화 하자.
추상 인터페이스를 이용하여 클래스를 생성하자. 관련이 적은 클래스들도 하나로 묶어 사용 할 수 있음
OOP의 장점을 살리는 코딩!
아래는 인터페이스와 추상 클래스에 관한 내용을 요약한 것인데, 사용법이라던지 쉬운 예제로 정리 되어 있다.
출처로 가서 예제로 익히자.
인터페이스 추상클래스 클래스가 아니다 클래스다 클래스와 관련이 없다 클래스와 관련이 있다
(주로 베이스 클래스로 사용)한 개의 클래스에 여러 개를 사용할 수 있다 한 개의 클래스에 여러 개를 사용할 수 없다. 구현 객체의 같은 동작을 보장하기 위한 목적 상속을 받아서 기능을 확장시키는 데 목적 출처 : https://loustler.io/languages/oop_interface_and_abstract_class/
(b) 객체의 구조를 숨기기
단지 private하게 변수를 선언 한다고, 구조를 숨기는 것이 아님
(1) 객체는 내부에서 값을 얻고 변화하고 계산 → 생성자건 매서드로 받건, private으로 선언한것은 그대로 안보여주고, 이를 통해 리턴 메서드를 위해서 사용되는 애들로써만 사용
(2) 외부로 나오는 메소드는 그 결과값만 리턴 → 관심있는 기능을 메서드로 구현하고 이를 호출하였을 때 결과를 보여주면 됨
이외의 다른 메소드들은 피하는 것이 좋음
(즉, 객체 통체로 꺼내서 get메소드를 이용하지 않는 것이 좋음)
2-5) 흐름제어
Control flow가 읽기 쉬어 한번에 이해할 수 있는 문맥으로 작성해야 함
(a) 왼쪽에 변수, 오른쪽에는 상수
if(length >= 10) while(bytes_received < bytest_expected)
(b) 부정이 아닌 긍정
if( a == b ) { // a!=b는 부정 // same } else { // different }
(c) 삼항 연산자
메인으로는 if/else를 사용하고 삼항 연산자는 매우 간단한 경우만 사용
(d) do-while 신텍스 사용 자제
do - 에서 한번은 무조건적으로 코드가 돌기 때문에, 검사 조건이 한번은 제외되어서 그런 듯
2-6) 오류 처리
(a) 가정문 보단 try-catch문을 사용할 것
(b) try-catch문도 가능하면 사용하지 않도록 할 것
try{ MealExpenses expenses = expenseReportDAO.getMeals(employee.getId()); total += expenses.getTotal(); } catch(MealExpensesNotFound e) { total += getMealPerDiem(); } // 직원의 id로 타입에 따라 결제방식이 다르다. public class PerDiemMealExpenses implements MealExpenses{ public int getTotal{ // 기본값으로 일일 기본 식비를 반환한다 } }//p138
인터페이스를 이용하여, 조건에 따라 반환하는 값이 다른 MealExpenses를 생성하여 문제를 해결했다.
(c) Null을 반환하지 말 것
빈 배열이나 0과 같은 값을 반환해라. 나중에 null로 인한 NullPointerException의 발생을 막을 수 있다.
2-7) 단위 테스트 하기
(1) 제대로 모든케이스를 포함
(2) 작은 단위로 테스트(단위 테스트이므로)
(3) 가능 한 모든 함수를 테스트
2-8) 클래스 생성하기
(a) 클래스 역시 작아야 함
어느 정도 작게냐면, 딱 하나의 책임만 하는 클래스를 생성 해보려고 하자
(단어 하나로 클래스의 모든 것을 표현 할 수 있을 크기가 좋다)
(b) 단일 책임 원칙을 따를 것
Single Responsibility Principle
위와 비슷하다. 클래스는 변경할 이유가 단 하나여야 한다. 예를 들어 버전을 관리하고, 때론 관리된 버전을 출력하는 클래스가 있다면, 이것이 수정 되야 한다면 책임이 두가지기 때문에 두가지 이유로 수정된다.
(c) 인스턴스 변수의 수가 작아야 함
3. 리팩토링이란?
3-1) 정의
프로그램의 외부 동작은 그대로 둔 채, 내부의 코드를 정리하면서 개선
이때, 개선이라고 함은
(1) 코드를 볼 때 가독성이 떨어지는 부분을 의미(클린코드가 아닌 부분을 클린코드로)
(2) 유지보수를 위해 객체지향성을 지키도록 바꿔줌
3-2) 리팩토링 대상 & 상황
(a-1) 대상1
- 중복 코드
- 긴 메소드
- 거대한 클래스
- Switch 문
- 절차지향으로 구현한 코드
(a-2) 대상2
- 메소드 정리 : 그룹으로 묶을 수 있는 코드, 수식을 메소드로 변경함
- 객체 간의 기능 이동 : 메소드 기능에 따른 위치 변경, 클래스 기능을 명확히 구분
- 데이터 구성 : 캡슐화 기법을 적용해 데이터 접근 관리
- 조건문 단순화 : 조건 논리를 단순하고 명확하게 작성
- 메소드 호출 단순화 : 메소드 이름이나 목적이 맞지 않을 때 변경
- 클래스 및 메소드 일반화 : 동일 기능 메소드가 여러개 있으면 수퍼클래스로 이동
(b) 상황
소프트웨어에 새로운 기능을 추가할 때, 코드를 수정하며 리팩토링까지 점검 / 적용
3-3) 리팩토링 목적
코드가 이해하기 쉽고, 수정하기 쉬움 → 협업 속도 상승 → 전체 개발 속도가 증가
※ 명심해야할 것은, 우선 코드가 제대로 돌아가야 한다는 것. 리팩토링은 우선적으로 해야 할 일이 아님을 명심
3-4) 리팩토링 절차
1. 아키텍처 관점 시작 → 디자인 패턴 적용 → 단계적으로 하위 기능에 대한 변경으로 진행
2. 의도하지 않은 기능 변경이나 버그 발생 대비해 회귀테스트 진행
작성한 TDD의 테스트로 관리3. 이클립스와 같은 IDE의 리팩터링 도구 이용
3-5) 리팩토링 예제
[예제 ▼]
더보기1번
// 수정 전 public int getFoodPrice(int arg1, int arg2) { return arg1 * arg2; }
함수명 직관적 수정, 변수명을 의미에 맞게 수정
// 수정 후 public int getTotalFoodPrice(int price, int quantity) { return price * quantity; }
2번// 수정 전 public int getTotalPrice(int price, int quantity, double discount) { return (int) ((price * quantity) * (price * quantity) * (discount /100)); }
price * quantity가 중복된다. 따로 변수로 추출하자
할인율을 계산하는 부분을 메소드로 따로 추출하자
할인율 함수 같은 경우는 항상 일정하므로 외부에서 건드리지 못하도록 private 선언
// 수정 후 public int getTotalFoodPrice(int price, int quantity, double discount) { int totalPriceQuantity = price * quantity; return (int) (totalPriceQuantity - getDiscountPrice(discount, totalPriceQuantity)) } private double getDiscountPrice(double discount, int totalPriceQuantity) { return totalPriceQuantity * (discount / 100); }
이 코드를 한번 더 리팩토링 해보면?
3번
// 수정 전 public int getTotalFoodPrice(int price, int quantity, double discount) { int totalPriceQuantity = price * quantity; return (int) (totalPriceQuantity - getDiscountPrice(discount, totalPriceQuantity)) } private double getDiscountPrice(double discount, int totalPriceQuantity) { return totalPriceQuantity * (discount / 100); }
totalPriceQuantity를 getter 메소드로 추출이 가능하다.
지불한다는 의미를 주기 위해 메소드 명을 수정해주자
// 수정 후 public int getFoodPriceToPay(int price, int quantity, double discount) { int totalPriceQuantity = getTotalPriceQuantity(price, quantity); return (int) (totalPriceQuantity - getDiscountPrice(discount, totalPriceQuantity)); } private double getDiscountPrice(double discount, int totalPriceQuantity) { return totalPriceQuantity * (discount / 100); } private int getTotalPriceQuantity(int price, int quantity) { return price * quantity; }
4. 클린코드와 리팩토링의 차이?
리팩토링이 더 넓은 의미를 가진다.
클린 코드는 단순히 가독성을 높이기 위한 작업으로 이루어져 있다면,
리팩토링은 클린 코드를 포함한 유지보수를 위한 코드 개선이 이루어진다.
클린코드와 같은 부분은 설계부터 잘 이루어져 있는 것이 중요하고,
리팩토링은 결과물이 나온 이후 수정이나 추가 작업이 진행될 때 개선해나가는 것이 올바른 방향이다.
참조
클린코드와 코드 리팩토링
클린코드와 리팩토링 “컴퓨터가 이해할수 있는 코드는 어느 바보나 다 짤 수 있다. 좋은 프로그래머는 사람이 이해할 수 있는 코드를 짠다.” Martin Fowler 프로젝트를 하게되면 여러 명의 개발자
devuna.tistory.com
클린코드와 시큐어코딩 | 👨🏻💻 Tech Interview
클린코드와 시큐어코딩 전문가들이 표현한 '클린코드' 클린코드란? 코드를 작성하는 의도와 목적이 명확하며, 다른 사람이 쉽게 읽을 수 있어야 함 즉, 가독성이 좋아야 한다. 가독성을 높인다
gyoogle.dev
https://gyoogle.dev/blog/computer-science/software-engineering/Clean%20Code%20&%20Refactoring.html
클린코드와 리팩토링 | 👨🏻💻 Tech Interview
클린코드와 리팩토링 클린코드와 리팩토링은 의미만 보면 비슷하다고 느껴진다. 어떤 차이점이 있을지 생각해보자 클린코드 클린코드란, 가독성이 높은 코드를 말한다. 가독성을 높이려면 다
gyoogle.dev
반응형'CS 지식 > ▷ Software Engineering' 카테고리의 다른 글
(4) MVC 패턴 (0) 2021.10.05 (3) 테스트 주도 개발(TDD) (0) 2021.08.15 (2) 시큐어 코딩 (0) 2021.08.08