4장 타입 코드 처리하기
이번 장에서 다룰 내용
- if 문에서 else를 사용하지 말 것과 switch를 사용하지 말 것으로 이른 바인딩 제거하기
- 클래스로 타입 코드 대체와 클래스로의 코드 이관으로 if문 제거하기
- 메서드 전문화로 문제가 있는 일반성 제거하기
- 인터페이스에서만 상속받을 것으로 코드 간 커플링(결합) 방지하기
- 메서드의 인라인화 및 삭제 후 컴파일하기를 통한 불필요한 메서드 제거
간단한 if 문 리팩터링
규칙: if 문에서 else를 사용하지 말 것
if 문에서 else를 사용하지 않는 것은 많은 개발자와 프로그래밍 가이드라인에서 권장하는 스타일 중 하나입니다. 이 규칙을 따르는 이유와 그로 인한 이점에 대해 설명하겠습니다.
왜 else를 피해야 하는가?
- 가독성 향상: else 없이 코드를 작성하면, 코드의 흐름이 선형적으로 되며, 중첩된 조건문이 줄어들기 때문에 코드의 가독성이 향상됩니다.
- 간결성: return 또는 throw 등을 사용하여 조건문 중간에 메서드를 종료하면, 나머지 코드는 기본 실행 경로로 간주될 수 있으며 else 블록 없이도 명확하게 표현됩니다.
- 유지 보수 용이: else를 사용하지 않으면, 특정 조건에 대한 로직만을 수정하거나 추가할 때, 기존 코드의 구조를 크게 변경할 필요가 없습니다.
else를 사용하지 않는 예시
// else를 사용한 예제
if (condition) {
return someValue;
} else {
return defaultValue;
}
// else를 사용하지 않은 예제
if (condition) {
return someValue;
}
return defaultValue;
물론, 모든 상황에서 else를 피할 수 있는 것은 아닙니다. 그러나 가능한 한 else를 피하고, 간결하고 선형적인 코드를 작성하는 것이 좋습니다.
이러한 스타일은 특히 "Guard Clauses" 또는 "Early Return" 패턴에서 자주 볼 수 있습니다. 이 패턴은 함수나 메서드의 시작 부분에서 잘못된 인자나 예외 상황을 먼저 처리하고, 메서드의 나머지 부분에서는 주 로직에만 집중할 수 있도록 설계됩니다.
마지막으로, 이러한 규칙은 가이드라인 중 하나일 뿐입니다. 실제로 코드를 작성할 때는 프로젝트의 요구사항, 팀의 코딩 컨벤션, 개인의 코딩 스타일 등 다양한 요소를 고려해야 합니다.
리팩터링 패턴: 클래스로 타입 코드 대체
"리팩터링"은 코드의 외부 동작은 그대로 유지한 채로 내부 구조를 개선하는 과정을 의미합니다. "타입 코드 대체"는 특정 코드의 의미나 목적을 나타내기 위해 사용되는 숫자나 문자열 같은 타입 코드를 보다 명확한 클래스나 열거형(enum)으로 대체하는 리팩터링 기법 중 하나입니다.
"리팩터링: 클래스로 타입 코드 대체"에 대해 자세히 설명하겠습니다.
- 문제점:
- 종종 우리는 특정 카테고리나 타입을 나타내기 위해 정수, 문자열 등의 타입 코드를 사용합니다.
- 이러한 타입 코드는 코드의 의미를 직접적으로 알기 어렵게 만들고, 유지 관리가 어려울 수 있습니다.
- 해결 방법:
- 타입 코드를 클래스나 열거형으로 대체하여 코드의 명확성과 유지 관리성을 향상시킵니다.
- 예제:
- // 문제점: 타입 코드 사용 class Employee { static final int ENGINEER = 0; static final int MANAGER = 1; static final int SALESMAN = 2; private int type; Employee(int type) { this.type = type; } } // 해결 방법: 클래스로 타입 코드 대체 abstract class EmployeeType { } class Engineer extends EmployeeType { } class Manager extends EmployeeType { } class Salesman extends EmployeeType { } class Employee { private EmployeeType type; Employee(EmployeeType type) { this.type = type; } }
- 장점:
- 코드의 의미가 명확해집니다.
- 새로운 타입을 추가하거나 기존 타입을 수정할 때 유연성이 증가합니다.
- 타입별로 다른 동작이 필요한 경우, 오버라이딩과 다형성을 활용하여 각 타입 클래스에서 구현할 수 있습니다.
요약하면, "클래스로 타입 코드 대체"는 코드의 명확성과 유지 관리성을 향상시키기 위해 타입 코드를 클래스나 열거형으로 대체하는 리팩터링 기법입니다.
클래스로 코드 이관하기
"리팩터링: 클래스로 코드 이관하기"는 코드의 구조와 책임을 더 명확하게 하기 위해 특정 코드나 기능을 다른 클래스로 옮기는 작업을 의미합니다. 이 작업은 주로 한 클래스에 너무 많은 책임이나 기능이 집중될 때 수행됩니다.
의미: 클래스는 특정 책임을 가진 객체를 나타내야 합니다. 한 클래스가 너무 많은 책임을 가지고 있으면 그 클래스의 코드는 복잡해지고 이해하기 어려워질 수 있습니다. 이 때, 관련된 기능이나 책임을 새로운 클래스로 분리하여 각 클래스의 책임을 명확하게 하는 작업을 "클래스로 코드 이관하기"라고 합니다.
목적:
- 책임 분리: 한 클래스에 중복된 책임이나 불필요한 책임을 제거하고, 각 클래스가 하나의 명확한 책임만을 가지도록 합니다.
- 재사용성 향상: 재사용 가능한 로직이나 기능을 독립된 클래스로 분리함으로써 다른 곳에서도 쉽게 재사용할 수 있습니다.
- 코드 구조 개선: 코드의 구조와 구성을 더 명확하고 깔끔하게 만들어, 유지 관리가 쉽게 합니다.
장점:
- 가독성: 각 클래스가 명확한 책임만을 가지므로 코드를 이해하기 쉬워집니다.
- 유지 관리성: 코드 변경이나 추가가 필요할 때, 변경의 영향을 최소화하고 관련 클래스만 수정하면 되기 때문에 유지 관리가 쉽습니다.
- 확장성: 새로운 기능이나 변화에 대응하기 쉽습니다. 필요한 클래스만 확장하거나 수정하면 되기 때문입니다.
- 재사용성: 독립적인 클래스로 분리된 기능은 다른 곳에서도 쉽게 재사용할 수 있습니다.
예제:
// 리팩터링 전: User 클래스가 사용자 관련 정보와 주문 관련 로직을 모두 가지고 있음
class User {
private String name;
private List<Order> orders;
// ... 사용자 관련 메서드 ...
void addOrder(Order order) {
orders.add(order);
}
double calculateTotalOrderPrice() {
double total = 0;
for (Order order : orders) {
total += order.getPrice();
}
return total;
}
}
// 리팩터링 후: 주문 관련 로직을 OrderManager 클래스로 이관
class User {
private String name;
}
class OrderManager {
private List<Order> orders;
void addOrder(Order order) {
orders.add(order);
}
double calculateTotalOrderPrice() {
double total = 0;
for (Order order : orders) {
total += order.getPrice();
}
return total;
}
}
요약하면, "클래스로 코드 이관하기"는 코드의 책임과 구조를 명확하게 하기 위해 특정 코드를 다른 클래스로 이동시키는 리팩터링 방법입니다. 이로 인해 코드의 가독성, 유지 관리성, 확장성, 재사용성이 향상됩니다.
리팩터링 패턴: 클래스로의 코드 이관
"리팩터링: 클래스로 코드 이관하기"는 코드의 구조와 책임을 더 명확하게 하기 위해 특정 코드나 기능을 다른 클래스로 옮기는 작업을 의미합니다. 이 작업은 주로 한 클래스에 너무 많은 책임이나 기능이 집중될 때 수행됩니다.
의미: 클래스는 특정 책임을 가진 객체를 나타내야 합니다. 한 클래스가 너무 많은 책임을 가지고 있으면 그 클래스의 코드는 복잡해지고 이해하기 어려워질 수 있습니다. 이 때, 관련된 기능이나 책임을 새로운 클래스로 분리하여 각 클래스의 책임을 명확하게 하는 작업을 "클래스로 코드 이관하기"라고 합니다.
목적:
- 책임 분리: 한 클래스에 중복된 책임이나 불필요한 책임을 제거하고, 각 클래스가 하나의 명확한 책임만을 가지도록 합니다.
- 재사용성 향상: 재사용 가능한 로직이나 기능을 독립된 클래스로 분리함으로써 다른 곳에서도 쉽게 재사용할 수 있습니다.
- 코드 구조 개선: 코드의 구조와 구성을 더 명확하고 깔끔하게 만들어, 유지 관리가 쉽게 합니다.
장점:
- 가독성: 각 클래스가 명확한 책임만을 가지므로 코드를 이해하기 쉬워집니다.
- 유지 관리성: 코드 변경이나 추가가 필요할 때, 변경의 영향을 최소화하고 관련 클래스만 수정하면 되기 때문에 유지 관리가 쉽습니다.
- 확장성: 새로운 기능이나 변화에 대응하기 쉽습니다. 필요한 클래스만 확장하거나 수정하면 되기 때문입니다.
- 재사용성: 독립적인 클래스로 분리된 기능은 다른 곳에서도 쉽게 재사용할 수 있습니다.
예제:
// 리팩터링 전: User 클래스가 사용자 관련 정보와 주문 관련 로직을 모두 가지고 있음
class User {
private String name;
private List<Order> orders;
// ... 사용자 관련 메서드 ...
void addOrder(Order order) {
orders.add(order);
}
double calculateTotalOrderPrice() {
double total = 0;
for (Order order : orders) {
total += order.getPrice();
}
return total;
}
}
// 리팩터링 후: 주문 관련 로직을 OrderManager 클래스로 이관
class User {
private String name;
}
class OrderManager {
private List<Order> orders;
void addOrder(Order order) {
orders.add(order);
}
double calculateTotalOrderPrice() {
double total = 0;
for (Order order : orders) {
total += order.getPrice();
}
return total;
}
}
요약하면, "클래스로 코드 이관하기"는 코드의 책임과 구조를 명확하게 하기 위해 특정 코드를 다른 클래스로 이동시키는 리팩터링 방법입니다. 이로 인해 코드의 가독성, 유지 관리성, 확장성, 재사용성이 향상됩니다.
불필요한 메서드 인라인화
"불필요한 메서드 인라인화(Inline Method)"는 리팩터링 기법 중 하나로, 메서드의 본문이 그 이름만큼 명확하지 않거나 해당 메서드가 불필요하게 복잡해 보일 때, 그 메서드의 본문을 호출하는 곳에 직접 삽입하고, 그 메서드를 제거하는 작업을 의미합니다.
목적:
- 간결성: 불필요한 간접 호출을 제거하여 코드를 간결하게 만듭니다.
- 명확성: 메서드의 이름과 실제 수행하는 로직 사이에 괴리가 있을 경우, 이를 제거하여 코드의 의도를 명확하게 합니다.
- 중복 제거: 여러 곳에서 비슷한 작업을 수행하는 메서드가 있을 때, 이를 인라인화하여 중복을 제거할 수 있습니다.
장점:
- 코드 이해의 용이성: 불필요한 메서드 호출로 인한 코드의 흐름을 쉽게 파악할 수 있습니다.
- 성능 향상: 경우에 따라서는 메서드 호출의 오버헤드가 줄어들어 성능이 약간 향상될 수 있습니다(하지만 현대의 컴파일러나 JIT 컴파일러는 이런 인라인화를 자동으로 처리하는 경우가 많으므로 큰 성능 향상을 기대하기는 어렵습니다).
- 리팩터링의 기반 마련: 다른 리팩터링 작업을 수행하기 전에 코드를 단순화하는 데 도움이 됩니다.
예제:
class Calculator {
int doubleTheValue(int value) {
return value * 2;
}
int computeValue() {
int baseValue = 5;
return doubleTheValue(baseValue);
}
}
// 불필요한 메서드 인라인화 후:
class Calculator {
int computeValue() {
int baseValue = 5;
return baseValue * 2;
}
}
위 예제에서 doubleTheValue 메서드는 간단한 연산만 수행하므로 인라인화하여 computeValue 메서드 내에서 직접 연산을 수행하도록 했습니다.
요약하면, "불필요한 메서드 인라인화"는 코드의 간결성과 명확성을 향상시키기 위해 불필요한 메서드를 제거하고 그 기능을 호출하는 곳에 직접 구현하는 리팩터링 기법입니다.
리팩터링 패턴:메서드의 인라인화
"메서드의 인라인화(Method Inlining)"는 특정 메서드의 본문을 해당 메서드를 호출하는 모든 곳에 직접 삽입하고, 그 메서드를 제거하는 리팩터링 기법입니다. 이 패턴은 코드의 간결성과 명확성을 높이기 위해 사용됩니다.
적용 시기:
- 메서드가 너무 간단하거나 메서드의 이름이 실제 동작을 제대로 설명하지 못할 때.
- 메서드가 한 곳에서만 사용되고 별도로 분리될 필요가 없을 때.
- 중복되는 메서드나 비슷한 로직을 가진 여러 메서드를 통합하고 싶을 때.
진행 방법:
- 메서드 호출을 대체할 수 있는 메서드 본문을 찾습니다.
- 해당 메서드를 호출하는 곳에서 메서드의 본문으로 대체합니다.
- 테스트를 실행하여 리팩터링으로 인한 오류가 없는지 확인합니다.
- 원래의 메서드를 제거합니다.
예제:
class Example {
int baseValue = 10;
int getValue() {
return calculateValue();
}
int calculateValue() {
return baseValue * 2;
}
}
// 메서드 인라인화 후:
class Example {
int baseValue = 10;
int getValue() {
return baseValue * 2;
}
}
위 예제에서 calculateValue 메서드는 단순히 값을 두 배로 만드는 간단한 연산만 수행합니다. 이러한 경우 메서드를 별도로 분리하는 것이 코드의 가독성을 해칠 수 있기 때문에, getValue 메서드 내에서 직접 연산을 수행하도록 메서드를 인라인화했습니다.
장점:
- 간결한 코드: 불필요한 메서드 호출로 인한 코드의 복잡성을 줄일 수 있습니다.
- 명확한 의도: 메서드의 이름과 실제 로직 사이에 괴리가 있을 때, 인라인화를 통해 코드의 의도를 더 명확하게 표현할 수 있습니다.
단, 이 리팩터링 기법을 사용할 때는 과도하게 적용하지 않도록 주의해야 합니다. 너무 많은 로직이 한 곳에 집중되면 코드의 가독성이 떨어질 수 있습니다. 따라서 상황에 맞게 적절히 판단하여 사용해야 합니다.
긴 if 문의 리팩터링
긴 if 문은 코드의 가독성을 저하시키며, 유지 관리가 어려울 수 있습니다. 따라서 긴 if 문을 리팩터링하여 코드의 명확성과 구조를 개선하는 것이 중요합니다. 다음은 긴 if 문을 리팩터링하는 몇 가지 방법입니다.
- 조건문 추출하기 (Extract Condition): 긴 조건문을 별도의 메서드로 추출하여, 메서드 이름을 통해 조건의 의미를 명확하게 할 수 있습니다.
- if (user.isActive() && user.hasValidSubscription() && !user.isBlocked()) { // 로직 } // 리팩터링 후: if (canAccessService(user)) { // 로직 } boolean canAccessService(User user) { return user.isActive() && user.hasValidSubscription() && !user.isBlocked(); }
- 가드절 사용하기 (Using Guard Clauses): 긍정적인 경로를 강조하고, 조건의 부정적인 결과를 빠르게 처리하여 본문의 주 로직에 집중할 수 있습니다.
- if (user != null) { // 로직 } else { return; } // 리팩터링 후: if (user == null) return; // 로직
- 다형성 사용하기 (Using Polymorphism): 객체 지향 프로그래밍에서 다형성을 사용하여 조건문을 제거하고 각 클래스에 적절한 동작을 정의할 수 있습니다.
- 조건문을 매핑으로 대체하기 (Replace Conditional with Mapping): 조건문 대신 사전(dictionary) 또는 매핑을 사용하여 로직을 처리할 수 있습니다.
- 상태나 전략 패턴 사용하기 (Using State or Strategy Pattern): 객체의 상태나 알고리즘이 변경되는 경우, 상태나 전략 패턴을 사용하여 동적으로 동작을 변경하도록 할 수 있습니다.
- 삼항 연산자 사용하기 (Using Ternary Operator): 간단한 조건문은 삼항 연산자를 사용하여 간결하게 표현할 수 있습니다. 하지만 복잡한 로직에서는 삼항 연산자의 남용이 가독성을 해칠 수 있으므로 주의해야 합니다.
장점:
- 가독성 향상: 코드의 의도가 더 명확하게 전달되며, 코드를 읽는 데 필요한 시간이 줄어듭니다.
- 유지 관리 용이: 조건문이 단순화되면 코드 변경이나 추가가 더 쉬워집니다.
- 오류 감소: 조건문의 복잡성이 줄어들면 실수할 가능성도 줄어듭니다.
긴 if 문을 리팩터링할 때는 개별 상황에 맞는 방법을 선택하여 적용해야 합니다. 중요한 것은 코드의 명확성과 유지 관리성을 향상시키는 것입니다.
일반성 제거
"일반성 제거(Remove Generality)"는 너무 과도하게 일반화된 코드나 구조를 좀 더 구체적이고 단순한 형태로 변경하는 리팩터링 기법입니다. 개발 초기 단계나 예상된 확장성을 고려하여 너무 과도하게 일반화된 코드는 종종 복잡성을 증가시키고 가독성을 해칠 수 있습니다.
일반성 제거의 예시:
- 불필요한 인터페이스 제거: 특정 기능에 대한 여러 구현이 없는데도 인터페이스를 사용한 경우, 인터페이스를 제거하고 구체 클래스를 직접 사용할 수 있습니다.
- 불필요한 추상 클래스 제거: 추상 클래스가 특별한 추가 기능 없이 하나의 구체 클래스만을 가지고 있는 경우, 추상 클래스를 제거하고 해당 구체 클래스만을 사용할 수 있습니다.
- 파라미터 제거: 메서드가 받는 파라미터 중 일부가 실제로 사용되지 않는 경우, 해당 파라미터를 제거할 수 있습니다.
- 불필요한 팩토리나 생성자 제거: 객체 생성에 여러 방법이나 조건이 존재하지 않는데도 복잡한 팩토리나 생성자가 있는 경우, 이를 단순화할 수 있습니다.
장점:
- 간결성: 코드가 더 간단해지며, 불필요한 부분을 제거함으로써 코드의 길이가 줄어듭니다.
- 가독성: 과도한 일반화로 인한 복잡성이 줄어들면 코드를 이해하는 데 걸리는 시간이 줄어듭니다.
- 유지 관리: 불필요한 부분이 제거되면 유지 관리가 더 쉬워집니다.
단점:
- 확장성: 너무 많은 일반성을 제거하면 미래의 확장성이 떨어질 수 있습니다. 따라서 확장성과 단순성 사이에서 적절한 균형을 찾아야 합니다.
일반성 제거는 코드의 복잡성을 줄이고 가독성을 향상시키는 데 도움이 됩니다. 그러나 과도하게 적용하면 확장성이 떨어질 수 있으므로 주의해야 합니다.
리팩터링 패턴: 메서드 전문화
"메서드 전문화(Method Specialization)"는 특정 메서드가 너무 많은 기능이나 책임을 가지고 있을 때, 이를 여러 개의 더 구체적인 기능을 수행하는 메서드로 나누는 리팩터링 패턴입니다.
일반적으로 메서드가 여러 가지 작업을 수행하거나 다양한 파라미터에 따라 다른 동작을 하는 경우, 이 메서드는 전문화되어야 합니다. 메서드 전문화를 통해 각각의 메서드는 하나의 책임만을 가지게 되어 코드의 가독성과 유지 관리성이 향상됩니다.
적용 시기:
- 메서드가 여러 가지 작업을 수행할 때.
- 메서드가 다양한 파라미터에 따라 다른 동작을 수행할 때.
- 메서드의 이름만으로 그 기능을 정확히 파악하기 어려울 때.
진행 방법:
- 메서드를 분리할 부분을 결정합니다.
- 새로운 메서드를 생성하고, 원래 메서드에서 분리할 부분의 코드를 이동시킵니다.
- 적절한 이름을 지어서 해당 기능을 잘 나타내도록 합니다.
- 원래의 메서드에서는 새로 생성된 메서드를 호출하도록 변경합니다.
- 테스트를 실행하여 리팩터링으로 인한 오류가 없는지 확인합니다.
예제:
void updateUserInfo(String name, boolean changeAddress, Address newAddress) {
user.setName(name);
if (changeAddress) {
user.setAddress(newAddress);
}
}
// 메서드 전문화 후:
void updateUserName(String name) {
user.setName(name);
}
void updateUserAddress(Address newAddress) {
user.setAddress(newAddress);
}
장점:
- 가독성 향상: 각 메서드는 하나의 명확한 기능만 수행하므로, 코드를 읽는 사람이 해당 메서드의 기능을 더 쉽게 이해할 수 있습니다.
- 유지 관리 용이: 각 메서드가 하나의 책임만을 가지므로, 수정이 필요할 때 해당 메서드만을 집중적으로 수정할 수 있습니다.
- 재사용성 증가: 전문화된 메서드는 다른 곳에서도 재사용하기 쉽습니다.
메서드 전문화는 코드의 명확성을 높이는데 중요한 리팩터링 기법 중 하나입니다. 하지만 과도한 메서드 분리는 코드의 구조를 복잡하게 만들 수 있으므로 적절한 수준에서의 전문화가 필요합니다.
switch가 허용되는 유일한 경우
switch 문을 피하는 것은 많은 프로그래밍 권장사항과 리팩터링 가이드라인 중 하나입니다. switch 문은 잘못 사용될 경우 코드의 가독성을 해치고 유지 관리를 어렵게 할 수 있습니다. 다음은 switch 문의 사용을 피해야 하는 주요 이유와 그에 대한 대안들입니다.
switch 문의 문제점:
- 확장성: 새로운 조건이나 케이스를 추가할 때마다 switch 문을 수정해야 합니다. 이는 OCP(Open/Closed Principle)에 위배되며, 코드 변경을 빈번하게 요구합니다.
- 가독성: 긴 switch 문은 코드의 가독성을 저하시키고, 해당 코드 부분의 책임을 파악하기 어렵게 만듭니다.
- 중복: 여러 switch 문에서 같은 로직을 사용하게 되면 중복 코드가 발생할 수 있습니다.
대안 방법:
- 다형성 사용: 객체 지향 프로그래밍의 다형성을 이용하여 각각의 조건에 대한 동작을 각각의 객체나 메서드로 분리할 수 있습니다.
- 예를 들어, 동물의 종류에 따라 소리를 내는 경우, 각 동물 클래스마다 makeSound 메서드를 정의하는 방식입니다.
- 매핑 사용: 사전(dictionary)나 맵(map)을 사용하여 특정 키에 대한 동작이나 값을 매핑할 수 있습니다. 이 방법은 간단한 경우에 특히 유용합니다.
- 전략 패턴 사용: 전략 패턴은 동일 계열의 알고리즘을 정의하고, 각각을 캡슐화하여 상호 교체가 가능하도록 합니다.
예제:
// switch 문 사용
switch (animalType) {
case "dog":
return "bark";
case "cat":
return "meow";
default:
return "unknown";
}
// 다형성을 사용한 리팩터링
abstract class Animal {
abstract String makeSound();
}
class Dog extends Animal {
@Override
String makeSound() {
return "bark";
}
}
class Cat extends Animal {
@Override
String makeSound() {
return "meow";
}
}
이런 방식으로 switch 문을 대체함으로써 코드의 확장성, 유지 관리성, 가독성을 향상시킬 수 있습니다.
규칙: switch를 사용하지 말 것
switch 문을 피하는 것은 많은 프로그래밍 권장사항과 리팩터링 가이드라인 중 하나입니다. switch 문은 잘못 사용될 경우 코드의 가독성을 해치고 유지 관리를 어렵게 할 수 있습니다. 다음은 switch 문의 사용을 피해야 하는 주요 이유와 그에 대한 대안들입니다.
switch 문의 문제점:
- 확장성: 새로운 조건이나 케이스를 추가할 때마다 switch 문을 수정해야 합니다. 이는 OCP(Open/Closed Principle)에 위배되며, 코드 변경을 빈번하게 요구합니다.
- 가독성: 긴 switch 문은 코드의 가독성을 저하시키고, 해당 코드 부분의 책임을 파악하기 어렵게 만듭니다.
- 중복: 여러 switch 문에서 같은 로직을 사용하게 되면 중복 코드가 발생할 수 있습니다.
대안 방법:
- 다형성 사용: 객체 지향 프로그래밍의 다형성을 이용하여 각각의 조건에 대한 동작을 각각의 객체나 메서드로 분리할 수 있습니다.
- 예를 들어, 동물의 종류에 따라 소리를 내는 경우, 각 동물 클래스마다 makeSound 메서드를 정의하는 방식입니다.
- 매핑 사용: 사전(dictionary)나 맵(map)을 사용하여 특정 키에 대한 동작이나 값을 매핑할 수 있습니다. 이 방법은 간단한 경우에 특히 유용합니다.
- 전략 패턴 사용: 전략 패턴은 동일 계열의 알고리즘을 정의하고, 각각을 캡슐화하여 상호 교체가 가능하도록 합니다.
예제:
// switch 문 사용
switch (animalType) {
case "dog":
return "bark";
case "cat":
return "meow";
default:
return "unknown";
}
// 다형성을 사용한 리팩터링
abstract class Animal {
abstract String makeSound();
}
class Dog extends Animal {
@Override
String makeSound() {
return "bark";
}
}
class Cat extends Animal {
@Override
String makeSound() {
return "meow";
}
}
이런 방식으로 switch 문을 대체함으로써 코드의 확장성, 유지 관리성, 가독성을 향상시킬 수 있습니다.
if 제거하기
if문을 제거하려는 움직임은 코드의 복잡성을 줄이고 가독성을 향상시키기 위한 목적에서 비롯됩니다. if문은 필요할 때 매우 유용하지만 과도하게 사용되면 코드가 복잡해지고 유지 관리가 어려워질 수 있습니다. 여기에 if문을 줄이거나 제거하는 방법과 그 이유를 간단히 설명하겠습니다.
1. 다형성 사용: 객체 지향 언어에서는 다형성을 이용해 if문을 줄일 수 있습니다. 서브클래스나 인터페이스를 이용해 동일한 메서드 호출로 다양한 동작을 수행하게 할 수 있습니다.
예시:
interface Animal {
void sound();
}
class Dog implements Animal {
public void sound() {
System.out.println("bark");
}
}
class Cat implements Animal {
public void sound() {
System.out.println("meow");
}
}
// 이를 사용하여 if 문 없이 동물의 소리를 출력
Animal animal = getAnimal(); // 어떤 동물인지는 이 시점에서 모르지만,
animal.sound(); // 올바른 소리를 출력
2. 매핑 구조 사용: 매핑 구조(예: 딕셔너리, 해시맵)를 사용하여 키-값 쌍으로 데이터를 저장하고 검색할 수 있습니다.
예시:
Map<String, String> animalSounds = new HashMap<>();
animalSounds.put("dog", "bark");
animalSounds.put("cat", "meow");
String animalType = "dog";
System.out.println(animalSounds.get(animalType));
3. 전략 패턴 사용: 전략 패턴은 알고리즘을 정의하고 각각을 캡슐화하여 상호 교체가 가능하도록 합니다.
4. 조건 객체 사용: 조건을 객체로 캡슐화하여 if문 없이 동작을 수행하도록 만듭니다.
이점:
- 가독성 향상: if문이 많으면 코드의 흐름을 파악하기 어려워질 수 있습니다. if문을 줄이면 코드의 가독성이 향상됩니다.
- 확장성: 새로운 조건이나 기능을 추가할 때 if문이 많으면 수정이 어렵습니다. if문을 줄이면 코드의 확장성이 좋아집니다.
- 오류 감소: if문이 많으면 조건을 잘못 판단하거나 누락할 가능성이 있습니다.
그러나 항상 if문을 제거해야 하는 것은 아닙니다. 경우에 따라 if문이 더 명확하고 간단할 수 있습니다. 주요 목표는 코드의 가독성과 유지 관리성을 향상시키는 것입니다.
코드 중복 처리
코드 중복은 많은 소프트웨어 개발 문제의 원인 중 하나입니다. 중복 코드는 버그를 일으키기 쉽고, 코드의 유지 관리를 어렵게 하며, 코드의 가독성을 저하시킵니다. 따라서 코드 중복을 제거하는 것은 효과적인 리팩터링의 주요 목표 중 하나입니다.
코드 중복을 처리하는 방법들을 아래에 설명하겠습니다.
1. 메서드 추출 (Extract Method) 특정 코드 부분이 여러 위치에서 반복될 때, 그 부분을 별도의 메서드로 추출하고 해당 메서드를 호출하도록 변경합니다.
예시:
public void printDetails() {
printName();
printAddress();
System.out.println("Age: " + age); // 중복
System.out.println("Gender: " + gender); // 중복
}
public void printSummary() {
printName();
System.out.println("Age: " + age); // 중복
System.out.println("Gender: " + gender); // 중복
}
// 리팩터링 후
public void printDetails() {
printName();
printAddress();
printPersonalDetails();
}
public void printSummary() {
printName();
printPersonalDetails();
}
private void printPersonalDetails() {
System.out.println("Age: " + age);
System.out.println("Gender: " + gender);
}
2. 클래스 추출 (Extract Class) 하나의 클래스에서 중복적으로 사용되는 속성과 메서드가 있을 때, 이들을 새로운 클래스로 분리합니다.
3. 템플릿 메서드 패턴 사용 공통 로직을 슈퍼클래스에서 정의하고, 차이점만을 서브클래스에서 구현하도록 합니다.
4. 상속 또는 컴포지션 사용 공통 로직이나 데이터를 기반 클래스나 컴포넌트로 이동시키고, 이를 상속받거나 포함시켜 중복을 제거합니다.
5. 고차 함수 사용 특히 함수형 프로그래밍에서 공통적인 동작 패턴을 가진 함수들의 중복을 제거하기 위해 고차 함수를 사용합니다.
6. 데이터 중복 제거 데이터 중복도 코드 중복의 원인이 될 수 있습니다. 상수, 설정 값, 룩업 테이블 등을 중앙화하거나 공유 리소스로 만들어 데이터 중복을 방지합니다.
이점:
- 유지 관리: 중복 코드를 제거하면 코드 변경이나 버그 수정이 한 곳에서만 이루어질 수 있습니다.
- 가독성: 중복을 제거하면 코드의 길이가 줄어들고, 구조가 명확해져 가독성이 향상됩니다.
- 오류 감소: 중복 코드는 동일한 로직 변경을 여러 위치에서 해야 하는 상황을 초래하므로, 오류를 발생시키기 쉽습니다.
항상 중복을 제거하는 것이 최선인 것은 아닙니다. 경우에 따라 중복이 코드의 명확성을 높일 수도 있습니다. 주요 목표는 코드의 가독성, 유지 관리성, 오류 발생 가능성을 최적화하는 것입니다.
규칙: 인터페이스에서만 상속받을 것
"인터페이스에서만 상속받을 것"이라는 규칙은 객체 지향 프로그래밍, 특히 자바와 같은 언어에서 인기 있는 원칙 중 하나입니다. 이 원칙은 여러 가지 이유로 권장됩니다. 여기에 그 이유와 함께 이 규칙을 따르는 것의 장점을 설명하겠습니다.
1. 다중 상속의 문제점 방지: 대부분의 객체 지향 언어는 다중 상속의 문제점을 피하기 위해 실제 클래스에서의 다중 상속을 허용하지 않습니다. 그러나 인터페이스를 사용하면 여러 인터페이스로부터 상속받을 수 있습니다. 이렇게 하면 구현의 중복 없이 여러 동작을 조합할 수 있습니다.
2. 구현과 인터페이스 분리: 인터페이스는 어떤 행동을 수행해야 하는지에 대한 계약만을 제공합니다. 실제 구현은 클래스에 있습니다. 이러한 분리는 코드의 유연성을 높이며, 다른 구현을 쉽게 교체할 수 있게 합니다.
3. SOLID 원칙 준수: SOLID는 객체 지향 설계의 원칙 5가지를 나타내는 약어입니다. 여기서 "I"는 Interface Segregation Principle(인터페이스 분리 원칙)을 나타냅니다. 이 원칙은 "클래스는 자신이 사용하지 않는 인터페이스는 상속받아서는 안 된다"는 것을 의미합니다. 인터페이스만을 상속받도록 하는 것은 이 원칙을 따르는 방법 중 하나입니다.
4. 캡슐화 강화: 인터페이스만을 상속받으면 클래스의 내부 구현을 숨길 수 있습니다. 이는 캡슐화 원칙을 강화하며, 내부 변경에 따른 외부의 영향을 최소화합니다.
5. 재사용성 향상: 인터페이스를 통한 상속은 코드의 재사용성을 향상시킵니다. 다양한 구현을 가진 여러 클래스에서 동일한 인터페이스를 사용할 수 있습니다.
결론: 인터페이스에서만 상속받는 것은 객체 지향 설계의 강력한 원칙 중 하나입니다. 이 원칙은 코드의 유연성, 재사용성, 가독성을 향상시키며, 유지 관리를 용이하게 합니다. 하지만 모든 상황에서 이 원칙을 강제하는 것은 아닙니다. 상황과 요구 사항에 따라 적절한 설계 방법을 선택하는 것이 중요합니다.
클래스에 있는 코드의 중복은 다 무엇일까??
클래스 내부의 코드 중복은 여러 형태로 나타날 수 있으며, 그 중복은 종종 프로그램의 유지 보수를 어렵게 만듭니다. 코드 중복의 여러 형태와 그 예시를 살펴보겠습니다:
- 동일한 로직을 가진 메서드 중복: 한 클래스 내에서 비슷하거나 동일한 기능을 하는 여러 메서드가 존재할 수 있습니다.
- public int addTen(int value) { return value + 10; } public int addTenToValue(int value) { return value + 10; }
- 동일한 코드 블록 중복: 메서드 내에서 동일한 코드 블록이 반복될 수 있습니다.
- public void process() { // ... 중복된 코드 시작 ... if (someCondition) { doSomething(); } // ... 중복된 코드 끝 ... // ... 다른 로직 ... // ... 중복된 코드 시작 ... if (someCondition) { doSomething(); } // ... 중복된 코드 끝 ... }
- 상수와 리터럴 중복: 동일한 값을 여러 위치에서 직접 사용하는 경우가 있습니다. 이러한 중복은 상수를 사용해 제거할 수 있습니다.
- public double calculateArea() { return 3.14 * radius * radius; // 3.14는 중복된 리터럴 }
- 중복된 초기화 코드: 생성자나 초기화 블록에서 중복되는 초기화 코드가 있을 수 있습니다.
- public MyClass() { this.someField = "default"; this.anotherField = 100; } public MyClass(String param) { this.someField = param; this.anotherField = 100; // 중복 초기화 }
- 중복된 예외 처리: 여러 메서드에서 동일한 예외 처리 방식을 사용하는 경우가 있습니다.
- public void methodA() { try { // 로직 } catch (SomeException e) { logError(e); // 중복된 예외 처리 } } public void methodB() { try { // 로직 } catch (SomeException e) { logError(e); // 중복된 예외 처리 } }
이러한 중복을 발견하면 적절한 리팩터링 기법을 사용하여 제거하는 것이 좋습니다. 중복 코드 제거는 코드의 유지 보수를 쉽게 만들고, 가독성을 높이며, 잠재적인 버그의 위험을 줄일 수 있습니다.
복잡한 if 체인 구문 리팩터링
복잡한 if 체인 구문은 코드의 가독성을 저하시키고, 유지 보수를 어렵게 만들 수 있습니다. 따라서 이러한 if 체인을 리팩터링하는 것은 중요한 작업 중 하나입니다. 여기에 복잡한 if 체인 구문을 리팩터링하는 몇 가지 방법을 제시하겠습니다:
1. 조건문 분리 (Decompose Conditional) 복잡한 조건을 별도의 메서드로 추출하여 각 조건을 설명하는 이름을 부여합니다. 이렇게 하면 코드의 가독성이 향상됩니다.
// 리팩터링 전
if (date.before(SUMMER_START) || date.after(SUMMER_END)) {
charge = winterRate * quantity;
} else {
charge = summerRate * quantity;
}
// 리팩터링 후
if (isWinter(date)) {
charge = winterRate * quantity;
} else {
charge = summerRate * quantity;
}
...
private boolean isWinter(Date date) {
return date.before(SUMMER_START) || date.after(SUMMER_END);
}
2. 다형성 사용 (Replace Conditional with Polymorphism) 복잡한 if 또는 switch 문을 사용하여 여러 유형의 동작을 처리하는 경우, 각 유형을 별도의 클래스로 분리하고 공통의 인터페이스 또는 기본 클래스를 사용하여 다형성을 적용합니다.
// 리팩터링 전
class Bird {
String type;
//...
int getSpeed() {
switch(type) {
case "EUROPEAN":
return getBaseSpeed();
case "AFRICAN":
return getBaseSpeed() - getLoadFactor();
case "NORWEGIAN_BLUE":
return (isNailed) ? 0 : getBaseSpeed() * 1.5;
}
throw new RuntimeException("Should be unreachable");
}
}
// 리팩터링 후
abstract class Bird {
abstract int getSpeed();
}
class European extends Bird {
int getSpeed() {
return getBaseSpeed();
}
}
class African extends Bird {
int getSpeed() {
return getBaseSpeed() - getLoadFactor();
}
}
class NorwegianBlue extends Bird {
int getSpeed() {
return (isNailed) ? 0 : getBaseSpeed() * 1.5;
}
}
3. 조건문을 지우기 (Remove Control Flag)if 문에서 조건에 따라 제어 플래그의 값을 변경하는 패턴을 찾아, return 문 또는 break 문을 사용하여 조기에 함수나 루프를 종료합니다.
4. 조건문 합치기 또는 분리 유사한 조건의 if 문이 여러 개 있을 경우, 조건을 합치거나 분리하여 로직을 명확하게 합니다.
복잡한 if 체인 구문을 리팩터링함으로써 코드의 가독성을 높이고, 유지 보수를 용이하게 만들며, 잠재적인 버그의 위험을 줄일 수 있습니다.
필요없는 코드 제거하기
필요없는 코드, 즉 "데드 코드(Dead Code)"는 프로그램의 기능에 아무런 영향을 주지 않는 코드를 의미합니다. 데드 코드는 코드의 가독성을 저하시키고, 유지 보수를 복잡하게 만들며, 프로그램의 크기를 불필요하게 증가시킬 수 있기 때문에 제거하는 것이 좋습니다.
다음은 필요없는 코드의 일반적인 예시와 이를 어떻게 제거하는지에 대한 방법입니다:
1. 사용되지 않는 변수 및 상수: 사용되지 않는 변수나 상수는 삭제합니다.
int unusedVariable = 10; // 사용되지 않는 변수
2. 사용되지 않는 메서드: 어디에서도 호출되지 않는 메서드는 삭제합니다.
public void unusedMethod() {
// ... 로직 ...
}
3. 사용되지 않는 클래스 또는 인터페이스: 어디에서도 참조되지 않는 클래스나 인터페이스는 삭제합니다.
4. 사용되지 않는 모듈 또는 패키지: 모듈이나 패키지 전체가 사용되지 않는 경우, 이를 프로젝트에서 제거합니다.
5. 사용되지 않는 라이브러리 또는 의존성: 프로젝트의 의존성 관리 도구(예: Maven, Gradle 등)를 사용하여 불필요한 라이브러리를 삭제합니다.
6. 주석 처리된 코드: 주석 처리된 코드는 혼동을 일으킬 수 있으므로, 필요할 경우 버전 관리 시스템(예: Git)의 히스토리를 참조하고 삭제합니다.
7. 항상 동일한 결과를 반환하는 조건문: 코드의 로직에 따라 항상 동일한 결과를 반환하는 조건문은 제거하고, 해당 결과만 반환하도록 수정합니다.
8. 빈 메서드 또는 빈 블록: 특별한 의도 없이 비어 있는 메서드나 블록은 삭제합니다.
9. 중복된 코드: 중복되는 코드는 하나의 공통 메서드로 리팩터링하거나 제거합니다.
필요없는 코드를 제거함으로써 코드베이스를 깔끔하게 유지하고, 팀원들의 혼란을 줄이며, 유지 보수를 간소화할 수 있습니다. 또한, 코드 리뷰나 정적 코드 분석 도구를 사용하여 정기적으로 데드 코드를 찾아 제거하는 것이 좋습니다.
리팩터링 패턴: 삭제 후 컴파일하기
"삭제 후 컴파일하기(Remove and Compile)"는 리팩터링 과정에서 데드 코드(사용되지 않는 코드)를 찾아내고 제거하는 간단하면서도 효과적인 방법입니다. 이 방법의 주요 아이디어는 사용되지 않는 코드나 불필요한 코드를 실제로 삭제하고, 컴파일러의 피드백을 통해 해당 코드가 실제로 필요없는지 확인하는 것입니다.
다음은 "삭제 후 컴파일하기" 방법을 사용하는 단계입니다:
- 삭제 대상 선정: 사용되지 않는 것으로 의심되는 메서드, 변수, 클래스, 인터페이스, 모듈 등을 찾습니다.
- 코드 삭제: 의심되는 코드를 실제로 삭제합니다.
- 컴파일: 코드를 삭제한 후 프로젝트를 컴파일합니다.
- 컴파일 오류 확인:
- 오류가 발생하지 않는다면: 해당 코드는 불필요하거나 사용되지 않는 코드였습니다. 삭제하는 것이 적절했습니다.
- 오류가 발생한다면: 해당 코드는 어딘가에서 사용되고 있었거나 필요한 코드였습니다. 오류 메시지를 통해 어떤 부분에서 문제가 발생했는지 파악하고, 필요한 코드를 복원하거나 적절히 수정합니다.
- 테스트 실행: 코드 변경 후에 항상 관련 테스트를 실행하여 코드의 정상 작동을 확인합니다. 테스트가 없다면 새로운 테스트 케이스를 작성하는 것이 좋습니다.
이 방법의 장점은 다음과 같습니다:
- 간단하고 직관적: 복잡한 분석이나 도구가 필요하지 않습니다.
- 컴파일러의 도움: 컴파일러가 불필요한 코드나 사용되지 않는 코드를 알려줍니다.
단, "삭제 후 컴파일하기" 방법은 컴파일 시점에서의 오류만을 잡아낼 수 있으므로, 런타임에서의 문제나 동작 오류는 감지하지 못할 수 있습니다. 따라서 항상 테스트 케이스를 함께 실행하여 코드의 안정성을 확보하는 것이 중요합니다.
요약:
- if 문에서 else를 사용하지 말 것. 그리고 switch를 사용하지 말 것 규칙에 따르면 else 또는 switch는 프로그램의 가장자리에만 있어야 합니다. else와 switch 모두 낮은 수준의 제어 흐름 연산자입니다. 애플리케이션의 핵심에서는 클래스로 타입 코드 대체 및 클래스로의 코드 이관 리팩터링 패턴을 사용해서 switch와 연속된 else if 구문을 높은 수준의 클래스와 메서드로 대체해야 합니다.
- 지나치게 일반화된 메서드는 리팩터링을 방해할 수 있습니다. 이런 경우 불필요한 일반성을 제거하기 위해 메서드 전문화 리팩터링 패턴을 사용할 수 있습니다.
- 인터페이스에서만 상속받을 것 규칙은 추상 클래스와 클래스 상속을 사용해 코드를 재사용하는 것을 방지합니다. 이러한 유형의 상속은 불필요하게 긴밀한 커플링을 발생시키기 때문입니다.
- 리팩터링 후 정리를 위한 두 가지 리팩터링 패턴을 추가했습니다.
- 메서드의 인라인화와 커파일하기로 더이상 가독성에 도움이 되지 않는 메서드를 제거할 수 있습니다.
'[F-Lab 멘토링 학습]' 카테고리의 다른 글
| 프로세스와 스레드의 차이 (0) | 2023.11.11 |
|---|---|
| Call-by-value vs Call-by-reference (0) | 2023.11.11 |
| (스터디) 파이브 라인스 오브 코드 리뷰 1-3장 (0) | 2023.11.04 |
| 성능 최적화를 위해 어떤 방법과 도구를 사용하나요?에 대한 답변 (1) | 2023.11.04 |
| HTTPS 암호화 작동원리 (0) | 2023.11.04 |