출처: DZone 이 가이드에는 코드의 가독성과 안정성을 향상시키기 위한 최고의 Java 사례와 참조가 포함되어 있습니다. 개발자에게는 매일 올바른 결정을 내려야 하는 큰 책임이 있으며, 그들이 올바른 결정을 내리는 데 도움이 될 수 있는 가장 좋은 것은 경험입니다. 그리고 그들 모두가 소프트웨어 개발에 대한 폭넓은 경험을 갖고 있는 것은 아니지만, 누구나 다른 사람의 경험을 활용할 수 있습니다. 나는 Java에 대한 경험을 통해 얻은 몇 가지 권장 사항을 준비했습니다. Java 코드의 가독성과 신뢰성을 향상시키는 데 도움이 되기를 바랍니다.
프로그래밍 원리
단지 작동하는 코드를 작성하지 마십시오 . 자신뿐만 아니라 나중에 소프트웨어 작업을 하게 될 다른 사람도 유지 관리할 수 있는 코드를 작성하도록 노력하십시오 . 개발자는 자신의 시간 중 80%를 코드를 읽는 데 보내고, 20%는 코드를 작성하고 테스트하는 데 보냅니다. 따라서 읽기 쉬운 코드 작성에 집중하세요. 코드에는 코드의 기능을 다른 사람이 이해할 수 있도록 주석이 필요하지 않습니다. 좋은 코드를 작성하기 위해 지침으로 사용할 수 있는 프로그래밍 원칙이 많이 있습니다. 아래에 가장 중요한 것들을 나열하겠습니다.- • KISS – “Keep It Simple, Stupid”의 약자입니다. 개발자가 여정 초기에 복잡하고 모호한 디자인을 구현하려고 시도한다는 것을 알 수 있습니다.
- • DRY - “같은 말을 반복하지 마세요.” 중복을 피하고 대신 시스템이나 방법의 단일 부분에 배치하십시오.
- • YAGNI - “필요하지 않을 것입니다.” 갑자기 "(기능, 코드 등)을 더 추가하면 어떨까?"라고 자문하기 시작한다면 실제로 추가할 가치가 있는지 생각해 볼 필요가 있을 것입니다.
- • 스마트 코드 대신 깔끔한 코드 - 간단히 말해서 자존심을 문에 두고 스마트 코드 작성을 잊어버리세요. 스마트 코드가 아닌 깔끔한 코드를 원합니다.
- • 조기 최적화 방지 - 조기 최적화의 문제는 병목 현상이 나타날 때까지 프로그램의 어느 위치에 있는지 전혀 알 수 없다는 것입니다.
- • 단일 책임 - 프로그램의 각 클래스나 모듈은 특정 기능 중 한 부분만 제공하는 데에만 관심을 가져야 합니다.
- • 구현 상속보다는 구성 - 복잡한 동작을 가진 객체는 클래스를 상속하고 새로운 동작을 추가하는 대신 개별 동작을 가진 객체의 인스턴스를 포함해야 합니다.
- • 체조는 9가지 규칙 으로 구성된 프로그래밍 연습입니다 .
- • 빠른 실패, 빠른 중지 - 이 원칙은 예상치 못한 오류가 발생할 경우 현재 작업을 중지하는 것을 의미합니다. 이 원칙을 준수하면 보다 안정적인 작동이 가능합니다.
패키지
- 기술 수준보다는 주제 영역 별로 패키지 구성의 우선순위를 정하세요 .
- 기술적인 이유로 클래스를 구성하는 것보다 오용을 방지하기 위해 캡슐화 및 정보 숨기기를 촉진하는 레이아웃을 선호합니다.
- 패키지를 불변 API가 있는 것처럼 취급하십시오. 내부 처리 전용으로 의도된 내부 메커니즘(클래스)을 노출하지 마십시오.
- 패키지 내에서만 사용하도록 만들어진 클래스를 노출하지 마세요.
클래스
공전
- 정적 클래스 생성을 허용하지 않습니다. 항상 개인 생성자를 만드세요.
- 정적 클래스는 불변으로 유지되어야 하며 서브클래싱이나 다중 스레드 클래스를 허용하지 않습니다.
- 정적 클래스는 방향 변경으로부터 보호되어야 하며 목록 필터링과 같은 유틸리티로 제공되어야 합니다.
계승
- 상속보다는 구성을 선택하세요.
- 보호된 필드를 설정하지 마십시오 . 대신 보안 액세스 방법을 지정하세요.
- 클래스 변수를 final 로 표시할 수 있으면 그렇게 하세요.
- 상속이 예상되지 않으면 클래스를 final 로 만드세요 .
- 서브클래스에서 재정의가 허용되지 않을 것으로 예상되는 경우 메서드를 final 로 표시합니다.
- 생성자가 필요하지 않은 경우 구현 논리 없이 기본 생성자를 생성하지 마세요. Java는 지정되지 않은 경우 기본 생성자를 자동으로 제공합니다.
인터페이스
- 클래스가 API를 구현하고 오염시킬 수 있도록 상수 패턴의 인터페이스를 사용하지 마세요. 대신 정적 클래스를 사용하십시오. 이는 정적 블록에서 더 복잡한 객체 초기화(예: 컬렉션 채우기)를 수행할 수 있다는 추가 이점이 있습니다.
- 인터페이스를 과도하게 사용 하지 마십시오 .
- 인터페이스를 구현하는 클래스가 하나만 있으면 인터페이스를 과도하게 사용하게 되어 득보다 실이 더 클 수 있습니다.
- "구현이 아닌 인터페이스를 위한 프로그램"은 각 도메인 클래스를 다소 동일한 인터페이스로 묶어야 한다는 의미는 아닙니다. 이렇게 하면 YAGNI 가 손상됩니다 .
- 클라이언트가 관심 있는 방법만 알 수 있도록 항상 인터페이스를 작고 구체적으로 유지하십시오. SOLID에서 ISP를 확인하세요.
종료자
- #finalize() 객체는 리소스를 정리할 때(예: 파일 닫기) 오류를 방지하는 수단으로만 신중하게 사용해야 합니다. 항상 명시적인 정리 방법(예: close() )을 제공하세요.
- 상속 계층 구조에서는 항상 try 블록 에서 부모의 finalize()를 호출하세요 . 클래스 정리는 finally 블록 에 있어야 합니다 .
- 명시적인 정리 메서드가 호출되지 않았고 종료자가 리소스를 닫은 경우 이 오류를 기록합니다.
- 로거를 사용할 수 없는 경우 스레드의 예외 핸들러를 사용하십시오(이는 로그에 캡처된 표준 오류를 전달하게 됩니다).
일반 규칙
진술
일반적으로 전제조건 확인 형태의 어설션은 "빠른 실패, 빠른 중지" 계약을 시행합니다. 가능한 한 원인에 가까운 프로그래밍 오류를 식별하기 위해 널리 사용해야 합니다. 개체 조건:- • 객체를 생성하거나 잘못된 상태에 놓이게 해서는 안 됩니다.
- • 생성자와 메서드에서는 항상 테스트를 사용하여 계약을 설명하고 시행합니다.
- • Java 키워드 Assert는 비활성화할 수 있고 일반적으로 깨지기 쉬운 구조이므로 피해야 합니다.
- • 전제조건 확인을 위한 자세한 if-else 조건을 피하려면 Assertions 유틸리티 클래스를 사용하십시오 .
제네릭
완전하고 매우 자세한 설명은 Java Generics FAQ 에서 확인할 수 있습니다 . 다음은 개발자가 알아야 할 일반적인 시나리오입니다.- 가능할 때마다 기본 클래스/인터페이스를 반환하는 것보다 유형 추론을 사용하는 것이 좋습니다.
// MySpecialObject o = MyObjectFactory.getMyObject(); public
T getMyObject(int type) { return (T) factory.create(type); } - 유형을 자동으로 결정할 수 없으면 인라인하세요.
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()); } } - 와일드카드:
구조에서 값만 가져올 때는 확장 와일드카드를 사용하고 , 구조에 값만 넣을 때는 슈퍼 와일드카드를 사용하고, 두 가지를 모두 수행할 때는 와일드카드를 사용하지 마세요.
- 모두가 PECS를 좋아합니다 ! ( 생산자 확장, 소비자 슈퍼 )
- 생산자 T에는 Foo를 사용합니다 .
- 소비자 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은 싱글톤 범위에 등록됩니다. 즉, 컨테이너에 의해 하나의 인스턴스만 생성되고 모든 소비자에 연결됩니다. 이는 성능이나 바인딩 제한 없이 일반 싱글톤과 동일한 의미를 제공합니다.
예외
-
수정 가능한 조건에는 확인된 예외를 사용하고 프로그래밍 오류에는 런타임 예외를 사용합니다. 예: 문자열에서 정수 가져오기.
나쁨: NumberFormatException은 RuntimeException을 확장하므로 프로그래밍 오류를 나타내기 위한 것입니다.
-
도메인 수준의 올바른 위치에서 예외를 처리해야 합니다.
잘못된 방법 - 데이터 객체 계층은 데이터베이스 예외가 발생할 때 무엇을 해야할지 모릅니다.
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 } } } -
일반적으로 예외는 발생된 시점에 기록되어서는 안 되며 실제로 처리되는 시점에 기록되어야 합니다. 로깅 예외가 발생하거나 다시 발생하면 로그 파일이 잡음으로 가득 차는 경향이 있습니다. 또한 예외 스택 추적은 예외가 발생한 위치를 계속 기록합니다.
-
표준 예외 사용을 지원합니다.
-
반환 코드 대신 예외를 사용하십시오.
다음을 수행하지 마십시오.
// 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
}
같음 및 해시코드
적절한 개체 및 해시 코드 동등 메서드를 작성할 때 고려해야 할 여러 가지 문제가 있습니다. 더 쉽게 사용하려면 java.util.Objects의 equals 및 hash를 사용하세요 .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를 도입합니다.병행
일반 규칙
- 스레드로부터 안전하지 않은 다음 라이브러리에 주의하세요. 여러 스레드에서 개체를 사용하는 경우 항상 개체와 동기화하세요.
- 날짜( 불변 불가 ) - 스레드로부터 안전한 새로운 날짜-시간 API를 사용합니다.
- SimpleDateFormat - 스레드로부터 안전한 새로운 Date-Time API를 사용합니다.
- 변수를 휘발성으로 만드는 것보다 java.util.concurrent.atomic 클래스를 사용하는 것이 좋습니다 .
- 원자 클래스의 동작은 일반 개발자에게 더 분명한 반면, 휘발성 클래스 에는 Java 메모리 모델에 대한 이해가 필요합니다.
- Atomic 클래스는 휘발성 변수를 보다 편리한 인터페이스로 래핑합니다.
- 휘발성이 적절한 사용 사례를 이해합니다 . ( 기사 참조 )
- 호출 가능 사용
확인된 예외가 필요하지만 반환 유형이 없는 경우. Void는 인스턴스화할 수 없으므로 의도를 전달하고 안전하게 null을 반환할 수 있습니다 .
스트림
- java.lang.Thread는 더 이상 사용되지 않습니다. 공식적으로는 그렇지 않지만 거의 모든 경우에 java.util.concurrent 패키지는 문제에 대한 보다 명확한 솔루션을 제공합니다.
- java.lang.Thread를 확장하는 것은 나쁜 습관으로 간주됩니다. 대신 Runnable을 구현 하고 생성자에 인스턴스가 있는 새 스레드를 만듭니다(상속보다 구성 규칙).
- 병렬 처리가 필요한 경우 실행기와 스레드를 선호합니다.
- 생성된 스레드의 구성을 관리하려면 항상 사용자 정의 스레드 팩토리를 지정하는 것이 좋습니다( 자세한 내용은 여기 참조 ).
- 서버가 종료될 때 스레드 풀이 즉시 종료될 수 있도록 중요하지 않은 스레드에 대해 실행기에서 DaemonThreadFactory를 사용하십시오( 자세한 내용은 여기 참조 ).
this.executor = Executors.newCachedThreadPool((Runnable runnable) -> {
Thread thread = Executors.defaultThreadFactory().newThread(runnable);
thread.setDaemon(true);
return thread;
});
- Java 동기화는 더 이상 느리지 않습니다(55~110ns). 이중 확인 잠금 과 같은 트릭을 사용하여 이를 피하지 마십시오 .
- 사용자가 클래스/인스턴스와 동기화할 수 있으므로 클래스보다는 내부 개체와의 동기화를 선호합니다.
- 교착 상태를 방지하려면 항상 여러 개체를 동일한 순서로 동기화하세요.
- 클래스와 동기화한다고 해서 본질적으로 내부 개체에 대한 액세스가 차단되는 것은 아닙니다. 리소스에 액세스할 때 항상 동일한 잠금을 사용하십시오.
- 동기화된 키워드는 메소드 시그니처의 일부로 간주되지 않으므로 상속되지 않습니다.
- 과도한 동기화를 피하십시오. 이로 인해 성능이 저하되고 교착 상태가 발생할 수 있습니다. 동기화가 필요한 코드 부분에만 동기화 키워드를 사용하세요.
컬렉션
- 가능하면 다중 스레드 코드에서 Java-5 병렬 컬렉션을 사용하십시오. 안전하고 우수한 특성을 가지고 있습니다.
- 필요한 경우 동기화된 목록 대신 CopyOnWriteArrayList를 사용하세요.
- Collections.unmodifying list(...) 를 사용하거나 new ArrayList(list) 에 대한 매개변수로 컬렉션을 받을 때 컬렉션을 복사하세요 . 클래스 외부에서 로컬 컬렉션을 수정하지 마세요.
- new ArrayList (list) 를 사용하여 외부에서 목록을 수정하지 말고 항상 컬렉션의 복사본을 반환하세요 .
- 각 컬렉션은 별도의 클래스로 래핑되어야 하므로 이제 컬렉션과 관련된 동작에는 홈이 있습니다 (예: 필터링 메서드, 각 요소에 규칙 적용).
여러 가지 잡다한
- 익명 클래스 대신 람다를 선택하세요.
- 람다 대신 메서드 참조를 선택하세요.
- int 상수 대신 열거형을 사용하세요.
- 정확한 답변이 필요한 경우 float 및 double을 사용하지 말고 대신 Money와 같은 BigDecimal을 사용하세요.
- 박스형 기본 형식보다는 기본 형식을 선택하세요.
- 코드에 매직넘버를 사용하는 것을 피해야 합니다. 상수를 사용하세요.
- Null을 반환하지 마세요. `Optional`을 사용하여 메소드 클라이언트와 통신합니다. 컬렉션에도 동일합니다. null이 아닌 빈 배열이나 컬렉션을 반환합니다.
- 불필요한 객체 생성을 피하고, 객체를 재사용하고, 불필요한 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 + ")");
지금은 그게 전부입니다. 이것이 도움이 되었기를 바랍니다.
GO TO FULL VERSION