🚀 자바 코드를 작성할 때 주의해야 할 점
자바는 강력한 언어지만, 잘못 사용하면 성능 저하, 메모리 누수, 보안 취약점 같은 문제가 발생할 수 있습니다. 안정적이고 효율적인 자바 코드를 작성하기 위해 다음과 같은 주의할 점들을 고려해야 합니다.
⚠️ 예외 처리
자바에서 예외 처리는 필수입니다. 예외가 발생할 수 있는 코드는 try-catch 블록을 적절하게 처리해야 합니다. 예외를 무시하거나 단순히 로그만 남기고 끝내면 예상치 못한 종료나 데이터 손실이 발생할 수 있습니다.
try {
// 위험한 코드 실행
} catch (IOException e) {
// 구체적인 예외 처리
} catch (Excetpion e) {
// 일반적인 예외 처리
}
- 구체적인 예외 처리 : 모든 예외를 포괄하는 catch 블록을 사용하기보다는, 발생할 수 있는 예외를 정확하게 분리해 처리하는 것이 중요합니다. 마지막에는 Exception을 처리합니다.
- 예외 전파 : throws 키워드를 통해 예외를 상위 메서드로 전파할 수 있지만, 과도하게 사용하면 코드가 복잡해질 수 있습니다. 적절한 범위에서 사용해야 합니다.
🧠 메모리 누수 방지
자바는 가비지 컬렉션(GC)이 메모리를 자동으로 관리하지만, 객체 참조를 잘못 관리하면 메모리 누수가 발생할 수 있습니다. 특히, 긴 생명주기를 가진 객체가 불필요하게 참조를 유지하면 문제가 될 수 있습니다.
=> 자바 8 이후로, 스트림 API, Optional 등의 특성들로 인해 코드에서 명시적으로 객체를 null로 설정하는 경우가 줄어들었습니다.
- 명시적 null 처리 : 사용이 끝난 객체 참조를 null로 초기화하거나, 컬렉션에서 객체를 제거해 참조를 끊어줘야 합니다.
- 리소스 해제 : 파일, 네트워크 소켓, 데이터베이스 연결 등 외부 리소스를 사용하는 경우 반드시 try-with-resources 구문이나 finally 블록을 사용해서 리소스를 닫아야 합니다.
try (BufferedReader br = new BufferedReader(new FileReader("file.txt"))) {
// 파일 읽기
} catch (IOException e) {
e.printStackTrace();
}
// try-with-resources가 끝나면 자동으로 리소스가 해제됨
🧵 스레드 관리
자바는 멀티스레드 프로그래밍을 지원하지만, 스레드 동기화를 제대로 하지 않으면 Race Condition(경합 조건), Deadlock(데드락) 등의 문제가 발생할 수 있습니다.
- 동기화 : 공유 자원을 사용하는 경우 synchronized 키워드를 활용해 스레드 안전성을 확보해야 합니다.
- 스레드 풀 사용 : 스레드를 직접 생성하기보다는 ExecutorService와 같은 스레드 풀을 사용해서 스레드 생성을 관리하는 것이 성능과 리소스 관리에 유리합니다.
ExecutorService executor = Executors.newFixedThreadPool(10);
executor.submit(() -> {
// 스레드 작업
});
executor.shutdown();
🔐 보안 취약점 방지
자바 코드에서 보안 취약점이 발생할 수 있습니다. 외부 입력을 신뢰하고 그대로 사용하면 SQL Injection, XSS 등 다양한 보안 문제가 발생할 수 있습니다. 이러한 취약점으로부터 안전한 코드를 작성하기 위해서는 입력 값을 항상 검증하고, 적절한 보안 정책을 적용해야 합니다.
- 입력 검증 : 모든 외부 입력을 반드시 검증해야 합니다. 예를 들어, 숫자형 입력, 이메일 형식 검증 등 기본적인 검증은 필수입니다.
String requertInput = request.getParameter("input");
if (isValid(requertInput)) {
// 검증된 입력 사용
} else {
// 입력 거부
}
🚀 성능 최적화
자바는 C나 C++과 같은 네이티브 언어에 비해 성능이 떨어질 수 있지만, 성능이 중요한 부분에서는 최적화를 통해 성능 문제를 해결할 수 있습니다.
- 적절한 자료구조 사용 : 데이터 처리에 적절한 자료구조를 선택해 불필요한 객체 생성을 줄이는 것이 중요합니다.
- 스트림 API 최적화 : 자바 8 이상에서는 스트림 API를 사용해서 병렬 처리 등을 활용할 수 있습니다.
List<String> list = Arrays.asList("a", "b", "c");
list.stream()
.parallel() // 병렬 처리
.forEach(System.out::println);
🙅♂️ NullPointerException 방지 (NPE)
NullPointerException(NPE)는 자바에서 발생할 수 있는 가장 흔한 예외 상황 중 하나입니다. 이를 방지하기 위해 항상 null 체크를 철저히 해야 합니다. 예를 들어, String의 equals() 메서드에서도 null 체크를 하지 않으면 NPE가 발생할 수 있으므로 항상 null 체크를 해주는 것이 좋습니다.
- Optional 사용 : Java 8 이후에는 Optional 클래스를 사용해 NPE를 방지할 수 있습니다.
// Bad Case : name이 null인 경우 NPE 발생
public boolean isKim(String name) {
return name.equals("Kim"); // name이 null이면 NPE 발생
}
// Good Case : 상수를 기준으로 null을 안전하게 비교
public boolean isKim(String name) {
return "Kim".equals(name); // null 안전하게 비교
}
// Good Case2 : StringUtils 클래스 사용해서 비교
public boolean isKim(String name) {
return StringUtils.equals(name, "Kim"); // null 안전하게 처리
}
빈 컬렉션에 null 값을 할당하는 것은 좋지 않은 습관입니다. 대신 Collections.EMPTY_LIST와 같은 상수를 사용해서 안전하게 빈 컬렉션을 설정합니다. 이를 통해 null 값을 사용하는 경우 발생할 수 있는 예외 상황을 방지할 수 있습니다.
// Bad Case : null 세팅
List<String> strs = null;
// Good Case : EMPTY_LIST로 빈 리스트로 초기화
List<String> strs = Collections.EMPTY_LIST
✌️ String 생성할 때 new 키워드 사용 X
new 키워드로 String 객체를 생성하면 항상 새로운 객체가 힙 메모리에 생성되어 비효율적입니다. 대신, 리터럴로 String Poll을 활용해 생성하면 메모리를 절약할 수 있습니다.
=> 따라서 가능한 경우 new 키워드를 사용하지 않고, 리터럴(String pool)을 활용하는 것이 좋습니다.
// Slow
String s = new String("hello");
// fast
String s = "hello";
💡 반복문 내에서 객체 생성 자제
반복문 내에서 객체를 반복적으로 생성하면 많은 자원을 소비하고 성능 저하를 초래할 수 있습니다. 따라서 반복문 외부에서 객체를 생성하고 재사용하는 것이 더 효율적입니다.
// Bad Case : 반복문 내에서 객체 생성
for (int i = 0; i < 5; i++) {
Foo f = new Foo(); // 매번 객체 생성
f.getMethod();
}
// Good Case : 객체 재사용
Foo f = new Foo(); // 반복문 밖에서 객체 생성
for (int i = 0; i < 5; i++) {
f.getMethod();
}
🙅♂️ Collections 반복 중 수정 X
컬렉션을 반복하면서 수정하면 ConcurrentModificationException이 발생할 수 있습니다. 이를 방지하기 위해 Iterator를 사용해서 안전하게 요소를 삭제해야 합니다.
// Bad Case : 반복하면서 리스트 수정
List<String> names = new ArrayList<>(List.of("Kim", "Lee", "Park"));
for (String name : names) {
if ("Kim".equals(name)) {
names.remove(name); // ConcurrentModificationException 발생 가능
}
}
// Good Case : Iterator 사용
List<String> names = new ArrayList<>(List.of("Kim", "Lee", "Park"));
Iterator<String> iterator = names.iterator();
while (iterator.hasNext()) {
if ("Kim".equals(iterator.next())) {
iterator.remove(); // 안전하게 요소 삭제
}
}
🔑 Switch-Case문에서 break 키워드 사용
switch 문에서 break 키워드를 사용하지 않으면 다음 case 블록이 실행될 수 있습니다. 각 case 블록의 끝에는 반드시 break를 추가해서 제어 흐름이 다음 블록으로 넘어가지 않도록 주의해야 합니다.
int index = 0;
switch (index) {
case 0 :
System.out.println("Zero");
break; // break 추가
case 1 :
System.out.println("One");
break;
case 2 :
System.out.println("Two");
break;
break;
// ...
default;
System.out.println("Default");
}
🔍 "=="와 "equals( )" 비교
== 연산자는 두 객체의 참조값을 비교하는 반면, equals() 메서드는 두 객체의 값을 비교합니다. 객체를 비교할 때는 항상 equals() 메서드를 사용해야 합니다.
// Bad Case : 참조 비교
String s1 = new String("hello");
String s2 = new String("hello");
System.out.println(s1 == s2); // false
// Good Case : 값 비교
System.out.println(s1.equals(s2)); // true
🛠️ StringBuffer vs StringBuilder
문자열을 자주 변경해야 하는 경우, StringBuilder 또는 StringBuffer를 사용합니다. StringBuffer는 멀티스레드 환경에서 안전하지만, 동기화로 인해 성능이 떨어질 수 있습니다. 따라서 단일 스레드 환경에서는 StringBuilder를 사용하는 것이 더 효율적입니다.
StringBuilder sb = new StringBuilder();
sb.append("Hello");
sb.append(" World");
📝 자바 파일 작성 시 표준
자바 파일을 작성할 때는 가독성을 높이고, 코드의 일관성을 유지할 수 있도록 표준을 지키는 것이 중요합니다.
- 접근 제어자 순서 : public, protected, private 순으로 정의합니다.
- 생선자 순서 : 필드가 적은 생성자를 먼저 정의해서 가독성을 높입니다.
- 메서드 그룹화 : 유사한 기능의 메서드를 그룹화해서 코드의 일관성을 유지합니다.
- 주석 사용 : 주석은 최소화하되, 코드 직관적으로 이해될 수 있도록 작성합니다.
자바는 강력한 기능을 제공하는 언어지만, 사용에 있어 주의가 필요합니다. 위의 주의사항들을 잘 지키면서, 안정적이고 효율적인 자바 코드를 작성할 수 있습니다.
'🌈 프로그래밍 언어 > 자바' 카테고리의 다른 글
[Java] 자바 반복문 - for, while, do-while 완벽 가이드 (0) | 2024.05.24 |
---|---|
[Java] 자바 조건문(If문, Switch문) 가이드 (0) | 2024.05.19 |
[Java] 자바 연산자 Operator 가이드 정리 (0) | 2024.05.13 |
[Java] 자바 변수 기본 가이드 라인 (0) | 2024.04.05 |
[Java] 프로그래밍 언어 자바란 무엇인가? (0) | 2023.03.27 |