JavaRush /Java Blog /Random-KO /커피 브레이크 #56. Java 모범 사례에 대한 빠른 가이드

커피 브레이크 #56. Java 모범 사례에 대한 빠른 가이드

Random-KO 그룹에 게시되었습니다
출처: DZone 이 가이드에는 코드의 가독성과 안정성을 향상시키기 위한 최고의 Java 사례와 참조가 포함되어 있습니다. 개발자에게는 매일 올바른 결정을 내려야 하는 큰 책임이 있으며, 그들이 올바른 결정을 내리는 데 도움이 될 수 있는 가장 좋은 것은 경험입니다. 그리고 그들 모두가 소프트웨어 개발에 대한 폭넓은 경험을 갖고 있는 것은 아니지만, 누구나 다른 사람의 경험을 활용할 수 있습니다. 나는 Java에 대한 경험을 통해 얻은 몇 가지 권장 사항을 준비했습니다. Java 코드의 가독성과 신뢰성을 향상시키는 데 도움이 되기를 바랍니다.커피 브레이크 #56.  Java 모범 사례에 대한 빠른 가이드 - 1

프로그래밍 원리

단지 작동하는 코드를 작성하지 마십시오 . 자신뿐만 아니라 나중에 소프트웨어 작업을 하게 될 다른 사람도 유지 관리할 수 있는 코드를 작성하도록 노력하십시오 . 개발자는 자신의 시간 중 80%를 코드를 읽는 데 보내고, 20%는 코드를 작성하고 테스트하는 데 보냅니다. 따라서 읽기 쉬운 코드 작성에 집중하세요. 코드에는 코드의 기능을 다른 사람이 이해할 수 있도록 주석이 필요하지 않습니다. 좋은 코드를 작성하기 위해 지침으로 사용할 수 있는 프로그래밍 원칙이 많이 있습니다. 아래에 가장 중요한 것들을 나열하겠습니다.
  • • KISS – “Keep It Simple, Stupid”의 약자입니다. 개발자가 여정 초기에 복잡하고 모호한 디자인을 구현하려고 시도한다는 것을 알 수 있습니다.
  • • DRY - “같은 말을 반복하지 마세요.” 중복을 피하고 대신 시스템이나 방법의 단일 부분에 배치하십시오.
  • YAGNI - “필요하지 않을 것입니다.” 갑자기 "(기능, 코드 등)을 더 추가하면 어떨까?"라고 자문하기 시작한다면 실제로 추가할 가치가 있는지 생각해 볼 필요가 있을 것입니다.
  • 스마트 코드 대신 깔끔한 코드 - 간단히 말해서 자존심을 문에 두고 스마트 코드 작성을 잊어버리세요. 스마트 코드가 아닌 깔끔한 코드를 원합니다.
  • 조기 최적화 방지 - 조기 최적화의 문제는 병목 현상이 나타날 때까지 프로그램의 어느 위치에 있는지 전혀 알 수 없다는 것입니다.
  • 단일 책임 - 프로그램의 각 클래스나 모듈은 특정 기능 중 한 부분만 제공하는 데에만 관심을 가져야 합니다.
  • • 구현 상속보다는 구성 - 복잡한 동작을 가진 객체는 클래스를 상속하고 새로운 동작을 추가하는 대신 개별 동작을 가진 객체의 인스턴스를 포함해야 합니다.
  • 체조는 9가지 규칙 으로 구성된 프로그래밍 연습입니다 .
  • 빠른 실패, 빠른 중지 - 이 원칙은 예상치 못한 오류가 발생할 경우 현재 작업을 중지하는 것을 의미합니다. 이 원칙을 준수하면 보다 안정적인 작동이 가능합니다.

패키지

  1. 기술 수준보다는 주제 영역 별로 패키지 구성의 우선순위를 정하세요 .
  2. 기술적인 이유로 클래스를 구성하는 것보다 오용을 방지하기 위해 캡슐화 및 정보 숨기기를 촉진하는 레이아웃을 선호합니다.
  3. 패키지를 불변 API가 있는 것처럼 취급하십시오. 내부 처리 전용으로 의도된 내부 메커니즘(클래스)을 노출하지 마십시오.
  4. 패키지 내에서만 사용하도록 만들어진 클래스를 노출하지 마세요.

클래스

공전

  1. 정적 클래스 생성을 허용하지 않습니다. 항상 개인 생성자를 만드세요.
  2. 정적 클래스는 불변으로 유지되어야 하며 서브클래싱이나 다중 스레드 클래스를 허용하지 않습니다.
  3. 정적 클래스는 방향 변경으로부터 보호되어야 하며 목록 필터링과 같은 유틸리티로 제공되어야 합니다.

계승

  1. 상속보다는 구성을 선택하세요.
  2. 보호된 필드를 설정하지 마십시오 . 대신 보안 액세스 방법을 지정하세요.
  3. 클래스 변수를 final 로 표시할 수 있으면 그렇게 하세요.
  4. 상속이 예상되지 않으면 클래스를 final 로 만드세요 .
  5. 서브클래스에서 재정의가 허용되지 않을 것으로 예상되는 경우 메서드를 final 로 표시합니다.
  6. 생성자가 필요하지 않은 경우 구현 논리 없이 기본 생성자를 생성하지 마세요. Java는 지정되지 않은 경우 기본 생성자를 자동으로 제공합니다.

인터페이스

  1. 클래스가 API를 구현하고 오염시킬 수 있도록 상수 패턴의 인터페이스를 사용하지 마세요. 대신 정적 클래스를 사용하십시오. 이는 정적 블록에서 더 복잡한 객체 초기화(예: 컬렉션 채우기)를 수행할 수 있다는 추가 이점이 있습니다.
  2. 인터페이스를 과도하게 사용 하지 마십시오 .
  3. 인터페이스를 구현하는 클래스가 하나만 있으면 인터페이스를 과도하게 사용하게 되어 득보다 실이 더 클 수 있습니다.
  4. "구현이 아닌 인터페이스를 위한 프로그램"은 각 도메인 클래스를 다소 동일한 인터페이스로 묶어야 한다는 의미는 아닙니다. 이렇게 하면 YAGNI 가 손상됩니다 .
  5. 클라이언트가 관심 있는 방법만 알 수 있도록 항상 인터페이스를 작고 구체적으로 유지하십시오. SOLID에서 ISP를 확인하세요.

종료자

  1. #finalize() 객체는 리소스를 정리할 때(예: 파일 닫기) 오류를 방지하는 수단으로만 신중하게 사용해야 합니다. 항상 명시적인 정리 방법(예: close() )을 제공하세요.
  2. 상속 계층 구조에서는 항상 try 블록 에서 부모의 finalize()를 호출하세요 . 클래스 정리는 finally 블록 에 있어야 합니다 .
  3. 명시적인 정리 메서드가 호출되지 않았고 종료자가 리소스를 닫은 경우 이 오류를 기록합니다.
  4. 로거를 사용할 수 없는 경우 스레드의 예외 핸들러를 사용하십시오(이는 로그에 캡처된 표준 오류를 전달하게 됩니다).

일반 규칙

진술

일반적으로 전제조건 확인 형태의 어설션은 "빠른 실패, 빠른 중지" 계약을 시행합니다. 가능한 한 원인에 가까운 프로그래밍 오류를 식별하기 위해 널리 사용해야 합니다. 개체 조건:
  • • 객체를 생성하거나 잘못된 상태에 놓이게 해서는 안 됩니다.
  • • 생성자와 메서드에서는 항상 테스트를 사용하여 계약을 설명하고 시행합니다.
  • • Java 키워드 Assert는 비활성화할 수 있고 일반적으로 깨지기 쉬운 구조이므로 피해야 합니다.
  • • 전제조건 확인을 위한 자세한 if-else 조건을 피하려면 Assertions 유틸리티 클래스를 사용하십시오 .

제네릭

완전하고 매우 자세한 설명은 Java Generics FAQ 에서 확인할 수 있습니다 . 다음은 개발자가 알아야 할 일반적인 시나리오입니다.
  1. 가능할 때마다 기본 클래스/인터페이스를 반환하는 것보다 유형 추론을 사용하는 것이 좋습니다.

    // MySpecialObject o = MyObjectFactory.getMyObject();
    public  T getMyObject(int type) {
    return (T) factory.create(type);
    }

  2. 유형을 자동으로 결정할 수 없으면 인라인하세요.

    public class MySpecialObject extends MyObject {
     public MySpecialObject() {
      super(Collections.emptyList());   // This is ugly, as we loose type
      super(Collections.EMPTY_LIST();    // This is just dumb
      // But this is beauty
      super(new ArrayList());
      super(Collections.emptyList());
     }
    }

  3. 와일드카드:

    구조에서 값만 가져올 때는 확장 와일드카드를 사용하고 , 구조에 값만 넣을 때는 슈퍼 와일드카드를 사용하고, 두 가지를 모두 수행할 때는 와일드카드를 사용하지 마세요.

    1. 모두가 PECS를 좋아합니다 ! ( 생산자 확장, 소비자 슈퍼 )
    2. 생산자 T에는 Foo를 사용합니다 .
    3. 소비자 T에는 Foo를 사용합니다.

싱글톤

싱글톤은 C++에서는 괜찮지만 Java에서는 적합하지 않은 클래식 디자인 패턴 스타일로 작성해서는 안 됩니다. 적절하게 스레드로부터 안전하더라도 다음을 구현하지 마십시오(성능 병목 현상이 발생합니다!).
public final class MySingleton {
  private static MySingleton instance;
  private MySingleton() {
    // singleton
  }
  public static synchronized MySingleton getInstance() {
    if (instance == null) {
      instance = new MySingleton();
    }
    return instance;
  }
}
지연 초기화가 정말로 필요한 경우에는 이 두 가지 접근 방식을 조합하여 사용할 수 있습니다.
public final class MySingleton {
  private MySingleton() {
   // singleton
  }
  private static final class MySingletonHolder {
    static final MySingleton instance = new MySingleton();
  }
  public static MySingleton getInstance() {
    return MySingletonHolder.instance;
  }
}
Spring: 기본적으로 Bean은 싱글톤 범위에 등록됩니다. 즉, 컨테이너에 의해 하나의 인스턴스만 생성되고 모든 소비자에 연결됩니다. 이는 성능이나 바인딩 제한 없이 일반 싱글톤과 동일한 의미를 제공합니다.

예외

  1. 수정 가능한 조건에는 확인된 예외를 사용하고 프로그래밍 오류에는 런타임 예외를 사용합니다. 예: 문자열에서 정수 가져오기.

    나쁨: NumberFormatException은 RuntimeException을 확장하므로 프로그래밍 오류를 나타내기 위한 것입니다.

  2. 다음을 수행하지 마십시오.

    // String str = input string
    Integer value = null;
    try {
       value = Integer.valueOf(str);
    } catch (NumberFormatException e) {
    // non-numeric string
    }
    if (value == null) {
    // handle bad string
    } else {
    // business logic
    }

    올바른 사용:

    // String str = input string
    // Numeric string with at least one digit and optional leading negative sign
    if ( (str != null) && str.matches("-?\\d++") ) {
       Integer value = Integer.valueOf(str);
      // business logic
    } else {
      // handle bad string
    }
  3. 도메인 수준의 올바른 위치에서 예외를 처리해야 합니다.

    잘못된 방법 - 데이터 객체 계층은 데이터베이스 예외가 발생할 때 무엇을 해야할지 모릅니다.

    class UserDAO{
        public List getUsers(){
            try{
                ps = conn.prepareStatement("SELECT * from users");
                rs = ps.executeQuery();
                //return result
            }catch(Exception e){
                log.error("exception")
                return null
            }finally{
                //release resources
            }
        }}
    

    권장 방법 - 데이터 계층은 예외를 다시 발생시키고 예외 처리에 대한 책임을 올바른 계층에 전달해야 합니다.

    === RECOMMENDED WAY ===
    Data layer should just retrow the exception and transfer the responsability to handle the exception or not to the right layer.
    class UserDAO{
       public List getUsers(){
          try{
             ps = conn.prepareStatement("SELECT * from users");
             rs = ps.executeQuery();
             //return result
          }catch(Exception e){
           throw new DataLayerException(e);
          }finally{
             //release resources
          }
      }
    }

  4. 일반적으로 예외는 발생된 시점에 기록되어서는 안 되며 실제로 처리되는 시점에 기록되어야 합니다. 로깅 예외가 발생하거나 다시 발생하면 로그 파일이 잡음으로 가득 차는 경향이 있습니다. 또한 예외 스택 추적은 예외가 발생한 위치를 계속 기록합니다.

  5. 표준 예외 사용을 지원합니다.

  6. 반환 코드 대신 예외를 사용하십시오.

같음 및 해시코드

적절한 개체 및 해시 코드 동등 메서드를 작성할 때 고려해야 할 여러 가지 문제가 있습니다. 더 쉽게 사용하려면 java.util.Objects의 equalshash를 사용하세요 .
public final class User {
 private final String firstName;
 private final String lastName;
 private final int age;
 ...
 public boolean equals(Object o) {
   if (this == o) {
     return true;
   } else if (!(o instanceof User)) {
     return false;
   }
   User user = (User) o;
   return Objects.equals(getFirstName(), user.getFirstName()) &&
    Objects.equals(getLastName(),user.getLastName()) &&
    Objects.equals(getAge(), user.getAge());
 }
 public int hashCode() {
   return Objects.hash(getFirstName(),getLastName(),getAge());
 }
}

자원 관리

리소스를 안전하게 해제하는 방법: try-with-resources 문은 문 끝에서 각 리소스가 닫히도록 보장합니다. java.io.Closeable을 구현하는 모든 객체를 포함하여 java.lang.AutoCloseable을 구현하는 모든 객체를 리소스로 사용할 수 있습니다.
private doSomething() {
try (BufferedReader br = new BufferedReader(new FileReader(path)))
 try {
   // business logic
 }
}

종료 후크 사용

JVM이 정상적으로 종료될 때 호출되는 종료 후크를 사용하십시오 . (그러나 정전으로 인한 갑작스러운 중단은 처리할 수 없습니다.) 이는 System.runFinalizersOnExit()가 true (기본값은 false) 인 경우에만 실행되는 finalize() 메서드를 선언하는 대신 권장되는 대안입니다. .
public final class SomeObject {
 var distributedLock = new ExpiringGeneralLock ("SomeObject", "shared");
 public SomeObject() {
   Runtime
     .getRuntime()
     .addShutdownHook(new Thread(new LockShutdown(distributedLock)));
 }
 /** Code may have acquired lock across servers */
 ...
 /** Safely releases the distributed lock. */
 private static final class LockShutdown implements Runnable {
   private final ExpiringGeneralLock distributedLock;
   public LockShutdown(ExpiringGeneralLock distributedLock) {
     if (distributedLock == null) {
       throw new IllegalArgumentException("ExpiringGeneralLock is null");
     }
     this.distributedLock = distributedLock;
   }
   public void run() {
     if (isLockAlive()) {
       distributedLock.release();
     }
   }
   /** @return True if the lock is acquired and has not expired yet. */
   private boolean isLockAlive() {
     return distributedLock.getExpirationTimeMillis() > System.currentTimeMillis();
   }
 }
}
리소스를 서버 간에 배포하여 리소스를 완전하게(재생할 수도 있음) 허용합니다. (이렇게 하면 정전과 같은 갑작스러운 중단에서 복구할 수 있습니다.) ExpiringGeneralLock(모든 시스템에 공통적인 잠금)을 사용하는 위의 예제 코드를 참조하세요.

날짜 시간

Java 8에서는 java.time 패키지에 새로운 날짜-시간 API가 도입되었습니다. Java 8에서는 이전 날짜-시간 API의 단점(비스레딩, 잘못된 디자인, 복잡한 시간대 처리 등)을 해결하기 위해 새로운 날짜-시간 API를 도입합니다.

병행

일반 규칙

  1. 스레드로부터 안전하지 않은 다음 라이브러리에 주의하세요. 여러 스레드에서 개체를 사용하는 경우 항상 개체와 동기화하세요.
  2. 날짜( 불변 불가 ) - 스레드로부터 안전한 새로운 날짜-시간 API를 사용합니다.
  3. SimpleDateFormat - 스레드로부터 안전한 새로운 Date-Time API를 사용합니다.
  4. 변수를 휘발성으로 만드는 것보다 java.util.concurrent.atomic 클래스를 사용하는 것이 좋습니다 .
  5. 원자 클래스의 동작은 일반 개발자에게 더 분명한 반면, 휘발성 클래스 에는 Java 메모리 모델에 대한 이해가 필요합니다.
  6. Atomic 클래스는 휘발성 변수를 보다 편리한 인터페이스로 래핑합니다.
  7. 휘발성이 적절한 사용 사례를 이해합니다 . ( 기사 참조 )
  8. 호출 가능 사용 확인된 예외가 필요하지만 반환 유형이 없는 경우. Void는 인스턴스화할 수 없으므로 의도를 전달하고 안전하게 null을 반환할 수 있습니다 .

스트림

  1. java.lang.Thread는 더 이상 사용되지 않습니다. 공식적으로는 그렇지 않지만 거의 모든 경우에 java.util.concurrent 패키지는 문제에 대한 보다 명확한 솔루션을 제공합니다.
  2. java.lang.Thread를 확장하는 것은 나쁜 습관으로 간주됩니다. 대신 Runnable을 구현 하고 생성자에 인스턴스가 있는 새 스레드를 만듭니다(상속보다 구성 규칙).
  3. 병렬 처리가 필요한 경우 실행기와 스레드를 선호합니다.
  4. 생성된 스레드의 구성을 관리하려면 항상 사용자 정의 스레드 팩토리를 지정하는 것이 좋습니다( 자세한 내용은 여기 참조 ).
  5. 서버가 종료될 때 스레드 풀이 즉시 종료될 수 있도록 중요하지 않은 스레드에 대해 실행기에서 DaemonThreadFactory를 사용하십시오( 자세한 내용은 여기 참조 ).
this.executor = Executors.newCachedThreadPool((Runnable runnable) -> {
   Thread thread = Executors.defaultThreadFactory().newThread(runnable);
   thread.setDaemon(true);
   return thread;
});
  1. Java 동기화는 더 이상 느리지 않습니다(55~110ns). 이중 확인 잠금 과 같은 트릭을 사용하여 이를 피하지 마십시오 .
  2. 사용자가 클래스/인스턴스와 동기화할 수 있으므로 클래스보다는 내부 개체와의 동기화를 선호합니다.
  3. 교착 상태를 방지하려면 항상 여러 개체를 동일한 순서로 동기화하세요.
  4. 클래스와 동기화한다고 해서 본질적으로 내부 개체에 대한 액세스가 차단되는 것은 아닙니다. 리소스에 액세스할 때 항상 동일한 잠금을 사용하십시오.
  5. 동기화된 키워드는 메소드 시그니처의 일부로 간주되지 않으므로 상속되지 않습니다.
  6. 과도한 동기화를 피하십시오. 이로 인해 성능이 저하되고 교착 상태가 발생할 수 있습니다. 동기화가 필요한 코드 부분에만 동기화 키워드를 사용하세요.

컬렉션

  1. 가능하면 다중 스레드 코드에서 Java-5 병렬 컬렉션을 사용하십시오. 안전하고 우수한 특성을 가지고 있습니다.
  2. 필요한 경우 동기화된 목록 대신 CopyOnWriteArrayList를 사용하세요.
  3. Collections.unmodifying list(...) 를 사용하거나 new ArrayList(list) 에 대한 매개변수로 컬렉션을 받을 때 컬렉션을 복사하세요 . 클래스 외부에서 로컬 컬렉션을 수정하지 마세요.
  4. new ArrayList (list) 를 사용하여 외부에서 목록을 수정하지 말고 항상 컬렉션의 복사본을 반환하세요 .
  5. 각 컬렉션은 별도의 클래스로 래핑되어야 하므로 이제 컬렉션과 관련된 동작에는 홈이 있습니다 (예: 필터링 메서드, 각 요소에 규칙 적용).

여러 가지 잡다한

  1. 익명 클래스 대신 람다를 선택하세요.
  2. 람다 대신 메서드 참조를 선택하세요.
  3. int 상수 대신 열거형을 사용하세요.
  4. 정확한 답변이 필요한 경우 float 및 double을 사용하지 말고 대신 Money와 같은 BigDecimal을 사용하세요.
  5. 박스형 기본 형식보다는 기본 형식을 선택하세요.
  6. 코드에 매직넘버를 사용하는 것을 피해야 합니다. 상수를 사용하세요.
  7. Null을 반환하지 마세요. `Optional`을 사용하여 메소드 클라이언트와 통신합니다. 컬렉션에도 동일합니다. null이 아닌 빈 배열이나 컬렉션을 반환합니다.
  8. 불필요한 객체 생성을 피하고, 객체를 재사용하고, 불필요한 GC 정리를 피하세요.

지연 초기화

지연 초기화는 성능 최적화입니다. 어떤 이유로 데이터가 "비싸다"고 간주될 때 사용됩니다. Java 8에서는 이를 위해 기능 제공자 인터페이스를 사용해야 합니다.
== Thread safe Lazy initialization ===
public final class Lazy {
   private volatile T value;
   public T getOrCompute(Supplier supplier) {
       final T result = value; // Just one volatile read
       return result == null ? maybeCompute(supplier) : result;
   }
   private synchronized T maybeCompute(Supplier supplier) {
       if (value == null) {
           value = supplier.get();
       }
       return value;
   }
}
Lazy lazyToString= new Lazy<>()
return lazyToString.getOrCompute( () -> "(" + x + ", " + y + ")");
지금은 그게 전부입니다. 이것이 도움이 되었기를 바랍니다.
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION