GitHub

https://github.com/Choidongjun0830

Java

[원티드 백엔드 챌린지 11월] 효율적이면서 현대스러운 코드 작성법

gogi masidda 2024. 11. 22. 15:35

의미있는 코드 Style

  • Java에서 인스턴스를 생성하는 방법
    • 1. 생성자. Constructor Method 이용
    • 2. 정적 팩토리 메서드: Static Factory 이용
      • 장점
        • 이름을 가질 수 있다. (의미 전달)
          • 반환될 객체의 특성을 쉽게 묘사.
          • 반면에 생성자는 클래스명과 같기 때문에. 
        • 호출될 때마다 인스턴스를 새로 생성하지는 않아도 된다. (싱글톤)
          • 시간이 지나고 프로젝트가 커지면 메모리 관리를 해야함. 
        • 반환 타입의 하위 타입 객체를 반환할 수 있다. (다형성) 
        • 입력 매개변수에 따라 매번 다른 클래스의 인스턴스를 반환 가능 (OCP)
          • 상위 클래스를 반환해도, 하위 클래스를 반환해도 된다. 
        • 정적 팩토리 메서드를 작성하는 시점에는 반환할 클래스의 객체가 존재하지 않아도 된다. 
          • 인터페이스로만 존재하고, 구현체로 존재하지 않아도 된다. 
      • 단점
        • 상속할 수 없다.
          • 기본 생성자의 접근 제한자가 private로 선언되기 때문에
        • 정적 팩토리 메서드 찾기가 어렵다
          • 일반적인 생성자는 클래스명과 동일한 이름이지만, 정적 팩토리 메서드는 다른 메서드 명을 사용해서 이런 코드가 익숙치 않은 동료 개발자는 인스턴스 생성 메서드를 찾기 어려움
          • 이름 짓기
            • from: 매개변수를 하나 받아서 해당 타입의 인스턴스를 반환하는 형변환 메서드
              • Date d = Date.from(instance);
            • of: 여러 매개변수를 받아 적합한 타입의 인스턴스를 반환하는 집계 메서드/ Entity -> DTO, DTO -> Entity
              • Set<Developer> dev = EnumSet.of(CHOI, DONG, JUN);
            • valueOf: 어떤 값으로 해당하는 객체를 만들 때. 
              • Boolean isTrue = Boolean.valueOf("true")  

 

정적 팩토리 메서드 예시 

@Getter
public class PaymentRequestDto {
	private final String pgCorpName;
    
    private PaymentRequestDto(String name) {
    	this.pgCorpName = PgCorp.valueOf(name.toUpperCase()).toString().toLowerCase();
   	}
    
    public static PaymentRequestDto of(String pgCorpName) {
    	return new PaymentRequestDto(pgCorpName);
    }
}

 


Builder 패턴

  • 생성자와 정적 팩토리의 한계: 선택적 매개변수가 많을 때 생성자와 정적 팩토리는 적절히 대응하기 어렵다.
    • 메서드를 호툴하는 과정에서 파라미터 순서가 중요하고 누락될 수 있는 휴먼 에러를 내포
    • 가독성이 떨어짐 
  • Builder 패턴을 사용하면 이러한 문제를 해결 가능
    • 방법
      • Builder 패턴을 클래스 내부에서 직접 구현
      • Lombok에서 제공하는 어노테이션으로 적용

직접 구현

public class NutritionFacts {
	private final int serviceSize;
    private final int servings;
    private final int calories;
    private final int fat;
    private final int sodium;
    private final int carbohydrate;
    
    public static class Builder {
    	private final int servingSize;
        private final int servings;
        
        private int calories = 0;
        private int fat = 0;
        private int sodium = 0;
        private int carbohydrate = 0;
        
        public Builder(int servingSize, int servings) {
        	this.serviceSize = serviceSize;
            this.servings = servings;
        }
        
        public Builder calories(int calories) {
        	this.calories = calories;
            return this;
        }
        
        public Builder fat(int fats) {
      		this.fat = fats;
            return this;
        }
        
        ...
	}
    
    private NutritionFacts(Builder builder) {
    	serviceSize = builder.serviceSize;
        servings = builder.servings;
        calories = builder.calories;
        fat = builder.fat;
        ...
    }
}

이렇게 구현되고, Builder는 inner class로, 또한, 생성자는 private으로 

 

Lombok 어노테이션 사용

@Builder
@RequiredArgsConstructor
public class TossApproveMessage extends CommonApproveMessage {
	private final String paymentKey;
    private final String orderId;
    private final int totalAmount;    
}

 

두가지 방법 중 적절히 선택해야함. 


불변 객체

  • 불변식 (Invariant) 
    • 프로그램이 실행되는 동안 또는 정해진 기간 동안 반드시 만족해야 하는 조건
    • 변경을 허용할 수는 있으나, 주어진 조건 내에서만 허용한다
      • 리스트의 경우, size는 반드시 0 이상이어야 하고, 한 순간이라도 음수 값이 될 수 없다.라는 조건식이 List.size()의 불변식 
  • 불변 클래스 선언 방법
    • 클래스에 final 붙이기
    • 클래스의 모든 필드에 final 붙이기
      • 항상 설계자의 의도대로 행동하도록
    • private 필드
    • 클래스의 확장을 막기
      • private만 쓰고, static factory 
      • 상속을 하면 설계자의 의도를 망가뜨릴 수도 
  • 불변 객체 특징
    • 단순함 (예상대로 동작하니까)
    • 자유롭게 공유할 수 있고 동일한 불변 객체 간의 내부 데이터를 공유할 수 있다
    • 그 자체로 실패 원자성을 제공한다. 
    • 미처 생각하지 못한 타입이나 범위의 값이 나오지 않는다. 

람다와 스트림

  • 동작 파라미터화
    • 메서드를 메서드의 파라미터로 전달하는 방식
    • new Predicate<>(), new Runnable 
    • 하지만, 함수형 파라미터를 잘 사용하지 않음. 
      • 가독성이 나빠서
      • 비즈니스 로직과 관련 없는 부가적인 코드가 많아서 
  • 람다
    • 익명 함수 기능
    • (int arg1, String arg2) -> {System.out.println("Two arguments " + arg1 + " and " + arg2);} 
    • 함수를 값으로 취급할 수 있다. 
    • 가독성이 좋아진다.
      • 대표적으로 Consumer와 Predicate 

메서드를 함수처럼 선언할 수 있고, 변수에 할당 가능

Predicate<Apple> filterByRed = (Apple a) -> "RED".equals(a.getColor());
Function<String, Integer> getLength1 = (String s) -> s.length();
Function<String, Integer> getLength2 = String::length;

함수형 인터페이스라는 Context에서 람다 표현식을 사용 가능

filterBy(apples, (Apple apple) -> "RED".equals(apple.getColor()));
filterBy(apples, (a) -> a.getWeight() > 15);

 

  • 유효한 람다 표현식
    • 람다 표현식의 문법은 유연함
(String s) -> s.length(); //s변수의 문자열 길이를 int 형으로 리턴
(String s) -> String::length; //s변수의 문자열 길이를 int 형으로 리턴
(Apple a) -> a.getWeight > 25; // 25보다 크면 true, 작으면 false 리턴
(Apple a, Apple b) -> a.getWeight().compare(b.getWeight());
(a,b) -> a.getWeight().compare(b.getWeight()); //Apple 타입 생략 가능 
() -> 25 //그냥 25 리턴
(Apple a1, Apple a2) -> { 
	System.out.println(a1);
    System.out.println(a2);
}

 

  • 람다식 메서드 참조
    • 실행하려는 메서드를 참조해서 매개 변수와 리턴 타입을 알아내어, 람다식에서 불필요한 선언부를 생략 가능
    • 연산자는 ::
.map((String s) -> s.length());
.map(String::length);

(Apple a, Apple b) -> a.getWeight().compare(b.getWeight());
(a, b) -> a.getWeight().compare(b.getWeight());
.comparing(Apple::getValue)

.comparing(apple -> apple.getWeight().getValue())
.comparing(Apple::getValue)

 

  • 스트림
    • 데이터 처리 연산을 지원하도록 Source Data에서 추출된 연속된 요소
    • 컬렉션 처리를 도와줌 
    • 스트림이라는 특정 파이프라인을 통해서 리스트에 있는 요소를 연속적으로 흘려 보낸다. 
    • 특징
      • 선언형 코드를 작성 가능
      • 여러 중간 연산을 연결해서 복잡한 데이터 처리 파이프 라인을 만들 수 있다. 
        • 메서드 체이닝
        • lazy evaluation
        • 멀티쓰레드로 구현하지않아도 멀티쓰레드로 구현됨. 
        • 지연 연산 
          • 스트림에는 중간 연산자와 최종 연산자가 있음
          • 중간 연산자의 리턴 값은 다른 스트림.
          • 최종 연산자에서만 결과를 도출 
          • 외부 반복이 아닌 내부 반복
            • 처리가 계속해서 되고, 최종 연산자에서 결과가 나옴
    • 함수들
      •  map()
        • 함수를 컬렉션의 요소에 하나씩 적용
      • filter()
        • 필터에 통과되는 것만 이후 동작 적용
      • sorted() 
    • null로 인해 발생하는 문제
      • 에러의 근원
        • NPE
      • 아무런 의미가 없다
      • null 처리를 위한 if-else 문으로 가독성이 떨어진다
      • 자바 철학에 위배가 된다
        • null만 Pointer 개념을 가진다. 
      • 타입 시스템에 구멍이 생긴다.
      • 올바른 null 처리 방법
        • 타입 시스템을 이용해서 값이 없는 경우, null이 아닌 빈 값을 갖도록 하고, 값이 있는 경우, 주어진 형식에 맞는 값을 갖도록 하는 방식이 좋다.
        • Optional<T> 
          • T 타입의 값을 캡슐화
          • 값이 존재하면 그 값을 감싸지만, 값이 존재하지 않는 경우, null이 아닌 Optional.Empty 값으로 감싼다.
          • Empty 값으로 Optional 생성
            • Optional.empty();
          • 값이 null이 아닐 경우, Optional 생성
            • Optional<String> pgCorpName = Optional.of(paymentRequestDto.getPgCorpName());
          • 값이 null이 될 수 있는 Optional 생성
            • Optional<String> pgCorpName = Optional.ofNullable(paymentRequestDto.getPgCorpName()); 
          • 유용한 Method
            • isPresent()
              • Optional 값이 존재하면 true, 그렇지 않으면 false
              • 성능상 이슈가 있어서 지양 
            • T get()
              • 값이 존재하면 반환, 그렇지 않으면 NoSuchElementException을 던짐
            • orElse(T other)
              • Optional이 값을 포함하지 않고 있을 때 Default 값을 제공할 수 있다.
            • orElseGet(Supplier<? extends T> other)
              • Optional 값이 없을 때만 Supplier가 실행됨
            • orElseThrow(() -> Exception 명)

Auto Boxing/Unboxing

  • 기본형 타입과 래퍼 클래스 간의 형 변환을 자동으로 처리해주는 기능
  • Auto Boxing: Integer.valueOf(int value)
  • Auto Unboxing: (Integer object).intValue()
  • 개발자의 편의성과 가독성에는 도움이 되지만, 성능 문제를 일으키는 숨은 요인 중 하나
    • stream을 사용하면 해결 가능 
728x90