입사 후 처음 몇 달은 코드를 짜는 것보다 "왜 이렇게 짜야 하는지" 를 모르는 채 따라가는 시간이 많습니다.
이 글은 그 시간을 조금이라도 줄이기 위해 씁니다.
학교에서는 잘 안 가르쳐주지만, 현장에서는 기본으로 여기는 것들입니다.
1. == 와 equals() 는 완전히 다르다
Java를 처음 배우면 가장 먼저 틀리는 부분입니다.
String a = new String("hello");
String b = new String("hello");
System.out.println(a == b); // false — 주소 비교
System.out.println(a.equals(b)); // true — 값 비교== 는 메모리 주소를 비교합니다. 문자열 비교는 반드시 equals() 를 써야 합니다.
단, 문자열 리터럴은 String Pool을 사용해서 == 가 true 로 나오기도 하지만, 이 동작에 의존하면 언젠가 반드시 버그가 납니다.
// 나쁜 예
if (status == "ACTIVE") { ... }
// 좋은 예
if ("ACTIVE".equals(status)) { ... } // null-safe 하게 상수를 앞에2. NullPointerException 은 막을 수 있다
NPE는 Java 개발자의 오랜 숙적입니다. 그런데 대부분은 방어 코드 한 줄로 막을 수 있습니다.
// 위험한 코드
String name = user.getName().toUpperCase();
// 방어적 코드
String name = (user != null && user.getName() != null)
? user.getName().toUpperCase()
: "UNKNOWN";Java 8부터는 Optional 을 활용하면 더 깔끔합니다.
Optional<String> name = Optional.ofNullable(user)
.map(User::getName)
.map(String::toUpperCase);
String result = name.orElse("UNKNOWN");메서드에서 null 을 반환하는 습관도 버려야 합니다. 빈 컬렉션이나 Optional 로 반환하세요.
// 나쁜 예
public List<User> getUsers() {
if (없으면) return null;
}
// 좋은 예
public List<User> getUsers() {
if (없으면) return Collections.emptyList();
}3. StringBuilder 를 써야 할 때를 알자
문자열을 반복문 안에서 + 로 연결하면 성능이 급격히 떨어집니다.
// 나쁜 예 — 반복마다 새 String 객체 생성
String result = "";
for (int i = 0; i < 10000; i++) {
result += i;
}
// 좋은 예 — 내부 버퍼에 추가
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
sb.append(i);
}
String result = sb.toString();단순한 "Hello " + name 한 줄은 컴파일러가 알아서 최적화해줍니다.
문제는 반복문 안입니다. 습관적으로 구분하세요.
4. 예외는 구체적으로 잡아라
Exception 을 통으로 잡는 건 현장에서 가장 자주 보이는 나쁜 습관 중 하나입니다.
// 나쁜 예 — 모든 예외를 삼켜버림
try {
process();
} catch (Exception e) {
e.printStackTrace(); // 로그도 제대로 안 남음
}
// 좋은 예 — 예외를 명확히 구분
try {
process();
} catch (IOException e) {
log.error("파일 처리 중 오류 발생: {}", e.getMessage(), e);
throw new RuntimeException("파일 처리 실패", e);
} catch (IllegalArgumentException e) {
log.warn("잘못된 입력값: {}", e.getMessage());
throw e;
}예외를 catch 하고 아무 처리도 안 하는 빈 catch 블록은 절대 금물입니다.
버그가 생겨도 추적이 불가능해집니다.
5. final 을 적극적으로 활용하라
final 은 단순히 "변경 금지" 이상의 의미가 있습니다.
코드를 읽는 사람에게 "이 값은 바뀌지 않는다" 는 의도를 전달합니다.
// 지역 변수에 final
public void process(final String input) {
final int length = input.length();
// length = 10; // 컴파일 에러 — 실수를 미리 막아줌
}
// 상수 정의
public class OrderStatus {
public static final String PENDING = "PENDING";
public static final String APPROVED = "APPROVED";
public static final String REJECTED = "REJECTED";
}특히 멀티스레드 환경에서 final 필드는 가시성(visibility)을 보장해줘서 동시성 버그를 예방하는 효과도 있습니다.
6. 컬렉션은 인터페이스 타입으로 선언하라
구현체 타입으로 변수를 선언하면 나중에 변경이 어려워집니다.
// 나쁜 예 — 구현체에 종속
ArrayList<String> list = new ArrayList<>();
HashMap<String, Integer> map = new HashMap<>();
// 좋은 예 — 인터페이스 타입으로 선언
List<String> list = new ArrayList<>();
Map<String, Integer> map = new HashMap<>();ArrayList 를 LinkedList 로 바꿔야 할 때, 인터페이스로 선언했다면 선언부 한 줄만 바꾸면 됩니다.
구현체로 선언했다면 해당 타입을 사용하는 모든 곳을 수정해야 합니다.
7. Stream API 를 알면 코드가 읽기 쉬워진다
Java 8 이후 현장에서는 Stream을 기본으로 씁니다. 익숙해지면 코드량이 줄고 의도가 명확해집니다.
List<User> users = getUserList();
// 전통적인 방식
List<String> activeNames = new ArrayList<>();
for (User user : users) {
if (user.isActive()) {
activeNames.add(user.getName().toUpperCase());
}
}
// Stream 방식
List<String> activeNames = users.stream()
.filter(User::isActive)
.map(User::getName)
.map(String::toUpperCase)
.collect(Collectors.toList());처음엔 낯설지만, 익숙해지면 훨씬 직관적으로 읽힙니다.
filter → 거른다, map → 변환한다, collect → 모은다. 영어 그대로입니다.
8. 로그는 습관이다 — System.out.println 은 쓰지 마라
개발할 때 System.out.println 으로 디버깅하는 건 이해합니다.
하지만 현장 코드에 남기면 안 됩니다.
// 절대 쓰면 안 됨 (운영 서버에서 콘솔 출력은 성능 저하)
System.out.println("user: " + user);
// 올바른 방법 — SLF4J + Logback/Log4j
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class UserService {
private static final Logger log = LoggerFactory.getLogger(UserService.class);
public void createUser(User user) {
log.info("사용자 생성 시작: id={}", user.getId());
// ...
log.debug("생성된 사용자 정보: {}", user);
}
}로그 레벨을 구분하는 것도 중요합니다.
| 레벨 | 용도 |
|---|---|
ERROR |
즉시 대응이 필요한 장애 |
WARN |
잠재적 문제, 주의 필요 |
INFO |
정상 흐름의 주요 이벤트 |
DEBUG |
개발/디버깅용 상세 정보 |
9. 객체는 불변(Immutable)으로 만들수록 좋다
객체가 생성된 후 상태가 바뀌지 않으면 버그가 줄어듭니다.
특히 멀티스레드 환경에서 불변 객체는 동기화 없이 안전하게 공유됩니다.
// 가변 객체 — 외부에서 마음대로 바꿀 수 있음
public class User {
private String name;
public void setName(String name) { this.name = name; }
}
// 불변 객체 — 생성 후 상태 변경 불가
public final class User {
private final String name;
private final String email;
public User(String name, String email) {
this.name = name;
this.email = email;
}
public String getName() { return name; }
public String getEmail() { return email; }
}Lombok을 쓴다면 @Value 어노테이션 하나로 불변 클래스를 만들 수 있습니다.
@Value // 모든 필드 final, getter만 생성, 생성자 자동 생성
public class UserDto {
String name;
String email;
}10. 코드 리뷰를 두려워하지 마라
마지막은 기술이 아닙니다.
신입 때 코드 리뷰 피드백을 받으면 움츠러들기 쉽습니다.
하지만 리뷰는 공격이 아니라 함께 코드를 개선하는 과정입니다.
피드백을 받았을 때 좋은 태도는 이렇습니다.
- "왜 이렇게 해야 하나요?" 라고 물어보는 것 — 이해 없이 수정만 하면 같은 실수를 반복합니다.
- 리뷰어가 지적한 내용을 메모해두는 것 — 패턴이 보이면 자신의 약점을 알 수 있습니다.
- 좋은 코드를 발견하면 "왜 좋은지" 분석하는 것 — 모방이 실력 향상의 지름길입니다.
코드는 혼자 짜는 게 아닙니다. 팀이 읽고 유지보수하는 것입니다.
"내가 6개월 후에 봐도 이해할 수 있는가?" 를 항상 질문해보세요.
마무리
이 10가지는 모두 현장에서 반복적으로 보이는 내용입니다.
한 번에 다 외우려 하지 말고, 코드를 짤 때마다 하나씩 떠올리는 것만으로도 충분합니다.
시간이 지나면 자연스럽게 몸에 밸 것입니다.