JetBrains가 제안하는 자바 모범 사례
[번역] Java Best Practices
java, best-practices, translation
좋은 코드는 일정한 규칙을 따르며, 그 규칙을 알수록 성공 가능성은 높아집니다. 이 글에서는 자바로 개발할 때 도움이 되는 몇 가지 베스트 프랙티스를 공유하고자 합니다. 소프트웨어 개발에 관한 전반적인 조언부터 자바 및 프로젝트에 특화된 필수 지식까지 폭넓게 다룰 예정입니다. 지금부터 시작하겠습니다!
일반 원칙
다음은 모던한 코딩에 있어 염두에 둬야 할 일반적인 규칙들입니다.
명확하게 작성할 것, 지나치게 영리하게 쓰지 말 것
코드의 가장 중요한 목적은 이해하기 쉽고 유지보수 가능한 상태를 유지하는 것입니다. 기술적인 능력을 뽐내기 위한 장치가 아니라는 의미입니다. 명확하게 작성된 코드는 디버깅이 쉬우며 유지보수와 확장도 원활해집니다. 이는 프로젝트에 참여하는 모든 사람에게 도움이 됩니다. 복잡성은 명예의 훈장이 아닙니니다. 단순함과 가독성이야말로 진정한 명예입니다.
아래 예시를 보세요.
영리하지만 난해
다음은 변수 a와 b의 값을 교환하는 다소 특이한 방법입니다. 영리하지만 처음 보는 사람이 즉시 이해하기는 어렵습니다.
명확한
다음은 보다 일반적이고 흔히 쓰이는 접근 방식입니다. 비록 코드가 한 줄 더 많아질지라도 직관적이고 명확해서 대부분의 프로그래머가 쉽게 이해할 수 있습니다.
짧고 간결하게 유지할 것
메서드와 클래스는 너무 길어지지 않도록 주의해야 합니다. 클래스의 길이나 단어 수에 대한 엄격한 규칙은 없지만, 가능한 명확하고 응집력 있는 구조를 유지하는 게 좋습니다. 메서드는 일반적으로 10~20줄 정도를 권장합니다. 메서드가 이보다 길어진다면, 더 작고 관리 가능한 단위로 나누는 편이 좋습니다.
이와 관련하여, 긴 메서드를 인지하는 연습을 해보고 싶다면, 테크니컬 코치인 Emily Bache가 만든 이 동영상을 보시는 걸 추천합니다.
또한 IntelliJ IDEA는 다양한 리팩토링 옵션을 제공하여 긴 메서드나 클래스를 간결하게 정리하는 데 도움을 줍니다. 예를 들어, 메서드를 추출(extract method)하여 길어진 메서드를 더 짧고 명확한 단위로 나눌 수 있게 도와줍니다.
이름은 어렵지만 신중하고 직관적으로 짓기
메서드와 변수의 이름을 잘 짓는 것은 코드를 직관적으로 이해할 수 있게 해주는 중요한 지표이며, 원활한 의사소통에 필수적입니다. 다음은 중요한 네이밍 컨벤션에 대한 가이드라인입니다.
- 한 글자짜리 변수는 피하세요.
- 메서드 이름은 수행하는 작업을 명확히 나타내야 합니다.
- 객체와 필드의 이름은 비즈니스 도메인에 맞춰 명확하게 지으세요.
예를 들어, 메서드 이름이 calculateTotalPrice()라면 이 메서드의 목적을 한눈에 알 수 있지만, 모호한 이름인 calculate()는 정확한 기능을 파악하기 어렵습니다. 변수의 경우도 customerEmailAddress라고 하면 즉각 이해할 수 있지만, cea처럼 축약하면 의미를 알기 어렵고 혼란을 초래할 수 있습니다.
또 다른 예로, 변수 이름을 단순히 timeout으로 짓기보다 단위를 명확히 표현한 timeoutInMs 또는 timeoutInMilliseconds를 사용하는 것이 좋습니다.
테스트, 테스트, 테스트
코드를 테스트하는 것은 애플리케이션이 예상대로 동작하고 향후 변경 사항이 발생해도 올바르게 동작할지 확인하기 위해 필수적입니다. 테스트는 초기에 문제를 발견하게 도와주므로 수정 비용과 난이도를 낮춰줍니다. 또한, 코드가 의도한 바대로 동작하도록 안내해주고, 이후 업데이트 시 코드가 망가지는 것을 최소화합니다.
좋은 테스트 이름은 각 테스트가 수행하는 작업과 확인 사항을 명확히 나타냅니다. 예를 들어 AlertWhenEmailIsMissing()이라는 테스트는 이메일이 누락되었을 때 알림을 검사한다는 것을 보여주며, 상세히 파고들지 않고도 내용을 이해할 수 있게 합니다.
테스트에 대한 자세한 정보는 Marit van Dijk의 블로그를 참고하세요.
언어 특화된 팁
다음 팁과 트릭을 통해 자바로 개발할 때 흔히 저지르는 실수를 방지하고, 코드의 품질을 한 단계 끌어올릴 수 있습니다.
복잡하고 긴 if문 대신 switch 표현식 사용하기
switch 표현식을 사용하면 다수의 조건을 하나의 구조로 정리할 수 있어 코드가 더욱 읽기 쉽고 명확해집니다. 이는 유지보수도 더 간단하게 만들어줍니다.
아래의 아이스크림과 주요 재료에 대한 예시를 살펴보겠습니다.
너무 많은 else-if를 사용한 경우
이 예시에서는 아이스크림 맛과 재료를 매칭할 때 else-if 문을 연쇄적으로 사용합니다. 아이스크림 맛이 많아질수록 if 문의 수가 급증하여 코드를 읽기 어렵게 만듭니다.
switch를 사용한 경우
다음은 여러 if-else 조건 대신 switch 표현식을 이용한 예시입니다. switch 표현식은 간결하고 깔끔하며, 단일 변수를 여러 상수값과 비교할 때 더욱 이해하기 쉽습니다.
IntelliJ IDEA는 if문을 단 몇 초만에 switch 표현식으로 바꿔주는 특별한 기능을 제공합니다.
switch 표현식의 더 훌륭한 예시를 보려면 자바 개발자 애드보케이트인 Mala Gupta가 쓴 최근 블로그 포스트를 확인해보세요.
빈 catch 블록을 피하기
자바에서 빈(empty) catch 블록이란 예외를 처리하는 코드가 없는 catch 절을 의미합니다. 이 경우 예외가 발생해도 아무 일도 일어나지 않아 프로그램이 문제를 무시한 채 계속 실행됩니다. 이는 문제를 발견하거나 디버깅하는 데 어려움을 줍니다.
빈 catch 블록
이 예시에서는 예외를 잡았지만 아무 작업도 하지 않습니다.
IntelliJ IDEA는 이런 문제를 발견하여 강조해주고 다음과 같은 해결책을 제안합니다:
예외를 로그로 기록
한 가지 방법은 e.printStackTrace()를 통해 예외를 콘솔에 출력하는 것입니다. 이는 디버깅에 도움을 줍니다.
예외를 기록한 후 다시 던지기
이상적인 방식은 IOException을 잡은 뒤, 오류 메시지를 출력하고 예외를 다시 던져서 나중에 더 적절히 처리하는 것입니다. 이렇게 하면 문제의 전체적인 흐름을 명확하게 확인할 수 있습니다.
예외를 기록하고 대체값을 반환하기
빈 catch 블록 문제를 해결하는 또 다른 방법은 예외를 기록한 후 의미 있는 대체값을 반환하는 것입니다.
배열(Array)보다는 컬렉션(Collection)을 사용
자바의 배열은 효율적이고 사용하기 쉽지만, 크기가 고정되어 있고 제한적인 연산만을 제공합니다. 따라서 데이터 조작이 다양한 경우에는 유연성이 떨어집니다.
자바의 컬렉션은 ArrayList나 HashSet과 같은 자료구조를 제공하며, 훨씬 뛰어난 유연성과 다양한 기능을 갖추고 있습니다. 예를 들어 ArrayList는 크기를 동적으로 조정할 수 있고, 다양한 유틸리티 메서드를 제공하며, 특히 제네릭과 함께 사용할 때 다루기가 훨씬 쉽습니다. 다음의 코드 예제를 통해 이를 확인해 봅시다:
배열
우리는 문자열(String)의 배열을 만들었습니다. 자바의 배열은 크기가 고정되어 있기 때문에, 만약 우리가 열한 번째 요소를 추가하고 싶다면 새로운 배열을 만들어 모든 요소를 복사해야만 합니다.
컬렉션
앞에서 본 코드의 대안으로 ArrayList라는 컬렉션 클래스를 사용할 수 있습니다. ArrayList와 같은 컬렉션은 프로그램 실행 중에 크기를 자유롭게 늘리고 줄일 수 있어 훨씬 더 유연합니다. 또한 이러한 컬렉션에는 데이터 조작에 강력한 도움을 주는 다양한 메서드(예: .add(), .remove(), .contains(), .size() 등)가 함께 제공됩니다.
불변성의 활용
불변 객체는 한 번 생성된 이후 상태를 변경할 수 없는 객체입니다. 불변성을 활용하면, 가변 상태의 변경을 관리하면서 발생하는 복잡성을 없애줘 코드가 보다 안전하고 명확해집니다. 또한 버그와 예기치 않은 부작용의 위험을 최소화하고, 일관된 동작을 보장하며, 애플리케이션을 유지보수하고 디버깅하는 과정을 간편하게 만들어 줍니다. 자바에서는 final 키워드를 사용해 불변성을 달성할 수 있습니다.
final이 없을 경우
이 예제에서는 Car 클래스를 만들고 브랜드와 모델을 출력해보겠습니다. 그 다음 자동차의 모델을 변경한 후 다시 출력해 보겠습니다. 콘솔 출력을 보면 Car 클래스의 상태가 바뀌었음을 알 수 있으며, 이를 통해 가변 객체의 동작을 확인할 수 있습니다.
final을 사용하는 경우
아래의 개선된 코드에서는 앞의 예제와 같은 작업을 수행하지만, 이번에는 Car 클래스가 final 클래스이며 setter 메서드가 없기 때문에 자동차의 모델이나 브랜드 값을 바꿀 수 없습니다. 콘솔 출력 결과를 보면 Car 클래스의 상태가 변하지 않은 그대로 유지되는 것을 확인할 수 있으며, 이를 통해 불변 객체의 동작을 확인할 수 있습니다.
상속보다는 구성을 선호
자바에서는 일반적으로 상속(inheritance, 슈퍼 클래스로부터 서브 클래스를 만들어 확장하는 방식)을 사용하는 것보다는 구성(composition, 다른 클래스의 객체를 필드나 참조로 가지는 방식)을 사용하는 편이 더 좋습니다. 구성을 적용하면 코드가 더욱 유연해지고 테스트하기 쉬워집니다.
상속
이 예제에서 GamingComputer 클래스는 BasicComputer 클래스로부터 상속을 받습니다. 이에 따른 문제점은, 만약 BasicComputer 클래스의 구현이 변경된다면 GamingComputer 클래스에까지 영향을 미쳐 망가져 버릴 수도 있다는 것입니다. 또한 GamingComputer 클래스는 언제나 BasicComputer 클래스의 일종(type)으로 고정되어버리는 제약이 생기기 때문에 유연성이 제한됩니다.
구성
두 번째 예제를 보면, Computer 클래스는 별도의 클래스 인스턴스인 Memory 클래스와 Processor 클래스를 필드로 갖고 있습니다. 이 각각의 클래스는 독립적으로 자신만의 메서드와 동작 방식을 정의합니다. 이 방식은 훨씬 더 유연하여, 다양한 Memory 또는 Processor 객체로 교체하거나 런타임에도 그 동작 방식을 바꿀 수 있으며, 이는 Computer 클래스를 변경하지 않아도 가능합니다.
좀 더 많은 예제를 살펴보려면 Easy Hacks: 자바에서 상속을 만드는 방법을 참조하시기 바랍니다.
람다를 사용해 함수형 인터페이스를 간결하게 만들기
자바에서 함수형 인터페이스란 단 하나의 추상 메서드만을 가진 인터페이스를 말합니다. 람다 표현식을 사용하면 익명 클래스를 작성할 때 필수적으로 들어가는 불필요한 코드 없이도 이를 간단하고 명료하게 구현할 수 있습니다.
람다를 사용하지 않음
다음 예제는 정렬을 위해 Comparator 인터페이스를 익명 내부 클래스로 구현한 것입니다. 이 방식은 코드가 불필요하게 길어지고 복잡한 인터페이스를 다룰 경우 가독성도 떨어질 수 있습니다.
람다를 사용
다음 예제는 위의 코드와 똑같은 일을 수행하지만, 익명 클래스 대신 람다 표현식을 이용합니다. 덕분에 코드가 훨씬 간결해지고 직관적으로 바뀌었습니다.
Enhanced for 루프 또는 스트림을 사용
자바에서는 고전적인 for 루프 보다 enhanced for 루프(for-each 루프)나 스트림을 사용하면 컬렉션 또는 배열을 보다 쉽고 간결하게 순회할 수 있습니다.
고전적 for 루프
아래의 코드는 기존 방식의 for 루프를 이용해 리스트를 순회합니다. 이 방식은 카운터 변수를 정의하고, 요소의 인덱스를 관리하며, 종료 조건도 명시해야 하므로 코드가 복잡해지고 가독성이 저하될 수 있습니다.
Enhanced for 루프
이 방식의 루프(forEach)는 별도의 카운터 변수가 필요하지 않고, 리스트 내의 모든 요소를 직접 가져다 주기 때문에 코드가 단순해지고 실수할 가능성도 줄어듭니다. IntelliJ IDEA에서는 이 방법을 검사 기능을 활용해 더욱 쉽게 적용할 수 있습니다.
스트림
스트림을 사용하는 경우에도 enhanced for 루프처럼 손쉽게 각 요소를 처리할 수 있습니다. 이뿐만 아니라 필터링, 매핑 등의 보다 복잡한 작업을 간편하게 수행할 수 있습니다.
try-with-resources 문을 사용해 리소스 보호하기
try-with-resources 문을 이용하면 자원을 사용할 때 이를 안전하게 닫아주는 작업을 자동화할 수 있습니다. 기존의 try 블록에서 리소스를 제대로 닫지 않으면 메모리 누수나 애플리케이션 오류 등 성능과 신뢰성에 악영향을 끼치는 문제가 발생할 수 있습니다.
리소스를 수동으로 닫는 경우
다음 예제는 시스템 리소스인 FileInputStream을 수동으로 닫는 방법을 보여줍니다. 만일 리소스를 닫는 과정에서 예외가 발생하는 경우, 리소스가 제대로 닫히지 않아 리소스 누수 또는 다른 문제를 유발할 수 있습니다.
try-with-resources 문을 사용하는 경우
개선된 예제에서는 FileInputStream을 try 블록 내에 선언하여 사용합니다. 이렇게 하면 자바가 자동으로 try 블록을 빠져나갈 때 (정상 종료든 예외 발생이든 상관없이) 리소스를 확실하게 닫아줍니다. 따로 코드를 작성하지 않아도 리소스가 보다 안전하게 관리됩니다.
지나치게 중첩된 코드 정리하기
지나치게 깊게 중첩된 코드는 얼핏 보면 놓치기 쉬운 논리적 문제점을 드러냅니다. 이러한 복잡성은 주로 조건문이 과도하게 많을 때 나타나지만, 조건문은 기본적으로 코딩에서 자주 사용하는 기능이기 때문에 이를 완전히 제거할 수는 없습니다. 하지만 코드를 보다 간결하게 만드는 방법을 고민해야 합니다.
지나치게 많은 조건문
아래 예제에서 지나치게 많은 조건문이 중첩되었을 때 코드가 얼마나 복잡하고 난해해지는지 볼 수 있습니다.
리팩토링 된 코드
리팩토링된 코드에서는 ‘가드 절(guard clauses)’ 기법을 사용해 중첩된 조건문을 제거합니다. 이를 통해 특정 조건이 충족되었을 때 함수를 즉시 종료하여, 기존의 논리를 동일하게 유지하면서 간결하고 보다 깨끗한 코드로 만들 수 있습니다.
프로젝트별
다양한 의존성을 가진 프로젝트를 작업할 때 유용한 권장사항과 비권장사항을 소개합니다.
의존성을 최신 상태로 유지하기
프로젝트의 의존성을 최신으로 유지하면 보안이 강화되고, 새로운 기능을 사용할 수 있으며, 버그가 수정됩니다. 정기적인 업데이트는 프로젝트가 원활히 운영되고 다른 도구와 호환되도록 합니다.
IntelliJ IDEA를 사용하면 의존성을 간편하게 최신 상태로 유지할 수 있습니다. 먼저 Preferences/Settings | Plugins 메뉴로 이동하여 JetBrains 마켓플레이스에서 ‘Package Search’ 플러그인을 설치합니다. 그런 다음 Dependencies 툴 창에서 프로젝트에 사용된 모든 의존성을 확인하고 옆에 있는 ‘Upgrade’ 링크를 클릭하여 최신 버전으로 업그레이드할 수 있습니다.
취약한 의존성과 API 찾기
프로젝트의 의존성 및 API에 존재 가능한 보안 취약점을 주기적으로 검사하면, 보안 위협을 최소화하고 프로젝트에 정의된 규칙을 준수하면서 원활하게 운영할 수 있습니다. 발견된 취약성을 빠르게 해결하면 프로젝트와 사용자들을 보안 위협으로부터 보호합니다.
IntelliJ IDEA에서 취약한 의존성을 찾으려면 메뉴에서 Code | Analyze Code를 선택한 다음 Show Vulnerable Dependencies를 클릭하면 됩니다. 결과는 Problems 툴 창의 Vulnerable Dependencies 탭에 나타납니다.
또는, 프로젝트 창에서 pom.xml이나 build.gradle 같은 파일이나 폴더를 마우스 오른쪽 버튼으로 클릭한 다음, 컨텍스트 메뉴에서 Analyze Code | Show Vulnerable Dependencies 메뉴를 선택해도 동일한 확인이 가능합니다.
취약점 검사를 명시적으로 하지 않더라도 IntelliJ IDEA는 pom.xml이나 build.gradle에서 발견된 취약성을 자동으로 강조표시하여 알려줍니다.
순환 의존성 피하기
순환 의존성이란 프로젝트 내의 구성 요소들이 서로를 원형으로 의존하는 경우를 말합니다. 예를 들어, 구성 요소 A가 구성 요소 B를 참조하는데, B 역시 다시 A를 참조한다면 순환 의존성이 발생한 것입니다. 이런 상황이 발생하면 프로젝트는 복잡해지고, 경계가 불분명해지며 유지보수가 어려워집니다. 따라서 이런 순환적 구조는 피하는 것이 좋습니다.
순환 의존성을 피하려면 IntelliJ IDEA의 Dependency Matrix 기능을 활용해보세요. 이를 통해 프로젝트 내 구성 요소 간의 의존성을 시각적으로 파악하고 관리할 수 있습니다.
결론
본 가이드가 여러분의 일상 업무를 좀 더 간단하게 만들어 주고, 명확하고 깔끔하며 전문적인 코드를 작성하도록 도와 더 효율적인 개발자가 되는 데 도움이 되었기를 바랍니다.
IntelliJ IDEA는 이번 글에서 소개된 여러 사항들을 손쉽게 발견하도록 도와주고, 자동 수정을 제공해주기도 합니다. 지금 바로 사용해보시고, 여러분의 경험과 의견을 들려주세요!