쾌락코딩

클린코드_오류처리

|

여기저기 흩어진 오류 처리 코드는 실제 코드가 하는 일을 파악하기 힘들게 한다. 오류 처리 코드로 인해 프로그램 논리를 이해하기 어려워진다면 깨끗한 코드라 부르기 어렵다.

오류 코드보다 예외를 사용하라

먼저 오류 코드를 사용해서 급급하게 오류를 처리하는 코드부터 보자.

public class DeviceController {
    ...
    public void sendShutDown() {
        DeviceHandle handle = getHandle(DEV1);
        // 디바이스 상태를 점검한다.'
        if (handle != DeviceHandle.INVALID) {
            // 레코드 필드에 디바이스 상태를 저장한다.
            retriveDeviceRecord(handle);
            //디바이스가 일시정지 상태가 아니라면 종료한다.
            if (record.getStatus() != DEVICE_SUSPENDED) {
                pauseDevice(handle);
                clearDeviceWorkQueue(handle);
                closeDevice(handle);
            } else {
                logger.log("Device suspended. Unable to shut down");
            }
        } else {
            logger.log("Invalid handle for: " + DEV1.toString());
        }
    }
    ...
}

위와 같이 사용하면 호출자 코드가 복잡해 진다. 함수를 호출한 즉시 오류를 확인해야 하기 때문이다. 따라서 오류가 발생하면 예외를 던지는 편이 낫다. 그러면 호출자 코드가 더 딸끔해진다. 아래 코드를 보고 비교해보자.

public class DeviceController {
    ...
    public void shutDown() {
        try {
            tryToShutDown();
        } catch (DeviceShutDownError e) {
            logger.log(e);
        }
    }

    private void tryToShutDown() throws DeviceShutDownError {
        DeviceHandle handle = getHandle(DEV1);
        DeviceHnadle record = retrieveDeviceRecord(handle);

        pauseDevice(handle);
        clearDeviceWorkQueue(handle);
        closeDevice(handle);
    }

    private DviceHandle getHandle(DeviceID id) {
        ...
        throw new DeviceShutDownError("Invalid handle for: " + id.toString())'
        ...
    }

    ...
}

코드가 확실히 깔끔해졌다. 품질도 나아졌다. 앞서 뒤섞였던 개념, 즉 디바이스를 종료하는 알고리즘과 오류를 처리하는 알고리즘을 분리했기 때문이다.

오류 처리 작성 팁

테스트 코드를 작성하자. 먼저 강제로 예외를 일으키는 테스트 케이스를 작성한 후 테스트를 통과하게 코드를 작성하는 방법을 권장한다. 그러면 자연스럽게 try 블록의 트랜잭션 범위부터 구현하게 되므로 범위 내에서 트랜잭션 본질을 유지하기 쉬워진다.

예를 들자면 테스트 코드에 작성된 코드가 아래와 같다고 하자.

public List<RecordedGrip> retireveSection(String sectionName) {
    try {
        FileInputStream strem = new FileInputStream(sectionName);
    } catch (Exception e) {
        throw new StorageExeption("retrieval error", e);
    }
    return new ArrayList<RecordedGrip>();
}

잘못된 파일 접근을 시도하게 되면 에러를 뿜어내는 코드다. 이제 리팩토링이 가능하다. catch 블록에서 예외 유형을 좁혀 실제로 FileInputStream 생성자가 던지는 FileNotFoundException을 잡아내자.

public List<RecordedGrip> retireveSection(String sectionName) {
    try {
        FileInputStream strem = new FileInputStream(sectionName);
    } catch (FileNotFoundExeption e) {
        throw new StorageExeption("retrieval error", e);
    }
    return new ArrayList<RecordedGrip>();
}

try-catch 구조로 범위를 정의했으므로 TDD를 사용해 필요한 나머지 논리를 추가하자. 나머지 논리는 FileInputStream을 생성하는 코드와 close 호출문 사이에 넣으며 오류나 예외가 전혀 발생하지 않는다고 가정한다.

예외에 의미를 제공하라

예외를 던질 때는 전후 상황을 충분히 덧붙인다. 그러면 오류가 발생한 원인과 위치를 찾기가 쉬워진다.

오류메시지에 정보를 담아 예외와 함께 던진다. 실패한 연산 이름실패 유형도 언급한다.

호출자를 고려해 예외 클래스를 정의하라

애플리케이션에서 오류를 정의할 때 프로그래머에게 가장 중요한 관심사는 오류를 잡아내는 방법이 되야한다.

아래코드는 오류를 형편 없이 분류한 사례다. 외부 라이브러리가 던질 예외를 모두 잡아낸다.

ACMEPort port = new ACMEPort(12);

try {
    port.open();
} catch (DeviceResponseException e) {
    reportPortError(e);
    logger.log("Device response exception", e);
} catch (ATM1212UnlockedException e) {
    reportPortError(e);
    logger.log("Unlock exception", e);
} catch (GMXError e) {
    reportPortError(e);
    logger.log("Device response exception");
} finally {
    ...
}

대다수 상황에서 우리가 오류를 처리 하는 방식은 (오류를 일으킨 원인과 무관하게) 비교적 일정하다. 오류를 기록하고, 프로그램을 계속 수행해도 좋은지 확인하는 것이다.

코드를 간결하게 고쳐보자. 호출하는 라이브러리 API를 감싸면서 예외 유형 하나를 반환하면 된다.

LocalPort port = new LocalPort(12);
try {
    port.open();
} catch (PortDeviceFailure e) {
    reportError(e);
    logger.log(e.getMessage(), e);
} finally {
    ...
}

new ACMEPort(12)new LocalPort(12)이 된것에 주목하자. LocalPort 클래스는 단순히 ACMEPort 클래스가 던지는 예외를 잡아 변환하는 Wrapper 클래스일 뿐이다.

LocalPort 클래스처럼 ACMEPort를 감싸는 클래스는 매우 유용하다. 실제로 외부 API를 사용할 때는 감싸기 기법이 최선이다. 그 이유는 아래와 같다.

  • 외부 라이브러리와 프로그램 사이의 의존성이 크게 줄어든다.
  • 에러 처리가 간결해진다.
  • 프로그램 테스트가 쉬워진다.
  • 외부 API 설계 방식에 의존하지 않아도 된다.

null을 반환하지 마라

null을 반환하는 습관은 일거리를 늘릴 뿐만 아니라 호출자에게 문제를 떠넘긴다. 누구 하나라도 null확인을 빼먹는다면 애플리케이션이 통제 불능에 빠질지도 모른다. 아래는 null을 반환하는 나쁜 코드다.

public void registerItem(Item item) {
    if (item != null) {
        ItemRegistry registry = peristentStore.getItemRegistry();
        if (registry != null) {
            Item existing = registry.getItem(item.getID());
            if (existing.getBillingPeriod().hasRetailOwner()) {
                existing.register(item);
            }
        }
    }
}

메서드에서 null을 반환하고 싶은 유혹이 든다면 그 대신 예외를 던지거나 특수 사례 객체를 반환하라. 만약 사용하려는 외부 API가 null을 반환한다면 어떡할까? 그럴땐 Wrapper 메서드를 구현해 예외를 던지거나 특수 사례 객체를 반환하는 방식을 고려하자. 아래의 코드를 보자.

List<Employee> employees = getEmployees();
if(employees != null) {
    for(Employee e : employees) {
        totalPay += e.getPay();
    }
}

getEmployees()는 null을 반환할 수도 있다. 혹시 null을 반환하지 않을 수는 없을까?

getEmployees()를 변경해 null 대신 빈 리스트를 반환하면 코드가 훨씬 깔끔해진다.

List<Employee> employees = getEmployees();
for(Employee e : employees) {
    totalPay += e.getPay();
}

public List<Employee> getEmployees() {
    if (..직원이 없다면..)
        return Collections.emptyList();
}

이렇게 하면 NullPointerException이 발생할 가능성도 줄어든다.

null을 전달하지 마라

메서드에서 null을 반환하는 방식도 나쁘지만 메서드로 null을 전달하는 방식은 더 나쁘다.

대다수 프로그래밍 언어는 호출자가 실수로 넘기는 null을 적절히 처리하는 방법이 없다. 그렇다면 애초에 null을 넘기지 못하도록 금지하는 정책이 합리적이다.

클린코드_객체와 자료 구조

|

클래스의 변수들은 왜 대부분 비공개로 정의할까? 그것은 남들이 변수에 의존하지 않게 만들고 싶어서다. 그렇다면 어째서 수많은 프로그래머가 getter와 setter함수를 당연하게 public으로 비공개 변수를 외부에 노출할까?

자료 전달 객체

사실 private 변수에 대해 getter와 setter를 제공해야 하는 상황은 명확하다. 그 클래스가 자료구조(자료 전달 객체 - DTO(Data Transfer Ojbect))이면 그렇게 하는 것이다(단순한 자료 구조에도 getter와 setter를 정의하라는 표준이 존재한다). DTO란 전형적인 공개 변수만 있고 함수가 없는 클래스다. 예를 들어 데이터베이스와 통신하거나 소켓에서 받은 메시지의 구문을 분석할 때 유용한 클래스들이다.

좀 더 일반적인 형태는 ‘빈(bean)’ 구조이다. 빈은 private 변수를 조회/설정 함수로 조작한다. 예시 코드는 아래와 같다.

public class Address {
    private String street;
    private String streetExtra;
    private String city;

    public Address(String street, String streetExtra, String city) {
        this.street = street;
        this.streetExtra = streetExtra;
        this.city = city;
    }

    public String getStreet() {
        return street;
    }

    public String getStreetExtra() {
        return streetExtra;
    }

    ....
}

활성레코드

활성레코드 라 불리는 것은 DTO의 특수한 형태다. public 변수가 있거나 비공개 변수에 getter/setter 함수가 있는 자료구조지만, 대게 save나 find와 같은 탐색 함수도 제공한다.

불행히도 활성 레코드에 비즈니스 규칙 메서드를 추가해 이런 자료 구조를 객체로 취급하는 개발자가 흔하다. 하지만 이는 바람직하지 않다. 그러면 자료 구조도 아니고 객체도 아닌 잡종 구조가 나오기 때문이다. 활성 레코드는 자료 구조로 취급하라.

자료 추상화

객체지향은 구현을 외부로 노출하지 않아야 좋은 코드가 된다.

public class Point {
    private double x;
    private double y;

    ...

    public double getX() {
        return x;
    }

    public double getY() {
        return y;
    }
}

위의 코드처럼 변수를 private으로 선언하더라도 각 값마다 getter와 setter를 제공한다면 구현을 외부로 노출하는 셈이다. 꼭 Point 객체를 생성해서 접근 해야 하지만 결과적으로 구현이 노출되는 것은 같다.

위의 예시처럼 변수 사이에 함수라는 계층을 넣는다고 구현이 저절로 감춰지진 않는다. 즉, DTO에 getter, setter를 추가해서 다룬다고 객체지향 패러다임의 객체가 되진 않는다. 객체란, 내부 구현을 감추고 캡슐화, 추상화를 고려해 기능을 제공하는 것이기 때문이다. 따라서, 추상 인터페이스(꼭 자바 문법의 interface가 아니라 추상적인 메서드가 될 수 도 있다)를 제공해 사용자가 구현을 모른 채 자료의 핵심을 조작할 수 있어야 진정한 의미의 객체(또는 클래스)다.

개발자는 객체가 포함하는 자료를 표현할 가장 좋은 방법을 심각하게 고민해야 한다. 아무 생각 없이 조회/설정 함수를 추가하는 방법이 가장 나쁘다.

잡종 구조

아무 생각 없이 조회/설정 함수를 추가하게 되면 잡종 구조가 나오게 된다. 절반은 객체, 절반은 자료 구조인 것이다. 잡종 구조는 중요한 기능을 수행하는 함수도 있고, 공개 변수나 공개 조회/설정 함수도 있다. 공개 조회/설정 함수는 비공개(private) 변수를 그대로 노출한다. 덕탠에 다른 함수가 절차적인 프로그래밍의 자료 구조 접근 방식처럼 비공개 변수를 사용하고픈 유혹에 빠쥐기 쉽다.

이런 잡종 구조는 새로운 함수는 물론이고 새로운 자료 구조도 추가하기 어렵다. 그러므로 잡종 구조는 되도록 피하는 편이 좋다. 프로그래머가 함수나 타입을 보호할지 공개할지 확신하지 못해 어중간하게 내놓은 설계에 불과하다.

###결론 단순히 자료구로라고 판단된다면 getter, setter 정도(상황에 따라 활성 레코드로)만 있는게 좋고, 객체라면 getter, setter 없이 추상적인 함수(기능)을 제공하자.

리액트 배열의 key 값 존재 이유

|

리액트를 사용하다보면, state로 배열을 관리해야 할 경우가 상당히 많다. 예를 들어 서버로부터 게시글 목록을 받아온다면, 아래와 같은 배열 리스트를 받아서 state로 저장할 수도 있다.

react_key1

그리고 난 후 이 배열을 이용해서, 알맞은 UI를 그리기 위해서 map함수를 사용해 element로 바꾸는 작업을 하곤 한다. 예를 들자면 아래와 같다.

react_key2

key 값은 뭐지?

mapping하는 부분을 보면 key 속성을 볼 수가 있다. 배열의 각 요소마다 고유한 key값을 지정해 주는 속성인데 만약 key값을 생략한다면 어떻게될까?

아무 이상없이 랜더링은 되지만, React는 아래와 같은 경고를 띄운다.

Each child in an array should have a unique “key” prop.

그럼 key값은 언제 필요한 것일까?

요소의 변화를 알아차리기 위해 필요하다

리액트 공식문서를 보면, key는 어떤 아이템이 변화되거나, 추가, 삭제되었는지를 알아차리기 위해 필요하다고 말한다.

리액트는 state에서 변경사항이 있는 부분만 캐치해서 리랜더링 해준다. 리액트 유저라면 알고겠지만, 굳이 변경이 없는 데이터까지 Dom을 조작해서 불필요한 자원을 낭비하지 않겠다는 것이다. 그렇다면 state의 배열에 어떠한 요소가 추가가 된다고 가정해보자. 배열에 어떤 요소를 추가했으니 배열이 변경된 것이라고 생각할 수 있는데, 과연 react는 배열 전체를 리랜더링 할까? 아니면 배열에 추가된 요소 한가지만 다시 리랜더링 할까?

리액트는 참 똑똑하게 배열에 추가된 딱 한가지 요소만 리랜더링한다. 다만, 배열의 key값을 고유하게 넘겨주었을 때만.

어떻게 그럴 수 있을까?

먼저 배열에 새로운 요소가 추가됬을 때를 알아보자.

요소가 추가되었을 때

react_key1 다시한번 이 배열에서, 맨 앞쪽에 아래와 같은 요소가 하나 추가되었다고 해보자.

{id:4, title:"add!", content:"yeah!"}

그리고 나서 아까 보았던 두 번째 사진과 같은 map함수를 돌리고, 랜더링을 하려고하면, 리액트는 기존에 랜더링 했던 배열 요소와 새로운 배열 요소를 비교하게 된다. 예를 들어 이전에 key값이 0이었던 post의 내용과 현재 key값이 0인 post의 값을 비교하는데, 이 예제에서는 key:0인 배열에는 변화가 없다. 따라서 변화로 간주하지 않는다.

key:1, key:2, key:3 역시 마찬가지인데, 배열에 새로운 key값인 4에 해당하는 배열은 기존에 없었다. 따라서 key:4인 배열은 새로 추가된 것으로 간주하고 이것만 랜더링 시켜준다.

요소가 변경되었을 때

이제 요소가 변경되었을 때를 알아보자. 위의 사진에서 첫 번째 요소가 아래와 같이 바꼈다고 가정해보자. react_key3

리액트는 아까와 동일한 방법으로 변경된 요소만 캐치해서 리랜더링한다(map을 돌면서 key값을 post.id로 설정한 것을 잊지 말자)

이미 랜더링 했던 배열 요소와 새로 랜더링 할 배열의 요소를 비교한다. 기존에 key:0 이었던 요소의 내용을 보니, 새로 받은 key:0인 배열과 비교했을 때 title과 content가 바뀐것을 알 수 있다. 그렇다면 key:0인 요소는 바뀐것이다. 이 부분은 리랜더링의 대상이 된다.

나머지 key:1, key:2, key:3은 변경이 없다. 리랜더링 하지 않을 것이다.

key를 map의 index로 하면 안되는 이유

그렇다면 배열을 element화 시키기 위해 map을 사용할 경우, key값을 index로 사용하면 안되는 이유를 알 수 있을 것이다.

배열의 첫 번째 위치에 새로운 값을 넣었다고 쳐보자.

{id:4, title:"add!", content:"yeah!"}

이 값을 배열의 맨 앞에 넣어다고 생각하면 될 것 같다. 그리고 나서 map을 돌릴 때, key를 단순히 index값을 주게 된다면 post.id는 무시된채 아래와 같은 상황이 될 것이다.

key: 0,  {id:4, title: 'add!', content:'yeah!'},
key: 1,  {id:0, title: 'hello!', content:'word'},
key: 2,  {id:1, title: 'myname!', content:'is!'},
key: 3,  {id:2, title: 'lee!', content:'yong!'},
key: 4,  {id:3, title: 'jun!', content:'blabla!'}

결국 리액트가 판단했을 때, 기존의 key와 value 매칭 쌍이 싹다 바뀐것이다. 기존에는 key:0인 것이 {id:0, title: ‘hello!’, content:’word’} 였는데, 새로 들어온 요소가 key:0이 되고, 기존의 key:0은 key:1이 되었으니 결과적으로 배열 전체가 완전히 바뀐것이라고 판단 할 수 밖에 없다. 이래선 리액트의 장점을 사용하지 못한 것이다.

결론

state로 배열을 관리한다면, map 사용시 key로 index를 사용하지 말자. key로 index를 사용한다면 배열의 처음이나 중간에 새로 데이터가 삽입될 시 그 부분만을 캐치하지 못한다. 삭제될 때도 마찬가지!

IntelliJ에서 git clone시 프로젝트 인식하기

|

Java 프로젝트를 git clone하고 인텔리제이에서 작업하려고 하면, 인텔리제이가 해당 코드를 소스코드(프로젝트)로 인식하지 못해서 실행은 물론 build조차 되지 않는 현상이 발생한다.

인텔리제이 오른쪽 윗 부분을 보면 아래 사진처럼 설정을 더하는 칸이 있다. 컨피그이미지

이 부분을 클릭해 보았지만 해결책은 아니었다.

프로젝트로 인식하기

폴더 디렉토리중에 소스코드가 담긴 루트 디렉토리(src)를 우클릭 하면 아래 이미지와 같은 창이 뜬다. 컨피그이미지2

해당 소스를 이렇게 소스트리로 명시적으로 설정해 주어야 인텔리제이가 소스코드로 인식한다.

private static이 필요한 경우

|

자바를 공부하다 보면 가끔 private static을 볼 수 있다. 공부 초창기를 떠올려보면 접근 제한자인 public, protected, private들도 헷갈렸는데 static까지 붙어버리니 이게 도대체 뭔가싶은 생각도 들었었다. 도대체 private static은 언제 쓰이는 것이고 왜 쓰이는 걸까?

private method가 필요한 이유와 동일하다.

결과부터 말하자면 private method가 필요한 이유와 동일하다. 초보자 입장에서 private static 이란게 생소해서 그럴 뿐(뭔가 어려워 보이고 대단한 역할을 할 것 처럼 보인다), 아주 간단하고 쉬운 내용이다.

그럼 private method가 필요한 코드를 보자. profile

doStudy()는 public 접근 제한자를 가지게 함으로써 다른 클래스파일에서도 사용이 가능하지만 doStudy()구현부의 영어 공부와 java공부는 이 Person클래스 내에서만 사용하면 된다. 그래서 private method면 된다.

private static도 동일하다. doStudy()static 함수라면, studyEnglish()studyJava()도 static이어야 하므로 private static을 사용하는 것 뿐이다.

profile

결론

정말 이게 끝이다. 처음에는 이게 도대체 뭘까 엄청 대단한 역할을 할 것 같고, 내가 쓸일이나 있을까? 하며 겁을 내곤 하지만, 알고보면 이렇게 간단한 것이다.