티스토리 뷰

if문 제거하기
getter와 setter
stream API
stream API와 Optional

 


if문 제거하기🌳

너무 많은 if문은 코드를 읽기도 어렵고, 수정과 디버깅을 어렵게 만든다!
같은 로직이라면 if문 없이 작성된 코드가 가독성이 높은 코드가 된다.

 

개선방법 🤨

1. 빠르게 반환하기 Early return

각각의 if문 안에 들어있던 널 체크와 나누기 연산 시 나누는 수 0인지 먼저 검사 후 예외 처리한다.

 

2. Enum 안으로 넣기
 
3. 생성 시점에 유효성 검사

체크를 사용하는 시점이 아니라 생성하는 시점에 하기

파라미터로 받은 값이 정상적인 값임을 검사하는 것이 유효성 검사이다.(검사하여 예외를 던져주기!)

생성 시 실패하는 것을 명시적으로 호출한 쪽에 알려주기 위함이다!

 

리팩토링

"결과의 변경 없이" 코드의 구조를 재조정 하는 것으로, 가독성을 높이거나 유지보수를 편하게 한다.

 
예외 처리를 추가한 부분은 리팩토링이라고 부를 수 없다!

리팩토링에 대한 결과가 변경되지 않았다는 것을 어떻게 알 수 있을까❓

  • 테스트 코드를 활용해볼 수 있다.
    • 테스트 후에도 동일하게 동작한다면 리팩토링!
    • 테스트 후 동일하게 동작하지 않으면 버그!
    • 테스트되지 않은 것에 대해서는 보장할 수 없다!

 
객체지향 자체는 테스트와 깊은 관련이 있다고 볼 수 있다.
작성한 코드가 실제 서비스가 되는 환경에서 실행될 수도 있고, 테스트가 되는 환경에서도 실행될 수 있다.
 
 


getter와 setter🌳(캡슐화, 결합도, 응집도의 중요성)

캡슐화

클라이언트 코드에게 서버 코드의 정보를 숨기는 것을 의미한다.

 

  • 모든 것을 숨길 수는 없지만, 적어도 클래스 내부 요소를 외부에 공개하지 않고, 어떤 일을 할 수 있는지만 공개한다.
  • 모든 필드는 private로 만들고 공개하고 싶은 메서드는 public이 된다.

 
이때 생성자를 통해서는 클래스가 어떤 필드를 가지고 있는지 간접적으로 노출이 되긴 한다.

  • 그러나 생성의 경우 제한적인 영역에서 사용이 되기 때문에 크게 문제가 되지 않는다.
    생성 역시 영향을 받는 코드를 작성하고 싶다면 팩토리 관련 디자인 패턴을 적용하면 된다.

 
👻 캡슐화된 코드란, 다르게 표현하면 결합도는 낮추고 응집도를 높인 코드라고 할 수 있다.
 

결합도

  • 클라이언트 코드는 A 클래스 내부 필드에 대해 잘 알고 있어, A 클래스의 작은 변화에도 그대로 노출된다.
    이런 상황을 Client와 A 클래스의 결합도가 높다고 이야기를 하고
    구체적으로는 Client가 A 클래스에 지나치게 의존하고 있는 것이다.
  • 이렇게 결합도가 높은 코드는 코드 변경을 어렵게 만들고 한쪽에서 일어난 변경을 다른 쪽으로 전파하는 문제가 있다.
  • Getter를 없애고 계산 결과를 받아오는 메서드에만 의존하도록 만들어 결합도를 낮출 수 있다👍

 

응집도

  • 관련 있는 것들끼리 얼마나 모여있는가
  • A 클래스의 필드들은 그 필드들을 활용하여 계산 결과를 만드는 로직과 분리되어있다.
    필드는 A 클래스가 가지고 있지만 로직은 클라이언트 코드에서 가지고 있다. 이런 코드가 바로 응집력이 낮은 코드이다.
  • 객체지향적인 코드는 결합도는 낮고, 응집도는 높은 코드를 의미한다.
    코드의 변경에 영향을 적게 받아, 변경에 강하다고 이야기할 수 있다. -> 유지보수하기 좋은 코드

 

생성자 vs setter

생성자가 없으면, set을 통해 생성 시 불완전하다.
하나 값을 초기화하지 않는 실수가 발생할 수도 있다!
그렇지 않다고 하더라도,
모든 필드를 파라미터로 받아서 생성자로 초기화하는 것보다 직관적인 방법은 아니기에, 생성자의 사용이 중요하다🌟
모든 필드를 초기화해야 하는 생성자가 있다면 이 코드를 사용하는 사람은 자연스레 그 생성자를 사용하게 될 것이다.
특히 불완전한 인스턴스를 생성하면 안 되는 경우에 더더욱 좋은 효과를 가져온다.👍👍
 

기술적인 문제로 getter가 필요한 경우

setter와 getter의 사용을 지양하지만 getter의 사용이 필요한 경우도 있다!
 
자바 웹 애플리케이션에서 보이는 전형적인 패턴
: 내부에서 사용되는 데이터 클래스와 외부와 접점이 있는 데이터 클래스를 나눠서 사용하는 경우

  • 외부와 접점이 있는 클래스를 DTO라고 칭한다. 외부에서 들어오는 요청이 먼저 DTO에 들어간다.
  • 내부에서는 이 DTO를 그대로 사용하는 게 아니라 변환 과정을 거치게 된다.
  • 이때 DTO가 가진 값을 꺼내와야 한다. 이때 DTO의 getter가 필요하다. DTO의 게터로 꺼내온 값을 내부에서 사용하는 데이터 클래스의 생성자에 파라미터로 넣어준다.
  • DTO가 컨트롤러의 반환값으로 사용되는 경우
    • 컨트롤러는 HTTP 요청을 받아서 HTPP 응답을 주게 되는데, 이때 DTO가 컨트롤러에 등록된 메서드에 반환값으로 사용되는 경우, getter가 반드시 필요하다.
    • 반환될 때 getter가 없으면 해당 필드는 HTTP 응답에 포함되지 않고, DTO에 getter가 하나도 없으면 HTPP 요청이 실패하게 된다.
    • 삽질 대상이다! 😢

 

가독성

getter와 setter를 없애면, 가독성 높은 코드를 만들 수 있다.
getter, setter 사용 시 코드를 열심히 읽어서 로직을 파악해야 하지만, 
사용하지 않으면 메서드 이름을 통해 바로 알 수 있어 상대적으로 가독성이 올라간다⬆️⬆️⬆️
 
🌟 특히 실무에서 보게 되는 복잡한 코드에서 잘 지어진 이름을 가진 메서드는 가독성을 크게 향상시킨다.

 


stream API🌳

기능적으로 for문과 if문을 대체할 수 있다. forEach, filter, map 등등..

 

Stream API를 적용할 수 있는 대상

Stream API는 Collection Interface 내에 존재하는 메서드로

Collection Interface를 구현하는 구현체에서 사용가능!

➡️ List(->ArrayList), Set(->HashSet), Queue

 

✅ Map(->HashMap)의 경우

Collection Interface 상속받지 않으므로 사용 불가능하지만 우회하여 사용가능하다!(아래에서 코드로 설명했습니당)

 

List Interface

Collection Interface의 서브타입인 List Interface가 있다.

List Interface에 대해 streamAPI를 호출한다.

.stream()을 통해 반환된 값으로 이어서 stream API를 실행시키는 것이다.

 

forEach와 for 문의 차이점

for문의 경우

index 사용 가능하므로 이전, 이후 변수 참조 가능

break 사용 가능하지만!

for (int i = 0; i < scoreList.size(); i++) {
    System.out.println(scoreList.get(i));
}

 

forEach의 경우

index 사용 불가능하며

함수라서 반환은 가능하지만 해당 streamAPI를 끝내지 않는, continue 같은 존재이다.

반복문 종료를 위해 throw new RuntimeException()을 사용할 수 있으나 일반적인 경우는 아니다!

scoreList.stream().forEach(score -> {
    System.out.println(score);

    if(integer.equals(100)) {
        throw new RuntimeException();
    }
});

 

중간에 중단해야 하는 경우 filter를 사용하면 된다👻

 

for문과 filter

for문의 경우

perfectScore변수를 미리 선언하여 null로 초기화하고

Integer perfectScore = null;

for (int i = 0; i < scoreList.size(); i++) {
    System.out.println(integerList.get(i));

    if(integerList.get(i).equals(100)) {
        perfectScore = scoreList.get(i);
        break;
    }
}

 

filter의 경우

perfectScore가 필터에 의해 초기화된다!

Integer perfectScore = scoreList.stream().filter(score -> {
    System.out.println(score);

    if(score.equals(100))
        return true;

    return false;
}).findAny().get();

 

+ findAny()의 경우 조건에 맞는 값을 하나 찾으면, 뒤를 실행하지 않는다.(최적화를 위해👍)

 

map(인터페이스 map 말고❌ stream API의 메서드⭕)

원래의 컬렉션에 대해서 특정 연산을 적용한 후, 새로운 컬렉션을 만들어내는 기능을 한다.

List<Integer> x10ScoreList = scoreList.stream()
        .map(score -> score * 10)
        .toList();

 

filter+map 응용해 보면!

80점 이상의 점수를 뽑아 10점을 뺀 리스트

// 두번에 걸쳐서
List<Integer> over80ScoreList = scoreList.stream()
        .filter(score -> score > 80)
        .toList();


List<Integer> over80Minus10ScoreList = over80ScoreList.stream()
        .map(score -> score - 10)
        .toList();
        
// 한번에도 가능!
List<Integer> over80Minus10ScoreList = scoreList.stream()
        .filter(score -> score > 80)
        .map(score -> score - 10)
        .toList();

 

Map에 stream API 사용하기

set으로 변경하여 사용할 수 있다! entry도 set이라 streamAPI 사용이 가능하다.

Map<String, Integer> scoreMap = new HashMap<>();
Set<Map.Entry<String, Integer>> scoreEntries = scoreMap.entrySet();
Set<String> keySet = scoreMap.keySet();
List<Integer> valueList = scoreMap.values().stream().toList();

// entrySet(): Map의 각 항목을 나타내는 Map.Entry 인터페이스의 집합을 반환

scoreEntries.stream().forEach(scoreEntry -> {
    String key = scoreEntry.getKey();
    Integer value = scoreEntry.getValue();

    // key, value 사용
});

 


Stream API와 Optional🌳

데이터베이스에서 데이터를 조회 후에

특정한 조건을 만족하는 데이터가 있다면 다음 작업을 실행하고, 없다면 예외를 발생시킨다!

이는 실무에서 자주 발생하는 패턴이다 👻

 

streamAPI에서 findAny() 메서드는 Optional을 반환한다.

비어있는 경우에는 예외를 던지도록 하는 게. orElseThrow()이다.

Integer perfectScore = scoreList.stream()
	.filter(score -> {
        if(score.equals(100))
            return true;

        return false;
	})
        .findAny()
        .orElseThrow();

 

.ifPresentOrElse

studentList.stream()
        .filter(student -> student.sameId(idToFind))
        .findAny()
        .ifPresentOrElse(student -> {
            throw new RuntimeException(idToFind + "는 이미 존재하는 아이디입니다.");
        }, () -> {
            System.out.println(idToFind + "는 사용할 수 있는 아이디입니다.");
        });

첫 번째 람다 표현식

  • findAny로 나온 Optional 객체가 비어있는 값이 아닐 때

두 번째 람다 표현식 

  • Optional 객체가 empty 일 때

-> 이 예제의 경우, 값이 존재하면 예외를 발생시켜야 하는 상황이다!

 

stream API는 for문보다 빠를까 느릴까❓

 

경우에 따라 다르지만, 대체로 실제로 느릴 수 있다🤨

(하지만 웹 애플리케이션 개발 시 체감할 만한 수준은 아닐 것이다!)

따라서 가독성 우선하여 개발 후 성능이 필요한 경우 수정하여 사용하면 된다.

 

 

+++ Parallel Stream

Stream API를 병렬적으로 멀티 쓰레드로 실행시키는 것을 말한다.

멀티쓰레드로 실행을 시켜줄 수 있기 때문에 경우에 따라서 성능을 향상시켜볼 수 있다!

다만, 성능이 향상되는 대신에 순서에 대한 보장을 할 수가 없고, 항상 좋은 성능을 내는 건 아니다.

오히려 스트림 보다 성능이 낮아지는 경우가 생길 수 있다.

 

'백엔드 > Java' 카테고리의 다른 글

[Java] 함수형 인터페이스 종류  (0) 2023.09.26
[자바와 객체지향] 객체지향 문법 정리  (0) 2023.09.21