Ch 5. 형식 맞추기
- 개념은 빈 행으로 분리하라
- 서로 밀접한 개념은 세로로 가까이 둬야한다
- protected 변수를 최대한 피해야 함
- 틈새 접근자 (Access Modifier) 상식
- private -> default -> protected -> public 순으로 보다 많은 접근을 허용
- private: 선언한 class 내부에서만 호출/사용 가능
- default는 선언한 class가 포함된 패키지 내의 다양한 class 에서 호출 가능
- protected는 default 범위 + 해당 class를 확장한 외부 패키지의 접근 까지 허용
- public은 모두가 알듯이, 그냥 어디서든 접근 가능
- protected의 경우, 해당 class에 선언된 친구가 아닌 변수를 자유롭게 호출 가능해지고, 심지어 외부 패키지의 변수를 갑자기 사용하는 케이스가 생길 수 있으므로, 밀접한 개념이 세로로 가까워야한다는 원칙에 벗어나기 때문이다.
- local variable은 각 함수 맨 처음에 선언
- 루프를 제어하는 변수는 루프 문 내부에 선언
for(int i=0;i<5;i++)
- 인스턴스 변수는 클래스 맨 처음에 선언
- 인스턴스 변수 vs. 스태틱 변수
- 알다시피 인스턴스 변수는 객체마다 할당되는 dynamic한 변수 (static 제어자를 명시하지 않는 변수)
- 스태틱 변수는 객체마다 할당되는 것이 아니라, 클래스마다 하나 할당되고 각 객체에서 동일한 해당 값을 참조하게 되는 static 변수 (static 제어자 명시)
- 호출하는 함수를 호출되는 함수보다 먼저 배치
- 상수의 경우 저차원 함수에 명시적으로 배치하지 말고, 고차원 함수에서 parameter로 넘기는 식으로 구현하는 것이 가독성에 좋다.
- 친화도가 높을수록 코드를 가까이 배치
- 함수의 호출 관계, 변수의 사용 관계, 비슷한 동작을 수행하는 메소드
- 짧은 행(120자 정도)을 선호
- 할당 연산자의 강조를 위해 = 앞 뒤에 공백을 줄 수 있지만, 함수와 인수는 서로 밀접하기에 함수이름와 이어지는 괄호 사이에는 공백을 넣지 않음
- 짧은 함수라도 들여쓰기로 범위를 제대로 표현할 것
Ch 6. 객체와 자료구조
변수를 private하게 선언하고 get,set 함수를 당연하게 공개해서 private 변수를 결국 외부에 노출시키는 이 일. 왜 하는걸까?
-
사실 변수를 private하게 선언하고 get, set 함수를 공개하는 형태는 초기에 클래스를 인터페이스화 할 때 (추상화 단계를 낮추려고!), 인터페이스에서는 변수를 사용할 수 없기에 변수 대신 getter, setter 함수를 제공하게 되면서 사용하게 된 방식.
-
무분별하게 그냥 private으로 변수를 선언하고, 각 변수들에 대해 getter, setter를 취하는 방식은 취하지마라!
- 객체가 포함하는 자료를 표현할 가장 좋은 방법을 고민하여 getter, setter를 작성하라.
- 예를들어 직교 좌표계를 담은 class라고 하면 그냥
private x, y
두개애 getter
, setter
만 두지 말고 setCartesian(double x, double y)
요런식으로 인터페이스/클래스만 봐도 어떤 좌표계를 표현하고 있는지 알 수 있게끔 !
-
절차적인 클래스
- Geometry 클래스에 있어서 shape에 따른 분기문을 사용하고 있으므로, 둘레 길이를 구하는 perimeter() 함수를 추가하고 싶다면 해당 위치 한 군데에만 추가하면 된다. (함수 추가 용이)
- 대신 새 도형을 추가하고 싶다면 새로운 클래스를 정의해야 하고, Geometry 클래스의 기존 분기문 부분에 새 도형에 대한 validation을 추가해야함 (종류 추가 시 기존 함수 수정 필요)
-
객체지향적인 클래스
- 함수를 추가 하기 위해서는 도형을 상속받는 모든 class에 해당 함수를 구현해줘야함 (함수 추가 시 모든 class 수정 필요)
- 종류 추가 시 기존 함수 수정 필요 X. 그냥 상속받는 class 하나만 작성하면 완료
-
절차적인 코드는 새로운 자료 구조를 추가하기 어렵다. 그러려면 모든 함수를 고쳐야한다. 객체 지향 코드는 새로운 함수를 추가하기 어렵다. 그러려면 모든 클래스를 고쳐야 한다.
-
디미터 법칙
- 모듈은 자신이 조작하는 객체의 속사정을 몰라야 한다. 즉, 조회 함수로 내부 구조를 공개하면 안된다. 자신이 부르는 다른 객체들의 속사정에 대해서도 공개하면 안된다.
ex) String a = ctxt.getOptions().getScratchDir().getAbsolutePath();
해당 경우를 train wreck
, 즉 기차 충돌이라고 부르는데 줄줄이 호출하는 메소드들을 통해서 우리는 불리는 객체들의 속사정에 대해서 하나씩 알게 된다.
- 책에서 객체랑 자료구조를 구분함. 내가 이해한 둘의 차이는, 자료구조는 내부에 필드들만 있고, 메소드가 없는 친구들을 얘기하는 것 같았다. 예를 들어 DB에서 받은 데이터를 담아두기 위한 Class들. 데이터를 담아두고 운반하기 위한 용도로 사용되는 아이들. (DTO, Data Transfer Object)
- 반면 객체는, 우리가 일반적으로 알듯이 메소드도 내부에 구현되어 있고 필드도 여러개있는 Class로부터 만드는 친구를 의미하고, 해당 특징(Class에 명시된)을 갖는 많은 객체를 붕어빵 틀 찍어내듯이 쭉쭉 만들어낼 수 있는, 고런 유형의 Class를 말하는 듯 하다.
- 따라서 해당
train wreck
이 진정 디미터 법칙을 위반하는지 여부는, 줄줄이 불리는 클래스들이 객체인지, 아니면 자료구조인지에 따라 달라진다는 것. 객체라면 위반이고, 자료구조라면 위반이 아니겠지.
- 둘 구분이 어렵다면, 애초에 자료구조는 무조건 함수 없이 공개 변수만 포함하고, 객체는 비공개 변수와 공개 함수를 포함하도록 구성한다면 헷갈리지 않을 것.
- 따라서 객체라서 위반일 때 개선 방법은,
ctxt.getOptions().getScratchDir().getAbsolutePath();
에 해당하는 method를 ctxt의 Class 내부에 하나 더 구현해 두는 것. 그러면 단순히 ctxt.newMethod()
와 같이 표현되므로 객체들의 속사정에 대해 덜 알게된다.
-
결론
- 객체는 동작을 공개하고 자료를 숨기기에 (각 자료를 특정 하나를 상속받는 방식으로 추상화해뒀기 때문) 기존 동작의 변경 없이 새로운 객체 타입 추가가 용이하다. 하지만 기존 객체에 새 동작 추가는 어렵다. (공개되는 부분, 실제로 숨겨져 있는 모든 자료들에 다 추가해야함)
- 자료 구조는 별다른 동작 없이 자료를 노출한다. 따라서 기존 자료 구조에 새 동작 추가는 easy (그냥 if문으로 분기타고 있는 형태라서, 새로 동작 추가하고 if문 또 만들어두면 됨. 여러 곳에서 코드 추가할 필요 x). 하지만 기존 함수에 새 자료구조 추가는 hard (모든 메소드의 분기문에서 새 자료구조에 대한 분기 추가해아함)
Ch 7. 오류 처리
- 오류 코드보다 예외를 사용하라.
- 그냥 매번 오류 코드 던지게 되면, if문이 길어질 수 있음. ㅎ 예외 던지면
try { ~~~ } catch (Exception e) {}
하면 되는데, 매번 오류 코드 던지면 if(메소드 반환 값 = 오류 코드?) { if(다음 메소드 반환 값 = 오류 코드?) { ...}}}
이런식으로 구성될 수 있기 때문.
- 개인적으로 드는 느낌은, 결국 외부 서비스한테 전달하려면 오류코드는 필수적이고 코드 더러울게 걱정이라면
적절하게
둘을 조합해서 사용하는 것이 best..
- try-Catch-Finally 문부터 작성
- try 블록은 어떤 면에서 트랜잭션과 비슷: try 블록에서 어떤 일이 있더라도 catch를 통해 프로그램 상태가 일관성 있게 유지되어야하기 때문
- 따라서 해당 부분에 대한 test를 짤 때에는 강제로 예외를 일으키는 TC를 작성한 후, 테스트를 통과하게 하는 코드를 작성하는 것이 good
- Checked Exception을 사용하지 말고 Unchecked Exception을 사용하라
- 여기서 이야기하는 Checked Exception은 Method 뒤에 종종 붙이는
throws ~Exception
키워드를 의미하는 듯
- 초기에 Checked Exception은 런타임을 미리 잡을 수 있다는 이유로 아주 획기적으로 여겨졌으나 ..
- 해당 키워드가 붙어있으면, 해당 메소드를 호출하는 단계에서 try {} catch {}로 잡지 않으면, 아무리 런타임 exception이라도 컴파일 타임에 잡히게 되어, Checked Exception처럼 작동하게 되기 때문이다.
- 사용하다보니, OCP (Open Closed Principle), 즉 변경에 닫혀있고 확장에 열려있어야 하는 원칙을 깨버리게 되었다. 이유는 ?
- Checked Exception 던져졌는데 catch 블록이 세 단계 위에 있다면?
- 해당 메소드 호출하는, 그리고 그 메소드를 호출하는, 그리고 .... 쭉쭉 다
throws ~~Exception
써줘야 한다구.. 결국 하나 수정하려면 다 수정해야하는 현상이 발생
- 최하위 함수를 변경해서 새로운 오류를 던진다고 가정했을 때, 함수는 선언부에 throws 절을 추가해야하고, 모든 함수가 최하위 함수에서 던지는 예외를 알아야 하므로 캡슐화가 깨지게 된다.
- 오류 메시지에 정보를 담아 예외와 함께 던지기: catch 블록에서 오류를 기록할 수 있도록!
- 외부 API 사용시, 외부 API가 던지는 각종 다양한 예외에 대해서 각각
catch(){} catch() {} ...
를 작성하지 말고, 해당 API를 감싸서 모든 Exception을 해당 애플리케이션의 자체 Exception으로 다시 호출하는 Class를 작성하고 (감싸기 기법) 실제 호출처에서는 자체 Exception만 catch 하는 방식으로 구성.
- 코드가 읽기 훨씬 깔끔해지고 이해하기도 쉽기 때문, Also 외부 라이브러리와 프로그램 간의 의존성도 크게 줄어듬.
- 일부러 NULL을 반환하지 마라
- null check을 빼먹게 되면 바로 NPE로 인해 애플리케이션이 통제 불능 상태가 된다.
- null을 반환하고 싶은 마음이 들 때에는, 그대신 예외를 던지거나 특수 사례 객체를 반환한다. 외부 API가 null을 반환할 가능성이 있을 때에도, 감싸기 기법에서 해당 방법을 적용한다.
- null을 반환할 바에야 차라리 빈 리스트를 반환해서 적어도 NPE는 회피하도록..
- 인수로도 null이 넘어오지 않도록 구성할 것!