깨끗한 코드는 읽기도 좋아야 하지만 안정성도 높아야한다.
오류처리를 프로그램 논리와 분리하면 독립적인 추론이 가능해지며 코드 유지보수성도 크게 높아진다.
우리에겐 해당하지 않는 내용.
Checked Exception
OCP 위반 : 하위 단계의 변경으로 인해 상위 단계의 모든 메서드 선언부를 수정해야 함.
캡슐화 깨짐 : 모든 함수가 최하위 함수에서 던지는 예외를 알아야 함.
public void a() throws FileNotFoundException { // 또 예외를 잡고
b();
}
private void b() throws FileNotFoundException { // 또 예외를 잡고
c();
}
private void c() throws FileNotFoundException {
FileInputStream stream = new FileInputStream("fileName");
}
→ 때로는 확인된 예외가 유용하기도 하지만, 일반적으로 의존성이라는 비용이 이익보다 크다.
public UserLevel getUserLevel(Long id) {
try {
User user = userRepository.findById(id);
return user.getLevel();
} catch (UserNotFoundException e) {
return UserLevel.BASIC;
}
}
-> user를 id로 찾아와서 있으면 getLevel을 없으면 BASIC을 리턴해라
: try/catch로 되어있어서 코드를 이해하는 흐름을 방해
public UserLevel getUserLevelOrDefault(Long id) {
User user = userRepository.findById(id);
if (user == null) {
return UserLevel.BASIC;
} else {
return user.getLevel();
}
}
→ 예외를 없애고 흐름에 따라 읽히게 사용
특수 사례 객체 사용 가능.
List<Employee> employees = getEmployees();
if (employee != null) {
for(Employee e : employees) {
totalPay += e.getPay();
}
}
직원 리스트를 가져와 총 급여를 구하는 로직
→ getEmployees가 null을 반환해 if 절이 생겼는데 굳이 null을 반환할 필요가 있을까?
List<Employee> employees = getEmployees();
for(Employee e : employees) {
totalPay += e.getPay();
}
/////////////////
public List<Employee> getEmployees() {
if (직원이 없다면)
return Collections.emptyList();
}
null인 경우 Collections.emptyList()를 리턴해주게 변경 : 특수 사례 객체
→ NullPointeException이 발생할 가능성이 줄어들었다.
public double xProjection(Point p1, Point p2) {
return (p2.x - p1.x) * 1.5
}
→ NullPointerException의 가능성 있다.
public double xProjection(Point p1, Point p2) {
if (p1 == null || p2 == null) {
throw InvalidArgumentException("~~");
}
return (p2.x - p1.x) * 1.5
}
→ 새로운 예외 유형으로 핸들링
: RuntimeException 발생 보다는 조금 나으나, 예외 처리를 해주어야 한다.
public double xProjection(Point p1, Point p2) {
assert p1 != null : "p1 should not be null";
assert p2 != null : "p2 should not be null";
return (p2.x - p1.x) * 1.5
}
→ assert 문을 사용해 핸들링
: 코드 읽기는 편하나, null 전달 받으면 여전히 RuntimeException 발생
val l = if (b != null) b.length else -1
val b: String? = "Kotlin"
if (b != null && b.length > 0) {
print("String of length ${b.length}")
} else {
print("Empty string")
}
?.
val a = "Kotlin"
println(a?.length) // Unnecessary safe call
val b: String? = null
println(b?.length)
?:
// if 조건문으로 처리한 경우
val l: Int = if (b != null) b.length else -1
// 동일한 코드를 ?: 연산자 사용 시
// null인 경우 default 값 할당
val l = b?.length ?: -1
!!
// 이렇게 해 놓고 null을 넣으면 NPE가 발생
val l = b!!.length
as?
// casting을 시도하고, casting이 불가능 하면 null을 반환
val aInt: Int? = a as? Int
val nullableList: List<Int?> = listOf(1, 2, null, 4)
val intList: List<Int> = nullableList.filterNotNull()
let
함수fun sendEmailTo(email: String) {
println("Sending email to $email")
}
// not null인 경우 특정 구문을 수행
fun main(args: Array) {
var email: String? = "yole@example.com"
email?.let { sendEmailTo(it) }
email = null
email?.let { sendEmailTo(it) }
}
lateinit
not null이지만 초기화 작업을 바로 하지 않아도 됨.
시스템 역시 깨끗해야 한다.
깨끗하지 못한 아키텍처는 도메인 논리를 흐리며 기민성을 떨어뜨린다.
깨끗한 시스템
= 떠오름
하위 계층(구성 요소)에는 없는 특성이나 행동이 상위 계층(전체 구조)에서 자발적으로 돌연히 출현하는 현상