[새배내] 블랙잭 미션을 하며 새로 배운 내용

Junseo Kim·2021년 3월 2일
1

[우아한테크코스3기]

목록 보기
11/27

2021.03.02 ~ 2021.03.15


업캐스팅 vs 다운캐스팅

업캐스팅: 하위 클래스를 상위 클래스로 타입을 변환하는 것을 의미한다.

CaffeineBeverage beverage = new Coffee(); 
CaffeineBeverage beverage = new Tea(); 

다운캐스팅: 상위 클래스를 하위 클래스의 타입으로 변환하는 것을 의미한다.

CaffeineBeverage beverage = new Coffee();

if (beverage instanceof Coffee) {
    Coffee coffee = (Coffee)beverage;
}

ArrayList

ArrayList는 2가지 종류가 있다.

java.util.ArrayList: 일반적으로 아는 ArrayList

java.util.Arrays.ArrayList: Arrays.asList로 생성하면 반환되는 ArrayList. 원소를 추가하는 기능이 없으므로 사이즈를 바꿀 수 없음.

singletonList

한 개 짜리 요소를 가진 List를 사용할 때는 singletonList를 사용하면 메모리를 좀 더 아낄 수 있다.

같은 클래스 다른 객체

같은 클래스의 경우 다른 객체라도 인스턴스 변수에 바로 접근할 수 있다.

LinkedHashMap

순서를 유지할 수 있는 Map 자료구조이다.

HashMap은 hash값이 낮은 순서대로 순회를 한다. 데이터가 늘어나면 hashCode 재분배가 일어나며 hash값이 변하기 때문에 순서를 유지할 수 없다.

LinkedHashMap은 HashMap에 LinkedList를 섞어놓은 자료구조이다. HashMap을 상속받고 있으면서 head, tail, before, after 변수를 사용하여 순서를 기억한다.

참고:
[자료구조] 코드로 알아보는 java의 LinkedHashMap

EntrySet

key와 value의 값이 모두 필요한 경우 사용. Key-Value값을 Entry(키와 값을 결합)의 형태로 Set에 저장하여 반환

정적팩토리 메서드 생성자 차이

정적팩토리 메서드: 자바 생성자가 할 수 없는 일들을 할 수 있다. 이미 존재하거나 재사용 가능한지 확인할 수 있다.

생성자: 강력하지 않음 생성만 가능. 이미 존재하거나 재사용 가능한지 확인할 수 없다. 제공된 인자를 통해 캡슐화된 프로퍼티를 초기화하는 것이 주된 임무이다. 생성자에 로직이 들어간다면 정적 팩토리 메서드 사용을 고려해봐야함. 주 생성자에는 제공되는 인자들이 완전해야한다.

정적팩토리 메서드 사용시 검증 위치

static 메서드는 어플리케이션이 종료될 때까지 메모리에 남아있게된다. 따라서 정적 팩토리 메서드에서 검증을 하게된다면 검증에 대한 메서드도 private static 하게 되기 때문에 정적팩토리 메서드 내부가 아닌 생성자 내에서 검증을 하는 편이 좋다.

추상클래스와 인터페이스

상속은 확장이고, 인터페이스는 구현이다. 상속은 확실한 is-a 관계에서 사용하면 된다. 변경되는 요구사항에서 확실한 is-a 관계를 찾기 힘들기 때문에 상속보다 조합을 사용하라는 말이 있지만 상속이 좋지않다거나 사용하면 안된다는 말은 아니다.

디폴트 메서드를 가지는 인터페이스는 인터페이스를 구현하는 구현체들에게 공통적인 메서드를 제공할 때 사용하며 각 구현체에서 override 해 줄 수도 있다.

추상클래스

  • 미완성 설계도
  • 인스턴스화 불가
  • 다중 상속 불가
  • 기능을 이용하고 확장시키는 목적
  • is-a 관계시 사용(부모 자식 관계가 명확할 때, 관련성이 높은 클래스 사이)

인터페이스

  • 인스턴스화 불가
  • 다중 상속
  • 인스턴스 변수를 가질 수 없음
  • 특정한 메서드를 구현하도록 강제하여 구현 객체의 같은 동작 보장
  • has-a 관계시 사용(관련성이 없는 클래스들이 동일한 동작을 하는 경우)

상속과 조합

상속은 코드를 재사용하지만 항상 최선의 방법은 아니다. 상속을 완전 제거하기보다는 올바르게 사용해야한다.

상위 클래스의 내부 구현이 달라지면 하위 클래스의 코드 변화가 없더라도 오동작할 수 있다.

상속받은 자식 클래스가 super.메서드로 부모 클래스의 메서드를 호출한다고 할 때, 호출된 부모클래스의 메서드가 자식클래스에 오버라이드 되어 있는 메서드를 호출한다면 부모 자신의 메서드가 아닌 오버라이드 된 메서드를 호출하게 되므로 오작동한다.(https://stackoverflow.com/questions/16476716/why-does-super-addall-calls-the-overridden-add-method)

이를 방지하려면 개발자가 상위클래스 내부를 상세히 알아야하기 때문에 캡슐화가 깨진다.

클래스나 메서드를 final, abstract로 두면 상속으로 인한 문제를 막을 수 있다. 클래스가 final이라면 상속 자체를 할 수 없고, abstract 메서드로 선언하면 상속받는 자식 클래스가 각각 구현을 해야하기 때문이다.

is-a 관계일때만 상속을 사용하고 불 완전한 개념을 완성시킬때만 상속을 사용하자

상속이 적절한 경우란 언제일까? 클래스의 행동을 확장(extend)하는 것이 아니라 정제(refine)할 때다. 확장이란 새로운 행동을 덧붙여 기존의 행동을 부분적으로 보완하는 것을 의미하고 정제란 부분적으로 불완전한 행동을 완전하게 만드는 것을 의미한다.

객체 지향 초기에 가장 중요시 여기는 개념은 재사용성(reusability)이었지만, 지금은 워낙 시스템이 방대해지고 잦은 변화가 발생하다 보니 유연성(flexiblity)이 더 중요한 개념이 되었다.

불변객체 장점

  • 유지 보수성이 크게 향상
  • 식별자 변경 문제 없음(생성 후 상태 변경이 불가능)
  • 객체가 완전하고 견고한 상태 or 아예 실패하는 실패 원자성을 가짐
  • 시간적 결합제거(가변 객체인 경우 연산들의 순서를 하나하나 기억해야함)
  • 스레드 안정성(객체가 여러 스레드에서 동시에 사용될 수 있고 예측 가능한 결과를 보장)
  • 단순성(객체가 단순해질수록 응집도가 높아지고, 유지보수는 더 쉬워짐)
  • 크기가 작다.(불변 객체의 경우 생성자 안에서만 상태를 초기화할 수 있기 때문)

Assertions hasSize

크기가 얼마인지 테스트

    @Test
    void hasSize() {
        List<Integer> list = new ArrayList<>();
        assertThat(list).hasSize(0);
    }

Collections.frequency

빈도 수를 알아낼 수 있음

    @Test
    void frequency() {
        List<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(1);
        list.add(3);
        assertThat(Collections.frequency(list, 1)).isEqualTo(2);
    }

스트림의 forEach 와 forEachOrdered

forEach 같은 경우 병렬 스트림에서 순서를 보장할 수 없다. 병렬스트림에서 순서를 보장하려면 forEachOrdered을 써야한다.

List<Integer> list = List.of(5, 40, 1, 2, 8, 9);
list.parallelStream()
        .sorted()
        .forEachOrdered(num -> System.out.print(num + ", "));
        
// -> 1, 2, 5, 8, 9, 40
List<Integer> list = List.of(5, 40, 1, 2, 8, 9);
list.parallelStream()
        .sorted()
        .forEach(num -> System.out.print(num + ", "));
        
// -> 8, 2, 1, 9, 40, 5 (병렬스트림에서 순서 보장 x)

추상클래스의 멤버변수 접근제한자

protected로 두거나, private으로 두고 getter를 사용하거나.(리뷰어분들 마다 생각이 다르신 것 같다.)

추상클래스에서 정의한 메서드 override 막기

추상클래스에서 정의한 메서드를 하위 클래스에서 override 못하게 하려면 메서드에 final을 붙여준다.

클래스 내부에서 객체를 생성하는 것과 외부에서 주입 받는 것의 차이

객체의 생성과 사용의 관심을 분리하기 위해 외부에서 주입받는다. 클래스를 객체의 생성과 독립적이게 만든다. 객체를 클래스 내에서 생성하게 되면 클래스의 수정없이 독립적으로 인스턴스 생성을 변경하는 것이 불가능하여 유연하지 못하다. 다른 객체를 필요로 할 때, 클래스를 재사용할 수 없게 한다.

의존성 주입

일급컬렉션이 꼭 불변일 필요는 없다

일급 컬렉션을 불변 객체로 만들 경우 해당 일급 컬렉션을 참조하고 있는 객체의 필드에는 항상 일급 컬렉션을 재할당해줘야하는 문제가 있다.(Collection을 새로 생성하는 등의 비효율성) 물론 side-effect는 발생하지 않는다.

또한 일급 컬렉션은 컬렉션이 감싸고 있는 객체 자체의 불변은 보장하지 않는다.
[회고] level1 - 블랙잭 미션 회고(학습 로그 말하기 참고)

일급컬렉션에 대하여
일급 컬렉션을 사용하는 이유

View의 getter

Q9. view에서는 출력을 위해 getter를 사용할 수 밖에 없다고 알고 있습니다. 그런데 객체들이 너무 포장이 되어 있어서 원래 값을 꺼낼 때 getter가 너무 이어지게 되는 것 같아요 ㅠㅠ view라서 괜찮은가? 라는 생각이 들기도 하지만 너무 데메테르 법칙을 위반하는 것 같기도 하여 조언을 구하고 싶어요ㅠㅠ

stream 매핑의 getter

stream을 통해 매핑시 getter를 사용하는 것도 getter와 마찬가지로 지양.

getter를 작성하지 않는 Tip

view에 대한 코딩을 하기 전까지는 도메인 객체에 getter 메서드들은 작성하지 않기.

테스트 관련

  • 생성 테스트: VO객체의 생성테스트는 isEqualTo 사용. 객체 생성시 예외 발생 하면 해당 예외 케이스 테스트.

  • 테스트는 성공하는 케이스보다 실패하는 테스트 많이 하기.

  • 추상 클래스의 테스트: private 메서드와 동일하게 하위클래스를 통해서 테스트

VO

모든 도메인 객체가 다 VO는 아니다. VO는 같은 값일 경우 같은 객체로 취급해주는 것. 이름이 같은 사람이라고 같은 사람인 것은 아니다. 객체를 VO로 보려면 객체 내의 모든 필드의 값이 다 같아야한다.

DTO 관련

  • dto의 필드로 꼭 원시값이나 String을 둘 필요는 없다. VO객체를 필드로 둔다면 사용해도 된다. 불변객체는 상태가 변하지 않기 때문에 부작용이 없기 때문에 view로 내보내도 된다. 굳이 모든 원시값을 dto의 필드에 둘 필요는 없다.

  • 도메인 모델은 dto의 존재를 모르는 것이 좋다. 도메인 모델이 dto의 존재를 안다면 dto에 변화가 생기면 도메인 모델도 변경을 해줘야하기 때문이다.

  • dto 생성시 도메인 객체를 받아 생성자나 정적 팩토리 메서드로 처리해줘도 된다.

public class UserDto {
  private Long id;
  private Name name;
  ..

  public UserDto(User user) {
    this.id = user.getId();
    this.name = user.getName()
    ..
  }
  // getter
}
public class Application {
  .. 
  public static void main(String[] args) {
    ..
    OutputView.printUser(new UserDto(user));
  }
}

길어지는 controller

controller가 길어지면 도메인 설계를 확인해봐야한다. 적절한 도메인 객체를 만들어 책임과 역할을 부여하고 컨트롤러에서는 그 도메인 객체를 사용해 비즈니스 요구사항을 풀어낸다. 도저히 도메인에서 할 수 없는 경우 controller에서 처리하는 것이다.

컨벤션

class Test {
  상수
  클래스 변수
  인스턴스 변수
  생성자
  팩토리 메서드
  메서드
  기본 메서드 (equals, hashcode, toString...)
}

참고
Google Java Style Guide

상태패턴

[Java] 상태 패턴(State Pattern)

퍼사드 패턴

[Java] 퍼사드 패턴(Facade pattern)

인텔리제이 다이어그램 보는법

package 오른쪽 클릭 -> Diagrams -> Show Diagrams -> Java Classes

테스트를 위한 생성자

생성자와 메서드는 다르다. 생성자가 많을수록 클라이언트가 편하다. 테스트를 위한 생성자가 있어도 된다.

vs

프로덕션코드에 사용하지도 않는 코드를 굳이 그것도 테스트를 위해서 만들 필요가 없다. 메서드와 마찬가지로 테스트를 위한 생성자가 있으면 안된다.

BigDecimal

숫자를 정밀하게 저장하고 표현할 수 있는 유일한 방법. double은 소수점 정밀도에 한계가 있어 값이 유실될 수 있다. 돈과 소수점을 다룰때는 필수적으로 사용해야하지만 속도가 조금 느리고 사용하기 불편하다는 단점이 존재하긴한다.

자주 사용하는 상수는 미리 정의되어 있다.

  • BigDecimal.ZERO
  • BigDecimal.ONE
  • BigDecimal.TEN

문자열을 전달하여 생성할 수 있다.(int나 double도 넘길 수 있다.)

BigDecimal bigDecimal = new BigDecimal("1.5");

참고
Java, BigDecimal 사용법 정리

Controller 관련

  • 도메인 객체에서 Controller를 의존하고 있으면 안된다!!
  • Controller는 보통 1개의 인스턴스로 만들어진다.

전략 패턴과 상태 패턴의 차이

  • 상태 패턴: 상태에 따른 행동을 상태 객체 스스로가 결정.(행동이 캡슐화)
  • 전략 패턴: 행동을 외부에서 주입.

동일 필드를 가지는 객체

동일한 필드를 가지고 있다고 해도 한 클래스로 명확한 역할을 나타내기 힘들다면 클래스를 나누는 것도 좋다.

읽은도서

0개의 댓글