콘텐츠로 이동

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

  • 정의 - 계층 간 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;
    

상태 패턴

state

  • 의도 - 객체 자신의 내부 상태에 따라 수행해야 하는 행위를 변경하는 효과를 주기 위해서
  • 활용 - 객체의 행위는 상태에 따라 달라진다 - 객체의 상태에 따라서 프로그램 실행 중에 수행해야 하는 행위를 변경할 때 활용하자 - 객체의 상태에 따라 수 많은 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());
          }
          ```