백엔드 데브코스 TIL #3

잼구·2023년 9월 22일
0

해당 게시글은 실무 자바 개발을 위한 OOP와 핵심 디자인 패턴 강의를 참고해서 작성한 게시글임을 밝힙니다.

Stream

map 과 forEach

map 은 매핑 즉, 스트림 요소를 다른 요소로 변환하는 중간 처리 기능에 초점이 맞춰져 있다. 그렇기 때문에 변환한 새로운 스트림을 리턴 한다.

forEach 는 루핑 즉, 스트림에서 요소를 하나씩 반복해서 가져와 처리하는 것에 초점이 맞춰져 있다. 그렇기 때문에 새로운 스트림을 리턴하지 않는다. 또한 최종 처리 메소드 이다.

TIP.
map 이 새로운 스트림을 리턴해도, 원본 스트림 원소가 레퍼런스 타입이라면 원본 스트림의 요소들과 동일한 객체를 참조 한다.

중간 break 는 Filter 와 findAny() findFalse() 조합.

원하는 원소를 찾으면 break; 하는 코드는 filter() 로 대체 가능하다.

List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");
Optional<String> result = names.stream().filter(name -> name.startsWith("C")).findAny();
result.ifPresent(System.out::println); // 결과로 "Charlie"가 출력될 수도 있지만, 항상 그런 것은 아님.

해당 코드는 매칭되는 원소를 찾으면 바로 멈춘다. = 전체 순회 X
하지만 극악의 확률로 리스트 순서 상 가장 처음으로 매칭되는 값이 출력 되지 않을 수 있다.

이유는? 밑에!

fidnAny() 와 findFirst()

findAny()의 api 명세서 이다.

Optional findAny()

스트림의 어떤 요소를 나타내는 Optional을 반환하거나, 스트림이 비어 있을 경우 빈 Optional을 반환합니다.
이는 단축(short-circuiting) 종료 연산입니다.
이 연산의 동작은 명시적으로 비결정적(nondeterministic)입니다; 스트림의 어떠한 요소라도 선택할 자유가 있습니다. 이는 병렬 연산에서 최대 성능을 허용하기 위함입니다; 단점은 동일한 소스에서 여러 번 호출할 때 동일한 결과를 반환하지 않을 수 있다는 것입니다. (안정적인 결과가 필요한 경우 findFirst()를 사용하세요.)

반환 값:
이 스트림의 어떤 요소를 나타내는 Optional이거나, 스트림이 비어 있을 경우 빈 Optional

예외:
NullPointerException - 선택된 요소가 null인 경우

참고:
findFirst()

findAny() 는 기본적으로 가장 처음 매칭되는 값이 return 된다는 보장이 없는 메소드이다. 그렇기때문에 순서가 중요하다면 findFirst() 을 사용하자.

TIP.
하지만 스트림이 순차적(sequential)일 경우, 대부분의 경우 첫 번째 요소나 그에 가까운 요소를 반환한다. 그러나 병렬 스트림(parallel stream)의 경우, 반환되는 요소는 어떤 특정 순서나 패턴에 따라 결정되는 것이 아니기 때문에 어떤 요소가 반환될지는 예측할 수 없다.

실제 stream 을 사용한 코드 개선

public class EnumValueValidator implements ConstraintValidator<EnumValueConstraint, CharSequence> {
  private List<String> acceptedValues;
  private Class<? extends Enum<?>> enumClass;
  @Override
  public void initialize(EnumValueConstraint annotation) {
      acceptedValues = Stream.of(annotation.enumClass().getEnumConstants())
              .map(Enum::name)
              .collect(Collectors.toList());
      this.enumClass = annotation.enumClass();
  }
  @Override
  public boolean isValid(CharSequence value, ConstraintValidatorContext context) {
      if (value == null) {
          return true;
      }
      return acceptedValues.contains(value.toString());
  }
}

해당 코드는 string 위에 붙일 시 해당 enum class 로 변환가능한지 validate 해주는 유효성 검사기 이다.

기존 로직

  • enum class 가 가진 value 를 List 로 만든다.
  • 해당 List 에서 값이 있는지 contains 로 찾아 낸다.
public class EnumValueValidator implements ConstraintValidator<EnumValueConstraint, CharSequence> {
    private Class<? extends Enum<?>> enumClass;


    @Override
    public boolean isValid(CharSequence value, ConstraintValidatorContext context) {
        if (value == null) {
            return true;
        }

        return Arrays.stream(enumClass.getEnumConstants())
                .anyMatch(enumVal -> enumVal.name().equals(value.toString()));

    }
}

바뀐 로직

  • 바로 stream 으로 매칭 되는 value 가 있는지 찾는다.

바뀐 후 이점은?

  • 가독성이 좋아졌다.
  • 코드가 매우 짧아짐.
  • 매번 enumVal.name() 를 호출해야 해서 부담이 있는게 아닌가 생각 할 수도 있지만, 유효성 검사기는 런타임에서 호출 될 당시 매번 인스턴스를 새롭게 만들기 때문에 바뀐로직과 이전로직의 성능은 비슷하다.

바뀐 후 단점은?

  • stream api 를 사용한 반복문은 contains(혹은 for문) 보다 보통 느리다. 하지만 체감할만한 수준은 아니다.

의존

의존 관계가 생기는 경우

  • 다른 클래스의 레퍼런스 변수를 사용하는 경우
  • 다른 클래스의 인스턴스를 생성하는 경우
  • 다른 클래스를 상속 받는 경우

클래스로 적어놨지만 인터페이스도 똑같음!
TIP. 인터페이스를 통해 의존 관계를 맺으면 변경에 용이해 진다. = 약한 의존관계가 된다.

의존 역전

고수준의 컴포넌트가 저수준 컴포넌트에 의존하지 않도록 의존관계를 역전 시키는 것.

3-tier layer 에서는 service 가 고수준, repo 가 저수준.
이유는?

  • service = "게시글을 저장" (비즈니스 로직) 이라는 추상적인 개념을 수행 -> 기술에 종속적이지 않음
  • repo = 실제 "기술"을 사용해 게시글을 저장 -> 기술에 종속적임

그렇다면 의존역전은?

  • 서비스 레이어가 레포지토리 레이어에 의존하지 않도록 하는 것!

방법은? 인터페이스를 활용한다.
인터페이스는 추상적 개념을 수행하기에 고수준이다. 다른 레포들이 인터페이스를 implements 하게 된다면 오른쪽으로 흐르던 의존이 반대로 역전되면서 의존역전이 이루어진다. 더 이상 고수준이 저수준에 의존하지 않는다.

TIP. 컨트롤러는 저수준 컴포넌트! 이유는? HTTP 혹은 키보드 입출력 등 기술 종속적이기 때문.

의존 주입

서비스에서 레포 인스턴스를 생성하면 또 다시 의존이 생기게 된다. 그렇기 때문에 레포 인스턴스를 다른 곳에서 생성해서 서비스에 넣어줘야한다.

  • 프레임워크를 사용하면 스프링이 이걸 해준다.
  • 스프링을 사용하지 않더라도, 다른 class 파일에서 인스턴스를 생성해 넣어주면 된다 -> 해당 파일에서만 원하는 구현체를 바꿔 주면 되어서 테스트에도 용이하다. (Test Double)

의존 주입패턴의 장점
어떤 인스턴스를 사용할지 코드수정 없이, 런타임에 지정 가능 하다.
런타임 = 실행하는 시점 + 실행하는 도중

스프링을 사용한 의존 주입 (구현체 런타임에 지정 하기)

실행하는 시점에 변경

  • @Profile
    @Profile("prod") @Profile("test") 어노테이션을 class 위에 주어서 스프링 부트 실행 시점에 생성을 결정 할 수 있다.
    어떤 profile 에 실행할지는 애플리케이션을 실행시킬때 옵션으로 지정하면 된다.

실행하는 도중에 변경

  • @Order
    @Order(0) @Order(1) 빈의 우선순위를 정할 수 있음.
public Service (List<Repository> repos) {
  	this.A = repos.get(0);
  	this.B = repos.get(1);
  }

List 로 해당 되는 인터페이스 구현체를 모두 주입 받을 수 있음. 그때 우선 순위로 특정 가능.

A 를 main 레포로 사용하고 싶을때는 changeRepo() 라는 함수를 만들어, 호출 될때마다 내부에서 A 레포를 변경하는 코드를 사용할 수 있다.

public void changeRepo() {
    var temp = this.A;
    this.A = this.B;
    this.B = temp;
  }
profile
잼구입니다

0개의 댓글