Stream 오류 제거
의문점
스크래핑 로직 실행 시간에 대해 의문을 가졌다.
평균 시간이 그럭저럭 다들 비슷한 수준에 머무는데, 이상하게 한 부분만 너무 느렸다. 증권사에 대한 스크래핑 내역이었는데,
특정 증권사만 중복 로그인이 감지되었을 경우 30초를 대기했다가 다시 수행할 수 있게 하는 로직이 들어있었고,
테스트 코드는 그에맞는 정말 30초를 기다리는지에 대한 여부를 테스트 하고 있었다.
🤔 그냥 30초를 기다리는것보단 특정한 법인이 아니라면 30초를 기다리지 않는다 를 테스트하면 되지 않을까?
이렇게 생각했던 이유는 30초를 테스트에서 기다리는 비용이 비싸다고 생각했고, 전체 테스트를 돌릴 때 이 부분때문에 30초를 더 기다려야 한다는 것이다. 그리고 milliseconds 차이값을 비교했기에 간헐적으로 실패하는 경우도 있었다.
그래서 더 고치고 싶었는데 일단 테스트를 돌려봤다.
디버깅
바로 디버깅 시작했는데 내가 stream 로직을 잘 구성하지 못하는 것인가? 라는 생각도 들었다.
stream 내부에 stream을 또 구성하고 있는 상황이었다.
이말은, stream 내부의 stream에선 어떤 값을 반환하던 간에 바깥에서 Stream으로 래핑을 하고 있으니 뭘해도 값이 존재한다는 가정이 먹힌다는 뜻이다.
그래서 위에서 나는 에러점이 보였던 것이다.
해당 부분을 사진을 좀 가져오려고 했는데 워낙에 적나라하게 코드를 다 보여주는 것 같아서 예시 코드를 가져ㅇ와봤다.
가장 짤막하게 예시를 들어볼 수 있는게 햄버거가 아닐까 싶었는데
안에 토핑을 생각하다가 간단하게 햄버거 이름, 빵종류, 소스가 무엇인지만 적어봤다.
물론 햄버거 재료중에 패티가 빠지면 섭한데 대충 보자. ㅋㅋㅋ
Java 14버전부터 해당 record 클래스가 등장했다.
나는 집에서는 java 17버전을 쓰고있기에 record로 적었고
이미지와 아래의 코드는 같은것이라고 생각하면 되겠다.
그러면서 getter가 기본으로 구현되어있고 getXXX() 방식이 아닌 바로 변수 이름과 같은 메소드를 호출할 수있다.
public final class Hamburger {
private final String name;
private final String bread;
private final List<Sauce> sauces;
}
아무튼 이런 햄버거와 안에 들어간 소스를 봐야하는데 소스도 정말 대충 value라고 이름을 지었다.
무튼 이렇게 두개 클래스가 있는데
이런 코드를 예시로 만들었다. 로직만 다를뿐이지 이슈가 있던 로직과 같은 구현법이다.
햄버거들에서 소스만 발라내서 매운 소스가 들어간지 여부를 확인하여 있으면 true 없으면 false를 반환하려고 했었던 로직인 것 같다.
잘보면 뭐 햄버거들중에 소스를 발라내고 그 소스들을 뒤져가면서 null이 아니며 spicy 소스인 데이터를 찾으면 바로 true 반환하려고 했던 것 같다.
위에서 설명했던 30초 로직은 해당 메소드에서 true가 반환되면 타게 되어있었다.
public으로 구현했지만 회사의 로직은 private으로 구현되어 있는 메소드였다.
각설하고, 이부분에 대해 테스트를 해보니 "spicy" 라는 값을 넣지 않아도 무조건 테스트가 항상 true였다.
그래서 걸러주어야 할 로직들도 모두 참이 되어버리는 것이다.
소스에 살사와 머스타드를 넣었음에도 불구하고 spicy 소스가 없는데 참이 나온다.
자세히 보면
stream내부에 stream이 있지 않는가?
내부에서 뭘 수행하던간에 아직 stream 연산을 끝맺어주지 않았기 때문에 Stream<> 객체를 반환해줄 것이고 그렇기 때문에 항상 그 내부 연산이 존재하던 않던 상관없이 Stream 객체 자체가 null이 아니고 존재하기 때문에 findFirst() 를 수행해도 항상 참을 반환하는 것이었다.
개선하기
그래서 어떻게 이 부분을 개선했는가?
public boolean isExistSpicySauce(List<Hamburger> hamburgers) {
final Set<Sauce> sauces = hamburgers.stream()
.map(hamburger -> hamburger.sauces())
.flatMap(List::stream)
.collect(Collectors.toSet());
return sauces
.stream()
.filter(sauce -> sauce.value() != null)
.anyMatch(sauce -> "spicy".equals(sauce.value()));
}
이런식으로 개선했다.
결과는 당연히 spicy가 포함되지 않았기 때문에 false를 반환했고 테스트도 성공한것을 볼 수 있다.
stream으로 엮어주고 Set으로 변환한 이유는 어떤 햄버거든 소스들만 추려 spicy만 있으면 됐기 때문에 이런식으로 구현을 해주었다.
회사 코드에선 한 법인의 여러개의 공동인증서를 가지고 판별하는 문제였기 때문에 이것처럼 구현해보았다.
stream내부에서 stream을 또 사용할 때에 주의해서 써야겠다고 이 부분을 수정하면서 느꼈다.
어쩌면 너무 당연한 얘기일 수 있었지만, 이 부분 때문에 모든 스크래핑에서 30초 정도의 중복 로그인 대기 시간이 해소되었고 30초 * n개의 증권사 스크래핑 시간을 특정 기관에서만 중복로그인 30초만 대기할 수 있도록 수정되었다.
무의미한 스크래핑 대기 시간을 해소했다는 얘기이다!!
아무튼 한동안 너무 신규 기능개발건에 대해 초점이 맞춰져서 힘들었었는데, 계속해서 기존 코드 부수고 고치며 성능개선하며 이전 코드들을 돌아볼 수 있는 시간을 생각을 전환하니 어느새 갖고있게 되었다.
해당 생각의 전환은 https://jojoldu.tistory.com/710 여기서 자극을 많이 받게 되었다.