극락이들의 CleanCode 가이드 문서입니다.
⇒ 궁금한 점이 있으면 구피(
)에게 질문해주세요!

객체지향 생활 체조 원칙
- 한 메서드에 오직 한 단계의 들여쓰기만 한다.
- else 예약어를 쓰지 않는다.
- 모든 원시 값과 문자열을 포장한다.
- 한 줄에 점을 하나만 찍는다.
- 줄여 쓰지 않는다(축약 금지).
- 모든 엔티티를 작게 유지한다.
- 3개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다.
- 일급 컬렉션을 쓴다.
- getter/setter/프로퍼티를 쓰지 않는다.
CleanCode 가이드 with 자바지기
함수(메소드)
작게 만들어라.
- 함수를 만드는 첫 번재 규칙은 '작게'다. 함수를 만드는 두 번째 규칙은 '더 작게'다.
한 가지만 해라.
- 함수는 한 가지를 해야 한다. 그 한 가지를 잘 해야 한다. 그 한 가지만 해야 한다.
함수 당 추상화 수준은 하나로
- 함수가 확실히 '한 가지' 작업만 하려면 함수 내 모든 문장이 동일한 추상화 수준에 있어야 한다.
- 코드는 위에서 아래로 이야기처럼 일해야 좋다.
서술적인 이름을 사용하라
- 이름이 길어도 괜찮다.
- 이름을 정하느라 시간을 들여도 괜찮다.
- 이름을 붙일 때는 일관성이 있어야 한다.
함수 인수
- 함수에서 이상적인 인수 개수는 0개(무항)이다. 다음은 1개이고, 다음은 2개이다.
- 3개는 가능한 피하는 편이 좋다.
- 4개 이상은 특별한 이유가 있어도 사용하면 안된다
- 인수가 2 ~ 3개 필요한 경우가 생긴다면 인수를 독자적인 클래스를 생성할 수 있는지 검토해 본다.
Circle makeCircle(double x, double y, double radius); Circle makeCircle(Point center, double radius);
side effect를 만들지 마라.
- side effect는 많은 경우 예상치 못한 버그를 발생시킨다.
명령과 조회를 분리하라.
- 함수는 뭔가를 수행하거나 답하거나 둘 중 하나만 해야 한다. 둘 다 하면 안된다.
- 개체 상태를 변경하거나 아니면 개체 정보를 반환하거나 둘 중 하나다.
오류 코드보다 예외를 사용하라.
- 오류 처리도 한 가지 작업이다.
- 함수는 '한 가지' 작업만 해야 한다. 오류 처리도 '한 가지' 작업에 속한다.
- 그러므로 오류를 처리하는 함수는 오류만 처리해야 마땅하다.
- try/catch 블록은 원래가 추하다. 코드 구조에 혼란을 일으키며, 정상적인 동작과 오류 처리 동작을 뒤섞는다. try/catch 블록을 별도 함수로 뽑아내는 편이 낫다.
public void delete(Page page) { try { deletePageAndAllReferences(page); } catch (Exception e) { logError(e); } }
반복하지 마라.
- 중복은 소프트웨어에서 모든 악의 근원이다.
함수를 구현하는 방법은?
소프트웨어를 구현하는 행위는 여느 글짓기와 비슷하다. 논문이나 기사를 쓸 때는 먼저 생각을 기록한 후 읽기 좋게 다듬는다. 초안은 대개 서투르고 어수선하므로 원하는 대로 읽힐 때까지 말을 다듬고 문장을 고치고 문단을 정리한다.필자는 함수를 구현할 때도 마찬가지다. 처음에는 길고 복잡하다. 들여쓰기 단계도 많고, 중복된 루프도 많다. 인수 목록도 아주 길다. 이름은 즉흥적이고 코드는 중복된다. 하지만 필자는 그 서투른 코드를 빠짐없이 테스트하는 단위 테스트 케이스도 만든다.그런 다음 필자는 코드를 다듬고, 함수를 만들고, 이름을 바꾸고, 중복을 제거한다. 메소드를 줄이고, 순서를 바꾼다. 때로는 전체 클래스를 쪼개기도 한다. 이 와중에도 코드는 항상 단위 테스트를 통과한다.결국에는 앞에서 다룬 규칙을 따르는 함수를 얻을 수 있다. 처음부터 딱 짜내지 않는다. 그게 가능한 사람은 없으리라.
code convention, format 맞추기
형식을 맞추는 목적
- code convention은 중요하다! 너무 중요해서 무시하기 어렵다.
- code convention은 의사소통의 일환이다. 의사소통은 전문 개발자의 일차적인 의무다.
적절한 행의 길이를 유지하라
가로 format 맞추기
- 120자 정도로 행 길이를 제한한다.
의미 있는 이름
의도를 분명히 밝혀라
- 좋은 이름을 지으려면 시간이 걸리지만 좋은 이름으로 절약하는 시간이 훨씬 더 많다.
- 그러므로 이름을 주의 깊게 살펴 더 나은 이름이 떠오르면 개선하기 바란다. 그러면 (자신을 포함해) 코드를 읽는 사람이 좀더 행복해지리라.
- 따로 주석이 필요하다면 의도를 분명히 드러내지 못했다는 소리다.
// 잘못된 사용 예 int d; //경과 시간(단위: 날짜 수)
그릇된 정보를 피하라
- 프로그래머는 코드에 그릇된 단서를 남겨서는 안 된다. 그릇된 단서는 코드 의미를 흐린다.
- 서로 흡사한 이름을 사용하지 않도록 주의한다.
- 유사한 개념은 유사한 표기법을 사용한다.
의미 있게 구분하라
- 컴파일러나 인터프리터만 통과하려는 생각으로 코드를 구현하는 프로그래머는 스스로 문제를 일으킨다.
- 연속적인 숫자를 덧붙인 이름(a1, a2, ..., aN) 덧붙이거나 불용어(Info, Data, a, an, the)를 추가하는 방식은 적절하지 못하다. 이름이 달라야 한다면 의미도 달라져야 한다.
인터페이스 클래스와 구현 클래스
- 인터페이스 이름은 접두어를 붙이지 않는 편이 낫다고 생각한다.
- IShapeFactory(인터페이스), ShapeFactory(구현 클래스) 구조로 이름을 짓는 것은 좋은 선택은 아니다.
- 오히려 인터페이스를 ShapeFactory로 이름을 짓고 구현 클래스의 의도를 드러낼 수 있는 이름을 짓는 것을 추천한다.
클래스 이름
- 클래스 이름과 객체 이름은 명사나 명사구가 적합하다.
- Customer, WikiPage, Account, AddressParser 등이 좋은 예다.
- Manager, Processor, Data, Info 등과 같은 단어는 피하고, 동사는 사용하지 않는다.
메소드 이름
- 메소드 이름은 동사나 동사구가 적합하다.
- postPayment, deletePage, save 등이 좋은 예다.
- 접근자, 변경자, 조건자는 자바 빈 표준에 따라 값 앞에 get, set, is를 붙인다.
- 생성자를 중복해 정의할 때는 정적 팩토리 메소드를 사용한다. 메소드를 인수를 설명하는 이름을 사용한다.
개념 하나에 단어 하나를 사용하라
- 추상적인 개념 하나에 단어 하나를 선택해 이를 고수한다.
- 일관성 있는 어휘는 코드를 사용할 프로그래머가 반갑게 여길 선물이다.
경계
외부 코드 사용하기
- API를 사용하는 사용자는 자신의 요구에 집중하는 인터페이스만 존재하기를 기대한다.
- java.util.List, java.util.Map와 같은 collection을 외부에 노출하는 경우 사용자는 너무 많은 인터페이스에 노출하게 된다.
Map<Integer, Sensor> sensors = new HashMap<>(); Sensor s = sensors.get(sensorId);
- 경계 인터페이스인 Map을 Sensors라는 클래스 안으로 숨긴다.
public class Sensors { Map<Integer, Sensor> sensors = new HashMap<>(); pubilc Sensor getById(String id) { return sensors.get(id); } }
- 위와 같이 Map을 사용자에게 직접 노출하는 경우 사용자에게 Map의 모든 인터페이스를 노출하지 않아도 된다.
- Sensors를 추가함으로써 필요한 인터페이스 하나만 노출하는 것도 가능하다.
- 또 하나의 장점은 Sensors 내부의 자료구조가 Map이 아닌 다른 자료구조로 변경되더라도 외부에 변경이 발생하지 않아도 된다.
경계 살피고 익히기
- 학습 테스트를 통해 외부 코드 사용 방법을 익힌다.
팀 규칙
- 모든 프로그래머는 자신이 선호하는 규칙이 있다. 하지만 팀에 속한다면 자신이 선호해야 할 규칙은 바로 팀 규칙이다.
- 팀은 한 가지 규칙에 합의해야 한다. 그리고 모든 팀원은 그 규칙을 따라야 한다.
객체와 자료 구조
자료/객체
- 객체는 추상화 뒤로 자료를 숨긴 채 자료를 다루는 함수만 공개한다.
- 자료 구조는 자료를 그대로 공개하며 별다른 함수는 제공하지 않는다.
디미터 법칙
- 디미터 법칙은 모듈은 자신이 조작하는 객체의 속사정을 몰라야 한다는 법칙이다.
- 객체는 자료를 숨기고 함수를 공개한다. 즉, 객체는 조회 함수로 내부 구조를 공개하면 안 된다는 것이다.
- 다음 코드는 디미터 법칙을 어기는 것으로 보인다.
final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();
자료 전달 객체
- 자료 구조체의 전형적인 형태는 공개 변수만 있고 함수가 없는 클래스다. 이런 자료 구조체를 Data Transfer Object(DTO)라고 한다.
- 자바에서 DTO의 일반적인 형태는 '자바 빈(java bean)' 구조다.
클래스
클래스는 작아야 한다.
- 클래스를 만들 때 첫 번째 규칙은 크기다. 클래스는 작아야 한다. 두 번째 규칙도 크기다. 더 작아야 한다.
- 단일 책임 원칙(Single Responsibility Principle, SRP)은 클래스나 모듈을 변경할 이유가 하나, 단 하나뿐이어야 한다는 원칙이다.
- 클래스는 책임, 즉 변경할 이유가 하나여야 한다는 의미다.
- 응집도(cohesion) - 클래스는 인스턴스 변수 수가 작아야 한다.
- 응집도를 유지하면 작은 클래스 여럿이 나온다.
- 큰 함수를 작은 함수 여럿으로 쪼개다 보면 종종 작은 클래스 여럿으로 쪼갤 기회가 생긴다. 그러면서 프로그램에 체계가 더 잡히고 구조가 더 투명해진다.
변경하기 쉬운 클래스
- 요구사항은 변화기 마련이다. 따라서 코드도 변하기 마련이다.
- 구현 클래스에 의존하게 되면 테스트가 어려우며, 변화에 빠르게 대응하기 힘들다. 변화에 따르게 대응하려면 DIP 원칙을 지키는 습관을 가져야 한다.
- DIP(Dependency Inversion Principle) 원칙은 클래스가 상세한 구현이 아니라 추상화(인터페이스)에 의존해야 한다는 원칙이다.
- 테스트가 가능할 정도로 시스템 결합도를 낮추면 유연성과 재사용성도 더 높아진다.
단위테스트
코드의 종류
- 프로덕션 코드(Production Code) 프로그램 구현을 담당하는 부분으로 사용자가 실제로 사용하는 소스 코드를 의미한다.
- 테스트 코드(test code) 는 프로덕션 코드가 정상적으로 동작하는지를 확인하는 코드이다.
main method 테스트 문제점
- Production code와 Test Code가 클래스 하나에 존재한다. 클래스 크기가 커짐. 복잡도 증가함.
- Test Code가 실 서비스에 같이 배포됨.
- main method 하나에서 여러 개의 기능을 테스트 함. 복잡도 증가.
- method 이름을 통해 어떤 부분을 테스트하는지에 대한 의도를 드러내기 힘듦.
- 테스트 결과를 사람이 수동으로 확인
TDD
Test Driven Development(테스트 주도 개발, TDD)
용어 정리
- 위 그림을 통해 확인할 수 있듯이 프로덕션 코드(Production Code) 프로그램 구현을 담당하는 부분으로 사용자가 실제로 사용하는 소스 코드를 의미한다.
- 테스트 코드(test code) 는 프로덕션 코드가 정상적으로 동작하는지를 확인하는 코드이다.
TDD란?
- TDD = TFD(Test First Development) + 리팩토링
- TDD란 프로그래밍 의사결정과 피드백 사이의 간극을 의식하고 이를 제어하는 기술이다. - 켄트벡, Test Driven Development by Example 중
- TDD의 아이러니 중 하나는 테스트 기술이 아니라는 점이다. TDD는 분석 기술이며, 설계 기술이기도 하다. - 켄트벡, Test Driven Development by Example 중
TDD를 하는 이유
- 디버깅 시간을 줄여준다.
- 동작하는 문서 역할을 한다.
- 변화에 대한 두려움을 줄여준다.
TDD 사이클

- 실패하는 테스트를 구현한다.
- 테스트가 성공하도록 프로덕션 코드를 구현한다.
- 프로덕션 코드와 테스트 코드를 리팩토링한다.
TDD 원칙
- 원칙 1 - 실패하는 단위 테스트를 작성할 때까지 프로덕션 코드(production code)를 작성하지 않는다.
- 원칙 2 - 컴파일은 실패하지 않으면서 실행이 실패하는 정도로만 단위 테스트를 작성한다.
- 원칙 3 - 현재 실패하는 테스트를 통과할 정도로만 실제 코드를 작성한다.
꼭 지켜야한 기본 CleanCode
- 자바 코드 컨벤션을 지키면서 프로그래밍한다.
- 기본적으로 Google Java Style Guide을 원칙으로 한다.
- 단, 들여쓰기는 '2 spaces'가 아닌 '4 spaces'로 한다.
- indent(인덴트, 들여쓰기) depth를 2를 넘지 않도록 구현한다. 1까지만 허용한다.
- 예를 들어 while문 안에 if문이 있으면 들여쓰기는 2이다.
- 힌트: indent(인덴트, 들여쓰기) depth를 줄이는 좋은 방법은 함수(또는 메서드)를 분리하면 된다.
- 3항 연산자를 쓰지 않는다.
- else 예약어를 쓰지 않는다.
- else 예약어를 쓰지 말라고 하니 switch/case로 구현하는 경우가 있는데 switch/case도 허용하지 않는다.
- 힌트: if문에서 값을 반환하는 방식으로 구현하면 else 예약어를 사용하지 않아도 된다.
- 모든 기능을 TDD로 구현해 단위 테스트가 존재해야 한다. 단, UI(System.out, System.in) 로직은 제외
- 핵심 로직을 구현하는 코드와 UI를 담당하는 로직을 구분한다.
- UI 로직을 InputView, ResultView와 같은 클래스를 추가해 분리한다.
- 함수(또는 메서드)의 길이가 10라인을 넘어가지 않도록 구현한다.
- 함수(또는 메소드)가 한 가지 일만 하도록 최대한 작게 만들어라.
- 배열 대신 컬렉션을 사용한다.
- 모든 원시 값과 문자열을 포장한다
- 줄여 쓰지 않는다(축약 금지).
- 일급 컬렉션을 쓴다.
- 모든 엔티티를 작게 유지한다.
- 3개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다.
상속
Starbuzz 커피 전문점을 통한 상속 이해
요구사항
Starbuzz 커피 전문점은 커피와 차를 판매한다. 커피와 차를 준비하는 과정은 각각 다음과 같다.커피와 차를 준비하는 과정을 자바 코드로 구현한다.
커피(coffee)
- 물을 끓인다.
- 필터를 활용해 커피를 내린다.
- 컵에 붓는다.
- 설탕과 우유를 추가한다.
차(tea)
- 물을 끓인다.
- 차 티백을 담근다.
- 컵에 붓는다.
- 레몬을 추가한다.
구현하기
public class Coffee { void prepareRecipe() { boilWater(); brewCoffeeGrinds(); pourInCup(); addSugarAndMilk(); } public void boilWater() { System.out.println("물을 끓인다."); } public void brewCoffeeGrinds() { System.out.println("필터를 활용해 커피를 내린다."); } public void pourInCup() { System.out.println("컵에 붓는다."); } public void addSugarAndMilk() { System.out.println("설탕과 우유를 추가한다."); } }
public class Tea { void prepareRecipe() { boilWater(); steepTeaBag(); pourInCup(); addLemon(); } public void boilWater() { System.out.println("물을 끓인다."); } public void steepTeaBag() { System.out.println("티백을 담근다."); } public void pourInCup() { System.out.println("컵에 붓는다."); } public void addLemon() { System.out.println("레몬을 추가한다."); } }
구현한 코드를 살펴보니 중복되는 부분이 있다. 중복 코드를 제거한다. 제거하는 방법은?
상속을 통한 중복 코드 제거
클래스 다이어그램
- 중복 코드를 별도의 클래스로 분리한다.
- 자바의 extends 키워드를 활용해 별도로 분리한 클래스를 상속한다.
구현 코드
public class CaffeineBeverage { protected void boilWater() { System.out.println("물을 끓인다."); } protected void pourInCup() { System.out.println("컵에 붓는다."); } }
public class Coffee extends CaffeineBeverage { void prepareRecipe() { boilWater(); brewCoffeeGrinds(); pourInCup(); addSugarAndMilk(); } public void brewCoffeeGrinds() { System.out.println("필터를 활용해 커피를 내린다."); } public void addSugarAndMilk() { System.out.println("설탕과 우유를 추가한다."); } }
public class Tea extends CaffeineBeverage { void prepareRecipe() { boilWater(); steepTeaBag(); pourInCup(); addLemon(); } public void steepTeaBag() { System.out.println("티백을 담근다."); } public void addLemon() { System.out.println("레몬을 추가한다."); } }
extends
- 부모 클래스의 모든 필드와 메소드를 자식 클래스가 상속하도록 지원하는 keyword이다.
- 상속을 할 경우 멤버 필드와 메소드를 하위 클래스에서 그대로 상속하게 된다.
추상화
Coffee와 Tea에서 중복되는 부분은 없을까?혹시 중복되는 부분이 있다면 중복을 제거할 수 있는 방법이 있을까?
추상화를 통한 중복 제거
- Coffee의 brewCoffeeGrinds()와 Tea의 steepTeaBag() 메소드의 역할은?
- 뜨거운 물을 사용해 커피나 차를 우려낸다. => brew() 메소드로 추상화
- Coffee의 addSugarAndMilk()와 Tea의 addLemon() 메소드의 역할은?
- 커피나 차에 첨가물을 추가한다. => addCondiments() 메소드로 추상화
public class Coffee extends CaffeineBeverage { void prepareRecipe() { boilWater(); brew(); pourInCup(); addCondiments(); } public void brew() { System.out.println("필터를 활용해 커피를 내린다."); } public void addCondiments() { System.out.println("설탕과 우유를 추가한다."); } }
public class Tea extends CaffeineBeverage { void prepareRecipe() { boilWater(); brew(); pourInCup(); addCondiments(); } public void brew() { System.out.println("티백을 담근다."); } public void addCondiments() { System.out.println("레몬을 추가한다."); } }
- 메소드 이름을 같은 이름을 가지도록 구현한 결과 prepareRecipe() 메소드가 똑같다.
- prepareRecipe() 메소드를 부모 클래스인 CaffeineBeverage로 이동해 중복을 제거한다.
public abstract class CaffeineBeverage { abstract void brew(); abstract void addCondiments(); void prepareRecipe() { boilWater(); brew(); pourInCup(); addCondiments(); } protected void boilWater() { System.out.println("물을 끓인다."); } protected void pourInCup() { System.out.println("컵에 붓는다."); } }
public class Coffee extends CaffeineBeverage { public void brew() { System.out.println("필터를 활용해 커피를 내린다."); } public void addCondiments() { System.out.println("설탕과 우유를 추가한다."); } }
public class Tea extends CaffeineBeverage { public void brew() { System.out.println("티백을 담근다."); } public void addCondiments() { System.out.println("레몬을 추가한다."); } }
abstract
- 클래스를 추상 클래스로 지정할 때 사용한다.
- 클래스를 abstract로 지정하면 new keyword를 통해 객체를 직접 생성할 수 없다.
- 메소드에 abstract를 사용할 경우 interface의 메소드와 같이 구현 부분은 없다.
- abstract로 선언한 메소드를 자식 클래스에서 반드시 구현해야 한다.
업캐스팅(upcasting) vs 다운캐스팅(downcasting)
생각해볼 꺼리
다음 각각의 정의에 대한 참과 거짓을 판단한다.
- 커피(Coffee)는 카페인 음료(CaffeineBeverage)이다.
- 카페인 음료(CaffeineBeverage)는 커피(Coffee)이다.
- 차(Tea)는 카페인 음료(CaffeineBeverage)이다.
- 카페인 음료(CaffeineBeverage)는 차(Tea)다.
각 정의에 따른 코드 구현
- 커피(Coffee)는 카페인 음료(CaffeineBeverage)이다.(O)
CaffeineBeverage beverage = new Coffee(); (O)
- 카페인 음료(CaffeineBeverage)는 커피(Coffee)이다.(X)
Coffee coffee = new CaffeineBeverage(); (X)
- 차(Tea)는 카페인 음료(CaffeineBeverage)이다.(O)
CaffeineBeverage beverage = new Tea(); (O)
- 카페인 음료(CaffeineBeverage)는 차(Tea)다.(X)
Tea tea = new CaffeineBeverage(); (X)
업캐스팅(upcasting)
- 하위 클래스를 상위 클래스로 타입을 변환하는 것을 의미한다.
CaffeineBeverage beverage = new Coffee(); (O) CaffeineBeverage beverage = new Tea(); (O)
다운캐스트(downcasting)
- 상위 클래스를 하위 클래스의 타입으로 변환하는 것을 의미한다.
CaffeineBeverage beverage = new Coffee(); if (beverage instanceof Coffee) { Coffee coffee = (Coffee)beverage; }
- instanceof - 인스턴스가 실제로 어떤 타입인지를 확인할 때 사용하는 자바 keyword
인터페이스
📖 자바 interface
인터페이스(interface)
인터페이스란? - 사전적 의미
하나의 시스템을 구성하는 2개의 구성 요소(하드웨어, 소프트웨어) 또는 2개의 시스템이 상호 작용할 수 있도록 접속되는 경계(boundary), 이 경계에서 상호 접속하기 위한 하드웨어, 소프트웨어, 조건, 규약 등을 포괄적으로 가리키는 말
실생활 인터페이스란?

JDBC(Java Database Connectivity)를 통한 인터페이스 이해하기
MSSQL DB로 서비스 시작
2년 후 상황
- 서비스 대박났다. 2년 사이에 데이터량이 증가하는 속도가 엄청 빨라졌다.
- 최초 DB 서버 한 대에서 데이터량 많아지면서 16대로 증가
- MSSQL 한 대 구매 비용이 2년 전 1천만원에서 5천만원으로 올랐다. 조만간 새로운 버전이 나오면서 비용 증가 예상
- 비용을 감당하려면 무료 데이터베이스인 MySQL로 변경하는 것이 좋겠다.
문제 상황
- 데이터베이스에 대한 작업을 담당하는 자바 소스 코드가 MSSQL와 밀접하게 연관되어 있어 이 소스 코드를 변경하는데 많은 비용이 발생한다.
- 많은 소스 코드가 변경되면서 서비스의 안정성을 확보할 수 없다.
해결 방법
- 자바 소스 코드와 DB 작업에 필요한 공통 부분을 추상화(abstraction)해 표준으로 만든다.
- 각 DB 회사들이 이 표준에 따라 구현하도록 한다.
- 자바를 기반으로 하는 소프트웨어는 DB 종류는 고려하지 않고 인터페이스만 지키면서 구현을 하면 추후 DB가 변경되더라도 소스 코드를 수정하지 않아도 된다.
JDBC 코드
- 데이터베이스 회사에서 구현한 코드를 로딩하기
public class DriverManager { public static Connection getConnection() throws SQLException { Class.forName("com.mysql.jdbc.Driver"); return DriverManager.getConnection( "jdbc:mysql:주소:포트/DB명" , "username", "password"); } }
- 데이터베이스 연결 인터페이스(Connection)
public interface Connection { PreparedStatement prepareStatement(String sql) throws SQLException; }
- SQL 쿼리에 인자를 전달하고 쿼리를 실행한다.
public interface PreparedStatement extends Statement { void setInt(int parameterIndex, int x) throws SQLException; // int 값 전달 void setString(int parameterIndex, String x) throws SQLException; // String 값 전달 […] // 자바의 기본 데이터베이스 모두 지원 ResultSet executeQuery() throws SQLException; // select 쿼리 int executeUpdate() throws SQLException; // insert, update, delete 쿼리 }
- select 쿼리를 실행한 결과를 자바 데이터로 변환
public interface ResultSet { int getInt(int columnIndex) throws SQLException; String getString(String columnLabel) throws SQLException; [...] // 자바가 지원하는 데이터 타입에 대한 메소드 존재함. boolean isFirst() throws SQLException; boolean isLast() throws SQLException; }
인터페이스(interface)
- interface는 자바에서 한 단계 더 높은 추상화를 하기 위해 사용된다.
- interface는 구현 로직은 존재하지 않으며 메소드에 대한 입력(input), 출력(output)만 정의하고 있다.
- interface를 활용해 추상화를 하는 이유는 소프트웨어에 변경이 발생할 경우 소스 코드에 변경을 최소화함으로써 유지보수 비용을 줄이고, 변화에 빠르게 대응하기 위함이다.
- 추상화를 함으로써 미래의 변화에 빠르게 대응할 수 있지만 추상화를 하려면 추상화에 따른 개발 비용이 발생한다.
함수형 프로그래밍
람다
람다(lambda)
람다와 클로저
람다는 익명 함수의 다른 표현이다. 즉, 함수는 함수인데 이름이 없는 경우를 의미한다.
Collection의 모든 값을 출력
// nextstep.fp.Lambda의 printAllOld method List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6); for (int number : numbers) { System.out.println(number); }
람다가 없던 시절
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6); numbers.forEach(new Consumer<Integer>() { public void accept(Integer value) { System.out.println(value); } });
람다를 활용하면…
// nextstep.fp.Lambda의 printAllLambda method List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6); numbers.forEach((Integer value) -> System.out.println(value)); numbers.forEach(value -> System.out.println(value)); // Type 추론이 가능해 Type 생략 가능 numbers.forEach(System.out::println); // :: 연산자 활용 가능 = numbers.forEach(x -> System.out.println(x));
람다 문법
input arguments -> body
람다 실습 1 - 익명 클래스를 람다로 전환
- 다음 테스트 코드에서 MoveStrategy에 대한 익명 클래스로 구현하고 있는데 람다를 적용해 구현한다.
// nextstep.fp.CarTest의 이동, 정지 method public class CarTest { @Test public void 이동() { Car car = new Car("pobi", 0); Car actual = car.move(new MoveStrategy() { @Override public boolean isMovable() { return true; } }); assertEquals(new Car("pobi", 1), actual); } @Test public void 정지() { Car car = new Car("pobi", 0); Car actual = car.move(new MoveStrategy() { @Override public boolean isMovable() { return false; } }); assertEquals(new Car("pobi", 0), actual); } }
람다 실습 2 - 람다를 활용해 중복 제거
// nextstep.fp.Lambda의 sumAll method List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6); public int sumAll(List<Integer> numbers) { int total = 0; for (int number : numbers) { total += number; } return total; }
// nextstep.fp.Lambda의 sumAllEven method List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6); public int sumAllEven(List<Integer> numbers) { int total = 0; for (int number : numbers) { if (number % 2 == 0) { total += number; } } return total; }
List에 담긴 값 중 3보다 큰 수만을 더해야 한다.이 기능을 구현하려고 보니 앞의 요구사항 1,2와 많은 중복이 발생한다. 람다를 활용해 중복을 제거한다.
// nextstep.fp.Lambda의 sumAll, sumAllEven, sumAllOverThree method 소스 코드를 확인하고 중복 제거한다.
힌트
- 변경되는 부분과 변경되지 않는 부분의 코드를 분리한다.
- 변경되는 부분을 인터페이스로 추출한다.
- 인터페이스에 대한 구현체를 익명 클래스(anonymous class)로 구현해 메소드의 인자로 전달한다.
- 구글에서 자바의 익명 클래스로 검색해 익명 클래스가 무엇인지 학습한다.
- 인터페이스는 다음과 같은 형태로 추출할 수 있다.
public interface Conditional { boolean test(Integer number); }
- Conditional을 활용해 공통 메소드의 구조는 다음과 같다.
public int sumAll(List<Integer> numbers, Conditional c) { // c.test(number)를 활용해 구현할 수 있다. }
- 익명 클래스를 자바 8의 람다를 활용해 구현한다.
스트림(stream)
map, filter, reduce
Collection에 담긴 데이터를 처리하려면 Collection을 순회하면서 각 Element를 처리하는 것이 일반적이다. 앞으로는 순회하는 것을 잊고, Element 처리에만 집중하자.
filter
요구사항은 파일 문자 중 길이가 12보다 큰 문자의 수를 구한다.
// nextstep.fp.StreamStudy countWords method String contents = new String(Files.readAllBytes( Paths.get("../ war-and-peace.txt")), StandardCharsets.UTF_8); List<String> words = Arrays.asList(contents.split("[\\P{L}]+")); long count = 0; for (String w : words) { if (w.length() > 12) count++; }
filter 활용해 구현
String contents = new String(Files.readAllBytes( Paths.get("../alice.txt")), StandardCharsets.UTF_8); List<String> words = Arrays.asList(contents.split("[\\P{L}]+")); long count = words.stream().filter(w -> w.length() > 12).count();
map
List에 담긴 모든 숫자 값을 2배한 결과 List를 생성한다.
// nextstep.fp.StreamStudy 클래스의 doubleNumbers method 참고 List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6); List<Integer> dobuleNumbers = numbers.stream().map(x -> 2 * x).collect(Collectors.toList());
reduce
List에 담긴 모든 숫자의 합을 구한다.
// nextstep.fp.StreamStudy 클래스의 sumAll method 참고 List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6); public int sumAll(List<Integer> numbers) { return numbers.stream().reduce(0, (x, y) -> x + y); }
다양한 stream method 실습
src/main/resources/fp 디렉토리 아래에 있는 war-and-peace.txt 파일을 읽어 다음 요구사항을 만족하세요.
map, reduce, filter 실습 1
List에 담긴 모든 숫자 중 3보다 큰 숫자를 2배 한 후 모든 값의 합을 구한다. 지금까지 학습한 map, reduce, filter를 활용해 구현해야 한다.
- nextstep.fp.StreamStudyTest 클래스의 sumOverThreeAndDouble() 테스트를 pass해야 한다.
map, reduce, filter 실습 2
nextstep.fp.StreamStudy 클래스의 printLongestWordTop100() 메서드를 구현한다. 요구사항은 다음과 같다.
- 단어의 길이가 12자를 초과하는 단어를 추출한다.
- 12자가 넘는 단어 중 길이가 긴 순서로 100개의 단어를 추출한다.
- 단어 중복을 허용하지 않는다. 즉, 서로 다른 단어 100개를 추출해야 한다.
- 추출한 100개의 단어를 출력한다. 모든 단어는 소문자로 출력해야 한다.
힌트
- 단어의 길이가 12자를 초과하는 단어를 추출한다.
- 12자가 넘는 단어 중 길이가 긴 순서로 100개의 단어를 추출한다.
- sorted() method 활용
- 단어 중복을 허용하지 않는다. 즉, 서로 다른 단어 100개를 추출해야 한다.
- distinct() method 활용
- 추출한 100개의 단어를 출력한다. 모든 단어는 소문자로 출력해야 한다.
- String.toLowerCase() method 활용
Optional
요구사항 1 - Optional을 활용해 조건에 따른 반환
nextstep.optional.User의 ageIsInRange1() 메소드는 30살 이상, 45살 이하에 해당하는 User가 존재하는 경우 true를 반환하는 메소드이다.같은 기능을 Optional을 활용해 ageIsInRange2() 메소드에 구현한다. 메소드 인자로 받은 User를 Optional로 생성하면 stream의 map, filter와 같은 메소드를 사용하는 것이 가능하다.nextstep.optional.UserTest의 테스트가 모두 pass해야 한다.
힌트
- Guide To Java 8 Optional 문서를 참고해 Optional 사용 방법을 익힌다.
- Optional.ofNullable(user)을 활용해 User을 Optional로 생성하는 것이 가능하다.
- Optional의 map(), filter() 메소드등을 활용해 필요한 데이터를 추출
- Optional의 isPresent() 메소드 활용
요구사항 2 - Optional에서 값을 반환
nextstep.optional.Users의 getUser() 메소드를 자바 8의 stream과 Optional을 활용해 구현한다.자바 8의 stream과 Optional을 사용하도록 리팩토링한 후 UsersTest의 단위 테스트가 통과해야 한다.
힌트
- Guide To Java 8 Optional 문서를 참고해 Optional 사용 방법을 익힌다.
- Optional의 orElse() 메소드 활용해 구현한다.
요구사항 3 - Optional에서 exception 처리
nextstep.optional.ExpressionTest의 테스트가 통과하도록 Expression의 of 메소드를 구현한다.단, of 메소드를 구현할 때 자바 8의 stream을 기반으로 구현한다.
힌트
- Guide To Java 8 Optional 문서를 참고해 Optional 사용 방법을 익힌다.
- 자바의 enum 전체 값은 values() 메소드를 통해 배열로 접근 가능하다.
- Arrays.stream()을 이용해 배열을 stream으로 생성할 수 있다.
- 일치하는 값 하나를 추출할 때 findFirst() 메소드를 활용 가능하다.
- Optional의 orElseThrow() 메소드 활용해 구현한다.
공부하면 좋은것!
