2021 03 15
2021-03-15¶
VO¶
- 정의 - 도메인에서 한 개 혹은 그 이상의 속성들을 묶어서 "특정 값"을 나태내는 객체 - ex) 좌표, 금액, 날짜 등
- 특징 - equals, hashCode 메서드를 재정의 할 것 - 값 자체를 나타내는 객체이기에, 내부의 속성과 타입이 같다면 같은 VO로 보는 것이 타당하다 - 불변객체로 만들 것 - VO는 속성값 자체가 해당 객체의 고유한 식별값으로, 내부 속성값을 바꿀 수 있게 하면 안 된다 - 값을 나타내는 게 중요한 것이지, 그 값을 가공해 지지고 볶는 것은 괜찮다 - 즉, getter/setter 의외에 비즈니스 로직을 두어도 괜찮다!
public class Money {
private final int money;
public Money(final int money) {
this.money = money;
}
public int calculateProfit(double profitRate) {
return (int) (money * profitRate);
}
public Money updateMoneyByProfit(final int profit) {
return new Money(money + profit);
}
public int getMoney() {
return money;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Money money1 = (Money) o;
return money == money1.money;
}
@Override
public int hashCode() {
return Objects.hash(money);
}
}
DTO¶
- 참고 1: 테코톡
- 참고 2: https://softwareengineering.stackexchange.com/questions/373284/what-is-the-use-of-dto-instead-of-entity
- 참고 3: https://velog.io/@maigumi/DTO%EC%9D%98-%EC%A0%81%EC%A0%88%ED%95%9C-%EC%82%AC%EC%9A%A9%EC%97%90-%EB%8C%80%ED%95%9C-%EA%B3%A0%EB%AF%BC
- 정의 - 계층 간 Data 전달을 위해 사용하는 객체 - Data를 담아서 전달하는 바구니 역할
- 특징 - 순수하게 Data 전달만을 위한 객체라서, getter/setter 메서드만 가진다 - getter/setter 이외의 메서드는 사용하지 말 것! - 애초에 계층간 "Data 이동"을 위한 객체인데, 다른 로직이 필요가 당연히 없다! - 생성자로 필드 초기화 하도록 하면, 불변성을 보장하게 설계 가능
- 필요성 - DB와 매핑되어 있는 클래스를 Entity 클래스라고 한다 - DTO 클래스는 실질적으로 프론트엔드에 비춰줄 데이터를 의미한다 - 이 둘을 왜 분리해야 하는가? - 객체는 역할에 따라서 생성해야 한다. - Entity 클래스는 DB와 매핑되어 있는 데이터를 정확히 저장하기 위한 클래스이고, - DTO 클래스는 View 단에서 출력에 필요한 데이터를 저장하는 클래스이다. - Entity 클래스 중 민감한 정보 (ex. 개인정보)를 포함한 객체를 통째로 View에 넘긴다? ㄴㄴ - 세상에 변하지 않는 것은 없다. 지금 당장 변하지 않는다고 정의가 되었더라고 변할 경우 대응할 여지는 남겨야 하는게 설계다
원시값 포장 vs VO¶
- 원시값 포장 - 원시값을 다루는 책임을 부여 - "원시값"만 포장 - 동등성 지킬수도 있고, 아니여도 된다 - 불변을 지킬수도 있고, 아니여도 된다
- VO - 객체를 "값"으로 표현하기 위함 - "값"을 포장해야 되기 때문에, 꼭 원시값이 아니여도 된다 - VO 객체 안에서 다른 객체를 참조할 수도 있다 - ex) 주소라는 VO 내부에 나라 Obj, 도시 Obj 등등... - 동등성 보장 - 불변 보장
- String이 자바에서 대표적인 VO - char 배열을 하나의 "값"으로 보기 때문에 이를 포장했다!
- 동등성 vs 동일성 - 동등성(equals) - 두 오브젝트가 같은 정보를 가지고 있을 경우 - 동일성(==) - 두 오브젝트가 완전히 같은 경우 - 메모리에 찍힌 정보가 완전히 같은 경우 - 따라서, 같은 참조값을 가진 객체 - 데이터를 메모리에 바로 나타내는 원시값들
함수형 인터페이스¶
- 참고: 자바의 정석
- 람다식 - 메서드를 하나의 '식'으로 표현한 것 - 오로지 람다식 자체만으로도 메서드의 역할을 대체 - 람다식으로 인해 메서드를 "변수"처럼 다루게 되었다
- 람다식 문법 - 이름과 반환타입 제거, 매개변수 선언부와 몸통 사이에 -> 추가 - 반환값이 있다면, return문 대신 expression으로 대체 가능 - 식의 연산결과가 자동 반환됨
- 함수형 인터페이스
- 모든 메서드는 클래스 내에 포함되어야 함...
- 람다식은 어떤 클래스 소속?
- 사실 람다식은 "익명 클래스의 객체"이다
- 하나의 메서드가 선언된 인터페이스를 정의하여 람다식을 다룬다!
- 기존의 자바 규칙 어기지 않으며 자연스러움
@Test public void lambda() { MyFunction f = (() -> System.out.println("hello")); f.myMethod(); System.out.println("f = " + f); } @FunctionalInterface interface MyFunction { void myMethod(); }- 사실상 메서드가 아닌, 객체를 주고받는 것이라 근본적으로 달라진 것은 없지만, 간결한 코드 작성 가능
- java.util.function 패키지
- java.util.function 패키지에 자주 쓰이는 형식의 메서드를 함수형 인터페이스로 정의해놓음
- Supplier\
- 매개변수는 없고, 반환값만 있음 - 데이터를 공급하는 역할 - Consumer\ - 매개변수만 있고, 반환값이 없음 - 매개변수를 받아서 소비하는 일을 구현 @Test public void supplierAndConsumer() { Supplier<String> supplierTest = (() -> "GOGO!"); Consumer<String> consumerTest = (text -> System.out.println("text = " + text)); consumerTest.accept(supplierTest.get()); // text = GOGO! }- Function\
- 일반적인 함수 - 하나의 매개변수를 받아서 결과를 반환 @Test public void function() { Function<String, Integer> length = (text -> text.length()); System.out.println(length.apply("Java")); // 4 }- Predicate\
- 조건식을 표현하는데 사용됨. 매개변수는 하나, 반환타입은 boolean @Test public void predicate() { Predicate<String> isEmptyStr = (s -> s.length() == 0); String braveGirls = "브레이브걸스"; if (isEmptyStr.test(braveGirls)) { System.out.println("String empty"); } else { System.out.println("braveGirls = " + braveGirls); // braveGirls = 브레이브걸스 } }- BiConsumer\
- 두 개의 매개변수만 있고, 반환값이 없음 - BiPredicate\ - 조건식을 표현하는데 사용됨 매개변수는 둘, 반환타입은 boolean - BiFunction\ - 두 개의 매개변수를 받아서 하나의 결과를 반환 @Test public void bi() { BiConsumer<String, Integer> biConsumer = ((text, integer) -> System.out.println(text + integer)); biConsumer.accept("빛과소금", 2); BiPredicate<String, String> biPredicate = ((text1, text2) -> text1.equals(text2)); String text1 = "샴푸의 요정"; String text2 = "샴푸의 요정"; if (biPredicate.test(text1, text2)) { System.out.println("text1 = " + text1); System.out.println("text2 = " + text2); } BiFunction<String, Character, String> replace = ((text, character) -> text.replace(character, '-')); final String replaced = replace.apply("banana", 'a'); System.out.println("replaced = " + replaced); }- UnaryOperator
- Function의 자손으로써, 매개변수와 결과의 타입이 같음 - BinaryOperator - BiFunction의 자손으로써, 매개변수와 결과의 타입이 같음 @Test public void op() { UnaryOperator<Integer> addFive = (number -> number + 5); final Integer fiveAdded = addFive.apply(10); System.out.println("fiveAdded = " + fiveAdded); BinaryOperator<Integer> addNumbers = ((num1, num2) -> num1 + num2); final Integer added = addNumbers.apply(10, 20); System.out.println("added = " + added); }
- Function의 합성과 Predicate의 결합
- Function의 합성
- 두 람다식을 합성해, 하나의 새로운 람다식을 만들기
- f.andThen(g) : f 하고 g
- f.compose(g) : g 하고 f
- identity() : 항등함수
@Test public void compose() { Function<String, Integer> length = (text -> text.length()); Function<Integer, String> toString = (num -> num.toString()); Function<String, String> stringLengthAsString = length.andThen(toString); Function<Integer, Integer> numberLength = length.compose(toString); System.out.println(stringLengthAsString.apply("Just Only You")); // 13 System.out.println(numberLength.apply(12345)); // 5 }- Predicate의 결합 - 여러 Predicate를 합쳐, 하나의 새로운 Predicate 생성하기 - and() - or() - negate() - isEqual()
@Test public void predicateAdd() { Predicate<String> isEmptyStr = (s -> s.length() == 0); Predicate<String> isLongerThanFive = (s -> s.length() > 5); Predicate<String> notEmpty = isEmptyStr.negate(); Predicate<String> nonsense = isEmptyStr.and(isLongerThanFive); Predicate<String> emptyOrFiveUp = isEmptyStr.or(isLongerThanFive); System.out.println(notEmpty.test("~~~")); // true System.out.println(nonsense.test("Always False")); // false System.out.println(emptyOrFiveUp.test("This is true")); // true }
- 메서드 참조
- 람다식이 하나의 메서드만 호출하는 경우, "메서드 참조"라는 방법으로 람다식을 간단하게 할 수 있음
- 이미 생성된 객체의 메서드를 람다식에서 사용한 경우, 클래스 이름 대신 해당 객체의 참조 변수를 적어줄 것
Function<String, Integer> f = (String s) -> Integer.parseInt(s); Function<String, Integer> f = Integer::parseInt; BiFunction<String, String, Boolean> f = (s1, s2) -> s1.equals(s2); BiFunction<String, String, Boolean> f = String::equals; MyClass obj = new MyClass(); Function<String, Boolean> f = (x) -> obj.equals(x); Function<String, Boolean> f = obj::equals;
상태 패턴¶
- 의도 - 객체 자신의 내부 상태에 따라 수행해야 하는 행위를 변경하는 효과를 주기 위해서
- 활용 - 객체의 행위는 상태에 따라 달라진다 - 객체의 상태에 따라서 프로그램 실행 중에 수행해야 하는 행위를 변경할 때 활용하자 - 객체의 상태에 따라 수 많은 if를 사용해야 할 때, 이를 피할 수 있는 방법 중에 하나!
- 정의 - 기능이 "상태"에 따라서 다르게 동작해야 할 때 사용하는 패턴! - "상태" 객체가 "기능"을 제공한다!
- 구현 - 상태 사용자 (Context) : 클라이언트로부터 기능 실행 요청을 받으면, 상태 객체에 처리를 위임하는 방식으로 구현 - 상태 (State) : 구체화된 여러 상태들의 기능들을 추상화함으로써, 변하는 부분을 담당 - 상태 콘크리트 (Concrete state) : 실제 상태의 기능들을 구현
- 장점 - 상태가 많아질 경우 조건문을 이용한 방식은 코드가 복잡해져 유지 보수 어렵게한다 - but, 상태 패턴은 상태 많아져도 코드 복잡도 증가 X - 상태별 동작 수정이 쉬워짐 - 상태에 관련된 코드가 한 곳에 모여있어 안전하고, 빠른 구현이 가능
- 상태 변경은 누가?
- 상태 사용자 (Context) 의 상태 변경은 누가 해줘야 할까? ... 때에 따라 알맞은 방식으로 고르자!
1. 상태 사용자가 직접 한다.
- 코드가 다소 복잡해질 수 있음
- 비교적 상태 갯수가 적고, 상태 변경 규칙이 거의 바뀌지 않는다면 고려해볼 것
- 콘텍스트가 알아서 상태를 변경하게 되면, 상태 객체는 정말 수행하는 작업만 작성하면 됨
public void increaseCoin(int coin) { state.increaseCoin(coin, this); if (hasCoin()) { changeState(new SelectableState()); } }2. 상태 내부에서 상태 사용자의 상태를 변경한다. - 컨텍스트에 영향을 안 주면서 상태를 추가/변경 가능 - 하지만, 상태가 많아지면, 변경 규칙 추적이 어려워짐 - 한 상태 클래스에서 다른 클래스에게 의존도 발생 ``` Java public void increaseCoin(int coin, VendingMachine vm) { vm.increaseCoin(coin); vm.changeState(new SelectableState()); } ```